fram-core 0.0.0__py3-none-any.whl → 0.1.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fram_core-0.1.0a2.dist-info/METADATA +42 -0
- fram_core-0.1.0a2.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0a2.dist-info}/WHEEL +1 -2
- fram_core-0.1.0a2.dist-info/licenses/LICENSE.md +8 -0
- framcore/Base.py +142 -0
- framcore/Model.py +73 -0
- framcore/__init__.py +9 -0
- framcore/aggregators/Aggregator.py +153 -0
- framcore/aggregators/HydroAggregator.py +837 -0
- framcore/aggregators/NodeAggregator.py +495 -0
- framcore/aggregators/WindSolarAggregator.py +323 -0
- framcore/aggregators/__init__.py +13 -0
- framcore/aggregators/_utils.py +184 -0
- framcore/attributes/Arrow.py +305 -0
- framcore/attributes/ElasticDemand.py +90 -0
- framcore/attributes/ReservoirCurve.py +37 -0
- framcore/attributes/SoftBound.py +19 -0
- framcore/attributes/StartUpCost.py +54 -0
- framcore/attributes/Storage.py +146 -0
- framcore/attributes/TargetBound.py +18 -0
- framcore/attributes/__init__.py +65 -0
- framcore/attributes/hydro/HydroBypass.py +42 -0
- framcore/attributes/hydro/HydroGenerator.py +83 -0
- framcore/attributes/hydro/HydroPump.py +156 -0
- framcore/attributes/hydro/HydroReservoir.py +27 -0
- framcore/attributes/hydro/__init__.py +13 -0
- framcore/attributes/level_profile_attributes.py +714 -0
- framcore/components/Component.py +112 -0
- framcore/components/Demand.py +130 -0
- framcore/components/Flow.py +167 -0
- framcore/components/HydroModule.py +330 -0
- framcore/components/Node.py +76 -0
- framcore/components/Thermal.py +204 -0
- framcore/components/Transmission.py +183 -0
- framcore/components/_PowerPlant.py +81 -0
- framcore/components/__init__.py +22 -0
- framcore/components/wind_solar.py +67 -0
- framcore/curves/Curve.py +44 -0
- framcore/curves/LoadedCurve.py +155 -0
- framcore/curves/__init__.py +9 -0
- framcore/events/__init__.py +21 -0
- framcore/events/events.py +51 -0
- framcore/expressions/Expr.py +490 -0
- framcore/expressions/__init__.py +28 -0
- framcore/expressions/_get_constant_from_expr.py +483 -0
- framcore/expressions/_time_vector_operations.py +615 -0
- framcore/expressions/_utils.py +73 -0
- framcore/expressions/queries.py +423 -0
- framcore/expressions/units.py +207 -0
- framcore/fingerprints/__init__.py +11 -0
- framcore/fingerprints/fingerprint.py +293 -0
- framcore/juliamodels/JuliaModel.py +161 -0
- framcore/juliamodels/__init__.py +7 -0
- framcore/loaders/__init__.py +10 -0
- framcore/loaders/loaders.py +407 -0
- framcore/metadata/Div.py +73 -0
- framcore/metadata/ExprMeta.py +50 -0
- framcore/metadata/LevelExprMeta.py +17 -0
- framcore/metadata/Member.py +55 -0
- framcore/metadata/Meta.py +44 -0
- framcore/metadata/__init__.py +15 -0
- framcore/populators/Populator.py +108 -0
- framcore/populators/__init__.py +7 -0
- framcore/querydbs/CacheDB.py +50 -0
- framcore/querydbs/ModelDB.py +34 -0
- framcore/querydbs/QueryDB.py +45 -0
- framcore/querydbs/__init__.py +11 -0
- framcore/solvers/Solver.py +48 -0
- framcore/solvers/SolverConfig.py +272 -0
- framcore/solvers/__init__.py +9 -0
- framcore/timeindexes/AverageYearRange.py +20 -0
- framcore/timeindexes/ConstantTimeIndex.py +17 -0
- framcore/timeindexes/DailyIndex.py +21 -0
- framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
- framcore/timeindexes/HourlyIndex.py +21 -0
- framcore/timeindexes/IsoCalendarDay.py +31 -0
- framcore/timeindexes/ListTimeIndex.py +197 -0
- framcore/timeindexes/ModelYear.py +17 -0
- framcore/timeindexes/ModelYears.py +18 -0
- framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
- framcore/timeindexes/ProfileTimeIndex.py +32 -0
- framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
- framcore/timeindexes/TimeIndex.py +90 -0
- framcore/timeindexes/WeeklyIndex.py +21 -0
- framcore/timeindexes/__init__.py +36 -0
- framcore/timevectors/ConstantTimeVector.py +135 -0
- framcore/timevectors/LinearTransformTimeVector.py +114 -0
- framcore/timevectors/ListTimeVector.py +123 -0
- framcore/timevectors/LoadedTimeVector.py +104 -0
- framcore/timevectors/ReferencePeriod.py +41 -0
- framcore/timevectors/TimeVector.py +94 -0
- framcore/timevectors/__init__.py +17 -0
- framcore/utils/__init__.py +36 -0
- framcore/utils/get_regional_volumes.py +369 -0
- framcore/utils/get_supported_components.py +60 -0
- framcore/utils/global_energy_equivalent.py +46 -0
- framcore/utils/isolate_subnodes.py +163 -0
- framcore/utils/loaders.py +97 -0
- framcore/utils/node_flow_utils.py +236 -0
- framcore/utils/storage_subsystems.py +107 -0
- fram_core-0.0.0.dist-info/METADATA +0 -5
- fram_core-0.0.0.dist-info/RECORD +0 -4
- fram_core-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from datetime import datetime, timedelta, tzinfo
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
import framcore.expressions._time_vector_operations as v_ops
|
|
10
|
+
from framcore.fingerprints import Fingerprint
|
|
11
|
+
from framcore.timeindexes.TimeIndex import TimeIndex # NB! full import path needed for inheritance to work
|
|
12
|
+
from framcore.timevectors import ReferencePeriod
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FixedFrequencyTimeIndex(TimeIndex):
|
|
16
|
+
"""TimeIndex with fixed frequency."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
start_time: datetime,
|
|
21
|
+
period_duration: timedelta,
|
|
22
|
+
num_periods: int,
|
|
23
|
+
is_52_week_years: bool,
|
|
24
|
+
extrapolate_first_point: bool,
|
|
25
|
+
extrapolate_last_point: bool,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Initialize a FixedFrequencyTimeIndex.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
start_time : datetime
|
|
33
|
+
The starting datetime of the time index.
|
|
34
|
+
period_duration : timedelta
|
|
35
|
+
The duration of each period.
|
|
36
|
+
num_periods : int
|
|
37
|
+
The number of periods in the time index.
|
|
38
|
+
is_52_week_years : bool
|
|
39
|
+
Whether to use 52-week years.
|
|
40
|
+
extrapolate_first_point : bool
|
|
41
|
+
Whether to allow extrapolation of the first point.
|
|
42
|
+
extrapolate_last_point : bool
|
|
43
|
+
Whether to allow extrapolation of the last point.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
if num_periods < 0:
|
|
47
|
+
message = f"num_periods must be a positive integer. Got {num_periods}."
|
|
48
|
+
raise ValueError(message)
|
|
49
|
+
if period_duration < timedelta(seconds=1):
|
|
50
|
+
message = f"period_duration must be at least one second. Got {period_duration}."
|
|
51
|
+
raise ValueError(message)
|
|
52
|
+
if not period_duration.total_seconds().is_integer():
|
|
53
|
+
message = f"period_duration must be a whole number of seconds, got {period_duration.total_seconds()} s"
|
|
54
|
+
raise ValueError(message)
|
|
55
|
+
if is_52_week_years and start_time.isocalendar().week == 53: # original: assert start_time.isocalendar().week != 53 # noqa: PLR2004
|
|
56
|
+
raise ValueError("Week of start_time must not be 53 when is_52_week_years is True.")
|
|
57
|
+
self._check_type(num_periods, int)
|
|
58
|
+
self._start_time = start_time
|
|
59
|
+
self._period_duration = period_duration
|
|
60
|
+
self._num_periods = num_periods
|
|
61
|
+
self._is_52_week_years = is_52_week_years
|
|
62
|
+
self._extrapolate_first_point = extrapolate_first_point
|
|
63
|
+
self._extrapolate_last_point = extrapolate_last_point
|
|
64
|
+
|
|
65
|
+
def __eq__(self, other) -> bool: # noqa: ANN001
|
|
66
|
+
"""Check if equal to other."""
|
|
67
|
+
if not isinstance(other, FixedFrequencyTimeIndex):
|
|
68
|
+
return False
|
|
69
|
+
return (
|
|
70
|
+
self._start_time == other._start_time
|
|
71
|
+
and self._period_duration == other._period_duration
|
|
72
|
+
and self._num_periods == other._num_periods
|
|
73
|
+
and self._is_52_week_years == other._is_52_week_years
|
|
74
|
+
and self._extrapolate_first_point == other._extrapolate_first_point
|
|
75
|
+
and self._extrapolate_last_point == other._extrapolate_last_point
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __hash__(self) -> int:
|
|
79
|
+
"""Return the hash value for the FixedFrequencyTimeIndex."""
|
|
80
|
+
return hash(
|
|
81
|
+
(
|
|
82
|
+
self._start_time,
|
|
83
|
+
self._period_duration,
|
|
84
|
+
self._num_periods,
|
|
85
|
+
self._is_52_week_years,
|
|
86
|
+
self._extrapolate_first_point,
|
|
87
|
+
self._extrapolate_last_point,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
"""Return a string representation of the FixedFrequencyTimeIndex."""
|
|
93
|
+
return (
|
|
94
|
+
f"FixedFrequencyTimeIndex("
|
|
95
|
+
f"start_time={self._start_time}, "
|
|
96
|
+
f"period_duration={self._period_duration}, "
|
|
97
|
+
f"num_periods={self._num_periods}, "
|
|
98
|
+
f"is_52_week_years={self._is_52_week_years}, "
|
|
99
|
+
f"extrapolate_first_point={self._extrapolate_first_point}, "
|
|
100
|
+
f"extrapolate_last_point={self._extrapolate_last_point})"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def get_fingerprint(self) -> Fingerprint:
|
|
104
|
+
"""Get the fingerprint."""
|
|
105
|
+
return self.get_fingerprint_default()
|
|
106
|
+
|
|
107
|
+
def get_timezone(self) -> tzinfo | None:
|
|
108
|
+
"""Get the timezone."""
|
|
109
|
+
return self._start_time.tzinfo
|
|
110
|
+
|
|
111
|
+
def get_start_time(self) -> datetime:
|
|
112
|
+
"""Get the start time."""
|
|
113
|
+
return self._start_time
|
|
114
|
+
|
|
115
|
+
def get_period_duration(self) -> timedelta:
|
|
116
|
+
"""Get the period duration."""
|
|
117
|
+
return self._period_duration
|
|
118
|
+
|
|
119
|
+
def get_num_periods(self) -> int:
|
|
120
|
+
"""Get the number of points."""
|
|
121
|
+
return self._num_periods
|
|
122
|
+
|
|
123
|
+
def is_constant(self) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Return True if the time index is constant (single period and both extrapolation flags are True).
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
bool
|
|
130
|
+
True if the time index is constant, False otherwise.
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
return self._num_periods == 1 and self._extrapolate_first_point == self._extrapolate_last_point is True
|
|
134
|
+
|
|
135
|
+
def is_whole_years(self) -> bool:
|
|
136
|
+
"""Return True if index covers one or more full years."""
|
|
137
|
+
start_time = self.get_start_time()
|
|
138
|
+
start_year, start_week, start_weekday = start_time.isocalendar()
|
|
139
|
+
if not start_week == start_weekday == 1:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
if not self.is_52_week_years():
|
|
143
|
+
period_duration = self.get_period_duration()
|
|
144
|
+
num_periods = self.get_num_periods()
|
|
145
|
+
stop_time = start_time + num_periods * period_duration
|
|
146
|
+
stop_year, stop_week, stop_weekday = stop_time.isocalendar()
|
|
147
|
+
assert stop_year >= start_year
|
|
148
|
+
return stop_week == stop_weekday == 1
|
|
149
|
+
|
|
150
|
+
period_duration = self.get_period_duration()
|
|
151
|
+
num_periods = self.get_num_periods()
|
|
152
|
+
seconds_52_week_year = 52 * 168 * 3600
|
|
153
|
+
num_years = (period_duration * num_periods).total_seconds() / seconds_52_week_year
|
|
154
|
+
return num_years.is_integer()
|
|
155
|
+
|
|
156
|
+
def get_reference_period(self) -> ReferencePeriod | None:
|
|
157
|
+
"""Get the reference period (only if is_whole_years() is True)."""
|
|
158
|
+
if self.is_whole_years():
|
|
159
|
+
start_year = self.get_start_time().isocalendar().year
|
|
160
|
+
|
|
161
|
+
if self._is_52_week_years:
|
|
162
|
+
num_years = (self.get_num_periods() * self.get_period_duration()) // timedelta(weeks=52)
|
|
163
|
+
else:
|
|
164
|
+
stop_year = self.get_stop_time().isocalendar().year
|
|
165
|
+
num_years = stop_year - start_year
|
|
166
|
+
return ReferencePeriod(start_year=start_year, num_years=num_years)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def is_52_week_years(self) -> bool:
|
|
170
|
+
"""Return True if 52-week years and False if real ISO time."""
|
|
171
|
+
return self._is_52_week_years
|
|
172
|
+
|
|
173
|
+
def is_one_year(self) -> bool:
|
|
174
|
+
"""Return True if exactly one whole year."""
|
|
175
|
+
start_time = self.get_start_time()
|
|
176
|
+
start_year, start_week, start_weekday = start_time.isocalendar()
|
|
177
|
+
if not start_week == start_weekday == 1:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
if not self.is_52_week_years():
|
|
181
|
+
period_duration = self.get_period_duration()
|
|
182
|
+
num_periods = self.get_num_periods()
|
|
183
|
+
stop_time = start_time + num_periods * period_duration
|
|
184
|
+
stop_year, stop_week, stop_weekday = stop_time.isocalendar()
|
|
185
|
+
if not stop_week == stop_weekday == 1:
|
|
186
|
+
return False
|
|
187
|
+
return start_year + 1 == stop_year
|
|
188
|
+
|
|
189
|
+
period_duration = self.get_period_duration()
|
|
190
|
+
num_periods = self.get_num_periods()
|
|
191
|
+
seconds_52_week_year = 52 * 168 * 3600
|
|
192
|
+
num_years = (period_duration * num_periods).total_seconds() / seconds_52_week_year
|
|
193
|
+
return num_years == 1.0
|
|
194
|
+
|
|
195
|
+
def extrapolate_first_point(self) -> bool:
|
|
196
|
+
"""Return True if first value can be extrapolated backwards to fill missing values."""
|
|
197
|
+
return self._extrapolate_first_point
|
|
198
|
+
|
|
199
|
+
def extrapolate_last_point(self) -> bool:
|
|
200
|
+
"""Return True if last value can be extrapolated forward to fill missing values."""
|
|
201
|
+
return self._extrapolate_last_point
|
|
202
|
+
|
|
203
|
+
def get_period_average(self, vector: NDArray, start_time: datetime, duration: timedelta, is_52_week_years: bool) -> float:
|
|
204
|
+
"""Get the average over the period from the vector."""
|
|
205
|
+
assert vector.shape == (self.get_num_periods(),)
|
|
206
|
+
target_timeindex = FixedFrequencyTimeIndex(
|
|
207
|
+
start_time=start_time,
|
|
208
|
+
period_duration=duration,
|
|
209
|
+
num_periods=1,
|
|
210
|
+
is_52_week_years=is_52_week_years,
|
|
211
|
+
extrapolate_first_point=self.extrapolate_first_point(),
|
|
212
|
+
extrapolate_last_point=self.extrapolate_last_point(),
|
|
213
|
+
)
|
|
214
|
+
target_vector = np.zeros(1, dtype=vector.dtype)
|
|
215
|
+
self.write_into_fixed_frequency(
|
|
216
|
+
target_vector=target_vector,
|
|
217
|
+
target_timeindex=target_timeindex,
|
|
218
|
+
input_vector=vector,
|
|
219
|
+
)
|
|
220
|
+
return target_vector[0]
|
|
221
|
+
|
|
222
|
+
def write_into_fixed_frequency(
|
|
223
|
+
self,
|
|
224
|
+
target_vector: NDArray,
|
|
225
|
+
target_timeindex: FixedFrequencyTimeIndex,
|
|
226
|
+
input_vector: NDArray,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Write the given input_vector into the target_vector according to the target_timeindex, applying necessary transformations.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
target_vector : NDArray
|
|
234
|
+
The array where the input_vector will be written to, modified in place.
|
|
235
|
+
target_timeindex : FixedFrequencyTimeIndex
|
|
236
|
+
The time index defining the fixed frequency structure for writing the input_vector into the target_vector.
|
|
237
|
+
input_vector : NDArray
|
|
238
|
+
The array containing the data to be written into the target_vector.
|
|
239
|
+
|
|
240
|
+
Notes
|
|
241
|
+
-----
|
|
242
|
+
- If the object is constant (as determined by `self.is_constant()`), the input_vector is expected to have a single value,
|
|
243
|
+
which will be used to fill the entire target_vector.
|
|
244
|
+
- Otherwise, the method delegates the operation to `_write_into_fixed_frequency_recursive` for handling more complex cases.
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
if self.is_constant():
|
|
248
|
+
assert input_vector.size == 1
|
|
249
|
+
target_vector.fill(input_vector[0])
|
|
250
|
+
else:
|
|
251
|
+
self._write_into_fixed_frequency_recursive(target_vector, target_timeindex, input_vector)
|
|
252
|
+
|
|
253
|
+
def _write_into_fixed_frequency_recursive( # noqa: C901
|
|
254
|
+
self,
|
|
255
|
+
target_vector: NDArray,
|
|
256
|
+
target_timeindex: FixedFrequencyTimeIndex,
|
|
257
|
+
input_vector: NDArray,
|
|
258
|
+
_depth: int = 0, # only for recursion depth tracking
|
|
259
|
+
) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Recursively write the input_vector into the target_vector according to the target_timeindex, applying necessary transformations.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
target_vector : NDArray
|
|
266
|
+
The array where the input_vector will be written to, modified in place.
|
|
267
|
+
target_timeindex : FixedFrequencyTimeIndex
|
|
268
|
+
The time index defining the fixed frequency structure for writing the input_vector into the target_vector.
|
|
269
|
+
input_vector : NDArray
|
|
270
|
+
The array containing the data to be written into the target_vector.
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
if _depth > 100: # noqa: PLR2004
|
|
274
|
+
raise RecursionError("Maximum recursion depth (100) exceeded in _write_into_fixed_frequency_recursive.")
|
|
275
|
+
|
|
276
|
+
if self == target_timeindex:
|
|
277
|
+
np.copyto(target_vector, input_vector)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
transformed_timeindex = None
|
|
281
|
+
|
|
282
|
+
# Check differences between self and target_timeindex and apply transformations recursively
|
|
283
|
+
if not target_timeindex._is_compatible_resolution(self):
|
|
284
|
+
transformed_timeindex, transformed_vector = self._transform_to_compatible_resolution(input_vector, target_timeindex)
|
|
285
|
+
|
|
286
|
+
elif target_timeindex.is_52_week_years() and not self.is_52_week_years():
|
|
287
|
+
transformed_timeindex, transformed_vector = self._convert_to_52_week_years(input_vector=input_vector)
|
|
288
|
+
|
|
289
|
+
elif not target_timeindex.is_52_week_years() and self.is_52_week_years():
|
|
290
|
+
transformed_timeindex, transformed_vector = self._convert_to_iso_time(input_vector=input_vector)
|
|
291
|
+
|
|
292
|
+
elif not self._is_same_period(target_timeindex):
|
|
293
|
+
if self.is_one_year():
|
|
294
|
+
transformed_timeindex, transformed_vector = self._repeat_oneyear(input_vector, target_timeindex)
|
|
295
|
+
else:
|
|
296
|
+
transformed_timeindex, transformed_vector = self._adjust_period(input_vector, target_timeindex)
|
|
297
|
+
|
|
298
|
+
elif not self.is_same_resolution(target_timeindex):
|
|
299
|
+
if target_timeindex.get_period_duration() < self._period_duration:
|
|
300
|
+
v_ops.disaggregate(
|
|
301
|
+
input_vector=input_vector,
|
|
302
|
+
output_vector=target_vector,
|
|
303
|
+
is_disaggfunc_repeat=True,
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
v_ops.aggregate(
|
|
307
|
+
input_vector=input_vector,
|
|
308
|
+
output_vector=target_vector,
|
|
309
|
+
is_aggfunc_sum=False,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Recursively write the transformed vector into the target vector
|
|
313
|
+
if transformed_timeindex is not None:
|
|
314
|
+
transformed_timeindex._write_into_fixed_frequency_recursive( # noqa: SLF001
|
|
315
|
+
target_vector=target_vector,
|
|
316
|
+
target_timeindex=target_timeindex,
|
|
317
|
+
input_vector=transformed_vector,
|
|
318
|
+
_depth=_depth + 1,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _convert_to_iso_time(self, input_vector: NDArray) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
322
|
+
"""
|
|
323
|
+
Convert the input vector to ISO time format.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
input_vector : NDArray
|
|
328
|
+
The input vector to be transformed into ISO time format.
|
|
329
|
+
|
|
330
|
+
Returns
|
|
331
|
+
-------
|
|
332
|
+
tuple[FixedFrequencyTimeIndex, NDArray]
|
|
333
|
+
A tuple containing the transformed FixedFrequencyTimeIndex and the transformed input vector.
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
transformed_vector = v_ops.convert_to_isotime(input_vector=input_vector, startdate=self._start_time, period_duration=self._period_duration)
|
|
337
|
+
|
|
338
|
+
transformed_timeindex = self.copy_with(
|
|
339
|
+
start_time=self._start_time,
|
|
340
|
+
num_periods=transformed_vector.size,
|
|
341
|
+
is_52_week_years=False,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return transformed_timeindex, transformed_vector
|
|
345
|
+
|
|
346
|
+
def _convert_to_52_week_years(self, input_vector: NDArray) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
347
|
+
"""
|
|
348
|
+
Convert the input vector to a 52-week year format.
|
|
349
|
+
|
|
350
|
+
This method adjusts the start time of the source index (if needed) and transforms the input vector to match the 52-week year format.
|
|
351
|
+
|
|
352
|
+
Parameters
|
|
353
|
+
----------
|
|
354
|
+
input_vector : NDArray
|
|
355
|
+
The input vector to be transformed.
|
|
356
|
+
startdate : datetime
|
|
357
|
+
The start date of the input vector.
|
|
358
|
+
period_duration : timedelta
|
|
359
|
+
The duration of each period in the input vector.
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
tuple[FixedFrequencyTimeIndex, NDArray]
|
|
364
|
+
A tuple containing the transformed FixedFrequencyTimeIndex and the transformed input vector.
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
adjusted_start_time, transformed_vector = v_ops.convert_to_modeltime(
|
|
368
|
+
input_vector=input_vector,
|
|
369
|
+
startdate=self._start_time,
|
|
370
|
+
period_duration=self._period_duration,
|
|
371
|
+
)
|
|
372
|
+
transformed_timeindex = self.copy_with(
|
|
373
|
+
start_time=adjusted_start_time,
|
|
374
|
+
num_periods=transformed_vector.size,
|
|
375
|
+
is_52_week_years=True,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return transformed_timeindex, transformed_vector
|
|
379
|
+
|
|
380
|
+
def _is_compatible_resolution(self, other: FixedFrequencyTimeIndex) -> bool:
|
|
381
|
+
"""Check if the period duration and start time are compatible with another FixedFrequencyTimeIndex."""
|
|
382
|
+
return self._is_compatible_period(other) and self._is_compatible_starttime(other)
|
|
383
|
+
|
|
384
|
+
def _is_compatible_period(self, other: FixedFrequencyTimeIndex) -> bool:
|
|
385
|
+
modulus = self._period_duration.total_seconds() % other.get_period_duration().total_seconds()
|
|
386
|
+
return modulus == 0
|
|
387
|
+
|
|
388
|
+
def _is_compatible_starttime(self, other: FixedFrequencyTimeIndex) -> bool:
|
|
389
|
+
delta = abs(self._start_time - other.get_start_time()).total_seconds()
|
|
390
|
+
modulus = delta % other._period_duration.total_seconds()
|
|
391
|
+
return modulus == 0
|
|
392
|
+
|
|
393
|
+
def _transform_to_compatible_resolution(
|
|
394
|
+
self,
|
|
395
|
+
input_vector: NDArray,
|
|
396
|
+
target_timeindex: FixedFrequencyTimeIndex,
|
|
397
|
+
) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
398
|
+
"""
|
|
399
|
+
Transform the input vector and source time index to match the target time index resolution.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
input_vector : NDArray
|
|
404
|
+
The input vector to be transformed.
|
|
405
|
+
target_timeindex : FixedFrequencyTimeIndex
|
|
406
|
+
The target time index to match the resolution of.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
tuple[FixedFrequencyTimeIndex, NDArray]
|
|
411
|
+
A tuple containing the transformed FixedFrequencyTimeIndex and the transformed input vector.
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
new_period_duration = timedelta(
|
|
415
|
+
seconds=math.gcd(
|
|
416
|
+
int(self._period_duration.total_seconds()),
|
|
417
|
+
int(target_timeindex.get_period_duration().total_seconds()),
|
|
418
|
+
int((self._start_time - target_timeindex.get_start_time()).total_seconds()),
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
transformed_timeindex = self.copy_with(
|
|
423
|
+
period_duration=new_period_duration,
|
|
424
|
+
num_periods=int(self._period_duration.total_seconds() // new_period_duration.total_seconds()) * self._num_periods,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
transformed_vector = np.zeros(transformed_timeindex.get_num_periods(), dtype=input_vector.dtype)
|
|
428
|
+
v_ops.disaggregate(
|
|
429
|
+
input_vector=input_vector,
|
|
430
|
+
output_vector=transformed_vector,
|
|
431
|
+
is_disaggfunc_repeat=True,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return transformed_timeindex, transformed_vector
|
|
435
|
+
|
|
436
|
+
def _is_same_period(self, other: FixedFrequencyTimeIndex) -> bool:
|
|
437
|
+
"""Check if the start and stop times are the same."""
|
|
438
|
+
return self._start_time == other.get_start_time() and self.get_stop_time() == other.get_stop_time()
|
|
439
|
+
|
|
440
|
+
def is_same_resolution(self, other: FixedFrequencyTimeIndex) -> bool:
|
|
441
|
+
"""Check if the period duration is the same."""
|
|
442
|
+
return self._period_duration == other.get_period_duration()
|
|
443
|
+
|
|
444
|
+
def get_stop_time(self) -> datetime:
|
|
445
|
+
"""Get the stop time of the TimeIndex."""
|
|
446
|
+
return self._start_time + self._period_duration * self._num_periods
|
|
447
|
+
|
|
448
|
+
def slice(
|
|
449
|
+
self,
|
|
450
|
+
input_vector: NDArray,
|
|
451
|
+
start_year: int,
|
|
452
|
+
num_years: int,
|
|
453
|
+
target_start_year: int,
|
|
454
|
+
target_num_years: int,
|
|
455
|
+
) -> NDArray:
|
|
456
|
+
"""Periodize the input vector to match the target timeindex."""
|
|
457
|
+
if self._is_52_week_years:
|
|
458
|
+
return v_ops.periodize_modeltime(input_vector, start_year, num_years, target_start_year, target_num_years)
|
|
459
|
+
return v_ops.periodize_isotime(input_vector, start_year, num_years, target_start_year, target_num_years)
|
|
460
|
+
|
|
461
|
+
def _slice_start(self, input_vector: NDArray, target_index: FixedFrequencyTimeIndex) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
462
|
+
"""
|
|
463
|
+
Slice the input vector to match the target time index.
|
|
464
|
+
|
|
465
|
+
This method handles slicing the input vector to fit the target time index,
|
|
466
|
+
ensuring that the start time aligns correctly.
|
|
467
|
+
"""
|
|
468
|
+
num_periods_to_slice = self._periods_between(
|
|
469
|
+
self._start_time,
|
|
470
|
+
target_index.get_start_time(),
|
|
471
|
+
self._period_duration,
|
|
472
|
+
)
|
|
473
|
+
transformed_timeindex = self.copy_with(
|
|
474
|
+
start_time=target_index.get_start_time(),
|
|
475
|
+
num_periods=self._num_periods - num_periods_to_slice,
|
|
476
|
+
)
|
|
477
|
+
transformed_vector = input_vector[num_periods_to_slice:]
|
|
478
|
+
|
|
479
|
+
return transformed_timeindex, transformed_vector
|
|
480
|
+
|
|
481
|
+
def _slice_end(self, input_vector: NDArray, target_index: FixedFrequencyTimeIndex) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
482
|
+
"""
|
|
483
|
+
Slice the input vector to match the target time index.
|
|
484
|
+
|
|
485
|
+
This method handles slicing the input vector to fit the target time index,
|
|
486
|
+
ensuring that the stop time aligns correctly.
|
|
487
|
+
"""
|
|
488
|
+
num_periods_to_slice = self._periods_between(
|
|
489
|
+
self.get_stop_time(),
|
|
490
|
+
target_index.get_stop_time(),
|
|
491
|
+
self._period_duration,
|
|
492
|
+
)
|
|
493
|
+
transformed_timeindex = self.copy_with(num_periods=self._num_periods - num_periods_to_slice)
|
|
494
|
+
transformed_vector = input_vector[:-num_periods_to_slice]
|
|
495
|
+
|
|
496
|
+
return transformed_timeindex, transformed_vector
|
|
497
|
+
|
|
498
|
+
def total_duration(self) -> timedelta:
|
|
499
|
+
"""Get the duration of the TimeIndex."""
|
|
500
|
+
return self._period_duration * self._num_periods
|
|
501
|
+
|
|
502
|
+
def _extend_start(
|
|
503
|
+
self,
|
|
504
|
+
input_vector: NDArray,
|
|
505
|
+
target_timeindex: FixedFrequencyTimeIndex,
|
|
506
|
+
) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
507
|
+
"""
|
|
508
|
+
Extend the start of the input vector to match the target time index.
|
|
509
|
+
|
|
510
|
+
This method handles extrapolation of the first point if allowed.
|
|
511
|
+
"""
|
|
512
|
+
if not self._extrapolate_first_point:
|
|
513
|
+
raise ValueError("Cannot extend start without extrapolation.")
|
|
514
|
+
|
|
515
|
+
num_periods_to_extend = self._periods_between(self._start_time, target_timeindex.get_start_time(), self._period_duration)
|
|
516
|
+
extended_vector = np.concatenate((np.full(num_periods_to_extend, input_vector[0]), input_vector))
|
|
517
|
+
|
|
518
|
+
transformed_timeindex = self.copy_with(
|
|
519
|
+
start_time=target_timeindex.get_start_time(),
|
|
520
|
+
num_periods=self._num_periods + num_periods_to_extend,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
return transformed_timeindex, extended_vector
|
|
524
|
+
|
|
525
|
+
def _extend_end(
|
|
526
|
+
self,
|
|
527
|
+
input_vector: NDArray,
|
|
528
|
+
target_timeindex: FixedFrequencyTimeIndex,
|
|
529
|
+
) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
530
|
+
if not self._extrapolate_last_point:
|
|
531
|
+
raise ValueError("Cannot extend end without extrapolation.")
|
|
532
|
+
|
|
533
|
+
num_periods_to_extend = self._periods_between(
|
|
534
|
+
self.get_stop_time(),
|
|
535
|
+
target_timeindex.get_stop_time(),
|
|
536
|
+
self._period_duration,
|
|
537
|
+
)
|
|
538
|
+
extended_vector = np.concatenate((input_vector, np.full(num_periods_to_extend, input_vector[-1])))
|
|
539
|
+
target_timeindex = self.copy_with(num_periods=self._num_periods + num_periods_to_extend)
|
|
540
|
+
|
|
541
|
+
return target_timeindex, extended_vector
|
|
542
|
+
|
|
543
|
+
def _repeat_oneyear(self, input_vector: NDArray, target_timeindex: FixedFrequencyTimeIndex) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
544
|
+
"""
|
|
545
|
+
Repeat the one-year time index.
|
|
546
|
+
|
|
547
|
+
This method creates a new time vector by repeating the input vector over the time period defined by the target time index.
|
|
548
|
+
|
|
549
|
+
Parameters
|
|
550
|
+
----------
|
|
551
|
+
input_vector : NDArray
|
|
552
|
+
The input vector to be repeated.
|
|
553
|
+
target_timeindex : FixedFrequencyTimeIndex
|
|
554
|
+
The target time index defining the start and duration of the target period.
|
|
555
|
+
|
|
556
|
+
Returns
|
|
557
|
+
-------
|
|
558
|
+
tuple[FixedFrequencyTimeIndex, NDArray]
|
|
559
|
+
A tuple containing the new FixedFrequencyTimeIndex and the transformed input vector.
|
|
560
|
+
|
|
561
|
+
"""
|
|
562
|
+
if self.is_52_week_years():
|
|
563
|
+
transformed_vector = self._repeat_one_year_modeltime(
|
|
564
|
+
input_vector=input_vector,
|
|
565
|
+
target_timeindex=target_timeindex,
|
|
566
|
+
)
|
|
567
|
+
else:
|
|
568
|
+
transformed_vector = self._repeat_one_year_isotime(
|
|
569
|
+
input_vector=input_vector,
|
|
570
|
+
target_timeindex=target_timeindex,
|
|
571
|
+
)
|
|
572
|
+
transformed_timeindex = self.copy_with(
|
|
573
|
+
start_time=target_timeindex.get_start_time(),
|
|
574
|
+
num_periods=transformed_vector.size,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return transformed_timeindex, transformed_vector
|
|
578
|
+
|
|
579
|
+
def _repeat_one_year_isotime(self, input_vector: NDArray, target_timeindex: FixedFrequencyTimeIndex) -> NDArray:
|
|
580
|
+
"""
|
|
581
|
+
Repeat the one-year ISO time index.
|
|
582
|
+
|
|
583
|
+
This method creates a new time vector by repeating the input vector over the time period defined by the target time index.
|
|
584
|
+
|
|
585
|
+
Parameters
|
|
586
|
+
----------
|
|
587
|
+
input_vector : NDArray
|
|
588
|
+
The input vector to be repeated.
|
|
589
|
+
target_timeindex : FixedFrequencyTimeIndex
|
|
590
|
+
The target time index defining the start and stop times for the repetition.
|
|
591
|
+
|
|
592
|
+
Returns
|
|
593
|
+
-------
|
|
594
|
+
NDArray
|
|
595
|
+
The repeated vector that matches the target time index.
|
|
596
|
+
|
|
597
|
+
"""
|
|
598
|
+
return v_ops.repeat_oneyear_isotime(
|
|
599
|
+
input_vector=input_vector,
|
|
600
|
+
input_start_date=self._start_time,
|
|
601
|
+
period_duration=self.get_period_duration(),
|
|
602
|
+
output_start_date=target_timeindex.get_start_time(),
|
|
603
|
+
output_end_date=target_timeindex.get_stop_time(),
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
def _repeat_one_year_modeltime(self, input_vector: NDArray, target_timeindex: FixedFrequencyTimeIndex) -> NDArray:
|
|
607
|
+
"""
|
|
608
|
+
Repeat the one-year model time index.
|
|
609
|
+
|
|
610
|
+
This method creates a new time vector by repeating the input vector over the time period defined by the target time index.
|
|
611
|
+
|
|
612
|
+
Parameters
|
|
613
|
+
----------
|
|
614
|
+
input_vector : NDArray
|
|
615
|
+
The input vector to be repeated.
|
|
616
|
+
target_timeindex : FixedFrequencyTimeIndex
|
|
617
|
+
The target time index defining the start and stop times for the repetition.
|
|
618
|
+
|
|
619
|
+
Returns
|
|
620
|
+
-------
|
|
621
|
+
NDArray
|
|
622
|
+
The repeated vector that matches the target time index.
|
|
623
|
+
|
|
624
|
+
"""
|
|
625
|
+
return v_ops.repeat_oneyear_modeltime(
|
|
626
|
+
input_vector=input_vector,
|
|
627
|
+
input_start_date=self._start_time,
|
|
628
|
+
period_duration=self.get_period_duration(),
|
|
629
|
+
output_start_date=target_timeindex.get_start_time(),
|
|
630
|
+
output_end_date=target_timeindex.get_stop_time(),
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
def _adjust_period(self, input_vector: NDArray, target_timeindex: FixedFrequencyTimeIndex) -> tuple[FixedFrequencyTimeIndex, NDArray]:
|
|
634
|
+
if target_timeindex.get_start_time() < self._start_time:
|
|
635
|
+
if self._extrapolate_first_point:
|
|
636
|
+
return self._extend_start(input_vector, target_timeindex)
|
|
637
|
+
raise ValueError(
|
|
638
|
+
(
|
|
639
|
+
"Cannot write into fixed frequency: incompatible time indices. "
|
|
640
|
+
"Start time of the target index is before the start time of the source index "
|
|
641
|
+
"and extrapolate_first_point is False.\n"
|
|
642
|
+
f"Input timeindex: {self}\n"
|
|
643
|
+
f"Target timeindex: {target_timeindex}"
|
|
644
|
+
),
|
|
645
|
+
)
|
|
646
|
+
if target_timeindex.get_stop_time() > self.get_stop_time():
|
|
647
|
+
if self._extrapolate_last_point:
|
|
648
|
+
return self._extend_end(input_vector, target_timeindex)
|
|
649
|
+
raise ValueError(
|
|
650
|
+
(
|
|
651
|
+
"Cannot write into fixed frequency: incompatible time indices. "
|
|
652
|
+
"'stop_time' of the target index is after the 'stop_time' of the source index "
|
|
653
|
+
"and 'extrapolate_last_point' is False.\n"
|
|
654
|
+
f"Input timeindex: {self}\n"
|
|
655
|
+
f"Target timeindex: {target_timeindex}"
|
|
656
|
+
),
|
|
657
|
+
)
|
|
658
|
+
if target_timeindex.get_start_time() > self.get_start_time():
|
|
659
|
+
return self._slice_start(input_vector, target_timeindex)
|
|
660
|
+
|
|
661
|
+
if target_timeindex.get_stop_time() < self.get_stop_time():
|
|
662
|
+
return self._slice_end(input_vector, target_timeindex)
|
|
663
|
+
return target_timeindex, input_vector
|
|
664
|
+
|
|
665
|
+
def _periods_between(self, first_time: datetime, second_time: datetime, period_duration: timedelta) -> int:
|
|
666
|
+
"""
|
|
667
|
+
Calculate the number of periods between two times.
|
|
668
|
+
|
|
669
|
+
Parameters
|
|
670
|
+
----------
|
|
671
|
+
first_time : datetime
|
|
672
|
+
The first time point.
|
|
673
|
+
second_time : datetime
|
|
674
|
+
The second time point.
|
|
675
|
+
period_duration : timedelta
|
|
676
|
+
The duration of each period.
|
|
677
|
+
|
|
678
|
+
Returns
|
|
679
|
+
-------
|
|
680
|
+
int
|
|
681
|
+
The number of periods between the two times.
|
|
682
|
+
|
|
683
|
+
"""
|
|
684
|
+
return abs(first_time - second_time) // period_duration
|
|
685
|
+
|
|
686
|
+
def copy_with(
|
|
687
|
+
self,
|
|
688
|
+
start_time: datetime | None = None,
|
|
689
|
+
period_duration: timedelta | None = None,
|
|
690
|
+
num_periods: int | None = None,
|
|
691
|
+
is_52_week_years: bool | None = None,
|
|
692
|
+
extrapolate_first_point: bool | None = None,
|
|
693
|
+
extrapolate_last_point: bool | None = None,
|
|
694
|
+
) -> FixedFrequencyTimeIndex:
|
|
695
|
+
"""
|
|
696
|
+
Create a copy of the FixedFrequencyTimeIndex with the same attributes, allowing specific fields to be overridden.
|
|
697
|
+
|
|
698
|
+
Parameters
|
|
699
|
+
----------
|
|
700
|
+
start_time : datetime, optional
|
|
701
|
+
Override for the start time.
|
|
702
|
+
period_duration : timedelta, optional
|
|
703
|
+
Override for the period duration.
|
|
704
|
+
num_periods : int, optional
|
|
705
|
+
Override for the number of periods.
|
|
706
|
+
is_52_week_years : bool, optional
|
|
707
|
+
Override for 52-week years flag.
|
|
708
|
+
extrapolate_first_point : bool, optional
|
|
709
|
+
Override for extrapolate first point flag.
|
|
710
|
+
extrapolate_last_point : bool, optional
|
|
711
|
+
Override for extrapolate last point flag.
|
|
712
|
+
|
|
713
|
+
Returns
|
|
714
|
+
-------
|
|
715
|
+
FixedFrequencyTimeIndex
|
|
716
|
+
A new instance with the updated attributes.
|
|
717
|
+
|
|
718
|
+
"""
|
|
719
|
+
return FixedFrequencyTimeIndex(
|
|
720
|
+
start_time=start_time if start_time is not None else self._start_time,
|
|
721
|
+
period_duration=period_duration if period_duration is not None else self._period_duration,
|
|
722
|
+
num_periods=num_periods if num_periods is not None else self._num_periods,
|
|
723
|
+
is_52_week_years=is_52_week_years if is_52_week_years is not None else self._is_52_week_years,
|
|
724
|
+
extrapolate_first_point=extrapolate_first_point if extrapolate_first_point is not None else self._extrapolate_first_point,
|
|
725
|
+
extrapolate_last_point=extrapolate_last_point if extrapolate_last_point is not None else self._extrapolate_last_point,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def copy_as_reference_period(self, reference_period: ReferencePeriod) -> FixedFrequencyTimeIndex:
|
|
729
|
+
"""
|
|
730
|
+
Create a copy of the FixedFrequencyTimeIndex with one period matching the given reference period.
|
|
731
|
+
|
|
732
|
+
Parameters
|
|
733
|
+
----------
|
|
734
|
+
reference_period : ReferencePeriod
|
|
735
|
+
The reference period to match for the output.
|
|
736
|
+
|
|
737
|
+
Returns
|
|
738
|
+
-------
|
|
739
|
+
FixedFrequencyTimeIndex
|
|
740
|
+
A new instance with the updated attributes.
|
|
741
|
+
|
|
742
|
+
"""
|
|
743
|
+
start_year = reference_period.get_start_year()
|
|
744
|
+
num_years = reference_period.get_num_years()
|
|
745
|
+
start_time = datetime.fromisocalendar(start_year, 1, 1)
|
|
746
|
+
if self.is_52_week_years():
|
|
747
|
+
period_duration = timedelta(weeks=52 * num_years)
|
|
748
|
+
else:
|
|
749
|
+
stop_time = datetime.fromisocalendar(start_year + num_years, 1, 1)
|
|
750
|
+
period_duration = stop_time - start_time
|
|
751
|
+
return self.copy_with(
|
|
752
|
+
start_time=start_time,
|
|
753
|
+
num_periods=1,
|
|
754
|
+
period_duration=period_duration,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def get_datetime_list(self) -> list[datetime]:
|
|
758
|
+
"""Return list of datetime including stop time."""
|
|
759
|
+
t = self.get_start_time()
|
|
760
|
+
n = self.get_num_periods()
|
|
761
|
+
d = self.get_period_duration()
|
|
762
|
+
return [t + i * d for i in range(n + 1)]
|