Skip to content

Commit efb9921

Browse files
Merge branch 'develop' into feature/restructure_fitfuncs_exceedance
# Conflicts: # CHANGELOG.md
2 parents 9edac4d + 4ae9abd commit efb9921

File tree

16 files changed

+720
-623
lines changed

16 files changed

+720
-623
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
build-and-test:
1313
name: 'Core / Unit Test Pipeline'
1414
runs-on: ubuntu-latest
15+
timeout-minutes: 20
1516
permissions:
1617
# For publishing results
1718
checks: write

CHANGELOG.md

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

1313
### Added
1414

15-
- `climada.util.interpolation` module for inter- and extrapolation util functions used in local exceedance intensity and return period functions [#930](https://github.com/CLIMADA-project/climada_python/pull/930)
1615
- `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)
17-
16+
- `climada.util.interpolation` module for inter- and extrapolation util functions used in local exceedance intensity and return period functions [#930](https://github.com/CLIMADA-project/climada_python/pull/930)
17+
1818
### Changed
1919

2020
- 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)
@@ -23,6 +23,9 @@ Code freeze date: YYYY-MM-DD
2323

2424
### Fixed
2525

26+
- File handles are being closed after reading netcdf files with `climada.hazard` modules [#953](https://github.com/CLIMADA-project/climada_python/pull/953)
27+
- 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)
28+
2629
### Deprecated
2730

2831
### Removed
@@ -471,4 +474,3 @@ updated:
471474

472475
- `climada.enginge.impact.Impact.calc()` and `climada.enginge.impact.Impact.calc_impact_yearset()`
473476
[#436](https://github.com/CLIMADA-project/climada_python/pull/436).
474-

climada/engine/test/test_impact_calc.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828

2929
from climada import CONFIG
3030
from climada.entity.entity_def import Entity
31-
from climada.entity import Exposures, ImpactFuncSet, ImpactFunc
32-
from climada.hazard.base import Hazard
31+
from climada.entity import Exposures, ImpactFuncSet, ImpactFunc, ImpfTropCyclone
32+
from climada.hazard.base import Hazard, Centroids
3333
from climada.engine import ImpactCalc, Impact
3434
from climada.engine.impact_calc import LOGGER as ILOG
3535
from climada.util.constants import ENT_DEMO_TODAY, DEMO_DIR
@@ -471,6 +471,34 @@ def test_stitch_risk_metrics(self):
471471
np.testing.assert_array_equal(eai_exp, [2.25, 1.25, 4.5])
472472
self.assertEqual(aai_agg, 8.0) # Sum of eai_exp
473473

474+
def test_single_exp_zero_mdr(self):
475+
"""Test for case where exposure has a single value and MDR or fraction contains zeros"""
476+
centroids = Centroids.from_lat_lon([-26.16], [28.20])
477+
haz = Hazard(
478+
intensity=sparse.csr_matrix(np.array([[31.5], [19.0]])),
479+
event_id=np.arange(2),
480+
event_name=[0,1],
481+
frequency=np.ones(2) / 2,
482+
fraction=sparse.csr_matrix(np.zeros((2,1))),
483+
date=np.array([0, 1]),
484+
centroids=centroids,
485+
haz_type='TC'
486+
)
487+
exp = Exposures({'value': [1.],
488+
'longitude': 28.22,
489+
'latitude': -26.17,
490+
'impf_TC': 1},
491+
crs="EPSG:4326")
492+
imp_evt = 0.00250988804927603
493+
aai_agg = imp_evt/2
494+
eai_exp = np.array([aai_agg])
495+
at_event = np.array([imp_evt, 0])
496+
exp.set_geometry_points()
497+
impf_tc = ImpfTropCyclone.from_emanuel_usa()
498+
impf_set = ImpactFuncSet([impf_tc])
499+
impf_set.check()
500+
imp = ImpactCalc(exp, impf_set, haz).impact(save_mat=True)
501+
check_impact(self, imp, haz, exp, aai_agg, eai_exp, at_event, at_event)
474502

475503
class TestImpactMatrixCalc(unittest.TestCase):
476504
"""Verify the computation of the impact matrix"""

climada/hazard/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,9 @@ def get_mdr(self, cent_idx, impf):
10611061
impf.id)
10621062
mdr_array = impf.calc_mdr(mdr.toarray().ravel()).reshape(mdr.shape)
10631063
mdr = sparse.csr_matrix(mdr_array)
1064-
return mdr[:, indices]
1064+
mdr_out = mdr[:, indices]
1065+
mdr_out.eliminate_zeros()
1066+
return mdr_out
10651067

10661068
def get_paa(self, cent_idx, impf):
10671069
"""

climada/hazard/isimip_data.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ def _read_one_nc(file_name, bbox=None, years=None):
5050
Contains data in the specified bounding box and for the
5151
specified time period
5252
"""
53-
data = xr.open_dataset(file_name, decode_times=False)
54-
if not bbox:
55-
bbox = bbox_world
56-
if not years:
57-
return data.sel(lat=slice(bbox[3], bbox[1]), lon=slice(bbox[0], bbox[2]))
58-
59-
time_id = years - int(data['time'].units[12:16])
60-
return data.sel(lat=slice(bbox[3], bbox[1]), lon=slice(bbox[0], bbox[2]),
61-
time=slice(time_id[0], time_id[1]))
53+
with xr.open_dataset(file_name, decode_times=False) as data:
54+
if not bbox:
55+
bbox = bbox_world
56+
if not years:
57+
return data.sel(lat=slice(bbox[3], bbox[1]), lon=slice(bbox[0], bbox[2]))
58+
59+
time_id = years - int(data['time'].units[12:16])
60+
return data.sel(lat=slice(bbox[3], bbox[1]), lon=slice(bbox[0], bbox[2]),
61+
time=slice(time_id[0], time_id[1]))

climada/hazard/storm_europe.py

Lines changed: 118 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -228,37 +228,33 @@ def _read_one_nc(cls, file_name, centroids, intensity_thres):
228228
new_haz : StormEurope
229229
Hazard instance for one single storm.
230230
"""
231-
ncdf = xr.open_dataset(file_name)
232-
233-
if centroids.size != (ncdf.sizes['latitude'] * ncdf.sizes['longitude']):
234-
ncdf.close()
235-
LOGGER.warning(('Centroids size doesn\'t match NCDF dimensions. '
236-
'Omitting file %s.'), file_name)
237-
return None
238-
239-
# xarray does not penalise repeated assignments, see
240-
# http://xarray.pydata.org/en/stable/data-structures.html
241-
stacked = ncdf['max_wind_gust'].stack(
242-
intensity=('latitude', 'longitude', 'time')
243-
)
244-
stacked = stacked.where(stacked > intensity_thres)
245-
stacked = stacked.fillna(0)
246-
247-
# fill in values from netCDF
248-
ssi_wisc = np.array([float(ncdf.attrs['ssi'])])
249-
intensity = sparse.csr_matrix(stacked)
250-
new_haz = cls(ssi_wisc=ssi_wisc,
251-
intensity=intensity,
252-
event_name=[ncdf.attrs['storm_name']],
253-
date=np.array([datetime64_to_ordinal(ncdf['time'].data[0])]),
254-
# fill in default values
255-
centroids=centroids,
256-
event_id=np.array([1]),
257-
frequency=np.array([1]),
258-
orig=np.array([True]),)
259-
260-
ncdf.close()
261-
return new_haz
231+
with xr.open_dataset(file_name) as ncdf:
232+
if centroids.size != (ncdf.sizes['latitude'] * ncdf.sizes['longitude']):
233+
LOGGER.warning(('Centroids size doesn\'t match NCDF dimensions. '
234+
'Omitting file %s.'), file_name)
235+
return None
236+
237+
# xarray does not penalise repeated assignments, see
238+
# http://xarray.pydata.org/en/stable/data-structures.html
239+
stacked = ncdf['max_wind_gust'].stack(
240+
intensity=('latitude', 'longitude', 'time')
241+
)
242+
stacked = stacked.where(stacked > intensity_thres)
243+
stacked = stacked.fillna(0)
244+
245+
# fill in values from netCDF
246+
ssi_wisc = np.array([float(ncdf.attrs['ssi'])])
247+
intensity = sparse.csr_matrix(stacked)
248+
new_haz = cls(ssi_wisc=ssi_wisc,
249+
intensity=intensity,
250+
event_name=[ncdf.attrs['storm_name']],
251+
date=np.array([datetime64_to_ordinal(ncdf['time'].data[0])]),
252+
# fill in default values
253+
centroids=centroids,
254+
event_id=np.array([1]),
255+
frequency=np.array([1]),
256+
orig=np.array([True]),)
257+
return new_haz
262258

263259
def read_cosmoe_file(self, *args, **kwargs):
264260
"""This function is deprecated, use StormEurope.from_cosmoe_file instead."""
@@ -309,69 +305,66 @@ def from_cosmoe_file(cls, fp_file, run_datetime, event_date=None,
309305
intensity_thres = cls.intensity_thres if intensity_thres is None else intensity_thres
310306

311307
# read intensity from file
312-
ncdf = xr.open_dataset(fp_file)
313-
ncdf = ncdf.assign_coords(date=('time',ncdf["time"].dt.floor("D").values))
314-
315-
if event_date:
316-
try:
317-
stacked = ncdf.sel(
318-
time=event_date.strftime('%Y-%m-%d')
319-
).groupby('date').max().stack(intensity=('y_1', 'x_1'))
320-
except KeyError as ker:
321-
raise ValueError('Extraction of date and coordinates failed. This is most likely '
322-
'because the selected event_date '
323-
f'{event_date.strftime("%Y-%m-%d")} is not contained in the '
324-
'weather forecast selected by fp_file {fp_file}. Please adjust '
325-
f'event_date or fp_file.') from ker
326-
considered_dates = np.datetime64(event_date)
327-
else:
328-
time_covered_step = ncdf['time'].diff('time')
329-
time_covered_day = time_covered_step.groupby('date').sum()
330-
# forecast run should cover at least 18 hours of a day
331-
considered_dates_bool = time_covered_day >= np.timedelta64(18,'h')
332-
stacked = ncdf.groupby('date').max()\
333-
.sel(date=considered_dates_bool)\
334-
.stack(intensity=('y_1', 'x_1'))
335-
considered_dates = stacked['date'].values
336-
stacked = stacked.stack(date_ensemble=('date', 'epsd_1'))
337-
stacked = stacked.where(stacked['VMAX_10M'] > intensity_thres)
338-
stacked = stacked.fillna(0)
339-
340-
# fill in values from netCDF
341-
intensity = sparse.csr_matrix(stacked['VMAX_10M'].T)
342-
event_id = np.arange(stacked['date_ensemble'].size) + 1
343-
date = np.repeat(
344-
np.array(datetime64_to_ordinal(considered_dates)),
345-
np.unique(ncdf['epsd_1']).size
346-
)
347-
orig = np.full_like(event_id, False)
348-
orig[(stacked['epsd_1'] == 0).values] = True
349-
if description is None:
350-
description = (model_name +
351-
' weather forecast windfield ' +
352-
'for run startet at ' +
353-
run_datetime.strftime('%Y%m%d%H'))
354-
355-
# Create Hazard
356-
haz = cls(
357-
intensity=intensity,
358-
event_id=event_id,
359-
centroids = cls._centroids_from_nc(fp_file),
360-
# fill in default values
361-
orig=orig,
362-
date=date,
363-
event_name=[date_i + '_ens' + str(ens_i)
364-
for date_i, ens_i
365-
in zip(date_to_str(date), stacked['epsd_1'].values + 1)],
366-
frequency=np.divide(
367-
np.ones_like(event_id),
368-
np.unique(ncdf['epsd_1']).size),
369-
)
308+
with xr.open_dataset(fp_file) as ncdf:
309+
ncdf = ncdf.assign_coords(date=('time',ncdf["time"].dt.floor("D").values))
310+
if event_date:
311+
try:
312+
stacked = ncdf.sel(
313+
time=event_date.strftime('%Y-%m-%d')
314+
).groupby('date').max().stack(intensity=('y_1', 'x_1'))
315+
except KeyError as ker:
316+
raise ValueError('Extraction of date and coordinates failed. This is most likely '
317+
'because the selected event_date '
318+
f'{event_date.strftime("%Y-%m-%d")} is not contained in the '
319+
'weather forecast selected by fp_file {fp_file}. Please adjust '
320+
f'event_date or fp_file.') from ker
321+
considered_dates = np.datetime64(event_date)
322+
else:
323+
time_covered_step = ncdf['time'].diff('time')
324+
time_covered_day = time_covered_step.groupby('date').sum()
325+
# forecast run should cover at least 18 hours of a day
326+
considered_dates_bool = time_covered_day >= np.timedelta64(18,'h')
327+
stacked = ncdf.groupby('date').max()\
328+
.sel(date=considered_dates_bool)\
329+
.stack(intensity=('y_1', 'x_1'))
330+
considered_dates = stacked['date'].values
331+
stacked = stacked.stack(date_ensemble=('date', 'epsd_1'))
332+
stacked = stacked.where(stacked['VMAX_10M'] > intensity_thres)
333+
stacked = stacked.fillna(0)
334+
335+
# fill in values from netCDF
336+
intensity = sparse.csr_matrix(stacked['VMAX_10M'].T)
337+
event_id = np.arange(stacked['date_ensemble'].size) + 1
338+
date = np.repeat(
339+
np.array(datetime64_to_ordinal(considered_dates)),
340+
np.unique(ncdf['epsd_1']).size
341+
)
342+
orig = np.full_like(event_id, False)
343+
orig[(stacked['epsd_1'] == 0).values] = True
344+
if description is None:
345+
description = (model_name +
346+
' weather forecast windfield ' +
347+
'for run startet at ' +
348+
run_datetime.strftime('%Y%m%d%H'))
349+
350+
# Create Hazard
351+
haz = cls(
352+
intensity=intensity,
353+
event_id=event_id,
354+
centroids = cls._centroids_from_nc(fp_file),
355+
# fill in default values
356+
orig=orig,
357+
date=date,
358+
event_name=[date_i + '_ens' + str(ens_i)
359+
for date_i, ens_i
360+
in zip(date_to_str(date), stacked['epsd_1'].values + 1)],
361+
frequency=np.divide(
362+
np.ones_like(event_id),
363+
np.unique(ncdf['epsd_1']).size),
364+
)
370365

371-
# close netcdf file
372-
ncdf.close()
373-
haz.check()
374-
return haz
366+
haz.check()
367+
return haz
375368

376369
def read_icon_grib(self, *args, **kwargs):
377370
"""This function is deprecated, use StormEurope.from_icon_grib instead."""
@@ -444,11 +437,12 @@ def from_icon_grib(cls, run_datetime, event_date=None, model_name='icon-eu-eps',
444437
gripfile_path_i = Path(file_i[:-4])
445438
with open(file_i, 'rb') as source, open(gripfile_path_i, 'wb') as dest:
446439
dest.write(bz2.decompress(source.read()))
447-
ds_i = xr.open_dataset(gripfile_path_i, engine='cfgrib')
448-
if ind_i == 0:
449-
stacked = ds_i
450-
else:
451-
stacked = xr.concat([stacked,ds_i], 'valid_time')
440+
441+
with xr.open_dataset(gripfile_path_i, engine='cfgrib') as ds_i:
442+
if ind_i == 0:
443+
stacked = ds_i
444+
else:
445+
stacked = xr.concat([stacked,ds_i], 'valid_time')
452446

453447
# create intensity matrix with max for each full day
454448
stacked = stacked.assign_coords(
@@ -524,35 +518,34 @@ def _centroids_from_nc(file_name):
524518
'longitude' variables in a netCDF file.
525519
"""
526520
LOGGER.info('Constructing centroids from %s', file_name)
527-
ncdf = xr.open_dataset(file_name)
528-
create_meshgrid = True
529-
if hasattr(ncdf, 'latitude'):
530-
lats = ncdf['latitude'].data
531-
lons = ncdf['longitude'].data
532-
elif hasattr(ncdf, 'lat'):
533-
lats = ncdf['lat'].data
534-
lons = ncdf['lon'].data
535-
elif hasattr(ncdf, 'lat_1'):
536-
if len(ncdf['lon_1'].shape)>1 & \
537-
(ncdf['lon_1'].shape == ncdf['lat_1'].shape) \
538-
:
539-
lats = ncdf['lat_1'].data.flatten()
540-
lons = ncdf['lon_1'].data.flatten()
521+
with xr.open_dataset(file_name) as ncdf:
522+
create_meshgrid = True
523+
if hasattr(ncdf, 'latitude'):
524+
lats = ncdf['latitude'].data
525+
lons = ncdf['longitude'].data
526+
elif hasattr(ncdf, 'lat'):
527+
lats = ncdf['lat'].data
528+
lons = ncdf['lon'].data
529+
elif hasattr(ncdf, 'lat_1'):
530+
if len(ncdf['lon_1'].shape)>1 & \
531+
(ncdf['lon_1'].shape == ncdf['lat_1'].shape) \
532+
:
533+
lats = ncdf['lat_1'].data.flatten()
534+
lons = ncdf['lon_1'].data.flatten()
535+
create_meshgrid = False
536+
else:
537+
lats = ncdf['lat_1'].data
538+
lons = ncdf['lon_1'].data
539+
elif hasattr(ncdf, 'clat'):
540+
lats = ncdf['clat'].data
541+
lons = ncdf['clon'].data
542+
if ncdf['clat'].attrs['units']=='radian':
543+
lats = np.rad2deg(lats)
544+
lons = np.rad2deg(lons)
541545
create_meshgrid = False
542546
else:
543-
lats = ncdf['lat_1'].data
544-
lons = ncdf['lon_1'].data
545-
elif hasattr(ncdf, 'clat'):
546-
lats = ncdf['clat'].data
547-
lons = ncdf['clon'].data
548-
if ncdf['clat'].attrs['units']=='radian':
549-
lats = np.rad2deg(lats)
550-
lons = np.rad2deg(lons)
551-
create_meshgrid = False
552-
else:
553-
raise AttributeError('netcdf file has no field named latitude or '
554-
'other know abrivation for coordinates.')
555-
ncdf.close()
547+
raise AttributeError('netcdf file has no field named latitude or '
548+
'other know abrivation for coordinates.')
556549

557550
if create_meshgrid:
558551
lats, lons = np.array([np.repeat(lats, len(lons)),

0 commit comments

Comments
 (0)