From dc9d455db33edfba6cb7a5ba956c6b7d2ebaacda Mon Sep 17 00:00:00 2001 From: ioes Date: Tue, 13 Oct 2026 19:37:42 +0900 Subject: [PATCH] Scope shallow-exit speed phase to its appended route tail --- pyproject.toml | 2 +- src/posim_routing/regional.py | 18 +++++++++++++-- tests/test_coastal_navigation.py | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1ad7cf5..c331cbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "posim-routing" -version = "0.1.6" +version = "0.1.7" description = "Global maritime and terrain checked coastal route planning" requires-python = ">=3.10" license = {text = "Apache-2.0"} diff --git a/src/posim_routing/regional.py b/src/posim_routing/regional.py index e4b7848..f9fb5e3 100644 --- a/src/posim_routing/regional.py +++ b/src/posim_routing/regional.py @@ -109,11 +109,19 @@ def active_point(self,state,key): self.discard(state,'regional_clearance_policy_changed') return None from .course import passed_vertex + # Persisted renewals from the previous coordinator may label their + # entire retained runway as an exit. Only the appended tail is an exit. + if plan.get('mode')=='shallow_exit' and plan.get('retained_checked_points',0): + plan.setdefault('shallow_exit_start_index',plan['retained_checked_points']) + exit_index=plan.get('shallow_exit_start_index') while plan['index']=exit_index else plan.get('retained_mode','coastal') + exit_handoff=exit_index is not None and index==exit_index-1 + reached=distance(state['position'],point)<(5 if exit_handoff or plan.get('mode') in ('port_departure','shallow_exit') else 12 if plan.get('mode')=='port_approach' else 80) previous=plan['points'][index-1] if index else plan.get('probe_position') - passed=(plan.get('mode') in ('coastal','coastal_transfer','coastal_approach') and previous is not None + passed=(not exit_handoff and plan.get('mode') in ('coastal','coastal_transfer','coastal_approach') and previous is not None and index=3000) prefer_coast=(point is not None and coast_needed and not needed and (state.get('regional_navigation') or {}).get('mode') not in ('coastal','coastal_transfer','coastal_approach')) @@ -263,6 +273,10 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog retained=plan['points'][plan['index']:end+1] value['points']=retained+value['points'];value['index']=0 value['retained_checked_points']=len(retained) + if value.get('mode')=='shallow_exit': + value['shallow_exit_start_index']=len(retained) + value['retained_mode']=plan.get('mode','coastal') + value['mode']=value['retained_mode'] value['renewed_from']=connection value['probe_position']=list(state['position']) for metric in ('minimum_regional_clearance_m',): diff --git a/tests/test_coastal_navigation.py b/tests/test_coastal_navigation.py index b787392..12d98ce 100644 --- a/tests/test_coastal_navigation.py +++ b/tests/test_coastal_navigation.py @@ -399,6 +399,44 @@ async def result():return dict(points=[new],index=0,mode='coastal',route_key=key self.assertNotIn('regional_waiting',sample) await navigator.close() + async def test_outward_tail_does_not_relabel_retained_forward_runway(self): + key=[0,0,1];start,first,end,exit_point=geo(800,1000),geo(800,2000),geo(800,3000),geo(1000,3000) + navigator=RegionalNavigator(None);navigator.key=key + sample=dict(lap=0,leg=0,waypoint=1,position=start,native={}, + regional_navigation=dict(points=[first,end],index=0,mode='coastal',route_key=key, + policy=REGIONAL_POLICY,job='runway',probe_position=geo(800,0))) + async def result():return dict(points=[exit_point],index=0,mode='shallow_exit',route_key=key, + policy=REGIONAL_POLICY,job='exit',probe_position=end, + continuation_of=dict(job='runway',endpoint=end)) + navigator.task=asyncio.create_task(result());await navigator.task + self.assertEqual(navigator.update(sample,geo(800,90000)),first) + plan=sample['regional_navigation'] + self.assertEqual(plan['mode'],'coastal');self.assertEqual(plan['shallow_exit_start_index'],2) + sample['position']=first + self.assertEqual(navigator.active_point(sample,key),end) + sample['position']=geo(800,2950) + self.assertEqual(navigator.active_point(sample,key),end) + self.assertEqual(plan['mode'],'coastal') + sample['position']=end + self.assertEqual(navigator.active_point(sample,key),exit_point) + self.assertEqual(plan['mode'],'shallow_exit');self.assertEqual(plan['index'],2) + await navigator.close() + + async def test_saved_exit_with_retained_points_migrates_only_its_phase(self): + key=[0,0,1];first,end,exit_point=geo(800,2000),geo(800,3000),geo(1000,3000) + plan=dict(points=[first,end,exit_point],index=0,mode='shallow_exit',route_key=key, + policy=REGIONAL_POLICY,job='saved',probe_position=geo(800,0),retained_checked_points=2) + sample=dict(position=geo(800,1000),native={},regional_navigation=plan) + navigator=RegionalNavigator(None) + self.assertEqual(navigator.active_point(sample,key),first) + self.assertEqual(plan['mode'],'coastal');self.assertEqual(plan['points'],[first,end,exit_point]) + sample['position']=first + self.assertEqual(navigator.active_point(sample,key),end) + sample['position']=end + self.assertEqual(navigator.active_point(sample,key),exit_point) + self.assertEqual(plan['mode'],'shallow_exit') + await navigator.close() + async def test_exhausted_coast_accepts_checked_replacement_water_route(self): async def rpc(*args,**kwargs):await asyncio.Event().wait() class Near: