fram-core 0.0.0__py3-none-any.whl → 0.1.0a1__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.
Files changed (103) hide show
  1. fram_core-0.1.0a1.dist-info/METADATA +41 -0
  2. fram_core-0.1.0a1.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +142 -0
  6. framcore/Model.py +73 -0
  7. framcore/__init__.py +9 -0
  8. framcore/aggregators/Aggregator.py +153 -0
  9. framcore/aggregators/HydroAggregator.py +837 -0
  10. framcore/aggregators/NodeAggregator.py +495 -0
  11. framcore/aggregators/WindSolarAggregator.py +323 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +305 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +37 -0
  17. framcore/attributes/SoftBound.py +19 -0
  18. framcore/attributes/StartUpCost.py +54 -0
  19. framcore/attributes/Storage.py +146 -0
  20. framcore/attributes/TargetBound.py +18 -0
  21. framcore/attributes/__init__.py +65 -0
  22. framcore/attributes/hydro/HydroBypass.py +42 -0
  23. framcore/attributes/hydro/HydroGenerator.py +83 -0
  24. framcore/attributes/hydro/HydroPump.py +156 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +714 -0
  28. framcore/components/Component.py +112 -0
  29. framcore/components/Demand.py +130 -0
  30. framcore/components/Flow.py +167 -0
  31. framcore/components/HydroModule.py +330 -0
  32. framcore/components/Node.py +76 -0
  33. framcore/components/Thermal.py +204 -0
  34. framcore/components/Transmission.py +183 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +67 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +155 -0
  40. framcore/curves/__init__.py +9 -0
  41. framcore/events/__init__.py +21 -0
  42. framcore/events/events.py +51 -0
  43. framcore/expressions/Expr.py +490 -0
  44. framcore/expressions/__init__.py +28 -0
  45. framcore/expressions/_get_constant_from_expr.py +483 -0
  46. framcore/expressions/_time_vector_operations.py +615 -0
  47. framcore/expressions/_utils.py +73 -0
  48. framcore/expressions/queries.py +423 -0
  49. framcore/expressions/units.py +207 -0
  50. framcore/fingerprints/__init__.py +11 -0
  51. framcore/fingerprints/fingerprint.py +293 -0
  52. framcore/juliamodels/JuliaModel.py +161 -0
  53. framcore/juliamodels/__init__.py +7 -0
  54. framcore/loaders/__init__.py +10 -0
  55. framcore/loaders/loaders.py +407 -0
  56. framcore/metadata/Div.py +73 -0
  57. framcore/metadata/ExprMeta.py +50 -0
  58. framcore/metadata/LevelExprMeta.py +17 -0
  59. framcore/metadata/Member.py +55 -0
  60. framcore/metadata/Meta.py +44 -0
  61. framcore/metadata/__init__.py +15 -0
  62. framcore/populators/Populator.py +108 -0
  63. framcore/populators/__init__.py +7 -0
  64. framcore/querydbs/CacheDB.py +50 -0
  65. framcore/querydbs/ModelDB.py +34 -0
  66. framcore/querydbs/QueryDB.py +45 -0
  67. framcore/querydbs/__init__.py +11 -0
  68. framcore/solvers/Solver.py +48 -0
  69. framcore/solvers/SolverConfig.py +272 -0
  70. framcore/solvers/__init__.py +9 -0
  71. framcore/timeindexes/AverageYearRange.py +20 -0
  72. framcore/timeindexes/ConstantTimeIndex.py +17 -0
  73. framcore/timeindexes/DailyIndex.py +21 -0
  74. framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
  75. framcore/timeindexes/HourlyIndex.py +21 -0
  76. framcore/timeindexes/IsoCalendarDay.py +31 -0
  77. framcore/timeindexes/ListTimeIndex.py +197 -0
  78. framcore/timeindexes/ModelYear.py +17 -0
  79. framcore/timeindexes/ModelYears.py +18 -0
  80. framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
  81. framcore/timeindexes/ProfileTimeIndex.py +32 -0
  82. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  83. framcore/timeindexes/TimeIndex.py +90 -0
  84. framcore/timeindexes/WeeklyIndex.py +21 -0
  85. framcore/timeindexes/__init__.py +36 -0
  86. framcore/timevectors/ConstantTimeVector.py +135 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +114 -0
  88. framcore/timevectors/ListTimeVector.py +123 -0
  89. framcore/timevectors/LoadedTimeVector.py +104 -0
  90. framcore/timevectors/ReferencePeriod.py +41 -0
  91. framcore/timevectors/TimeVector.py +94 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +36 -0
  94. framcore/utils/get_regional_volumes.py +369 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +46 -0
  97. framcore/utils/isolate_subnodes.py +163 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +107 -0
  101. fram_core-0.0.0.dist-info/METADATA +0 -5
  102. fram_core-0.0.0.dist-info/RECORD +0 -4
  103. 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)]