Skip to content

Commit f362c2b

Browse files
authored
Fix dist_approx with "geosphere" method and close points (#792)
* u_coord.dist_approx: fix geosphere with log * Update CHANGELOG
1 parent 4be3ab5 commit f362c2b

File tree

3 files changed

+32
-14
lines changed

3 files changed

+32
-14
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Code freeze date: YYYY-MM-DD
1616

1717
### Fixed
1818

19+
- Fix the dist_approx util function when used with method="geosphere" and log=True and points that are very close. [#792](https://github.com/CLIMADA-project/climada_python/pull/792)
20+
1921
### Deprecated
2022

2123
### Removed

climada/util/coordinates.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,13 @@ def dist_approx(lat1, lon1, lat2, lon2, log=False, normalize=True,
357357
if log:
358358
vec1, vbasis = latlon_to_geosph_vector(lat1, lon1, rad=True, basis=True)
359359
vec2 = latlon_to_geosph_vector(lat2, lon2, rad=True)
360-
scal = 1 - 2 * hav
361-
fact = dist / np.fmax(np.spacing(1), np.sqrt(1 - scal**2))
362-
vtan = fact[..., None] * (vec2[:, None, :] - scal[..., None] * vec1[:, :, None])
360+
vtan = vec2[:, None, :] - (1 - 2 * hav[..., None]) * vec1[:, :, None]
363361
vtan = np.einsum('nkli,nkji->nklj', vtan, vbasis)
362+
# faster version of `vtan_norm = np.linalg.norm(vtan, axis=-1)`
363+
vtan_norm = np.sqrt(np.einsum("...l,...l->...", vtan, vtan))
364+
# for consistency, set dist to 0 if vtan is 0
365+
dist[vtan_norm < np.spacing(1)] = 0
366+
vtan *= dist[..., None] / np.fmax(np.spacing(1), vtan_norm[..., None])
364367
else:
365368
raise KeyError("Unknown distance approximation method: %s" % method)
366369
return (dist, vtan) if log else dist

climada/util/test/test_coordinates.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,20 @@ def test_geosph_vector(self):
277277
def test_dist_approx_pass(self):
278278
"""Test approximate distance functions"""
279279
data = np.array([
280-
# lat1, lon1, lat2, lon2, dist, dist_sph
280+
# lat1, lon1, lat2, lon2, dist_equirect, dist_geosphere
281281
[45.5, -32.1, 14, 56, 7702.88906574, 8750.64119051],
282282
[45.5, 147.8, 14, -124, 7709.82781473, 8758.34146833],
283283
[45.5, 507.9, 14, -124, 7702.88906574, 8750.64119051],
284284
[45.5, -212.2, 14, -124, 7709.82781473, 8758.34146833],
285285
[-3, -130.1, 4, -30.5, 11079.7217421, 11087.0352544],
286286
])
287+
# conversion factors from reference data (in km, see above) to other units
288+
factors_km_to_x = {
289+
"m": 1e3,
290+
"radian": np.radians(1.0) / u_coord.ONE_LAT_KM,
291+
"degree": 1.0 / u_coord.ONE_LAT_KM,
292+
"km": 1.0,
293+
}
287294
compute_dist = np.stack([
288295
u_coord.dist_approx(data[:, None, 0], data[:, None, 1],
289296
data[:, None, 2], data[:, None, 3],
@@ -297,9 +304,7 @@ def test_dist_approx_pass(self):
297304
self.assertAlmostEqual(d[0], cd[0])
298305
self.assertAlmostEqual(d[1], cd[1])
299306

300-
for units, factor in zip(["radian", "degree", "km"],
301-
[np.radians(1.0), 1.0, u_coord.ONE_LAT_KM]):
302-
factor /= u_coord.ONE_LAT_KM
307+
for units, factor in factors_km_to_x.items():
303308
compute_dist = np.stack([
304309
u_coord.dist_approx(data[:, None, 0], data[:, None, 1],
305310
data[:, None, 2], data[:, None, 3],
@@ -309,21 +314,29 @@ def test_dist_approx_pass(self):
309314
method="geosphere", units=units)[:, 0, 0],
310315
], axis=-1)
311316
self.assertEqual(compute_dist.shape[0], data.shape[0])
317+
places = 4 if units == "m" else 7
312318
for d, cd in zip(data[:, 4:], compute_dist):
313-
self.assertAlmostEqual(d[0] * factor, cd[0])
314-
self.assertAlmostEqual(d[1] * factor, cd[1])
319+
self.assertAlmostEqual(d[0] * factor, cd[0], places=places)
320+
self.assertAlmostEqual(d[1] * factor, cd[1], places=places)
315321

316322
def test_dist_approx_log_pass(self):
317323
"""Test log-functionality of approximate distance functions"""
318324
data = np.array([
319-
# lat1, lon1, lat2, lon2, dist, dist_sph
325+
# lat1, lon1, lat2, lon2, dist_equirect, dist_geosphere
320326
[0, 0, 0, 1, 111.12, 111.12],
321327
[-13, 179, 5, -179, 2011.84774049, 2012.30698122],
328+
[24., 85., 23.99999967, 85., 3.666960e-5, 3.666960e-5],
329+
[24., 85., 24., 85., 0, 0],
322330
])
331+
# conversion factors from reference data (in km, see above) to other units
332+
factors_km_to_x = {
333+
"m": 1e3,
334+
"radian": np.radians(1.0) / u_coord.ONE_LAT_KM,
335+
"degree": 1.0 / u_coord.ONE_LAT_KM,
336+
"km": 1.0,
337+
}
323338
for i, method in enumerate(["equirect", "geosphere"]):
324-
for units, factor in zip(["radian", "degree", "km"],
325-
[np.radians(1.0), 1.0, u_coord.ONE_LAT_KM]):
326-
factor /= u_coord.ONE_LAT_KM
339+
for units, factor in factors_km_to_x.items():
327340
dist, vec = u_coord.dist_approx(data[:, None, 0], data[:, None, 1],
328341
data[:, None, 2], data[:, None, 3],
329342
log=True, method=method, units=units)
@@ -613,7 +626,7 @@ def test_match_centroids(self):
613626
u_coord.match_centroids(gdf, centroids)
614627
self.assertIn('Set hazard and GeoDataFrame to same CRS first!',
615628
str(cm.exception))
616-
629+
617630
def test_dist_sqr_approx_pass(self):
618631
"""Test approximate distance helper function."""
619632
lats1 = 45.5

0 commit comments

Comments
 (0)