Simulation period#
Six attributes on Catchment used to describe one thing: start, end and
temporal_resolution were given, and date_index, dt and conversion_factor were derived
from them in the constructor and then stored beside them as if they were independent. Storing a
derivation is how the three drift apart — reassigning end left date_index describing the old
span, with nothing to notice — and it is why the same pd.date_range branch was written out four
times across the package.
SimulationPeriod holds the three inputs and derives the rest on read, so they cannot disagree.
It is frozen: a run covers the period it was built for, and a model that needs a different one
gets a new period rather than a mutated one.
model.period.date_index # one entry per step
model.period.days # how many steps
len(model.period) # the same number
SimulationPeriod#
hapi.period.SimulationPeriod
dataclass
#
The span a run covers, and the calendar and unit factors it implies.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
datetime
|
First step of the simulation. |
end |
datetime
|
Last step of the simulation. |
temporal_resolution |
TemporalResolution
|
|
Examples:
- The calendar is derived, so it always matches the span:
>>> from hapi.period import SimulationPeriod >>> period = SimulationPeriod.parse("2009-01-01", "2009-01-10") >>> len(period) 10 >>> period.date_index[0].strftime("%Y-%m-%d") '2009-01-01' - An hourly period covers the same span with a different step:
>>> from hapi.period import SimulationPeriod >>> hourly = SimulationPeriod.parse( ... "2009-01-01", "2009-01-02", temporal_resolution="Hourly" ... ) >>> len(hourly) 25 >>> round(hourly.conversion_factor, 1) 3.6 - It is frozen, so a derived value can never be left describing a different span:
>>> from hapi.period import SimulationPeriod >>> period = SimulationPeriod.parse("2009-01-01", "2009-01-10") >>> period.end = "2010-01-01" # doctest: +ELLIPSIS Traceback (most recent call last): ... dataclasses.FrozenInstanceError: cannot assign to field 'end'
Source code in src/hapi/period.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
__len__() -> int
#
int: Number of steps, so len(period) reads as the span.
Source code in src/hapi/period.py
246 247 248 | |
__post_init__()
#
Normalise the resolution and check the span runs forwards.
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
The resolution is not one of :data: |
Source code in src/hapi/period.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
conversion_factor: float
property
#
float: Depth-to-discharge factor -- mm over the catchment to m3/s at this step.
It is the number of seconds in a step divided by 1000, so an hourly step is a twenty-fourth of a daily one.
Examples:
- The two resolutions differ by exactly a factor of 24:
>>> from hapi.period import SimulationPeriod >>> daily = SimulationPeriod.parse("2009-01-01", "2009-01-10") >>> hourly = SimulationPeriod.parse( ... "2009-01-01", "2009-01-02", temporal_resolution="Hourly" ... ) >>> daily.conversion_factor 86.4 >>> round(daily.conversion_factor / hourly.conversion_factor, 1) 24.0
date_index: pd.DatetimeIndex
cached
property
#
pandas.DatetimeIndex: One entry per step, from :attr:start to :attr:end.
Derived rather than stored: this is the value that used to be computed in the
constructor and could then outlive a change to the span it described. Cached rather
than rebuilt, which the frozen class makes safe -- the inputs it derives from cannot
change, so the cache cannot go stale. It is read once per from_model (so once per
calibration trial) and twice per SimulationResults._step_bounds call.
Examples:
- One entry per step, inclusive of both ends:
>>> from hapi.period import SimulationPeriod >>> period = SimulationPeriod.parse("2009-01-01", "2009-01-05") >>> [step.strftime("%m-%d") for step in period.date_index] ['01-01', '01-02', '01-03', '01-04', '01-05'] - Built once and handed back, because the span it describes cannot change:
>>> from hapi.period import SimulationPeriod >>> period = SimulationPeriod.parse("2009-01-01", "2009-12-31") >>> period.date_index is period.date_index True
days: int
property
#
int: Number of steps the period covers.
dt: float
property
#
float: The routing time-step factor.
One for both resolutions today. It is a property rather than a stored 1 so the
Muskingum routing has a single place to read it from; whether an hourly run should
route with a different value is an open question, filed as issue #218 rather than
silently answered here.
Note that the catchment routing reads this while the lake paths in
:mod:hapi.wrapper pass :attr:conversion_factor (86.4 daily) into the same dt
parameter of the same Routing.muskingum_v. Both predate this class and neither was
changed when it was extracted, so one of the two is routing on a time step 86.4x off
from the other. Which one is the same physics question as issue #218.
freq: str
property
#
str: The pandas offset alias for this resolution.
Examples:
- Each supported resolution maps to the alias
pd.date_rangeexpects:>>> from hapi.period import SimulationPeriod >>> SimulationPeriod.parse("2009-01-01", "2009-01-10").freq 'D' >>> SimulationPeriod.parse( ... "2009-01-01", "2009-01-02", temporal_resolution="Hourly" ... ).freq 'h'
parse(start: str, end: str, fmt: str = '%Y-%m-%d', temporal_resolution: str = 'Daily') -> SimulationPeriod
classmethod
#
Build a period from the string dates a configuration or a script supplies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
str
|
Start date. |
required |
end
|
str
|
End date. |
required |
fmt
|
str
|
|
'%Y-%m-%d'
|
temporal_resolution
|
str
|
|
'Daily'
|
Returns:
| Type | Description |
|---|---|
SimulationPeriod
|
The parsed period. |
Raises:
| Type | Description |
|---|---|
ValueError
|
A date does not match |
Examples:
>>> from hapi.period import SimulationPeriod
>>> SimulationPeriod.parse("01/2009/01", "10/2009/01", fmt="%d/%Y/%m").days
10
Source code in src/hapi/period.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |