Skip to content

Commit daa09ff

Browse files
author
Chahan Kropf
committed
Simplify non degree crs handling
1 parent 3147715 commit daa09ff

File tree

1 file changed

+52
-43
lines changed

1 file changed

+52
-43
lines changed

climada/util/coordinates.py

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
are not considered."""
8686

8787

88-
def check_if_geo_coords(lat, lon):
88+
def check_if_geo_coords(lat, lon, raise_error=True):
8989
"""Check if latitude and longitude arrays are likely in geographic coordinates,
9090
testing if min/max values are within -90 to 90 for latitude and -540 to 540
9191
for longitude. Lon coordinates of <-360 or >360 are allowed to cover cases
@@ -115,7 +115,7 @@ def check_if_geo_coords(lat, lon):
115115
lat.min() >= -91 and lat.max() <= 91 and lon.min() >= -541 and lon.max() <= 541
116116
) and ((lat.max() - lat.min()) <= 181 and (lon.max() - lon.min()) <= 361)
117117

118-
if not like_geo:
118+
if not like_geo and raise_error:
119119
raise ValueError(
120120
"Input lat and lon coordinates do not seem to correspond"
121121
" to geographic coordinates in degrees. This can be because"
@@ -124,7 +124,7 @@ def check_if_geo_coords(lat, lon):
124124
" If you use degree values outside of these ranges,"
125125
" please shift the coordinates to the valid ranges."
126126
)
127-
return True
127+
return like_geo
128128

129129

130130
def get_crs_unit(coords):
@@ -142,19 +142,11 @@ def get_crs_unit(coords):
142142
The unit of measurement for the coordinate system, as specified in the
143143
CRS axis information.
144144
"""
145-
146-
unit = coords.crs.axis_info[0].unit_name # assume both axes have the same unit
147-
if unit == "degree":
148-
pass
149-
elif unit == "metre":
150-
unit = "m"
151-
elif unit == "kilometre":
152-
unit = "km"
153-
else:
154-
raise ValueError(
155-
f"Unknown unit: {unit}. Please provide a crs that has a unit "
156-
"of 'degree', 'metre' or 'kilometre'."
157-
)
145+
try:
146+
unit = coords.crs.axis_info[0].unit_name # assume both axes have the same unit
147+
except:
148+
LOGGER.warning("The units of the input crs are undefined.")
149+
unit = "undefined"
158150
return unit
159151

160152

@@ -1061,6 +1053,10 @@ def assign_coordinates(*args, **kwargs):
10611053
return match_coordinates(*args, **kwargs)
10621054

10631055

1056+
def estimate_matching_threshold(coords_to_assign):
1057+
return 2 * max(abs(r) for r in get_resolution(coords_to_assign.T))
1058+
1059+
10641060
def match_coordinates(
10651061
coords,
10661062
coords_to_assign,
@@ -1132,36 +1128,31 @@ def match_coordinates(
11321128
if coords_to_assign.shape[0] == 0:
11331129
return -np.ones(coords.shape[0]).astype(int)
11341130

1131+
if distance not in nearest_neighbor_funcs:
1132+
raise ValueError(
1133+
f'Coordinate assignment with "{distance}" distance is not supported.'
1134+
)
11351135
nearest_neighbor_funcs = {
11361136
"euclidean": _nearest_neighbor_euclidean,
11371137
"haversine": _nearest_neighbor_haversine,
11381138
"approx": _nearest_neighbor_approx,
11391139
}
1140-
if distance not in nearest_neighbor_funcs:
1141-
raise ValueError(
1142-
f'Coordinate assignment with "{distance}" distance is not supported.'
1143-
)
11441140

11451141
coords = coords.astype("float64")
11461142
coords_to_assign = coords_to_assign.astype("float64")
11471143
if np.array_equal(coords, coords_to_assign):
11481144
assigned_idx = np.arange(coords.shape[0])
11491145
else:
11501146
if threshold is None:
1151-
threshold = 2 * max(abs(r) for r in get_resolution(coords_to_assign.T))
1152-
LOGGER.info(
1153-
"No exact centroid match found. Reprojecting coordinates "
1154-
"to nearest neighbor closer than the threshold = %s %s",
1155-
threshold,
1156-
unit,
1157-
)
1147+
threshold = estimate_matching_threshold(coords_to_assign)
1148+
11581149
# pairs of floats can be sorted (lexicographically) in NumPy
11591150
coords_view = coords.view(dtype="float64,float64").reshape(-1)
11601151
coords_to_assign_view = coords_to_assign.view(dtype="float64,float64").reshape(
11611152
-1
11621153
)
11631154

1164-
# assign each hazard coordsinate to an element in coords using searchsorted
1155+
# assign each hazard coordinates to an element in coords using searchsorted
11651156
coords_sorter = np.argsort(coords_view)
11661157
sort_assign_idx = np.fmin(
11671158
coords_sorter.size - 1,
@@ -1180,11 +1171,12 @@ def match_coordinates(
11801171

11811172
# assign remaining coordinates to their geographically nearest neighbor
11821173
if threshold > 0 and exact_assign_idx.size != coords_view.size:
1183-
# convert to proper units before proceeding to nearest neighbor search
1184-
if unit == "degree":
1185-
# check that coords are indeed geographic before converting
1186-
check_if_geo_coords(coords[:, 0], coords[:, 1])
1187-
check_if_geo_coords(coords_to_assign[:, 0], coords_to_assign[:, 1])
1174+
LOGGER.info(
1175+
"No exact centroid match found. Reprojecting coordinates "
1176+
"to nearest neighbor closer than the threshold = %s %s",
1177+
threshold,
1178+
unit,
1179+
)
11881180

11891181
not_assigned_idx_mask = assigned_idx == -1
11901182
assigned_idx[not_assigned_idx_mask] = nearest_neighbor_funcs[distance](
@@ -1210,26 +1202,31 @@ def match_centroids(
12101202
Parameters
12111203
----------
12121204
coord_gdf : gpd.GeoDataFrame
1213-
GeoDataframe with defined geometry column and crs
1205+
GeoDataframe with defined geometry column with points to match and crs
12141206
centroids : Centroids
1215-
(Hazard) centroids to match (as raster or vector centroids).
1207+
(Hazard) centroids to match
12161208
distance : str, optional
12171209
Distance to use in case of vector centroids.
12181210
Possible values are "euclidean", "haversine" and "approx".
1211+
For "haversine" and "approx" only geographic crs (in degrees) are accepted.
1212+
For euclidean any crs is accepted. Note that euclidean distance
1213+
for geographic crs differs from the true distance.
12191214
Default: "euclidean"
12201215
threshold : float, optional
1221-
If the distance (in km for coordinates defined in degrees, in the unit of
1222-
the crs otherwise) to the nearest neighbor exceeds `threshold`,
1223-
the index `-1` is assigned.
1224-
Set `threshold` to 0, to disable nearest neighbor matching.
1225-
Default: ``NEAREST_NEIGHBOR_THRESHOLD`` (100<km or crs.unit>)
1216+
If the distance (in the unit of the crs otherwise) to the nearest
1217+
neighbor exceeds `threshold`, the index `-1` is assigned.
1218+
Set `threshold` to 0, to enforce exact coordinate matching only.
1219+
Default: 2 times the minimal grid spacing assuming centroids to ge
1220+
on a grid
12261221
12271222
See Also
12281223
--------
12291224
climada.util.coordinates.match_grid_points: method to associate centroids to
12301225
coordinate points when centroids is a raster
12311226
climada.util.coordinates.match_coordinates: method to associate centroids to
12321227
coordinate points
1228+
climada.util.coordinates.estimate_matching_threshold: method to estimate the
1229+
default threshold
12331230
12341231
Notes
12351232
-----
@@ -1327,6 +1324,9 @@ def _nearest_neighbor_approx(
13271324
"Only degree unit is supported for nearest neighbor matching using"
13281325
"'approx' distance."
13291326
)
1327+
# check that coords are indeed geographic degrees
1328+
check_if_geo_coords(centroids[:, 0], centroids[:, 1])
1329+
check_if_geo_coords(coordinates[:, 0], coordinates[:, 1])
13301330
# Compute only for the unique coordinates. Copy the results for the
13311331
# not unique coordinates
13321332
_, idx, inv = np.unique(coordinates, axis=0, return_index=True, return_inverse=True)
@@ -1361,7 +1361,7 @@ def _nearest_neighbor_approx(
13611361

13621362
if check_antimeridian:
13631363
assigned = _nearest_neighbor_antimeridian(
1364-
centroids, coordinates, threshold, assigned
1364+
centroids, coordinates, threshold, assigned, unit=unit
13651365
)
13661366

13671367
return assigned
@@ -1395,6 +1395,9 @@ def _nearest_neighbor_haversine(centroids, coordinates, unit, threshold):
13951395
"Only coordinates in degree units are supported for nearest neighbor matching using"
13961396
"'haversine' distance."
13971397
)
1398+
# check that coords are indeed geographic degrees
1399+
check_if_geo_coords(centroids[:, 0], centroids[:, 1])
1400+
check_if_geo_coords(coordinates[:, 0], coordinates[:, 1])
13981401
# Construct tree from centroids
13991402
tree = BallTree(np.radians(centroids), metric="haversine")
14001403
# Select unique exposures coordinates
@@ -1501,14 +1504,14 @@ def _nearest_neighbor_euclidean(
15011504

15021505
if check_antimeridian:
15031506
assigned = _nearest_neighbor_antimeridian(
1504-
centroids, coordinates[idx], threshold, assigned
1507+
centroids, coordinates[idx], threshold, assigned, unit=unit
15051508
)
15061509

15071510
# Copy result to all exposures and return value
15081511
return assigned[inv]
15091512

15101513

1511-
def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned):
1514+
def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned, unit):
15121515
"""Recompute nearest neighbors close to the anti-meridian with the Haversine distance
15131516
15141517
Parameters
@@ -1530,6 +1533,12 @@ def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned):
15301533
np.array
15311534
with as many rows as coordinates containing the centroids indexes
15321535
"""
1536+
# first check that unit is in degree
1537+
if unit != "degree":
1538+
raise ValueError(
1539+
"Only coordinates in degree units are supported for nearest neighbor matching using"
1540+
"'haversine' distance a the antimeridian."
1541+
)
15331542
lon_min = min(centroids[:, 1].min(), coordinates[:, 1].min())
15341543
lon_max = max(centroids[:, 1].max(), coordinates[:, 1].max())
15351544
if lon_max - lon_min > 360:

0 commit comments

Comments
 (0)