Skip to content

Commit f6af0dd

Browse files
committed
Follow real shorelines without retaining coastal grid bends
1 parent b552cb7 commit f6af0dd

3 files changed

Lines changed: 51 additions & 9 deletions

File tree

‎src/posim_routing/coastal.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
MAXIMUM_STANDOFF_M = 600.
2626
MINIMUM_DEPTH_M = 2.
2727
REGIONAL_BUFFER_M = 200.
28-
WAYPOINT_FORMAT = "checked-segment-ends-v5-coastal-transfer"
28+
WAYPOINT_FORMAT = "checked-segment-ends-v7-real-shore"
2929
MAXIMUM_COASTAL_DETOUR_RATIO = 2.0
3030
MAXIMUM_COASTAL_DISTANCE_M = 1500.
3131

@@ -140,7 +140,8 @@ def coastal_corridor(terrain, spec, start, target, timeout=25., guard=None, prog
140140
field = TerrainField(terrain["xs"], terrain["ys"], terrain["heights"])
141141
xs, ys, heights = field.xs, field.ys, field.heights
142142
finite = np.isfinite(heights)
143-
land = finite & (heights > -MINIMUM_DEPTH_M+DEPTH_EPSILON_M)
143+
# Follow actual land; submerged banks constrain safety without attracting coastal waypoints.
144+
land = finite & (heights >= 0)
144145
if guard is not None:
145146
land |= ~guard.grid(xs, ys)
146147
if not land.any():
@@ -295,7 +296,8 @@ def shorten(route, costs, weights, lookahead=None, shore_bound=False):
295296
old = costs[route[candidate]]-costs[route[index]]
296297
if (safe[iy, ix].all()
297298
and (not shore_bound or (coast_distance[iy, ix] <= shore_limit).all())
298-
and weighted <= old*1.08
299+
and (not shore_bound or length <= 7500.)
300+
and (shore_bound or weighted <= old*1.08)
299301
and validator.segment(a, b)):
300302
chosen = candidate
301303
break
@@ -386,7 +388,7 @@ def route_length(points):
386388
endpoint = nodes[end]
387389
try:
388390
approach = shorten(nodes[:entry+1], costs, penalty, lookahead=10)
389-
following = shorten(nodes[entry:end+1], costs, penalty, lookahead=10, shore_bound=True)
391+
following = shorten(nodes[entry:end+1], costs, penalty, shore_bound=True)
390392
points = approach[:-1] + following
391393
onward = shorten(nodes[end:], costs, penalty)
392394
coastal_onward = shorten(nodes[end:follow_end+1], costs, penalty, shore_bound=True)
@@ -424,6 +426,7 @@ def route_length(points):
424426
index=0, mode="coastal_transfer" if transfer_required else "coastal", planning_seconds=time.monotonic()-started,
425427
search_nodes=expanded, minimum_regional_clearance_m=validator.minimum,
426428
preferred_standoff_m=STANDOFF_M,
429+
coastal_reference_basis="land_shoreline",
427430
shallow_water_buffer_m=REGIONAL_BUFFER_M,
428431
shallow_boundary_depth_m=MINIMUM_DEPTH_M,
429432
endpoint_standoff_m=float(coast_distance[endpoint]),

‎src/posim_routing/regional.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import asyncio,math,time
77
from pyproj import Geod
88
GEO=Geod(ellps='WGS84')
9-
REGIONAL_POLICY='buffered-water-v21-shallow-200m-coastal-320m'
9+
REGIONAL_POLICY='buffered-water-v22-real-shore-shallow-200m-coastal-320m'
1010
COMPATIBLE_POLICIES={REGIONAL_POLICY}
1111
PLANNING_BUDGET_SECONDS=660.
1212
REGIONAL_BUFFER_M=200.

‎tests/test_coastal_navigation.py‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,26 @@ def test_doubled_detour_budget_can_join_previously_rejected_coast(self):
8585
self.assertLessEqual(plan['coastal_through_length_m']/plan['shortest_checked_length_m'], 2.)
8686
self.assert_coastal_band(grid(), start, plan)
8787

88+
def test_clear_coastal_run_does_not_keep_grid_staircases(self):
89+
for amplitude, period in ((0., 1000.), (500., 2000.)):
90+
with self.subTest(shore_amplitude=amplitude):
91+
terrain = grid()
92+
x, y = np.meshgrid(terrain['xs'], terrain['ys'])
93+
terrain['heights'] = np.where(
94+
x <= amplitude*np.sin(y/period*2*np.pi), 20., -15.).tolist()
95+
start = geo(1200, -4000)
96+
plan = coastal_corridor(terrain, SPEC, start, geo(1200, 9000))
97+
self.assertEqual(plan['mode'], 'coastal')
98+
through = plan['points'] + plan['onward_points']
99+
# A clear run needs bounded segment ends, not one turn per
100+
# ten grid cells or every small shoreline indentation.
101+
self.assertLessEqual(len(through), 4)
102+
self.assertLess(plan['coastal_through_length_m'], 14500.)
103+
self.assertTrue(all(distance(a, b) <= 7501.
104+
for a, b in zip([start]+through[:-1], through)))
105+
self.assert_coastal_band(terrain, start, plan)
106+
self.assert_swept_safe(terrain, start, dict(points=through))
107+
88108
def test_safe_backward_frontier_is_rejected(self):
89109
axis=np.linspace(-10000,10000,257)
90110
x,y=np.meshgrid(axis,axis)
@@ -195,14 +215,33 @@ def test_shallow_and_unknown_coast_is_not_a_destination(self):
195215
heights[:, (x > 0) & (x < 1700)] = -1.
196216
terrain['heights'] = heights.tolist()
197217
plan=coastal_corridor(terrain, SPEC, geo(3000,-4000),geo(6500,9000))
198-
self.assertIsNotNone(plan)
199-
self.assertEqual(plan['shallow_water_buffer_m'],200.)
200-
self.assert_swept_safe(terrain,geo(3000,-4000),plan)
201-
self.assertGreater(FORWARD.transform(*plan['points'][-1])[0],1900.)
218+
# The bank blocks the coastal band around real land; use checked open water.
219+
self.assertIsNone(plan)
220+
water = corridor(terrain, SPEC, geo(3000,-4000),geo(6500,9000))
221+
self.assert_swept_safe(terrain,geo(3000,-4000),water)
222+
self.assertGreater(FORWARD.transform(*water['points'][-1])[0],1900.)
202223
heights[:, (x > 0) & (x < 1700)] = np.nan
203224
terrain['heights'] = heights.tolist()
204225
self.assertIsNone(coastal_corridor(terrain, SPEC, geo(3000, -4000), geo(6500, 9000)))
205226

227+
def test_submerged_banks_do_not_attract_coastal_waypoints(self):
228+
for actual_land in (False, True):
229+
with self.subTest(actual_land=actual_land):
230+
terrain = grid()
231+
x, y = np.meshgrid(terrain['xs'], terrain['ys'])
232+
heights = np.where(x <= 0, -1., -15.)
233+
if actual_land:
234+
heights[x <= -6000] = 20.
235+
terrain['heights'] = heights.tolist()
236+
# The safe side of this bank is open water, kilometres from
237+
# real land. It must not become a scenic coastline to follow.
238+
self.assertIsNone(coastal_corridor(
239+
terrain, SPEC, geo(800, -4000), geo(800, 9000)))
240+
water = corridor(terrain, SPEC, geo(800, -4000), geo(800, 9000))
241+
self.assertEqual(len(water['points']), 1)
242+
end_x, _ = FORWARD.transform(*water['points'][0])
243+
self.assertLess(abs(end_x-800.), 1.)
244+
206245
def test_two_point_five_metre_coast_is_available_to_wamv(self):
207246
terrain=grid();heights=np.array(terrain['heights'])
208247
heights[heights<0]=-2.5;terrain['heights']=heights.tolist()

0 commit comments

Comments
 (0)