Skip to content

Commit 1e6821a

Browse files
committed
Advance through verified water until port arrival terrain is resident
1 parent baa9b8f commit 1e6821a

5 files changed

Lines changed: 65 additions & 7 deletions

File tree

‎README.ko.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ path = planner.route(start, goal)
8282

8383
`port_approach.corridor(tiles, spec, start, target)`는 도착 영역으로 들어가는 가능한 경로를 비교합니다. `connections.CoastConnector`는 OSM 육지 형상으로 전역 항로와 항구 사이의 연결을 단축합니다. 이 검사는 해안선 여유만 확인하며 수심은 확인하지 않습니다.
8484

85+
도착 영역이 현재 적재된 지형 밖에 있으면 검증한 지역 접근 경로를 따라 이동합니다. 실제로 500 m 진행하면 전체 도착 경로를 다시 검사합니다. 수심이 없거나 도착 영역이 막혀 있으면 차량이 대기합니다.
86+
8587
항구를 지나쳐 돌아오는 경로를 줄이기 위해 앞쪽 외해 경유점도 연결 후보로 검사합니다.
8688
필수 통과점과 보존하도록 지정한 경로 구간은 유지합니다. 탐색에 실패하면 입력 경로를
8789
유지합니다. 실제 주행에는 수치 지형 검사가 필요합니다.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ shortened route preserves its exit and coastline preference.
8383

8484
`port_approach.corridor(tiles, spec, start, target)` compares feasible entries to the arrival region. `connections.CoastConnector` shortens graph-to-port connections using OSM land polygons; it checks coastline margins, not bathymetry.
8585

86+
If the arrival area is outside resident terrain, the navigator advances along a verified regional approach. It retries the complete arrival corridor after 500 m of measured progress. Unknown depth or a blocked arrival area keeps the vessel waiting.
87+
8688
The connector considers earlier offshore waypoints to avoid overshooting a port.
8789
It retains required passage points and any preserved route prefix. A failed
8890
search retains the supplied route. Numeric terrain checks remain necessary.

‎src/posim_routing/port_approach.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
APPROACH_DISTANCE_M = 5000.
99

1010

11+
class ArrivalOutsideResidentTerrain(RuntimeError):
12+
"""The vessel must advance before its arrival area is resident."""
13+
14+
1115
@lru_cache(maxsize=8)
1216
def forward_frame(spec):
1317
from posim_terrain.terrain import projection
@@ -28,12 +32,18 @@ def bend_lookahead(native, regional, seed_spec):
2832
def shortest_arrival(planner, field, origin, candidates):
2933
"""Compare feasible region entries within one shared planning budget."""
3034
best, best_floor, best_length = None, None, math.inf
31-
for goal in sorted(candidates, key=lambda point: math.dist(origin[:2], point[:2])):
35+
floors = [(goal, field.maximum(*goal[:2], 25.)) for goal in candidates]
36+
coverage = field
37+
while hasattr(coverage, 'field'):
38+
coverage = coverage.field
39+
if all(floor is None for _, floor in floors) and all(
40+
coverage.maximum(*goal[:2], 25.) is None for goal in candidates):
41+
raise ArrivalOutsideResidentTerrain('port_arrival_region_outside_resident_terrain')
42+
for goal, floor in sorted(floors, key=lambda item: math.dist(origin[:2], item[0][:2])):
3243
if math.dist(origin[:2], goal[:2]) >= best_length-.01:
3344
break
3445
if time.monotonic() > planner.deadline:
3546
break
36-
floor = field.maximum(*goal[:2], 25.)
3747
if floor is None or floor > -3.:
3848
continue
3949
try:

‎src/posim_routing/regional.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False):
159159
guide_exhausted=(plan.get('route_key')==key and plan.get('mapped_guidance')
160160
and plan.get('points') and plan['index']>=len(plan['points']))
161161
needed=needed or bool(guide_exhausted)
162-
prefer_port=terminal and plan.get('mode')!='port_approach'
162+
approach_runway=(plan.get('mode')=='port_approach_lookahead' and point is not None
163+
and distance(state['position'],plan['probe_position'])<500.)
164+
prefer_port=terminal and plan.get('mode')!='port_approach' and not approach_runway
163165
if terminal and point is not None and not prefer_port and not (self.task and self.task.done()):return point
164166
# Prepare the next coastal lookahead while the current checked plan
165167
# still has runway. Otherwise its end briefly falls back to an offshore
@@ -234,7 +236,7 @@ async def prepare_port(self,state,target,key):
234236
# A port's narrow entrance is resolved by the same triangles already
235237
# in Gazebo. A 20 km / 257 grid can close that entrance and create a
236238
# cyclic offshore frontier. No scenery preference replaces this goal.
237-
from .port_approach import corridor
239+
from .port_approach import corridor, ArrivalOutsideResidentTerrain
238240
native=await self.rpc('/promotional/state')
239241
stream=await self.rpc('/stream/state')
240242
epoch=(native.get('origin') or {}).get('epoch',0)
@@ -247,7 +249,15 @@ async def prepare_port(self,state,target,key):
247249
from .land import guard_from_spec
248250
from posim_terrain.canal_override import in_region
249251
guard=None if in_region(current) else await asyncio.to_thread(guard_from_spec,spec,64000.)
250-
value=await asyncio.to_thread(corridor,tiles,spec,current,target,guard=guard)
252+
try:
253+
value=await asyncio.to_thread(corridor,tiles,spec,current,target,guard=guard)
254+
except ArrivalOutsideResidentTerrain:
255+
# Robot-centred streaming cannot load a distant entrance while the
256+
# ship is stationary. Advance on verified regional water, with the
257+
# native resident corridor rechecking every movement command.
258+
value=await self.prepare(state,target,key,needed=True,extent=6000)
259+
value.update(mode='port_approach_lookahead',arrival_centre=list(target))
260+
return value
251261
latest=await self.rpc('/promotional/state')
252262
if latest['session_nonce']!=native['session_nonce'] or (latest.get('origin') or {}).get('epoch',0)!=epoch:
253263
raise RuntimeError('port_frame_changed')
@@ -269,7 +279,7 @@ async def terrain_region(self, centre, extent):
269279
except Exception:break
270280
if (sources.get('baseline_status') or {}).get('status')!='unavailable':break
271281
raise RuntimeError('regional_terrain_unavailable')
272-
async def prepare(self,state,target,key,nearest=None,needed=True):
282+
async def prepare(self,state,target,key,nearest=None,needed=True,extent=None):
273283
from posim_terrain.canal_override import in_region
274284
canal=in_region(state['position'])
275285
if self.coastal and not canal and nearest is None and self.index_task is not None:
@@ -289,7 +299,7 @@ async def prepare(self,state,target,key,nearest=None,needed=True):
289299
# distant coast. The 1:10M discovery line never supplies depth.
290300
heading,_,_=GEO.inv(*pos,*nearest[1])
291301
centre=list(GEO.fwd(*pos,heading,min(6000.,nearest[0]-6000.))[:2])
292-
extent=4000 if canal else 20000
302+
extent=extent or (4000 if canal else 20000)
293303
if self.before_prepare is not None:await self.before_prepare()
294304
job=await self.terrain_region(centre,extent)
295305
grid=await self.rpc('/terrain/jobs/'+job['id']+'/files/terrain.json',timeout=20)

‎tests/test_port_residency.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Arrival coverage may need another checked streaming step."""
2+
import math
3+
import unittest
4+
from types import SimpleNamespace
5+
from unittest.mock import AsyncMock,patch
6+
from posim_routing.port_approach import shortest_arrival,ArrivalOutsideResidentTerrain
7+
from posim_routing.regional import RegionalNavigator,REGIONAL_POLICY
8+
9+
class ArrivalCoverage(unittest.TestCase):
10+
def test_missing_arrival_coverage_is_distinct_from_shallow_water(self):
11+
planner=SimpleNamespace(deadline=math.inf,route=lambda *_: self.fail('unknown goal routed'))
12+
with self.assertRaises(ArrivalOutsideResidentTerrain):
13+
shortest_arrival(planner,SimpleNamespace(maximum=lambda *_:None),[0,0,0],[[2000,0,0]])
14+
with self.assertRaisesRegex(RuntimeError,'no_confirmed_corridor'):
15+
shortest_arrival(planner,SimpleNamespace(maximum=lambda *_:-2.),[0,0,0],[[2000,0,0]])
16+
blocked=SimpleNamespace(maximum=lambda *_:None,field=SimpleNamespace(maximum=lambda *_:-10.))
17+
with self.assertRaisesRegex(RuntimeError,'no_confirmed_corridor'):
18+
shortest_arrival(planner,blocked,[0,0,0],[[2000,0,0]])
19+
20+
class PortRenewal(unittest.IsolatedAsyncioTestCase):
21+
async def test_verified_runway_is_used_before_resident_arrival_retry(self):
22+
nav=RegionalNavigator(AsyncMock());key=[0,1,11]
23+
state=dict(lap=0,leg=1,waypoint=11,position=[118.10,24.425],native={},
24+
regional_navigation=dict(route_key=key,points=[[118.093,24.428]],index=0,
25+
mode='port_approach_lookahead',probe_position=[118.10,24.425],policy=REGIONAL_POLICY))
26+
nav.prepare_port=AsyncMock()
27+
self.assertEqual(nav.update(state,[118.077,24.436],terminal=True),[118.093,24.428])
28+
self.assertIsNone(nav.task)
29+
state['position']=[118.094,24.427]
30+
nav.update(state,[118.077,24.436],terminal=True)
31+
self.assertIsNotNone(nav.task)
32+
await nav.close()
33+
34+
if __name__=='__main__':unittest.main()

0 commit comments

Comments
 (0)