diff --git a/src/posim_routing/coastal.py b/src/posim_routing/coastal.py index a60b98c..f80a4df 100644 --- a/src/posim_routing/coastal.py +++ b/src/posim_routing/coastal.py @@ -25,7 +25,7 @@ MAXIMUM_STANDOFF_M = 600. MINIMUM_DEPTH_M = 2. REGIONAL_BUFFER_M = 200. -WAYPOINT_FORMAT = "checked-segment-ends-v5-coastal-transfer" +WAYPOINT_FORMAT = "checked-segment-ends-v7-real-shore" MAXIMUM_COASTAL_DETOUR_RATIO = 2.0 MAXIMUM_COASTAL_DISTANCE_M = 1500. @@ -140,7 +140,8 @@ def coastal_corridor(terrain, spec, start, target, timeout=25., guard=None, prog field = TerrainField(terrain["xs"], terrain["ys"], terrain["heights"]) xs, ys, heights = field.xs, field.ys, field.heights finite = np.isfinite(heights) - land = finite & (heights > -MINIMUM_DEPTH_M+DEPTH_EPSILON_M) + # Follow actual land; submerged banks constrain safety without attracting coastal waypoints. + land = finite & (heights >= 0) if guard is not None: land |= ~guard.grid(xs, ys) if not land.any(): @@ -295,7 +296,8 @@ def shorten(route, costs, weights, lookahead=None, shore_bound=False): old = costs[route[candidate]]-costs[route[index]] if (safe[iy, ix].all() and (not shore_bound or (coast_distance[iy, ix] <= shore_limit).all()) - and weighted <= old*1.08 + and (not shore_bound or length <= 7500.) + and (shore_bound or weighted <= old*1.08) and validator.segment(a, b)): chosen = candidate break @@ -386,7 +388,7 @@ def route_length(points): endpoint = nodes[end] try: approach = shorten(nodes[:entry+1], costs, penalty, lookahead=10) - following = shorten(nodes[entry:end+1], costs, penalty, lookahead=10, shore_bound=True) + following = shorten(nodes[entry:end+1], costs, penalty, shore_bound=True) points = approach[:-1] + following onward = shorten(nodes[end:], costs, penalty) coastal_onward = shorten(nodes[end:follow_end+1], costs, penalty, shore_bound=True) @@ -424,6 +426,7 @@ def route_length(points): index=0, mode="coastal_transfer" if transfer_required else "coastal", planning_seconds=time.monotonic()-started, search_nodes=expanded, minimum_regional_clearance_m=validator.minimum, preferred_standoff_m=STANDOFF_M, + coastal_reference_basis="land_shoreline", shallow_water_buffer_m=REGIONAL_BUFFER_M, shallow_boundary_depth_m=MINIMUM_DEPTH_M, endpoint_standoff_m=float(coast_distance[endpoint]), diff --git a/src/posim_routing/regional.py b/src/posim_routing/regional.py index f9fb5e3..e9d8e80 100644 --- a/src/posim_routing/regional.py +++ b/src/posim_routing/regional.py @@ -6,7 +6,7 @@ import asyncio,math,time from pyproj import Geod GEO=Geod(ellps='WGS84') -REGIONAL_POLICY='buffered-water-v21-shallow-200m-coastal-320m' +REGIONAL_POLICY='buffered-water-v22-real-shore-shallow-200m-coastal-320m' COMPATIBLE_POLICIES={REGIONAL_POLICY} PLANNING_BUDGET_SECONDS=660. REGIONAL_BUFFER_M=200. diff --git a/tests/test_coastal_navigation.py b/tests/test_coastal_navigation.py index 12d98ce..a0dc27b 100644 --- a/tests/test_coastal_navigation.py +++ b/tests/test_coastal_navigation.py @@ -85,6 +85,26 @@ def test_doubled_detour_budget_can_join_previously_rejected_coast(self): self.assertLessEqual(plan['coastal_through_length_m']/plan['shortest_checked_length_m'], 2.) self.assert_coastal_band(grid(), start, plan) + def test_clear_coastal_run_does_not_keep_grid_staircases(self): + for amplitude, period in ((0., 1000.), (500., 2000.)): + with self.subTest(shore_amplitude=amplitude): + terrain = grid() + x, y = np.meshgrid(terrain['xs'], terrain['ys']) + terrain['heights'] = np.where( + x <= amplitude*np.sin(y/period*2*np.pi), 20., -15.).tolist() + start = geo(1200, -4000) + plan = coastal_corridor(terrain, SPEC, start, geo(1200, 9000)) + self.assertEqual(plan['mode'], 'coastal') + through = plan['points'] + plan['onward_points'] + # A clear run needs bounded segment ends, not one turn per + # ten grid cells or every small shoreline indentation. + self.assertLessEqual(len(through), 4) + self.assertLess(plan['coastal_through_length_m'], 14500.) + self.assertTrue(all(distance(a, b) <= 7501. + for a, b in zip([start]+through[:-1], through))) + self.assert_coastal_band(terrain, start, plan) + self.assert_swept_safe(terrain, start, dict(points=through)) + def test_safe_backward_frontier_is_rejected(self): axis=np.linspace(-10000,10000,257) x,y=np.meshgrid(axis,axis) @@ -195,14 +215,33 @@ def test_shallow_and_unknown_coast_is_not_a_destination(self): heights[:, (x > 0) & (x < 1700)] = -1. terrain['heights'] = heights.tolist() plan=coastal_corridor(terrain, SPEC, geo(3000,-4000),geo(6500,9000)) - self.assertIsNotNone(plan) - self.assertEqual(plan['shallow_water_buffer_m'],200.) - self.assert_swept_safe(terrain,geo(3000,-4000),plan) - self.assertGreater(FORWARD.transform(*plan['points'][-1])[0],1900.) + # The bank blocks the coastal band around real land; use checked open water. + self.assertIsNone(plan) + water = corridor(terrain, SPEC, geo(3000,-4000),geo(6500,9000)) + self.assert_swept_safe(terrain,geo(3000,-4000),water) + self.assertGreater(FORWARD.transform(*water['points'][-1])[0],1900.) heights[:, (x > 0) & (x < 1700)] = np.nan terrain['heights'] = heights.tolist() self.assertIsNone(coastal_corridor(terrain, SPEC, geo(3000, -4000), geo(6500, 9000))) + def test_submerged_banks_do_not_attract_coastal_waypoints(self): + for actual_land in (False, True): + with self.subTest(actual_land=actual_land): + terrain = grid() + x, y = np.meshgrid(terrain['xs'], terrain['ys']) + heights = np.where(x <= 0, -1., -15.) + if actual_land: + heights[x <= -6000] = 20. + terrain['heights'] = heights.tolist() + # The safe side of this bank is open water, kilometres from + # real land. It must not become a scenic coastline to follow. + self.assertIsNone(coastal_corridor( + terrain, SPEC, geo(800, -4000), geo(800, 9000))) + water = corridor(terrain, SPEC, geo(800, -4000), geo(800, 9000)) + self.assertEqual(len(water['points']), 1) + end_x, _ = FORWARD.transform(*water['points'][0]) + self.assertLess(abs(end_x-800.), 1.) + def test_two_point_five_metre_coast_is_available_to_wamv(self): terrain=grid();heights=np.array(terrain['heights']) heights[heights<0]=-2.5;terrain['heights']=heights.tolist()