Skip to content

Commit 6fa9315

Browse files
committed
Merge branch 'develop' into calibrate-impact-functions
2 parents 53feef6 + 7b849b3 commit 6fa9315

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1698
-1430
lines changed

AUTHORS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@
2929
* Raphael Portmann
3030
* Nicolas Colombi
3131
* Leonie Villiger
32-
* Timo Schmid
32+
* Timo Schmid
33+
* Kam Lam Yeung
34+
* Sarah Hülsen

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,41 @@ Code freeze date: YYYY-MM-DD
1010

1111
### Dependency Changes
1212

13+
Added:
14+
15+
- `pyproj` >=3.5
16+
- `pyarrow` >=14.0
17+
- `numexpr` >=2.8
18+
19+
Removed:
20+
21+
- `proj` (in favor of `pyproj`)
22+
1323
### Added
1424

1525
- `climada.util.calibrate` module for calibrating impact functions [#692](https://github.com/CLIMADA-project/climada_python/pull/692)
26+
- Convenience method `api_client.Client.get_dataset_file`, combining `get_dataset_info` and `download_dataset`, returning a single file objet. [#821](https://github.com/CLIMADA-project/climada_python/pull/821)
27+
- Read and Write methods to and from csv files for the `DiscRates` class. [#818](ttps://github.com/CLIMADA-project/climada_python/pull/818)
1628

1729
### Changed
1830

31+
- Update Developer and Installation Guides for easier accessibility by new developers. [808](https://github.com/CLIMADA-project/climada_python/pull/808)
32+
- Add `shapes` argument to `geo_im_from_array` to allow flexible turning on/off of plotting coastline in `plot_intensity`. [#805](https://github.com/CLIMADA-project/climada_python/pull/805)
1933
- Update `CONTRIBUTING.md` to better explain types of contributions to this repository [#797](https://github.com/CLIMADA-project/climada_python/pull/797)
2034
- The default tile layer in Exposures maps is not Stamen Terrain anymore, but [CartoDB Positron](https://github.com/CartoDB/basemap-styles). Affected methods are `climada.engine.Impact.plot_basemap_eai_exposure`,`climada.engine.Impact.plot_basemap_impact_exposure` and `climada.entity.Exposures.plot_basemap`. [#798](https://github.com/CLIMADA-project/climada_python/pull/798)
35+
- Recommend using Mamba instead of Conda for installing CLIMADA [#809](https://github.com/CLIMADA-project/climada_python/pull/809)
36+
- `Hazard.from_xarray_raster` now allows arbitrary values as 'event' coordinates [#837](https://github.com/CLIMADA-project/climada_python/pull/837)
37+
- `climada.test.get_test_file` now compares the version of the requested test dataset with the version of climada itself and selects the most appropriate dataset. In this way a test file can be updated without the need of changing the code of the unittest. [#822](https://github.com/CLIMADA-project/climada_python/pull/822)
38+
- Explicitly require `pyproj` instead of `proj` (the latter is now implicitly required) [#845](https://github.com/CLIMADA-project/climada_python/pull/845)
2139

2240
### Fixed
2341

42+
- `Hazard.from_xarray_raster` now stores strings as default values for `Hazard.event_name` [#795](https://github.com/CLIMADA-project/climada_python/pull/795)
2443
- Fix the dist_approx util function when used with method="geosphere" and log=True and points that are very close. [#792](https://github.com/CLIMADA-project/climada_python/pull/792)
44+
- `climada.util.yearsets.sample_from_poisson`: fix a bug ([#819](https://github.com/CLIMADA-project/climada_python/issues/819)) and inconsistency that occurs when lambda events per year (`lam`) are set to 1. [[#823](https://github.com/CLIMADA-project/climada_python/pull/823)]
45+
- In the TropCyclone class in the Holland model 2008 and 2010 implementation, a doublecounting of translational velocity is removed [#833](https://github.com/CLIMADA-project/climada_python/pull/833)
46+
- `climada.util.test.test_finance` and `climada.test.test_engine` updated to recent input data from worldbank [#841](https://github.com/CLIMADA-project/climada_python/pull/841)
47+
- Set `nodefaults` in Conda environment specs because `defaults` are not compatible with conda-forge [#845](https://github.com/CLIMADA-project/climada_python/pull/845)
2548

2649
### Deprecated
2750

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ This is the Python (3.9+) version of CLIMADA - please see [here](https://github.
2121
## Getting started
2222

2323
CLIMADA runs on Windows, macOS and Linux.
24-
The released versions of the CLIMADA core can be installed directly through Anaconda:
24+
The released versions of CLIMADA are available from [conda-forge](https://anaconda.org/conda-forge/climada).
25+
Use the [Mamba](https://mamba.readthedocs.io/en/latest/) package manager to install it:
2526

2627
```shell
27-
conda install -c conda-forge climada
28+
mamba install -c conda-forge climada
2829
```
2930

30-
It is **highly recommended** to install CLIMADA into a **separate** Anaconda environment.
31+
It is **highly recommended** to install CLIMADA into a **separate** Conda environment.
3132
See the [installation guide](https://climada-python.readthedocs.io/en/latest/guide/install.html) for further information.
3233

3334
Follow the [tutorials](https://climada-python.readthedocs.io/en/stable/tutorial/1_main_climada.html) in a Jupyter Notebook to see what can be done with CLIMADA and how.

climada/engine/test/test_cost_benefit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from climada.test import get_test_file
3636

3737

38-
HAZ_TEST_MAT = get_test_file('atl_prob_no_name')
38+
HAZ_TEST_MAT = get_test_file('atl_prob_no_name', file_format='matlab')
3939
ENT_TEST_MAT = get_test_file('demo_today', file_format='MAT-file')
4040

4141

climada/engine/unsequa/test/test_unsequa.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,9 @@
4040
TEST_UNC_OUTPUT_IMPACT, TEST_UNC_OUTPUT_COSTBEN)
4141
from climada.util.api_client import Client
4242

43-
apiclient = Client()
44-
ds = apiclient.get_dataset_info(name=TEST_UNC_OUTPUT_IMPACT, status='test_dataset')
45-
_target_dir, [test_unc_output_impact] = apiclient.download_dataset(ds)
4643

47-
ds = apiclient.get_dataset_info(name=TEST_UNC_OUTPUT_COSTBEN, status='test_dataset')
48-
_target_dir, [test_unc_output_costben] = apiclient.download_dataset(ds)
44+
test_unc_output_impact = Client().get_dataset_file(name=TEST_UNC_OUTPUT_IMPACT, status='test_dataset')
45+
test_unc_output_costben = Client().get_dataset_file(name=TEST_UNC_OUTPUT_COSTBEN, status='test_dataset')
4946

5047

5148
def impf_dem(x_paa=1, x_mdd=1):

climada/entity/disc_rates/base.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,11 @@ def from_mat(cls, file_name, var_names=None):
259259
return cls(years=years, rates=rates)
260260

261261
def read_mat(self, *args, **kwargs):
262-
"""This function is deprecated, use DiscRates.from_mats instead."""
263-
LOGGER.warning("The use of DiscRates.read_mats is deprecated."
264-
"Use DiscRates.from_mats instead.")
262+
"""This function is deprecated, use ``DiscRates.from_mat`` instead."""
263+
LOGGER.warning(
264+
"The use of DiscRates.read_mat is deprecated."
265+
"Use DiscRates.from_mat instead."
266+
)
265267
self.__dict__ = DiscRates.from_mat(*args, **kwargs).__dict__
266268

267269
@classmethod
@@ -307,8 +309,7 @@ def read_excel(self, *args, **kwargs):
307309
"""This function is deprecated, use DiscRates.from_excel instead."""
308310
LOGGER.warning("The use of DiscRates.read_excel is deprecated."
309311
"Use DiscRates.from_excel instead.")
310-
self.__dict__ = DiscRates.from_mat(*args, **kwargs).__dict__
311-
312+
self.__dict__ = DiscRates.from_excel(*args, **kwargs).__dict__
312313

313314
def write_excel(self, file_name, var_names=None):
314315
"""
@@ -341,3 +342,68 @@ def write_excel(self, file_name, var_names=None):
341342
disc_ws.write(i_yr, 0, disc_yr)
342343
disc_ws.write(i_yr, 1, disc_rt)
343344
disc_wb.close()
345+
346+
@classmethod
347+
def from_csv(
348+
cls, file_name, year_column="year", disc_column="discount_rate", **kwargs
349+
):
350+
"""
351+
Read DiscRate from a csv file following template and store variables.
352+
353+
Parameters
354+
----------
355+
file_name: str
356+
filename including path and extension
357+
year_column: str, optional
358+
name of the column that contains the years,
359+
Default: "year"
360+
disc_column: str, optional
361+
name of the column that contains the discount rates,
362+
Default: "discount_rate"
363+
**kwargs:
364+
any additional arguments, e.g., `sep`, `delimiter`, `head`,
365+
are forwarded to ``pandas.read_csv``
366+
367+
Returns
368+
-------
369+
climada.entity.DiscRates :
370+
The disc rates from the csv file
371+
"""
372+
dfr = pd.read_csv(file_name, **kwargs)
373+
try:
374+
years = dfr[year_column].values.astype(int, copy=False)
375+
rates = dfr[disc_column].values
376+
except KeyError as err:
377+
raise ValueError(
378+
f"missing column in csv file ({year_column} or {disc_column})"
379+
) from err
380+
381+
return cls(years=years, rates=rates)
382+
383+
def write_csv(
384+
self, file_name, year_column="year", disc_column="discount_rate", **kwargs
385+
):
386+
"""
387+
Write DiscRate to a csv file following template and store variables.
388+
389+
Parameters
390+
----------
391+
file_name: str
392+
filename including path and extension
393+
year_column: str, optional
394+
name of the column that contains the years,
395+
Default: "year"
396+
disc_column: str, optional
397+
name of the column that contains the discount rates,
398+
Default: "discount_rate"
399+
**kwargs:
400+
any additional arguments, e.g., `sep`, `delimiter`, `head`,
401+
are forwarded to ``pandas.read_csv``
402+
"""
403+
dfr = pd.DataFrame(
404+
{
405+
year_column: self.years,
406+
disc_column: self.rates,
407+
}
408+
)
409+
dfr.to_csv(file_name, **kwargs)

climada/entity/disc_rates/test/test_base.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import unittest
2222
import numpy as np
2323
import copy
24+
from pathlib import Path
25+
from tempfile import TemporaryDirectory
2426

2527
from climada import CONFIG
2628
from climada.entity.disc_rates.base import DiscRates
@@ -216,23 +218,46 @@ def test_demo_file_pass(self):
216218
self.assertEqual(disc_rate.rates.max(), 0.02)
217219

218220

219-
class TestWriter(unittest.TestCase):
220-
"""Test excel reader for discount rates"""
221+
class TestWriteRead(unittest.TestCase):
222+
"""Test file write read cycle for discount rates"""
223+
224+
@classmethod
225+
def setUpClass(cls):
226+
cls._td = TemporaryDirectory()
227+
cls.tempdir = Path(cls._td.name)
228+
229+
@classmethod
230+
def tearDownClass(cls):
231+
cls._td.cleanup()
221232

222-
def test_write_read_pass(self):
233+
def test_write_read_excel_pass(self):
223234
"""Read demo excel file."""
224235
years = np.arange(1950, 2150)
225236
rates = np.ones(years.size) * 0.03
226237
disc_rate = DiscRates(years=years, rates=rates)
227238

228-
file_name = CONFIG.disc_rates.test_data.dir().joinpath('test_disc.xlsx')
239+
file_name = self.tempdir.joinpath('test_disc.xlsx')
229240
disc_rate.write_excel(file_name)
230241

231242
disc_read = DiscRates.from_excel(file_name)
232243

233244
self.assertTrue(np.array_equal(disc_read.years, disc_rate.years))
234245
self.assertTrue(np.array_equal(disc_read.rates, disc_rate.rates))
235246

247+
def test_write_read_csv_pass(self):
248+
"""Write and read csv file."""
249+
years = np.arange(1950, 2150)
250+
rates = np.ones(years.size) * 0.03
251+
disc_rate = DiscRates(years=years, rates=rates)
252+
253+
file_name = self.tempdir.joinpath('test_disc.csv')
254+
disc_rate.write_csv(file_name)
255+
256+
disc_read = DiscRates.from_csv(file_name)
257+
258+
self.assertTrue(np.array_equal(disc_read.years, disc_rate.years))
259+
self.assertTrue(np.array_equal(disc_read.rates, disc_rate.rates))
260+
236261

237262
# Execute Tests
238263
if __name__ == "__main__":
@@ -243,5 +268,5 @@ def test_write_read_pass(self):
243268
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestNetPresValue))
244269
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestReaderExcel))
245270
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestReaderMat))
246-
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWriter))
271+
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWriteRead))
247272
unittest.TextTestRunner(verbosity=2).run(TESTS)

climada/entity/measures/test/test_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@
3333
from climada.entity.measures.measure_set import MeasureSet
3434
from climada.entity.measures.base import Measure, IMPF_ID_FACT
3535
from climada.util.constants import EXP_DEMO_H5, HAZ_DEMO_H5
36+
from climada.test import get_test_file
3637
import climada.util.coordinates as u_coord
37-
import climada.hazard.test as hazard_test
3838
import climada.entity.exposures.test as exposures_test
3939

4040
DATA_DIR = CONFIG.measures.test_data.dir()
4141

42-
HAZ_TEST_MAT = Path(hazard_test.__file__).parent / 'data' / 'atl_prob_no_name.mat'
42+
HAZ_TEST_MAT = get_test_file('atl_prob_no_name', file_format='matlab')
4343
ENT_TEST_MAT = Path(exposures_test.__file__).parent / 'data' / 'demo_today.mat'
4444

4545
class TestApply(unittest.TestCase):

climada/hazard/base.py

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -463,12 +463,13 @@ def from_xarray_raster(
463463
):
464464
"""Read raster-like data from an xarray Dataset
465465
466-
This method reads data that can be interpreted using three coordinates for event,
467-
latitude, and longitude. The data and the coordinates themselves may be organized
468-
in arbitrary dimensions in the Dataset (e.g. three dimensions 'year', 'month',
469-
'day' for the coordinate 'event'). The three coordinates to be read can be
470-
specified via the ``coordinate_vars`` parameter. See Notes and Examples if you
471-
want to load single-event data that does not contain an event dimension.
466+
This method reads data that can be interpreted using three coordinates: event,
467+
latitude, and longitude. The names of the coordinates to be read from the
468+
dataset can be specified via the ``coordinate_vars`` parameter. The data and the
469+
coordinates themselves may be organized in arbitrary dimensions (e.g. two
470+
dimensions 'year' and 'altitude' for the coordinate 'event'). See Notes and
471+
Examples if you want to load single-event data that does not contain an event
472+
dimension.
472473
473474
The only required data is the intensity. For all other data, this method can
474475
supply sensible default values. By default, this method will try to find these
@@ -513,12 +514,14 @@ def from_xarray_raster(
513514
514515
Default values are:
515516
516-
* ``date``: The ``event`` coordinate interpreted as date
517+
* ``date``: The ``event`` coordinate interpreted as date or ordinal, or
518+
ones if that fails (which will issue a warning).
517519
* ``fraction``: ``None``, which results in a value of 1.0 everywhere, see
518520
:py:meth:`Hazard.__init__` for details.
519521
* ``hazard_type``: Empty string
520522
* ``frequency``: 1.0 for every event
521-
* ``event_name``: String representation of the event time
523+
* ``event_name``: String representation of the event date or empty strings
524+
if that fails (which will issue a warning).
522525
* ``event_id``: Consecutive integers starting at 1 and increasing with time
523526
crs : str, optional
524527
Identifier for the coordinate reference system of the coordinates. Defaults
@@ -553,13 +556,16 @@ def from_xarray_raster(
553556
and Examples) before loading the Dataset as Hazard.
554557
* Single-valued data for variables ``frequency``. ``event_name``, and
555558
``event_date`` will be broadcast to every event.
559+
* The ``event`` coordinate may take arbitrary values. In case these values
560+
cannot be interpreted as dates or date ordinals, the default values for
561+
``Hazard.date`` and ``Hazard.event_name`` are used, see the
562+
``data_vars``` parameter documentation above.
556563
* To avoid confusion in the call signature, several parameters are keyword-only
557564
arguments.
558565
* The attributes ``Hazard.haz_type`` and ``Hazard.unit`` currently cannot be
559566
read from the Dataset. Use the method parameters to set these attributes.
560567
* This method does not read coordinate system metadata. Use the ``crs`` parameter
561568
to set a custom coordinate system identifier.
562-
* This method **does not** read lazily. Single data arrays must fit into memory.
563569
564570
Examples
565571
--------
@@ -802,14 +808,48 @@ def strict_positive_int_accessor(array: xr.DataArray) -> np.ndarray:
802808
raise ValueError(f"'{array.name}' data must be larger than zero")
803809
return array.values
804810

805-
def date_to_ordinal_accessor(array: xr.DataArray) -> np.ndarray:
811+
def date_to_ordinal_accessor(
812+
array: xr.DataArray, strict: bool = True
813+
) -> np.ndarray:
806814
"""Take a DataArray and transform it into ordinals"""
807-
if np.issubdtype(array.dtype, np.integer):
808-
# Assume that data is ordinals
809-
return strict_positive_int_accessor(array)
815+
try:
816+
if np.issubdtype(array.dtype, np.integer):
817+
# Assume that data is ordinals
818+
return strict_positive_int_accessor(array)
819+
820+
# Try transforming to ordinals
821+
return np.array(u_dt.datetime64_to_ordinal(array.values))
822+
823+
# Handle access errors
824+
except (ValueError, TypeError) as err:
825+
if strict:
826+
raise err
827+
828+
LOGGER.warning(
829+
"Failed to read values of '%s' as dates or ordinals. Hazard.date "
830+
"will be ones only",
831+
array.name,
832+
)
833+
return np.ones(array.shape)
834+
835+
def year_month_day_accessor(
836+
array: xr.DataArray, strict: bool = True
837+
) -> np.ndarray:
838+
"""Take an array and return am array of YYYY-MM-DD strings"""
839+
try:
840+
return array.dt.strftime("%Y-%m-%d").values
841+
842+
# Handle access errors
843+
except (ValueError, TypeError) as err:
844+
if strict:
845+
raise err
810846

811-
# Try transforming to ordinals
812-
return np.array(u_dt.datetime64_to_ordinal(array.values))
847+
LOGGER.warning(
848+
"Failed to read values of '%s' as dates. Hazard.event_name will be "
849+
"empty strings",
850+
array.name,
851+
)
852+
return np.full(array.shape, "")
813853

814854
def maybe_repeat(values: np.ndarray, times: int) -> np.ndarray:
815855
"""Return the array or repeat a single-valued array
@@ -840,8 +880,12 @@ def maybe_repeat(values: np.ndarray, times: int) -> np.ndarray:
840880
None,
841881
np.ones(num_events),
842882
np.array(range(num_events), dtype=int) + 1,
843-
list(data[coords["event"]].values),
844-
np.array(u_dt.datetime64_to_ordinal(data[coords["event"]].values)),
883+
list(
884+
year_month_day_accessor(
885+
data[coords["event"]], strict=False
886+
).flat
887+
),
888+
date_to_ordinal_accessor(data[coords["event"]], strict=False),
845889
],
846890
# The accessor for the data in the Dataset
847891
accessor=[

0 commit comments

Comments
 (0)