Skip to content

Commit b09bd89

Browse files
committed
Removes from_triplet logic, better date handling, improves tests.
1 parent f334b8c commit b09bd89

2 files changed

Lines changed: 59 additions & 87 deletions

File tree

climada/trajectories/snapshot.py

Lines changed: 49 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
import copy
2727
import datetime
2828
import logging
29-
import warnings
29+
from typing import cast
3030

31+
import numpy as np
3132
import pandas as pd
3233

3334
from climada.entity.exposures import Exposures
@@ -52,6 +53,10 @@ class Snapshot:
5253
date : datetime.date | str | pd.Timestamp
5354
The date of the Snapshot, it can be an string representing a year,
5455
a datetime object or a string representation of a datetime object.
56+
measure : Measure | None, default None.
57+
Measure associated with the Snapshot. The measure object is *not* applied
58+
to the other parameters of the object (Exposure, Hazard, Impfset).
59+
Users should leave it to None, and use `apply_measure()` instead (see notes).
5560
ref_only : bool, default False
5661
Should the `Snapshot` contain deep copies of the Exposures, Hazard and Impfset (False)
5762
or references only (True).
@@ -61,19 +66,27 @@ class Snapshot:
6166
date : datetime
6267
Date of the snapshot.
6368
measure: Measure | None
64-
The possible measure applied to the snapshot.
69+
A possible measure associated with the snapshot.
6570
6671
Notes
6772
-----
6873
69-
The object creates deep copies of the exposure hazard and impact function set.
74+
Providing a measure to the init assumes that the (Exposure, Hazard, Impfset) triplet
75+
already corresponds to the triplet once the measure is applied. As Measure objects
76+
contain "the changes to apply", the creating a consistent Snapshot with a measure should
77+
be done by first creating a Snapshot with the "baseline" (Exposure, Hazard, Impfset) triplet
78+
and calling `<Snapshot>.apply_measure(<measure>)`, which returns a new Snapshot object
79+
with the measure applied.
80+
81+
Instantiating a Snapshot with a measure directly does not garantee the
82+
consistency between the triplet and the measure, and should be avoided.
83+
84+
If `ref_only` is True (default) the object creates deep copies of the
85+
exposure, hazard, and impact function set.
7086
7187
Also note that exposure, hazard and impfset are read-only properties.
72-
Consider snapshot as immutable objects.
88+
Consider snapshots as immutable objects.
7389
74-
To create a snapshot with a measure, create a snapshot `snap` without
75-
the measure and call `snap.apply_measure(measure)`, which returns a new Snapshot object
76-
with the measure applied to its risk dimensions.
7790
"""
7891

7992
def __init__(
@@ -82,74 +95,16 @@ def __init__(
8295
exposure: Exposures,
8396
hazard: Hazard,
8497
impfset: ImpactFuncSet,
85-
measure: Measure | None,
8698
date: datetime.date | str | pd.Timestamp,
99+
measure: Measure | None = None,
87100
ref_only: bool = False,
88-
_from_factory: bool = False,
89101
) -> None:
90-
if not _from_factory:
91-
warnings.warn(
92-
"Direct instantiation of 'Snapshot' is discouraged. "
93-
"Use 'Snapshot.from_triplet()' instead.",
94-
UserWarning,
95-
stacklevel=2,
96-
)
97102
self._exposure = exposure if ref_only else copy.deepcopy(exposure)
98103
self._hazard = hazard if ref_only else copy.deepcopy(hazard)
99104
self._impfset = impfset if ref_only else copy.deepcopy(impfset)
100105
self._measure = measure if ref_only else copy.deepcopy(measure)
101106
self._date = self._convert_to_timestamp(date)
102107

103-
@classmethod
104-
def from_triplet(
105-
cls,
106-
*,
107-
exposure: Exposures,
108-
hazard: Hazard,
109-
impfset: ImpactFuncSet,
110-
date: datetime.date | str | pd.Timestamp,
111-
ref_only: bool = False,
112-
) -> "Snapshot":
113-
"""Create a Snapshot from exposure, hazard and impact functions set
114-
115-
This method is the main point of entry for the creation of Snapshot. It
116-
creates a new Snapshot object for the given date with copies of the
117-
hazard, exposure and impact function set given in argument (or
118-
references if ref_only is True)
119-
120-
Parameters
121-
----------
122-
exposure : Exposures
123-
hazard : Hazard
124-
impfset : ImpactFuncSet
125-
date : datetime.date | str | pd.Timestamp
126-
ref_only : bool
127-
If true, uses references to the exposure, hazard and impact
128-
function objects. Note that modifying the original objects after
129-
computations using the Snapshot might lead to inconsistencies in
130-
results.
131-
132-
Returns
133-
-------
134-
Snapshot
135-
136-
Notes
137-
-----
138-
139-
To create a Snapshot with a measure, first create the Snapshot without
140-
the measure using this method, and use `apply_measure(measure)` afterward.
141-
142-
"""
143-
return cls(
144-
exposure=exposure,
145-
hazard=hazard,
146-
impfset=impfset,
147-
measure=None,
148-
date=date,
149-
ref_only=ref_only,
150-
_from_factory=True,
151-
)
152-
153108
@property
154109
def exposure(self) -> Exposures:
155110
"""Exposure data for the snapshot."""
@@ -176,7 +131,7 @@ def date(self) -> pd.Timestamp:
176131
return self._date
177132

178133
@property
179-
def impact_calc_data(self) -> dict:
134+
def impact_calc_kwargs(self) -> dict:
180135
"""Convenience function for ImpactCalc class."""
181136
return {
182137
"exposures": self.exposure,
@@ -185,20 +140,36 @@ def impact_calc_data(self) -> dict:
185140
}
186141

187142
@staticmethod
188-
def _convert_to_timestamp(date_arg) -> pd.Timestamp:
189-
"""Convert date argument of type str or datetime.date to pandas Timestamp object."""
143+
def _convert_to_timestamp(
144+
date_arg: str | datetime.date | pd.Timestamp | np.datetime64,
145+
) -> pd.Timestamp:
146+
"""
147+
Convert date argument of type str, datetime.date,
148+
np.datetime64, or pandas Timestamp to a pandas Timestamp object.
149+
"""
190150
if isinstance(date_arg, str):
191-
# Try to parse the string as a date
192151
try:
193-
return pd.Timestamp(date_arg)
194-
except ValueError as exc:
195-
raise ValueError("String must be in the format 'YYYY-MM-DD'") from exc
196-
if isinstance(date_arg, datetime.date):
197-
return pd.Timestamp(date_arg)
198-
if isinstance(date_arg, pd.Timestamp):
199-
return date_arg
152+
date = pd.Timestamp(date_arg)
153+
except (ValueError, TypeError) as exc:
154+
raise ValueError(
155+
"String must be in a valid date format (e.g., 'YYYY-MM-DD')"
156+
) from exc
157+
158+
elif isinstance(date_arg, (datetime.date, pd.Timestamp, np.datetime64)):
159+
date = pd.Timestamp(date_arg)
160+
161+
else:
162+
raise TypeError(
163+
f"Unsupported type: {type(date_arg)}. Must be str, date, Timestamp, or datetime64."
164+
)
165+
166+
# Final check for NaT (Not-a-Time)
167+
if date is pd.NaT:
168+
raise ValueError(
169+
f"Could not resolve '{date_arg}' to a valid Pandas Timestamp."
170+
)
200171

201-
raise TypeError("date_arg must be an str, datetime.date or pandas.Timestamp")
172+
return cast(pd.Timestamp, date)
202173

203174
def apply_measure(self, measure: Measure) -> "Snapshot":
204175
"""Create a new snapshot by applying a Measure object.
@@ -226,6 +197,5 @@ def apply_measure(self, measure: Measure) -> "Snapshot":
226197
date=self.date,
227198
measure=measure,
228199
ref_only=True, # Avoid unecessary copies of new objects
229-
_from_factory=True,
230200
)
231201
return snap

climada/trajectories/test/test_snapshot.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,14 @@ def test_not_from_factory_warning(mock_context):
8181
[
8282
("2023", pd.Timestamp(2023, 1, 1)),
8383
("2023-01-01", pd.Timestamp(2023, 1, 1)),
84+
(np.datetime64("2023-01-01"), pd.Timestamp(2023, 1, 1)),
8485
(datetime.date(2023, 1, 1), pd.Timestamp(2023, 1, 1)),
86+
(pd.Timestamp(2023, 1, 1), pd.Timestamp(2023, 1, 1)),
8587
],
8688
)
8789
def test_init_valid_dates(mock_context, input_date, expected):
8890
"""Test various valid date input formats using parametrization."""
89-
snapshot = Snapshot.from_triplet(
91+
snapshot = Snapshot(
9092
exposure=mock_context["exp"],
9193
hazard=mock_context["haz"],
9294
impfset=mock_context["imp"],
@@ -97,7 +99,7 @@ def test_init_valid_dates(mock_context, input_date, expected):
9799

98100
def test_init_invalid_date_format(mock_context):
99101
with pytest.raises(ValueError, match="String must be in the format"):
100-
Snapshot.from_triplet(
102+
Snapshot(
101103
exposure=mock_context["exp"],
102104
hazard=mock_context["haz"],
103105
impfset=mock_context["imp"],
@@ -110,16 +112,16 @@ def test_init_invalid_date_type(mock_context):
110112
TypeError,
111113
match=r"date_arg must be an str, datetime.date or pandas.Timestamp",
112114
):
113-
Snapshot.from_triplet(
115+
Snapshot(
114116
exposure=mock_context["exp"],
115117
hazard=mock_context["haz"],
116118
impfset=mock_context["imp"],
117-
date=2023.5,
118-
) # type: ignore
119+
date=2023.5, # type: ignore
120+
)
119121

120122

121123
def test_properties(mock_context):
122-
snapshot = Snapshot.from_triplet(
124+
snapshot = Snapshot(
123125
exposure=mock_context["exp"],
124126
hazard=mock_context["haz"],
125127
impfset=mock_context["imp"],
@@ -140,7 +142,7 @@ def test_properties(mock_context):
140142

141143

142144
def test_reference(mock_context):
143-
snapshot = Snapshot.from_triplet(
145+
snapshot = Snapshot(
144146
exposure=mock_context["exp"],
145147
hazard=mock_context["haz"],
146148
impfset=mock_context["imp"],
@@ -156,7 +158,7 @@ def test_reference(mock_context):
156158

157159

158160
def test_apply_measure(mock_context):
159-
snapshot = Snapshot.from_triplet(
161+
snapshot = Snapshot(
160162
exposure=mock_context["exp"],
161163
hazard=mock_context["haz"],
162164
impfset=mock_context["imp"],

0 commit comments

Comments
 (0)