Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Code freeze date: YYYY-MM-DD

### Added

- `climada.util.coordinates.bounding_box_global` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
- `climada.util.coordinates.bounding_box_from_countries` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
- `climada.util.coordinates.bounding_box_from_cardinal_bounds` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
- `climada.engine.impact.Impact.local_return_period` method [#971](https://github.com/CLIMADA-project/climada_python/pull/971)
- `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)
- `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)
Expand All @@ -28,6 +31,7 @@ Code freeze date: YYYY-MM-DD

### Changed

- `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)
- 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)
- 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)
- Renamed `climada.util.plot.subplots_from_gdf` to `climada.util.plot.plot_from_gdf` [#929](https://github.com/CLIMADA-project/climada_python/pull/929)
Expand Down
89 changes: 89 additions & 0 deletions climada/util/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@
return str(resolution) + "m"


def get_country_geometries(

Check warning on line 729 in climada/util/coordinates.py

View check run for this annotation

Jenkins - WCR / Pylint

too-many-locals

LOW: Too many local variables (19/15)
Raw output
Used when a function or method has too many local variables.
country_names=None, extent=None, resolution=10, center_crs=True
):
"""Natural Earth country boundaries within given extent
Expand Down Expand Up @@ -783,6 +783,12 @@
if country_names:
if isinstance(country_names, str):
country_names = [country_names]

# raise error if a country name is not recognized
for country_name in country_names:
if not country_name in nat_earth[["ISO_A3", "WB_A3", "ADM0_A3"]].values:
raise ValueError(f"ISO code {country_name} not recognized.")

country_mask = np.isin(
nat_earth[["ISO_A3", "WB_A3", "ADM0_A3"]].values,
country_names,
Expand Down Expand Up @@ -1687,6 +1693,89 @@
return admin1_info, admin1_shapes


def bounding_box_global():
"""
Return global bounds in EPSG 4326

Returns
-------
tuple:
The global bounding box as (min_lon, min_lat, max_lon, max_lat)
"""
return (-180, -90, 180, 90)


def bounding_box_from_countries(country_names, buffer=1.0):
"""
Return bounding box in EPSG 4326 containing given countries.

Parameters
----------
country_names : list or str
list with ISO 3166 alpha-3 codes of countries, e.g ['ZWE', 'GBR', 'VNM', 'UZB']
buffer : float, optional
Buffer to add to both sides of the bounding box. Default: 1.0.

Returns
-------
tuple
The bounding box containing all given coutries as (min_lon, min_lat, max_lon, max_lat)
"""

country_geometry = get_country_geometries(country_names).geometry
longitudes, latitudes = [], []
for multipolygon in country_geometry:
if isinstance(multipolygon, Polygon): # if entry is polygon
for coord in polygon.exterior.coords: # Extract exterior coordinates
longitudes.append(coord[0])
latitudes.append(coord[1])

Check warning on line 1731 in climada/util/coordinates.py

View check run for this annotation

Jenkins - WCR / Code Coverage

Not covered lines

Lines 1729-1731 are not covered by tests
else: # if entry is multipolygon
for polygon in multipolygon.geoms:
for coord in polygon.exterior.coords: # Extract exterior coordinates
longitudes.append(coord[0])
latitudes.append(coord[1])

return latlon_bounds(np.array(latitudes), np.array(longitudes), buffer=buffer)


def bounding_box_from_cardinal_bounds(*, northern, eastern, western, southern):
"""
Return and normalize bounding box in EPSG 4326 from given cardinal bounds.

Parameters
----------
northern : (int, float)
Northern boundary of bounding box
eastern : (int, float)
Eastern boundary of bounding box
western : (int, float)
Western boundary of bounding box
southern : (int, float)
Southern boundary of bounding box

Returns
-------
tuple
The resulting normalized bounding box (min_lon, min_lat, max_lon, max_lat) with -180 <= min_lon < max_lon < 540

Check warning on line 1759 in climada/util/coordinates.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (119/100)
Raw output
Used when a line is longer than a given number of characters.

"""

# latitude bounds check
if not ((90 >= northern > southern >= -90)):

Check warning on line 1764 in climada/util/coordinates.py

View check run for this annotation

Jenkins - WCR / Pylint

superfluous-parens

LOW: Unnecessary parens after 'not' keyword
Raw output
Used when a single item in parentheses follows an if, for, or other keyword.
raise ValueError(

Check warning on line 1765 in climada/util/coordinates.py

View check run for this annotation

Jenkins - WCR / Code Coverage

Not covered line

Line 1765 is not covered by tests
"Given northern bound is below given southern bound or out of bounds"
)

eastern = (eastern + 180) % 360 - 180
western = (western + 180) % 360 - 180

# Ensure eastern > western
if western > eastern:
eastern += 360

return (western, southern, eastern, northern)


def get_admin1_geometries(countries):
"""
return geometries, names and codes of admin 1 regions in given countries
Expand Down
75 changes: 75 additions & 0 deletions climada/util/test/test_coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2294,6 +2294,80 @@ def test_mask_raster_with_geometry(self):
)


class TestBoundsFromUserInput(unittest.TestCase):
"""Unit tests for the bounds_from_user_input function."""

def test_bounding_box_global(self):
"""Test for 'global' area selection."""
result = u_coord.bounding_box_global()
expected = (-180, -90, 180, 90)
np.testing.assert_almost_equal(result, expected)

def test_bounding_box_from_countries(self):
"""Test for a list of ISO country codes."""
result = u_coord.bounding_box_from_countries(
["ITA"], buffer=1.0
) # Testing with Italy (ITA)
# Real expected bounds for Italy (calculated or manually known)
expected = [
5.6027283120000675,
34.48924388200004,
19.517425977000073,
48.08521494500006,
] # Italy's bounding box

# invalid input
with self.assertRaises(ValueError):
u_coord.bounding_box_from_countries(["invalid_ISO", "DEU"])

def test_bounding_box_from_cardinal_bounds(self):
"""Test for conversion from cardinal bounds to bounds."""
np.testing.assert_array_almost_equal(
u_coord.bounding_box_from_cardinal_bounds(
northern=90, southern=-20, eastern=30, western=20
),
(20, -20, 30, 90),
)
np.testing.assert_array_almost_equal(
u_coord.bounding_box_from_cardinal_bounds(
northern=90, southern=-20, eastern=20, western=30
),
(30, -20, 380, 90),
)
np.testing.assert_array_almost_equal(
u_coord.bounding_box_from_cardinal_bounds(
northern=90, southern=-20, eastern=170, western=-170
),
(-170, -20, 170, 90),
)
np.testing.assert_array_almost_equal(
u_coord.bounding_box_from_cardinal_bounds(
northern=90, southern=-20, eastern=-170, western=170
),
(170, -20, 190, 90),
)
np.testing.assert_array_almost_equal(
u_coord.bounding_box_from_cardinal_bounds(
northern=90, southern=-20, eastern=170, western=175
),
(175, -20, 530, 90),
)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add tests for invalid bounding box.

# some invalid cases
with self.assertRaises(TypeError):
u_coord.bounding_box_from_cardinal_bounds(
southern=-20, eastern=30, western=20
)
with self.assertRaises(TypeError):
u_coord.bounding_box_from_cardinal_bounds([90, -20, 30, 20])
with self.assertRaises(TypeError):
u_coord.bounding_box_from_cardinal_bounds(90, -20, 30, 20)
with self.assertRaises(TypeError):
u_coord.bounding_box_from_cardinal_bounds(
northern="90", southern=-20, eastern=30, western=20
)


# Execute Tests
if __name__ == "__main__":
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestFunc)
Expand All @@ -2302,4 +2376,5 @@ def test_mask_raster_with_geometry(self):
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRasterMeta))
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRasterIO))
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestDistance))
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestBoundsFromUserInput))
unittest.TextTestRunner(verbosity=2).run(TESTS)
Loading