diff --git a/climada/hazard/forecast.py b/climada/hazard/forecast.py index 61095539ba..c2705134a0 100644 --- a/climada/hazard/forecast.py +++ b/climada/hazard/forecast.py @@ -18,3 +18,74 @@ Define Forecast variant of Hazard. """ + +import logging + +import numpy as np + +from climada.hazard.base import Hazard +from climada.util.forecast import Forecast + +LOGGER = logging.getLogger(__name__) + + +class HazardForecast(Forecast, Hazard): + """A hazard object with forecast information""" + + def __init__( + self, + lead_time: np.ndarray | None = None, + member: np.ndarray | None = None, + **hazard_kwargs, + ): + """ + Initialize a HazardForecast object. + + Parameters + ---------- + lead_time : np.ndarray of np.timedelta64 or None, optional + Forecast lead times. Default is empty array. + member : np.ndarray or None, optional + Ensemble member identifiers as integers. Default is empty array. + **hazard_kwargs + keyword arguments to pass to :py:class:`~climada.hazard.base.Hazard` See + py:meth`~climada.hazard.base.Hazard.__init__` for details. + """ + super().__init__(lead_time=lead_time, member=member, **hazard_kwargs) + + @classmethod + def from_hazard(cls, hazard: Hazard, lead_time: np.ndarray, member: np.ndarray): + """ + Create a HazardForecast object from a Hazard object. + + Parameters + ---------- + hazard : climada.hazard.base.Hazard + Hazard object to convert into a HazardForecast. + lead_time : np.ndarray of np.timedelta64 or None, optional + Forecast lead times. Default is empty array. + member : np.ndarray or None, optional + Ensemble member identifiers as integers. Default is empty array. + + Returns + ------- + HazardForecast + A HazardForecast object with the same attributes as the input hazard, + but with lead_time and member attributes set from instance of HazardForecast. + """ + return cls( + lead_time=lead_time, + member=member, + haz_type=hazard.haz_type, + pool=hazard.pool, + units=hazard.units, + centroids=hazard.centroids, + event_id=hazard.event_id, + frequency=hazard.frequency, + frequency_unit=hazard.frequency_unit, + event_name=hazard.event_name, + date=hazard.date, + orig=hazard.orig, + intensity=hazard.intensity, + fraction=hazard.fraction, + ) diff --git a/climada/hazard/test/test_base.py b/climada/hazard/test/test_base.py index b4cbafd0e3..ea607893dd 100644 --- a/climada/hazard/test/test_base.py +++ b/climada/hazard/test/test_base.py @@ -46,27 +46,29 @@ """ +def hazard_kwargs(): + return { + "haz_type": "TC", + "pool": None, + "intensity": sparse.csr_matrix( + [[0.2, 0.3, 0.4], [0.1, 0.1, 0.01], [4.3, 2.1, 1.0], [5.3, 0.2, 0.0]] + ), + "fraction": sparse.csr_matrix( + [[0.02, 0.03, 0.04], [0.01, 0.01, 0.01], [0.3, 0.1, 0.0], [0.3, 0.2, 0.0]] + ), + "centroids": Centroids(lat=np.array([1, 3, 5]), lon=np.array([2, 4, 6])), + "event_id": np.array([1, 2, 3, 4]), + "event_name": ["ev1", "ev2", "ev3", "ev4"], + "date": np.array([1, 2, 3, 4]), + "orig": np.array([True, False, False, True]), + "frequency": np.array([0.1, 0.5, 0.5, 0.2]), + "frequency_unit": "1/week", + "units": "m/s", + } + + def dummy_hazard(): - fraction = sparse.csr_matrix( - [[0.02, 0.03, 0.04], [0.01, 0.01, 0.01], [0.3, 0.1, 0.0], [0.3, 0.2, 0.0]] - ) - intensity = sparse.csr_matrix( - [[0.2, 0.3, 0.4], [0.1, 0.1, 0.01], [4.3, 2.1, 1.0], [5.3, 0.2, 0.0]] - ) - - return Hazard( - "TC", - intensity=intensity, - fraction=fraction, - centroids=Centroids(lat=np.array([1, 3, 5]), lon=np.array([2, 4, 6])), - event_id=np.array([1, 2, 3, 4]), - event_name=["ev1", "ev2", "ev3", "ev4"], - date=np.array([1, 2, 3, 4]), - orig=np.array([True, False, False, True]), - frequency=np.array([0.1, 0.5, 0.5, 0.2]), - frequency_unit="1/week", - units="m/s", - ) + return Hazard(**hazard_kwargs()) class TestLoader(unittest.TestCase): diff --git a/climada/hazard/test/test_forecast.py b/climada/hazard/test/test_forecast.py index f895711f83..cd4acda21a 100644 --- a/climada/hazard/test/test_forecast.py +++ b/climada/hazard/test/test_forecast.py @@ -19,28 +19,72 @@ Tests for Hazard Forecast. """ +import numpy as np +import numpy.testing as npt +import pandas as pd import pytest +from scipy.sparse import csr_matrix -from climada.hazard import Hazard +from climada.hazard.base import Hazard +from climada.hazard.forecast import HazardForecast +from climada.hazard.test.test_base import hazard_kwargs # --- Examples for fixtures and test organization --- # @pytest.fixture -def hazard(): - return Hazard() +def haz_kwargs(): + return hazard_kwargs() -def test_empty_hazard(hazard): - assert hazard.size == 0 - assert hazard.haz_type == "" +@pytest.fixture +def hazard(haz_kwargs): + return Hazard(**haz_kwargs) + + +@pytest.fixture +def lead_time(): + return pd.timedelta_range("1h", periods=6).to_numpy() -class TestSomething: +@pytest.fixture +def member(): + return np.arange(6) - @pytest.fixture(autouse=True) - def haz_type(self, hazard): - hazard.haz_type = "foo" - def test_haz_type(self, hazard): - assert hazard.haz_type == "foo" +@pytest.fixture +def haz_fc(lead_time, member, haz_kwargs): + return HazardForecast( + lead_time=lead_time, + member=member, + **haz_kwargs, + ) + + +def assert_hazard_kwargs(hazard: Hazard, **kwargs): + for key, value in kwargs.items(): + attr = getattr(hazard, key) + if isinstance(value, (np.ndarray, list)): + npt.assert_array_equal(attr, value) + elif isinstance(value, csr_matrix): + npt.assert_array_equal(attr.todense(), value.todense()) + else: + assert attr == value + + +def test_init_hazard_forecast(haz_fc, member, lead_time, haz_kwargs): + assert isinstance(haz_fc, HazardForecast) + npt.assert_array_equal(haz_fc.lead_time, lead_time) + assert haz_fc.lead_time.dtype == lead_time.dtype + npt.assert_array_equal(haz_fc.member, member) + assert_hazard_kwargs(haz_fc, **haz_kwargs) + + +def test_from_hazard(lead_time, member, hazard, haz_kwargs): + haz_fc_from_haz = HazardForecast.from_hazard( + hazard, lead_time=lead_time, member=member + ) + assert isinstance(haz_fc_from_haz, HazardForecast) + npt.assert_array_equal(haz_fc_from_haz.lead_time, lead_time) + npt.assert_array_equal(haz_fc_from_haz.member, member) + assert_hazard_kwargs(haz_fc_from_haz, **haz_kwargs)