Skip to content

Commit 328344c

Browse files
committed
Continue checked runway and exclude shallow water by 200 metres
1 parent c97ac4a commit 328344c

8 files changed

Lines changed: 227 additions & 31 deletions

File tree

‎README.ko.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ path = planner.route(start, goal)
6969

7070
후속 경유점은 검증된 구간의 끝을 표시합니다. 차량이 지나가는 전체 폭의
7171
수심과 지도상 육지를 확인한 뒤 직선 구간의 중간 지점을 줄입니다.
72+
육지와 수심 2 m 미만인 수역을 해안 경계로 보고 200 m를 띄웁니다.
73+
이미 이 범위 안에 있는 차량은 선체 안전을 검사한 경로로 간격을 늘리며 빠져나옵니다.
74+
네이티브 지형에서 실제 로컬 경로의 여유 거리를 다시 확인한 뒤 추진을 승인합니다.
75+
7276
해안에 합류한 뒤에는 육지에서 1.5 km 이내인 구간을 따릅니다.
7377
서로 떨어진 해안 구간 사이에서는 전체 수상 연결 경로를 검사합니다. 해안 거리 한도는 다음 해안에 합류한 뒤 적용합니다.
7478
먼바다 출구는 연결 검증에 사용하며, 별도 경로를 검사한 뒤 이동합니다.
@@ -80,10 +84,10 @@ path = planner.route(start, goal)
8084
|---|---|
8185
| 수상 차량 로컬 수심 여유 | 1.5 m |
8286
| 수상 차량 반경 + 추적 여유 | 3.5 m + 4 m |
83-
| 해안 선호 거리 | 400 m |
84-
| 해안 추종 중 육지와의 최대 거리 | 1.5 km |
87+
| 해안 선호 거리 | 320 m |
88+
| 지역 해안 추종 중 육지·얕은 수역과의 최대 거리 | 1.5 km |
8589
| 해안 선호에 따른 추가 거리 한도 | 100% (전체 거리 2.0배) |
86-
| 해안 지역 여유 폭 / 수심 | 40 m / 2 m |
90+
| 얕은 수역 제외 거리 / 경계 수심 | 200 m / 2 m |
8791
| 도착 반경 | 300 m |
8892
| 도착 목표 검사 | 반경 350 m / 수심 3 m |
8993

@@ -124,7 +128,8 @@ path = planner.route(start, goal)
124128
같은 구간에서 일반 경유점을 통과할 때만 `retain_coast=True`를 사용합니다.
125129
로컬 목표가 항로 경유점이면 최종 항구를 `progress_target`으로 전달합니다.
126130
`state['regional_waiting']`이 있으면 검증된 다음 경로가 준비될 때까지 추진을
127-
멈춥니다. 물리 계산과 센서 수신은 계속합니다. 선택적인 해안 경로 준비에 실패하거나
131+
멈춥니다. 물리 계산과 센서 수신은 계속합니다. 갱신 경로는 검증된 기존 경로의
132+
끝에서 시작하며, 남아 있는 전진 경유점을 유지합니다. 선택적인 해안 경로 준비에 실패하거나
128133
결과가 비어 있어도 검증된 해안 경로를 외해 목표로 바꾸지 않습니다.
129134
계획 감시는 제한 시간 안의 작업을 유지하고 완료된 결과부터 반영합니다.
130135
경로 준비 지연만으로 차량의 물리적 후퇴를 요청하지 않습니다.

‎README.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ selecting a local depth-checked goal. The detour can initially increase distance
6161
to the course target. At its checked endpoint, the caller waits for the next
6262
checked segment. Longitude wrapping supports the international date line.
6363

64+
Coastal lookahead treats land and water shallower than 2 m as shoreline boundaries.
65+
Routes keep a 200 m exclusion around these boundaries. A vessel already inside
66+
the buffer first follows a hull-checked outward exit with increasing clearance.
67+
The native resident corridor rechecks the buffer before propulsion is approved.
68+
6469
Coastal lookahead favours shore only while progressing toward the destination
6570
port. Its final stretch must approach both the course target and the port.
6671
The full coastal route may add at most 100% to a shorter checked route to the
@@ -80,10 +85,10 @@ Optional renewals keep the current checked coastal runway until a coastal contin
8085
|---|---|
8186
| Surface local depth clearance | 1.5 m |
8287
| Surface hull radius + tracking margin | 3.5 m + 4 m |
83-
| Coastal preferred distance | 400 m |
84-
| Maximum coastal-following distance from land | 1.5 km |
88+
| Coastal preferred distance | 320 m |
89+
| Maximum regional coastal-following distance from land or shallow water | 1.5 km |
8590
| Maximum extra distance for coastal preference | 100% (2.0× total length) |
86-
| Coastal regional buffer / depth | 40 m / 2 m |
91+
| Shallow-water exclusion buffer / boundary depth | 200 m / 2 m |
8792
| Arrival radius | 300 m |
8893
| Arrival target screening | 350 m buffer / 3 m depth |
8994

@@ -125,7 +130,8 @@ Set `retain_coast=True` only for an ordinary shape advance within the same leg.
125130
Pass the final port as `progress_target` when the local target is a course waypoint.
126131
When `state['regional_waiting']` is present, the caller holds thrust until a
127132
checked replacement is ready. Physics and sensor sampling continue during the
128-
wait. Failed or empty optional renewal cannot replace a checked coastal route
133+
wait. Renewal starts at the checked runway endpoint and retains its remaining
134+
forward bends. Failed or empty optional renewal cannot replace a checked coastal route
129135
with an offshore goal. A planning watchdog preserves pending work within its
130136
budget and consumes completed results before retrying. Planning delays do not
131137
request a physical retreat.

‎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.5"
7+
version = "0.1.6"
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: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020

2121
GEO = Geod(ellps="WGS84")
2222
COAST_DISCOVERY_M = 80000.
23-
STANDOFF_M = 400.
23+
STANDOFF_M = 320.
2424
MAXIMUM_STANDOFF_M = 600.
2525
MINIMUM_DEPTH_M = 2.
26-
REGIONAL_BUFFER_M = 40.
26+
REGIONAL_BUFFER_M = 200.
2727
WAYPOINT_FORMAT = "checked-segment-ends-v5-coastal-transfer"
2828
MAXIMUM_COASTAL_DETOUR_RATIO = 2.0
2929
MAXIMUM_COASTAL_DISTANCE_M = 1500.
@@ -139,7 +139,7 @@ def coastal_corridor(terrain, spec, start, target, timeout=25., guard=None, prog
139139
field = TerrainField(terrain["xs"], terrain["ys"], terrain["heights"])
140140
xs, ys, heights = field.xs, field.ys, field.heights
141141
finite = np.isfinite(heights)
142-
land = finite & (heights >= 0)
142+
land = finite & (heights > -MINIMUM_DEPTH_M)
143143
if guard is not None:
144144
land |= ~guard.grid(xs, ys)
145145
if not land.any():
@@ -423,6 +423,8 @@ def route_length(points):
423423
index=0, mode="coastal_transfer" if transfer_required else "coastal", planning_seconds=time.monotonic()-started,
424424
search_nodes=expanded, minimum_regional_clearance_m=validator.minimum,
425425
preferred_standoff_m=STANDOFF_M,
426+
shallow_water_buffer_m=REGIONAL_BUFFER_M,
427+
shallow_boundary_depth_m=MINIMUM_DEPTH_M,
426428
endpoint_standoff_m=float(coast_distance[endpoint]),
427429
maximum_preferred_standoff_m=MAXIMUM_STANDOFF_M,
428430
progress_to_course_waypoint_m=float(progress[endpoint]),

‎src/posim_routing/regional.py‎

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
import asyncio,math,time
77
from pyproj import Geod
88
GEO=Geod(ellps='WGS84')
9-
REGIONAL_POLICY='buffered-water-v19-coastal-400m'
10-
COMPATIBLE_POLICIES={REGIONAL_POLICY,'buffered-water-v18-coastal-350m'}
9+
REGIONAL_POLICY='buffered-water-v21-shallow-200m-coastal-320m'
10+
COMPATIBLE_POLICIES={REGIONAL_POLICY}
1111
PLANNING_BUDGET_SECONDS=660.
12-
REGIONAL_BUFFER_M=40.
12+
REGIONAL_BUFFER_M=200.
1313
REGIONAL_DEPTH_M=2.
1414

1515
def distance(a,b):return GEO.inv(*a,*b)[2]
@@ -69,6 +69,7 @@ def start_task(self,work):
6969
# its own bounded provider deadline. One budget includes all retries.
7070
self.task_started=time.monotonic()
7171
work_task=asyncio.create_task(work)
72+
work_task.add_done_callback(lambda task: task.exception() if not task.cancelled() else None)
7273
self.task=asyncio.create_task(asyncio.wait_for(work_task,PLANNING_BUDGET_SECONDS))
7374
# Cancellation before the wrapper starts must still close its work.
7475
self.task.add_done_callback(lambda _: work_task.cancel() if not work_task.done() else None)
@@ -110,7 +111,7 @@ def active_point(self,state,key):
110111
from .course import passed_vertex
111112
while plan['index']<len(plan['points']):
112113
index=plan['index'];point=plan['points'][index]
113-
reached=distance(state['position'],point)<(5 if plan.get('mode')=='port_departure' else 12 if plan.get('mode')=='port_approach' else 80)
114+
reached=distance(state['position'],point)<(5 if plan.get('mode') in ('port_departure','shallow_exit') else 12 if plan.get('mode')=='port_approach' else 80)
114115
previous=plan['points'][index-1] if index else plan.get('probe_position')
115116
passed=(plan.get('mode') in ('coastal','coastal_transfer','coastal_approach') and previous is not None
116117
and index<len(plan['points'])-1 and passed_vertex(previous,point,plan['points'][index+1],
@@ -220,7 +221,7 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
220221
# cannot fall back to the original straight line across that land.
221222
guide_exhausted=(plan.get('route_key')==key and plan.get('mapped_guidance')
222223
and plan.get('points') and plan['index']>=len(plan['points']))
223-
needed=needed or bool(guide_exhausted)
224+
needed=needed or bool(guide_exhausted) or (plan.get('mode')=='shallow_exit' and plan.get('points') and plan['index']>=len(plan['points']))
224225
approach_runway=(plan.get('mode')=='port_approach_lookahead' and point is not None
225226
and distance(state['position'],plan['probe_position'])<500.)
226227
prefer_port=terminal and (point is None or (plan.get('mode')!='port_approach' and not approach_runway))
@@ -245,10 +246,28 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
245246
# local segment did not itself need a detour or coast discovery.
246247
expired=state.get('regional_planning') or {}
247248
needed=needed or (expired.get('status')=='expired' and expired.get('reason') in
248-
('regional_clearance_policy_changed','regional_goal_progress_stalled','suez_fine_grid','predicted_stopping_corridor','checked_route_wait_stalled'))
249+
('regional_clearance_policy_changed','regional_goal_progress_stalled','suez_fine_grid','predicted_stopping_corridor','checked_route_wait_stalled','shallow_water_clearance'))
249250
if self.task and self.task.done():
250251
try:
251252
value=self.task.result()
253+
connection=value.pop('continuation_of',None)
254+
if connection:
255+
# Renew from the checked runway's END, retaining the
256+
# remaining forward bends. Replanning from the moving hull
257+
# can choose an already passed bay as a new coast entry.
258+
if (plan.get('job')!=connection['job'] or plan.get('route_key')!=key
259+
or connection['endpoint'] not in plan.get('points',[])):
260+
raise RuntimeError('coastal_runway_changed')
261+
end=plan['points'].index(connection['endpoint'])
262+
if plan['index']>end:raise RuntimeError('coastal_runway_already_passed')
263+
retained=plan['points'][plan['index']:end+1]
264+
value['points']=retained+value['points'];value['index']=0
265+
value['retained_checked_points']=len(retained)
266+
value['renewed_from']=connection
267+
value['probe_position']=list(state['position'])
268+
for metric in ('minimum_regional_clearance_m',):
269+
if plan.get(metric) is not None and value.get(metric) is not None:
270+
value[metric]=min(plan[metric],value[metric])
252271
# A failed optional preference must preserve a previously
253272
# checked detour while waiting for a better coastal region.
254273
coast_plan=(plan.get('route_key')==key and plan.get('mode') in ('coastal','coastal_transfer','coastal_approach')
@@ -280,7 +299,13 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
280299
coast_needed=nearest is not None and (self.probed is None or distance(self.probed,state['position'])>=3000)
281300
if (prefer_port or needed or coast_needed or renew_coast) and not self.task and time.monotonic()>=self.retry:
282301
state['regional_planning']=dict(status='background',mode='port_approach' if terminal else 'coastal' if coast_needed else 'detour')
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))
302+
if terminal:work=self.prepare_port(state,target,key)
303+
elif renew_coast and point is not None:
304+
work=self.prepare_continuation(state,target,key,plan,extent=self.recovery_extent,
305+
progress_target=progress_target,coastal_target=coastal_target)
306+
else:work=self.prepare(state,target,key,nearest,needed,extent=self.recovery_extent,
307+
progress_target=progress_target,coastal_target=coastal_target)
308+
self.start_task(work)
284309
self.recovery_extent=None
285310
if point is not None:return point
286311
plan=state.get('regional_navigation') or {}
@@ -326,6 +351,23 @@ def departure_update(self,state,target,key):
326351
state['regional_waiting']=dict(reason='resident_departure_corridor_pending')
327352
return state['position']
328353

354+
async def prepare_continuation(self,state,target,key,plan,extent=None,progress_target=None,coastal_target=None):
355+
endpoint=list(plan['points'][-1]);connection=dict(job=plan['job'],endpoint=endpoint)
356+
sample=dict(state,position=endpoint)
357+
nearest=None
358+
if self.coast_index is not None:
359+
from .coastal import COAST_DISCOVERY_M,MAXIMUM_COASTAL_DISTANCE_M
360+
separation,shore=self.coast_index.nearest(endpoint)
361+
if separation<COAST_DISCOVERY_M:
362+
nearest=(separation,shore)
363+
if separation>MAXIMUM_COASTAL_DISTANCE_M and hasattr(self.coast_index,'progressive_shore'):
364+
nearest=self.coast_index.progressive_shore(endpoint,coastal_target or target)
365+
value=await self.prepare(sample,target,key,nearest,True,extent=extent,
366+
progress_target=progress_target,coastal_target=coastal_target)
367+
if not value.get('points'):raise RuntimeError('coastal_continuation_unavailable')
368+
value['continuation_of']=connection
369+
return value
370+
329371
async def prepare_departure(self,state,target,key):
330372
from .port_approach import corridor
331373
from .land import guard_from_spec
@@ -466,6 +508,9 @@ async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,pro
466508
coastal_goal=coastal_target if nearest is not None and coastal_target is not None else target
467509
value=None;coast_error=None
468510
if not canal and (nearest or needed):
511+
from .shallow import exit_corridor
512+
value=await asyncio.to_thread(exit_corridor,grid,job['spec'],current,progress_target,guard=guard)
513+
if value is None and not canal and (nearest or needed):
469514
from .coastal import coastal_corridor
470515
try:value=await asyncio.to_thread(coastal_corridor,grid,job['spec'],current,coastal_goal,guard=guard,progress_target=progress_target)
471516
except RuntimeError as error:coast_error=str(error)[:160]
@@ -539,6 +584,9 @@ async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,pro
539584
if not value['points']:value['coastal_preference_error']=coast_error or 'no_verified_progressive_coastal_corridor'
540585
value.update(route_key=key,job=job['id'],source=job['manifest']['source'],policy=REGIONAL_POLICY,
541586
mapped_land_checked=guard is not None)
587+
if not canal and value['points']:
588+
value.setdefault('shallow_water_buffer_m',REGIONAL_BUFFER_M)
589+
value.setdefault('shallow_boundary_depth_m',REGIONAL_DEPTH_M)
542590
value['probe_position']=current
543591
value['destination_progress_target']=list(progress_target)
544592
if coastal_target is not None and value.get('mode') in ('coastal','coastal_transfer','coastal_approach'):

‎src/posim_routing/shallow.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Checked outward migration when a hull is already inside a shallow buffer."""
2+
import math
3+
import numpy as np
4+
from scipy.ndimage import distance_transform_edt
5+
from scipy.interpolate import RegularGridInterpolator
6+
7+
MINIMUM_DEPTH_M=2.
8+
SHALLOW_BUFFER_M=200.
9+
10+
11+
def clearance_field(field):
12+
dx,dy=float(np.max(np.diff(field.xs))),float(np.max(np.diff(field.ys)))
13+
shallow=~np.isfinite(field.heights) | (field.heights > -MINIMUM_DEPTH_M)
14+
distances=distance_transform_edt(~shallow,sampling=(dy,dx))
15+
return RegularGridInterpolator((field.ys,field.xs),distances,
16+
bounds_error=False,fill_value=0.)
17+
18+
19+
def increasing_clearance(clearance,points):
20+
"""An outward exception can only leave the existing shallow exclusion."""
21+
previous=None
22+
for a,b in zip(points,points[1:]):
23+
count=max(2,math.ceil(math.dist(a[:2],b[:2])/10.)+1)
24+
samples=np.linspace(a[:2],b[:2],count)
25+
values=clearance(samples[:,::-1])
26+
for value in values:
27+
if previous is not None and value < previous-1.:return False
28+
previous=float(value)
29+
return True
30+
31+
32+
def exit_corridor(terrain,spec,start,target,guard=None):
33+
from posim_terrain.terrain import projection
34+
from .navigation import TerrainField,DetourPlanner
35+
from .planning import CachedTerrain
36+
from .land import LandExcludedField
37+
_,forward,inverse=projection(spec)
38+
raw=TerrainField(terrain['xs'],terrain['ys'],terrain['heights'])
39+
field=CachedTerrain(raw)
40+
if guard is not None:field=LandExcludedField(field,guard)
41+
origin=[*forward.transform(*start),0.];goal=np.array(forward.transform(*target))
42+
planner=DetourPlanner(field,origin,True,maximum_radius=2000,maximum_length=3000)
43+
planner.radius=SHALLOW_BUFFER_M;planner.clearance=MINIMUM_DEPTH_M
44+
if planner.safe(origin[:2],0.):return None
45+
# Migration cannot teleport a vessel that was already within 200 m.
46+
# Its bounded outward segment must remain hull-safe and increase separation.
47+
planner.radius=40.
48+
if not planner.safe(origin[:2],0.):raise RuntimeError('shallow_exit_hull_unsafe')
49+
clearance=clearance_field(raw);bearing=math.atan2(goal[1]-origin[1],goal[0]-origin[0])
50+
for travel in (100.,150.,200.,250.,350.,500.,750.,1000.):
51+
candidates=[]
52+
for angle in range(0,360,15):
53+
theta=bearing+math.radians(angle)
54+
end=[origin[0]+travel*math.cos(theta),origin[1]+travel*math.sin(theta),0.]
55+
planner.radius=SHALLOW_BUFFER_M+2*max(float(np.max(np.diff(raw.xs))),float(np.max(np.diff(raw.ys))))
56+
if not planner.safe(end[:2],0.):continue
57+
planner.radius=40.
58+
if not increasing_clearance(clearance,[origin,end]) or not planner.segment(origin,end):continue
59+
progress=float(np.linalg.norm(goal-origin[:2])-np.linalg.norm(goal-end[:2]))
60+
candidates.append((progress,end))
61+
if candidates:
62+
_,end=max(candidates,key=lambda value:value[0])
63+
return dict(points=[list(inverse.transform(*end[:2]))],index=0,mode='shallow_exit',
64+
shallow_water_buffer_m=SHALLOW_BUFFER_M,required_depth_m=MINIMUM_DEPTH_M,
65+
escaping_shallow_buffer=True,maximum_exit_length_m=travel,
66+
exit_start_clearance_m=float(clearance([[origin[1],origin[0]]])[0]),
67+
scope='hull-checked propulsion exit with increasing shallow-water separation')
68+
raise RuntimeError('shallow_exit_unavailable')

0 commit comments

Comments
 (0)