8585are 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
130130def 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+
10641060def 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