Skip to content

Commit 15c5d0e

Browse files
author
Chahan Kropf
committed
Use crs instead of unit
1 parent daa09ff commit 15c5d0e

File tree

1 file changed

+47
-33
lines changed

1 file changed

+47
-33
lines changed

climada/util/coordinates.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import shapely.vectorized
4747
import shapely.wkt
4848
from cartopy.io import shapereader
49+
from pyproj.crs import CRS as PCRS
4950
from shapely.geometry import MultiPolygon, Point, box
5051
from sklearn.neighbors import BallTree
5152

@@ -127,14 +128,14 @@ def check_if_geo_coords(lat, lon, raise_error=True):
127128
return like_geo
128129

129130

130-
def get_crs_unit(coords):
131+
def get_crs_unit(crs):
131132
"""
132133
Retrieve the unit of measurement for the coordinate reference system (CRS).
133134
134135
Parameters
135136
----------
136-
coords : GeoDataFrame
137-
An object with a coordinate reference system (CRS) attribute.
137+
crs : PyProj CRS
138+
Coordinate reference system (CRS)
138139
139140
Returns
140141
-------
@@ -143,7 +144,7 @@ def get_crs_unit(coords):
143144
CRS axis information.
144145
"""
145146
try:
146-
unit = coords.crs.axis_info[0].unit_name # assume both axes have the same unit
147+
unit = crs.axis_info[0].unit_name # assume both axes have the same unit
147148
except:
148149
LOGGER.warning("The units of the input crs are undefined.")
149150
unit = "undefined"
@@ -1057,12 +1058,20 @@ def estimate_matching_threshold(coords_to_assign):
10571058
return 2 * max(abs(r) for r in get_resolution(coords_to_assign.T))
10581059

10591060

1061+
def degree_to_km(deg):
1062+
return np.deg2rad(deg) * EARTH_RADIUS_KM
1063+
1064+
1065+
def km_to_degree(km):
1066+
return np.rad2deg(km / EARTH_RADIUS_KM)
1067+
1068+
10601069
def match_coordinates(
10611070
coords,
10621071
coords_to_assign,
10631072
distance="euclidean",
1064-
unit="degree",
10651073
threshold=None,
1074+
crs=DEF_CRS,
10661075
**kwargs,
10671076
):
10681077
"""To each coordinate in `coords`, assign a matching coordinate in `coords_to_assign`
@@ -1122,21 +1131,23 @@ def match_coordinates(
11221131
across the antimeridian. However, when exact matches are enforced with `threshold=0`, lat/lon
11231132
coordinates need to be given in the same longitudinal range (such as (-180, 180)).
11241133
"""
1134+
crs = PCRS.from_user_input(DEF_CRS)
1135+
11251136
if coords.shape[0] == 0:
11261137
return np.array([])
11271138

11281139
if coords_to_assign.shape[0] == 0:
11291140
return -np.ones(coords.shape[0]).astype(int)
11301141

1131-
if distance not in nearest_neighbor_funcs:
1132-
raise ValueError(
1133-
f'Coordinate assignment with "{distance}" distance is not supported.'
1134-
)
11351142
nearest_neighbor_funcs = {
11361143
"euclidean": _nearest_neighbor_euclidean,
11371144
"haversine": _nearest_neighbor_haversine,
11381145
"approx": _nearest_neighbor_approx,
11391146
}
1147+
if distance not in nearest_neighbor_funcs:
1148+
raise ValueError(
1149+
f'Coordinate assignment with "{distance}" distance is not supported.'
1150+
)
11401151

11411152
coords = coords.astype("float64")
11421153
coords_to_assign = coords_to_assign.astype("float64")
@@ -1175,14 +1186,14 @@ def match_coordinates(
11751186
"No exact centroid match found. Reprojecting coordinates "
11761187
"to nearest neighbor closer than the threshold = %s %s",
11771188
threshold,
1178-
unit,
1189+
get_crs_unit(crs),
11791190
)
11801191

11811192
not_assigned_idx_mask = assigned_idx == -1
11821193
assigned_idx[not_assigned_idx_mask] = nearest_neighbor_funcs[distance](
11831194
coords_to_assign,
11841195
coords[not_assigned_idx_mask],
1185-
unit,
1196+
crs,
11861197
threshold,
11871198
**kwargs,
11881199
)
@@ -1265,14 +1276,11 @@ def match_centroids(
12651276
"Please provide coord_gdf and centroids with valid crs attributes."
12661277
) from exc
12671278

1268-
# get unit of coordinate systems from axis of crs
1269-
unit = get_crs_unit(coord_gdf)
1270-
12711279
assigned = match_coordinates(
12721280
np.stack([coord_gdf.geometry.y.values, coord_gdf.geometry.x.values], axis=1),
12731281
centroids.coord,
1282+
crs=centroids.crs,
12741283
distance=distance,
1275-
unit=unit,
12761284
threshold=threshold,
12771285
)
12781286
return assigned
@@ -1288,7 +1296,7 @@ def _dist_sqr_approx(lats1, lons1, cos_lats1, lats2, lons2):
12881296

12891297

12901298
def _nearest_neighbor_approx(
1291-
centroids, coordinates, unit, threshold, check_antimeridian=True
1299+
centroids, coordinates, crs, threshold, check_antimeridian=True
12921300
):
12931301
"""Compute the nearest centroid for each coordinate using the
12941302
squared equirectangular approximation distance d = ((dlon)cos(lat))^2+(dlat)^2.
@@ -1318,15 +1326,16 @@ def _nearest_neighbor_approx(
13181326
np.array
13191327
with as many rows as coordinates containing the centroids indexes
13201328
"""
1321-
# first check that unit is in degree
1322-
if unit != "degree":
1329+
if not crs.is_geographic:
13231330
raise ValueError(
1324-
"Only degree unit is supported for nearest neighbor matching using"
1331+
"Only geographic crs are supported for nearest neighbor matching using"
13251332
"'approx' distance."
13261333
)
1327-
# check that coords are indeed geographic degrees
1334+
# check that coords are indeed geographic degrees (in case we do no trust the crs)
13281335
check_if_geo_coords(centroids[:, 0], centroids[:, 1])
13291336
check_if_geo_coords(coordinates[:, 0], coordinates[:, 1])
1337+
# convert the degree threshold in to km at equator threshold
1338+
threshold_km = degree_to_km(threshold)
13301339
# Compute only for the unique coordinates. Copy the results for the
13311340
# not unique coordinates
13321341
_, idx, inv = np.unique(coordinates, axis=0, return_index=True, return_inverse=True)
@@ -1345,7 +1354,7 @@ def _nearest_neighbor_approx(
13451354
min_idx = dist.argmin()
13461355
# Raise a warning if the minimum distance is greater than the
13471356
# threshold and set an unvalid index -1
1348-
if np.sqrt(dist.min()) > threshold:
1357+
if np.sqrt(dist.min()) * ONE_LAT_KM > threshold_km:
13491358
num_warn += 1
13501359
min_idx = -1
13511360

@@ -1361,13 +1370,13 @@ def _nearest_neighbor_approx(
13611370

13621371
if check_antimeridian:
13631372
assigned = _nearest_neighbor_antimeridian(
1364-
centroids, coordinates, threshold, assigned, unit=unit
1373+
centroids, coordinates, threshold, assigned, crs=crs
13651374
)
13661375

13671376
return assigned
13681377

13691378

1370-
def _nearest_neighbor_haversine(centroids, coordinates, unit, threshold):
1379+
def _nearest_neighbor_haversine(centroids, coordinates, crs, threshold):
13711380
"""Compute the neareast centroid for each coordinate using a Ball tree with haversine distance.
13721381
13731382
Parameters
@@ -1389,13 +1398,12 @@ def _nearest_neighbor_haversine(centroids, coordinates, unit, threshold):
13891398
np.array
13901399
with as many rows as coordinates containing the centroids indexes
13911400
"""
1392-
# first check that unit is in degree
1393-
if unit != "degree":
1401+
if not crs.is_geographic:
13941402
raise ValueError(
1395-
"Only coordinates in degree units are supported for nearest neighbor matching using"
1403+
"Only geographic crs are supported for nearest neighbor matching using"
13961404
"'haversine' distance."
13971405
)
1398-
# check that coords are indeed geographic degrees
1406+
# check that coords are indeed geographic degrees (in case we do no trust the crs)
13991407
check_if_geo_coords(centroids[:, 0], centroids[:, 1])
14001408
check_if_geo_coords(coordinates[:, 0], coordinates[:, 1])
14011409
# Construct tree from centroids
@@ -1433,7 +1441,7 @@ def _nearest_neighbor_haversine(centroids, coordinates, unit, threshold):
14331441

14341442

14351443
def _nearest_neighbor_euclidean(
1436-
centroids, coordinates, unit, threshold, check_antimeridian=True
1444+
centroids, coordinates, crs, threshold, check_antimeridian=True
14371445
):
14381446
"""Compute the neareast centroid for each coordinate using a k-d tree.
14391447
@@ -1462,6 +1470,7 @@ def _nearest_neighbor_euclidean(
14621470
np.array
14631471
with as many rows as coordinates containing the centroids indexes
14641472
"""
1473+
unit = get_crs_unit(crs)
14651474
if (
14661475
unit == "degree"
14671476
): # if unit is in degree convert to radians for dist calculations
@@ -1504,14 +1513,14 @@ def _nearest_neighbor_euclidean(
15041513

15051514
if check_antimeridian:
15061515
assigned = _nearest_neighbor_antimeridian(
1507-
centroids, coordinates[idx], threshold, assigned, unit=unit
1516+
centroids, coordinates[idx], threshold, assigned, crs=crs
15081517
)
15091518

15101519
# Copy result to all exposures and return value
15111520
return assigned[inv]
15121521

15131522

1514-
def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned, unit):
1523+
def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned, crs):
15151524
"""Recompute nearest neighbors close to the anti-meridian with the Haversine distance
15161525
15171526
Parameters
@@ -1533,11 +1542,16 @@ def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned,
15331542
np.array
15341543
with as many rows as coordinates containing the centroids indexes
15351544
"""
1545+
if not crs.is_geographic:
1546+
raise ValueError(
1547+
"Only geographic crs are supported for nearest neighbor matching using"
1548+
"'haversine' distance at the antimeridian."
1549+
)
15361550
# first check that unit is in degree
1537-
if unit != "degree":
1551+
if get_crs_unit(crs) != "degree":
15381552
raise ValueError(
15391553
"Only coordinates in degree units are supported for nearest neighbor matching using"
1540-
"'haversine' distance a the antimeridian."
1554+
"'haversine' distance at the antimeridian."
15411555
)
15421556
lon_min = min(centroids[:, 1].min(), coordinates[:, 1].min())
15431557
lon_max = max(centroids[:, 1].max(), coordinates[:, 1].max())
@@ -1559,7 +1573,7 @@ def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned,
15591573
if np.any(cent_strip_bool):
15601574
cent_strip = centroids[cent_strip_bool]
15611575
strip_assigned = _nearest_neighbor_haversine(
1562-
cent_strip, coord_strip, "degree", threshold
1576+
cent_strip, coord_strip, crs, threshold
15631577
)
15641578
new_coords = cent_strip_bool.nonzero()[0][strip_assigned]
15651579
new_coords[strip_assigned == -1] = -1

0 commit comments

Comments
 (0)