Skip to content

Commit 5044b73

Browse files
Merge branch 'develop' into feature/cyclostrophic-as-parameter
2 parents d87d16d + bc37227 commit 5044b73

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Code freeze date: YYYY-MM-DD
1212

1313
### Added
1414

15+
- `climada.util.coordinates.bounding_box_global` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
16+
- `climada.util.coordinates.bounding_box_from_countries` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
17+
- `climada.util.coordinates.bounding_box_from_cardinal_bounds` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
1518
- `climada.engine.impact.Impact.local_return_period` method [#971](https://github.com/CLIMADA-project/climada_python/pull/971)
1619
- `doc.tutorial.climada_util_local_exceedance_values.ipynb` tutorial explaining `Hazard.local_exceedance_intensity`, `Hazard.local_return_period`, `Impact.local_exceedance_impact`, and `Impact.local_return_period` methods [#971](https://github.com/CLIMADA-project/climada_python/pull/971)
1720
- `Hazard.local_exceedance_intensity`, `Hazard.local_return_period` and `Impact.local_exceedance_impact`, that all use the `climada.util.interpolation` module [#918](https://github.com/CLIMADA-project/climada_python/pull/918)
@@ -28,6 +31,7 @@ Code freeze date: YYYY-MM-DD
2831

2932
### Changed
3033

34+
- `climada.util.coordinates.get_country_geometries` function: Now throwing a ValueError if unregognized ISO country code is given (before, the invalid ISO code was ignored) [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
3135
- Improved scaling factors implemented in `climada.hazard.trop_cyclone.apply_climate_scenario_knu` to model the impact of climate changes to tropical cyclones [#734](https://github.com/CLIMADA-project/climada_python/pull/734)
3236
- In `climada.util.plot.geo_im_from_array`, NaNs are plotted in gray while cells with no centroid are not plotted [#929](https://github.com/CLIMADA-project/climada_python/pull/929)
3337
- Renamed `climada.util.plot.subplots_from_gdf` to `climada.util.plot.plot_from_gdf` [#929](https://github.com/CLIMADA-project/climada_python/pull/929)

climada/util/coordinates.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,12 @@ def get_country_geometries(
783783
if country_names:
784784
if isinstance(country_names, str):
785785
country_names = [country_names]
786+
787+
# raise error if a country name is not recognized
788+
for country_name in country_names:
789+
if not country_name in nat_earth[["ISO_A3", "WB_A3", "ADM0_A3"]].values:
790+
raise ValueError(f"ISO code {country_name} not recognized.")
791+
786792
country_mask = np.isin(
787793
nat_earth[["ISO_A3", "WB_A3", "ADM0_A3"]].values,
788794
country_names,
@@ -1687,6 +1693,89 @@ def _ensure_utf8(val):
16871693
return admin1_info, admin1_shapes
16881694

16891695

1696+
def bounding_box_global():
1697+
"""
1698+
Return global bounds in EPSG 4326
1699+
1700+
Returns
1701+
-------
1702+
tuple:
1703+
The global bounding box as (min_lon, min_lat, max_lon, max_lat)
1704+
"""
1705+
return (-180, -90, 180, 90)
1706+
1707+
1708+
def bounding_box_from_countries(country_names, buffer=1.0):
1709+
"""
1710+
Return bounding box in EPSG 4326 containing given countries.
1711+
1712+
Parameters
1713+
----------
1714+
country_names : list or str
1715+
list with ISO 3166 alpha-3 codes of countries, e.g ['ZWE', 'GBR', 'VNM', 'UZB']
1716+
buffer : float, optional
1717+
Buffer to add to both sides of the bounding box. Default: 1.0.
1718+
1719+
Returns
1720+
-------
1721+
tuple
1722+
The bounding box containing all given coutries as (min_lon, min_lat, max_lon, max_lat)
1723+
"""
1724+
1725+
country_geometry = get_country_geometries(country_names).geometry
1726+
longitudes, latitudes = [], []
1727+
for multipolygon in country_geometry:
1728+
if isinstance(multipolygon, Polygon): # if entry is polygon
1729+
for coord in polygon.exterior.coords: # Extract exterior coordinates
1730+
longitudes.append(coord[0])
1731+
latitudes.append(coord[1])
1732+
else: # if entry is multipolygon
1733+
for polygon in multipolygon.geoms:
1734+
for coord in polygon.exterior.coords: # Extract exterior coordinates
1735+
longitudes.append(coord[0])
1736+
latitudes.append(coord[1])
1737+
1738+
return latlon_bounds(np.array(latitudes), np.array(longitudes), buffer=buffer)
1739+
1740+
1741+
def bounding_box_from_cardinal_bounds(*, northern, eastern, western, southern):
1742+
"""
1743+
Return and normalize bounding box in EPSG 4326 from given cardinal bounds.
1744+
1745+
Parameters
1746+
----------
1747+
northern : (int, float)
1748+
Northern boundary of bounding box
1749+
eastern : (int, float)
1750+
Eastern boundary of bounding box
1751+
western : (int, float)
1752+
Western boundary of bounding box
1753+
southern : (int, float)
1754+
Southern boundary of bounding box
1755+
1756+
Returns
1757+
-------
1758+
tuple
1759+
The resulting normalized bounding box (min_lon, min_lat, max_lon, max_lat) with -180 <= min_lon < max_lon < 540
1760+
1761+
"""
1762+
1763+
# latitude bounds check
1764+
if not ((90 >= northern > southern >= -90)):
1765+
raise ValueError(
1766+
"Given northern bound is below given southern bound or out of bounds"
1767+
)
1768+
1769+
eastern = (eastern + 180) % 360 - 180
1770+
western = (western + 180) % 360 - 180
1771+
1772+
# Ensure eastern > western
1773+
if western > eastern:
1774+
eastern += 360
1775+
1776+
return (western, southern, eastern, northern)
1777+
1778+
16901779
def get_admin1_geometries(countries):
16911780
"""
16921781
return geometries, names and codes of admin 1 regions in given countries

climada/util/test/test_coordinates.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,6 +2294,80 @@ def test_mask_raster_with_geometry(self):
22942294
)
22952295

22962296

2297+
class TestBoundsFromUserInput(unittest.TestCase):
2298+
"""Unit tests for the bounds_from_user_input function."""
2299+
2300+
def test_bounding_box_global(self):
2301+
"""Test for 'global' area selection."""
2302+
result = u_coord.bounding_box_global()
2303+
expected = (-180, -90, 180, 90)
2304+
np.testing.assert_almost_equal(result, expected)
2305+
2306+
def test_bounding_box_from_countries(self):
2307+
"""Test for a list of ISO country codes."""
2308+
result = u_coord.bounding_box_from_countries(
2309+
["ITA"], buffer=1.0
2310+
) # Testing with Italy (ITA)
2311+
# Real expected bounds for Italy (calculated or manually known)
2312+
expected = [
2313+
5.6027283120000675,
2314+
34.48924388200004,
2315+
19.517425977000073,
2316+
48.08521494500006,
2317+
] # Italy's bounding box
2318+
2319+
# invalid input
2320+
with self.assertRaises(ValueError):
2321+
u_coord.bounding_box_from_countries(["invalid_ISO", "DEU"])
2322+
2323+
def test_bounding_box_from_cardinal_bounds(self):
2324+
"""Test for conversion from cardinal bounds to bounds."""
2325+
np.testing.assert_array_almost_equal(
2326+
u_coord.bounding_box_from_cardinal_bounds(
2327+
northern=90, southern=-20, eastern=30, western=20
2328+
),
2329+
(20, -20, 30, 90),
2330+
)
2331+
np.testing.assert_array_almost_equal(
2332+
u_coord.bounding_box_from_cardinal_bounds(
2333+
northern=90, southern=-20, eastern=20, western=30
2334+
),
2335+
(30, -20, 380, 90),
2336+
)
2337+
np.testing.assert_array_almost_equal(
2338+
u_coord.bounding_box_from_cardinal_bounds(
2339+
northern=90, southern=-20, eastern=170, western=-170
2340+
),
2341+
(-170, -20, 170, 90),
2342+
)
2343+
np.testing.assert_array_almost_equal(
2344+
u_coord.bounding_box_from_cardinal_bounds(
2345+
northern=90, southern=-20, eastern=-170, western=170
2346+
),
2347+
(170, -20, 190, 90),
2348+
)
2349+
np.testing.assert_array_almost_equal(
2350+
u_coord.bounding_box_from_cardinal_bounds(
2351+
northern=90, southern=-20, eastern=170, western=175
2352+
),
2353+
(175, -20, 530, 90),
2354+
)
2355+
2356+
# some invalid cases
2357+
with self.assertRaises(TypeError):
2358+
u_coord.bounding_box_from_cardinal_bounds(
2359+
southern=-20, eastern=30, western=20
2360+
)
2361+
with self.assertRaises(TypeError):
2362+
u_coord.bounding_box_from_cardinal_bounds([90, -20, 30, 20])
2363+
with self.assertRaises(TypeError):
2364+
u_coord.bounding_box_from_cardinal_bounds(90, -20, 30, 20)
2365+
with self.assertRaises(TypeError):
2366+
u_coord.bounding_box_from_cardinal_bounds(
2367+
northern="90", southern=-20, eastern=30, western=20
2368+
)
2369+
2370+
22972371
# Execute Tests
22982372
if __name__ == "__main__":
22992373
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestFunc)
@@ -2302,4 +2376,5 @@ def test_mask_raster_with_geometry(self):
23022376
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRasterMeta))
23032377
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRasterIO))
23042378
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestDistance))
2379+
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestBoundsFromUserInput))
23052380
unittest.TextTestRunner(verbosity=2).run(TESTS)

0 commit comments

Comments
 (0)