Skip to content

Commit bfb0124

Browse files
Merge branch 'develop' into feature/from_netcdf_fast
2 parents c1f8847 + 493deb0 commit bfb0124

File tree

17 files changed

+1047
-175
lines changed

17 files changed

+1047
-175
lines changed

.readthedocs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
version: 2
22

3+
sphinx:
4+
configuration: doc/conf.py
5+
36
build:
47
os: "ubuntu-22.04"
58
tools:

CHANGELOG.md

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

1313
### Added
1414

15+
- Add `osm-flex` package to CLIMADA core [#981](https://github.com/CLIMADA-project/climada_python/pull/981)
16+
- `doc.tutorial.climada_entity_Exposures_osm.ipynb` tutorial explaining how to use `osm-flex`with CLIMADA
17+
- `climada.util.coordinates.bounding_box_global` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
18+
- `climada.util.coordinates.bounding_box_from_countries` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
19+
- `climada.util.coordinates.bounding_box_from_cardinal_bounds` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)
1520
- `climada.engine.impact.Impact.local_return_period` method [#971](https://github.com/CLIMADA-project/climada_python/pull/971)
1621
- `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)
1722
- `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 +33,8 @@ Code freeze date: YYYY-MM-DD
2833

2934
### Changed
3035

36+
- `Centroids.append` now takes multiple arguments and provides a performance boost when doing so [#989](https://github.com/CLIMADA-project/climada_python/pull/989)
37+
- `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)
3138
- 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)
3239
- 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)
3340
- Renamed `climada.util.plot.subplots_from_gdf` to `climada.util.plot.plot_from_gdf` [#929](https://github.com/CLIMADA-project/climada_python/pull/929)
@@ -43,6 +50,7 @@ Code freeze date: YYYY-MM-DD
4350

4451
### Fixed
4552

53+
- Resolved an issue where windspeed computation was much slower than in Climada v3 [#989](https://github.com/CLIMADA-project/climada_python/pull/989)
4654
- File handles are being closed after reading netcdf files with `climada.hazard` modules [#953](https://github.com/CLIMADA-project/climada_python/pull/953)
4755
- Avoids a ValueError in the impact calculation for cases with a single exposure point and MDR values of 0, by explicitly removing zeros in `climada.hazard.Hazard.get_mdr` [#933](https://github.com/CLIMADA-project/climada_python/pull/948)
4856

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ install_test : ## Test installation was successful
2929

3030
.PHONY : data_test
3131
data_test : ## Test data APIs
32-
python script/jenkins/test_data_api.py
32+
pytest $(PYTEST_JUNIT_ARGS) script/jenkins/test_data_api.py
3333

3434
.PHONY : notebook_test
3535
notebook_test : ## Test notebooks in doc/tutorial
36-
python script/jenkins/test_notebooks.py report
36+
pytest $(PYTEST_JUNIT_ARGS) script/jenkins/test_notebooks.py
3737

3838
.PHONY : integ_test
3939
integ_test : ## Integration tests execution with xml reports

climada/entity/exposures/base.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,40 @@ def gdf(self):
132132
@property
133133
def latitude(self):
134134
"""Latitude array of exposures"""
135-
return self.data.geometry.y.values
135+
try:
136+
return self.data.geometry.y.values
137+
except ValueError as valerr:
138+
nonpoints = list(
139+
self.data[
140+
self.data.geometry.type != "Point"
141+
].geometry.type.drop_duplicates()
142+
)
143+
if nonpoints:
144+
raise ValueError(
145+
"Can only calculate latitude from Points."
146+
f" GeoDataFrame contains {', '.join(nonpoints)}."
147+
" Please see the lines_polygons module tutorial."
148+
) from valerr
149+
raise
136150

137151
@property
138152
def longitude(self):
139153
"""Longitude array of exposures"""
140-
return self.data.geometry.x.values
154+
try:
155+
return self.data.geometry.x.values
156+
except ValueError as valerr:
157+
nonpoints = list(
158+
self.data[
159+
self.data.geometry.type != "Point"
160+
].geometry.type.drop_duplicates()
161+
)
162+
if nonpoints:
163+
raise ValueError(
164+
"Can only calculate longitude from Points."
165+
f" GeoDataFrame contains {', '.join(nonpoints)}."
166+
" Please see the lines_polygons module tutorial."
167+
) from valerr
168+
raise
141169

142170
@property
143171
def geometry(self):

climada/entity/exposures/test/test_base.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import rasterio
2828
import scipy as sp
2929
from rasterio.windows import Window
30-
from shapely.geometry import Point
30+
from shapely.geometry import MultiPolygon, Point, Polygon
3131
from sklearn.metrics import DistanceMetric
3232

3333
import climada.util.coordinates as u_coord
@@ -652,6 +652,39 @@ def test_to_crs_epsg_crs(self):
652652
Exposures.to_crs(self, crs="GCS", epsg=26915)
653653
self.assertEqual("one of crs or epsg must be None", str(cm.exception))
654654

655+
def test_latlon_with_polygons(self):
656+
"""Check for proper error message if the data frame contains non-Point shapes"""
657+
poly = Polygon(
658+
[(10.0, 0.0), (10.0, 1.0), (11.0, 1.0), (11.0, 0.0), (10.0, 0.0)]
659+
)
660+
point = Point((1, -1))
661+
multi = MultiPolygon(
662+
[
663+
(
664+
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
665+
[((0.1, 1.1), (0.1, 1.2), (0.2, 1.2), (0.2, 1.1))],
666+
)
667+
]
668+
)
669+
poly = Polygon()
670+
exp = Exposures(geometry=[poly, point, multi, poly])
671+
with self.assertRaises(ValueError) as valer:
672+
exp.latitude
673+
self.assertEqual(
674+
"Can only calculate latitude from Points."
675+
" GeoDataFrame contains Polygon, MultiPolygon."
676+
" Please see the lines_polygons module tutorial.",
677+
str(valer.exception),
678+
)
679+
with self.assertRaises(ValueError) as valer:
680+
exp.longitude
681+
self.assertEqual(
682+
"Can only calculate longitude from Points."
683+
" GeoDataFrame contains Polygon, MultiPolygon."
684+
" Please see the lines_polygons module tutorial.",
685+
str(valer.exception),
686+
)
687+
655688

656689
class TestImpactFunctions(unittest.TestCase):
657690
"""Test impact function handling"""

climada/hazard/centroids/centr.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,16 @@ def from_pnt_bounds(cls, points_bounds, res, crs=DEF_CRS):
331331
}
332332
)
333333

334-
def append(self, centr):
335-
"""Append Centroids
334+
def append(self, *centr):
335+
"""Append Centroids to the current centroid object for concatenation.
336+
337+
This method checks that all centroids use the same CRS, appends the list of centroids to
338+
the initial Centroid object and eventually concatenates them to create a single centroid
339+
object with the union of all centroids.
336340
337341
Note that the result might contain duplicate points if the object to append has an overlap
338-
with the current object.
342+
with the current object. Remove duplicates by either using :py:meth:`union`
343+
or calling :py:meth:`remove_duplicate_points` after appending.
339344
340345
Parameters
341346
----------
@@ -351,22 +356,25 @@ def append(self, centr):
351356
union : Union of Centroid objects.
352357
remove_duplicate_points : Remove duplicate points in a Centroids object.
353358
"""
354-
if not u_coord.equal_crs(self.crs, centr.crs):
355-
raise ValueError(
356-
f"The given centroids use different CRS: {self.crs}, {centr.crs}. "
357-
"The centroids are incompatible and cannot be concatenated."
358-
)
359-
self.gdf = pd.concat([self.gdf, centr.gdf])
359+
for other in centr:
360+
if not u_coord.equal_crs(self.crs, other.crs):
361+
raise ValueError(
362+
f"The given centroids use different CRS: {self.crs}, {other.crs}. "
363+
"The centroids are incompatible and cannot be concatenated."
364+
)
365+
self.gdf = pd.concat([self.gdf] + [other.gdf for other in centr])
360366

361367
def union(self, *others):
362-
"""Create the union of Centroids objects
368+
"""Create the union of the current Centroids object with one or more other centroids
369+
objects by passing the list of centroids to :py:meth:`append` for concatenation and then
370+
removes duplicates.
363371
364372
All centroids must have the same CRS. Points that are contained in more than one of the
365373
Centroids objects will only be contained once (i.e. duplicates are removed).
366374
367375
Parameters
368376
----------
369-
others : list of Centroids
377+
others : Centroids
370378
Centroids contributing to the union.
371379
372380
Returns
@@ -375,8 +383,8 @@ def union(self, *others):
375383
Centroids object containing the union of all Centroids.
376384
"""
377385
centroids = copy.deepcopy(self)
378-
for cent in others:
379-
centroids.append(cent)
386+
centroids.append(*others)
387+
380388
return centroids.remove_duplicate_points()
381389

382390
def remove_duplicate_points(self):

climada/hazard/centroids/test/test_centr.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,20 @@ def test_append_dif_crs(self):
816816
with self.assertRaises(ValueError):
817817
self.centr.append(centr2)
818818

819+
def test_append_multiple_arguments(self):
820+
"""Test passing append() multiple arguments in the form of a list of Centroids."""
821+
# create a single centroid
822+
lat, lon = np.array([1, 2]), np.array([1, 2])
823+
centr = Centroids(lat=lat, lon=lon)
824+
# create a list of centroids
825+
coords = [(np.array([3, 4]), np.array([3, 4]))]
826+
centroids_list = [Centroids(lat=lat, lon=lon) for lat, lon in coords]
827+
828+
centr.append(*centroids_list)
829+
830+
np.testing.assert_array_equal(centr.lat, [1, 2, 3, 4])
831+
np.testing.assert_array_equal(centr.lon, [1, 2, 3, 4])
832+
819833
def test_remove_duplicate_pass(self):
820834
"""Test remove_duplicate_points"""
821835
centr = Centroids(

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

0 commit comments

Comments
 (0)