Skip to content

Commit a4b3759

Browse files
committed
Avoid false shallow-water barriers from depth roundoff
1 parent 4af19c2 commit a4b3759

4 files changed

Lines changed: 27 additions & 5 deletions

File tree

‎src/posim_routing/coastal.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pyproj import Geod
1818
from scipy.ndimage import distance_transform_edt, maximum_filter, label
1919
from scipy.spatial import cKDTree
20+
from .navigation import DEPTH_EPSILON_M
2021

2122
GEO = Geod(ellps="WGS84")
2223
COAST_DISCOVERY_M = 80000.
@@ -139,7 +140,7 @@ def coastal_corridor(terrain, spec, start, target, timeout=25., guard=None, prog
139140
field = TerrainField(terrain["xs"], terrain["ys"], terrain["heights"])
140141
xs, ys, heights = field.xs, field.ys, field.heights
141142
finite = np.isfinite(heights)
142-
land = finite & (heights > -MINIMUM_DEPTH_M)
143+
land = finite & (heights > -MINIMUM_DEPTH_M+DEPTH_EPSILON_M)
143144
if guard is not None:
144145
land |= ~guard.grid(xs, ys)
145146
if not land.any():
@@ -159,7 +160,7 @@ def coastal_corridor(terrain, spec, start, target, timeout=25., guard=None, prog
159160
size = (2*math.ceil(margin/dy)+1, 2*math.ceil(margin/dx)+1)
160161
bounds = maximum_filter(np.where(finite, heights, math.inf), size=size,
161162
mode="constant", cval=math.inf)
162-
safe = bounds <= -MINIMUM_DEPTH_M
163+
safe = bounds <= -MINIMUM_DEPTH_M+DEPTH_EPSILON_M
163164
if guard is not None:
164165
safe &= guard.grid(xs, ys, margin)
165166
coast_distance = distance_transform_edt(~land, sampling=(dy, dx))

‎src/posim_routing/navigation.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import time
1010
import numpy as np
1111

12+
# Interpolating a flat two-metre floor can produce -1.9999999999999998.
13+
# Ignore arithmetic roundoff without changing the represented depth policy.
14+
DEPTH_EPSILON_M = 1e-9
15+
1216

1317
class TerrainField:
1418
def __init__(self, xs, ys, heights):
@@ -85,7 +89,7 @@ def __init__(self, field, start, boat, step=4., timeout=25., maximum_radius=600.
8589
def safe(self, xy, z, radius=None):
8690
if math.dist(xy,self.origin)>self.maximum_radius:return False
8791
floor=self.field.maximum(*xy,self.radius if radius is None else radius)
88-
return floor is not None and floor<0 and z-floor>=self.clearance
92+
return floor is not None and floor<0 and z-floor>=self.clearance-DEPTH_EPSILON_M
8993

9094
def segment(self, a, b, record=False):
9195
distance=math.dist(a[:2],b[:2]);count=max(1,math.ceil(distance/1.))
@@ -97,7 +101,7 @@ def segment(self, a, b, record=False):
97101
f=k/count;xy=[a[i]+(b[i]-a[i])*f for i in (0,1)]
98102
if math.dist(xy,self.origin)>self.maximum_radius:return False
99103
floor=self.field.maximum(*xy,radius)
100-
if floor is None or floor>=0 or z-floor<self.clearance:return False
104+
if floor is None or floor>=0 or z-floor<self.clearance-DEPTH_EPSILON_M:return False
101105
minimum=min(minimum,z-floor)
102106
if record:self.minimum=min(self.minimum,minimum)
103107
return True

‎src/posim_routing/shallow.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import numpy as np
44
from scipy.ndimage import distance_transform_edt
55
from scipy.interpolate import RegularGridInterpolator
6+
from .navigation import DEPTH_EPSILON_M
67

78
MINIMUM_DEPTH_M=2.
89
SHALLOW_BUFFER_M=200.
910

1011

1112
def clearance_field(field):
1213
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+
shallow=~np.isfinite(field.heights) | (field.heights > -MINIMUM_DEPTH_M+DEPTH_EPSILON_M)
1415
distances=distance_transform_edt(~shallow,sampling=(dy,dx))
1516
return RegularGridInterpolator((field.ys,field.xs),distances,
1617
bounds_error=False,fill_value=0.)

‎tests/test_shallow.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ def terrain():
1414
return dict(xs=axis.tolist(),ys=axis.tolist(),heights=np.where(x<0,-1.,-15.).tolist())
1515

1616
class ShallowExit(unittest.TestCase):
17+
def test_roundoff_on_exact_depth_plateau_does_not_create_a_shoal(self):
18+
axis=np.linspace(-1000,1000,41)
19+
for floor,expected in ((np.nextafter(-2.,0.),True),(-2.+1e-6,False)):
20+
raw=TerrainField(axis,axis,np.full((41,41),floor))
21+
planner=DetourPlanner(CachedTerrain(raw),[0,0,0],True)
22+
planner.radius=200.;planner.clearance=2.
23+
self.assertEqual(planner.safe([0,0],0.),expected)
24+
self.assertEqual(planner.segment([0,0,0],[100,100,0]),expected)
25+
clearance=clearance_field(raw)
26+
self.assertEqual(float(clearance([[0,0]])[0])>0,expected)
27+
def test_roundoff_tolerance_does_not_accept_unknown_depth(self):
28+
axis=np.linspace(-1000,1000,41);heights=np.full((41,41),-2.)
29+
heights[20,20]=np.nan
30+
planner=DetourPlanner(CachedTerrain(TerrainField(axis,axis,heights)),[0,0,0],True)
31+
planner.radius=200.;planner.clearance=2.
32+
self.assertFalse(planner.safe([0,0],0.))
1733
def test_already_clear_position_needs_no_departure(self):
1834
self.assertIsNone(exit_corridor(terrain(),SPEC,geo(400,0),geo(1000,2000)))
1935
def test_inside_buffer_exits_under_propulsion_with_increasing_separation(self):

0 commit comments

Comments
 (0)