fram-core 0.0.0__py3-none-any.whl → 0.1.0__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.0.dist-info/METADATA +42 -0
  2. fram_core-0.1.0.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +161 -0
  6. framcore/Model.py +90 -0
  7. framcore/__init__.py +10 -0
  8. framcore/aggregators/Aggregator.py +172 -0
  9. framcore/aggregators/HydroAggregator.py +849 -0
  10. framcore/aggregators/NodeAggregator.py +530 -0
  11. framcore/aggregators/WindSolarAggregator.py +315 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +307 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +23 -0
  17. framcore/attributes/SoftBound.py +16 -0
  18. framcore/attributes/StartUpCost.py +65 -0
  19. framcore/attributes/Storage.py +158 -0
  20. framcore/attributes/TargetBound.py +16 -0
  21. framcore/attributes/__init__.py +63 -0
  22. framcore/attributes/hydro/HydroBypass.py +49 -0
  23. framcore/attributes/hydro/HydroGenerator.py +100 -0
  24. framcore/attributes/hydro/HydroPump.py +178 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +911 -0
  28. framcore/components/Component.py +136 -0
  29. framcore/components/Demand.py +144 -0
  30. framcore/components/Flow.py +189 -0
  31. framcore/components/HydroModule.py +371 -0
  32. framcore/components/Node.py +99 -0
  33. framcore/components/Thermal.py +208 -0
  34. framcore/components/Transmission.py +198 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +82 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +146 -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 +591 -0
  44. framcore/expressions/__init__.py +30 -0
  45. framcore/expressions/_get_constant_from_expr.py +477 -0
  46. framcore/expressions/_utils.py +73 -0
  47. framcore/expressions/queries.py +416 -0
  48. framcore/expressions/units.py +227 -0
  49. framcore/fingerprints/__init__.py +11 -0
  50. framcore/fingerprints/fingerprint.py +292 -0
  51. framcore/juliamodels/JuliaModel.py +171 -0
  52. framcore/juliamodels/__init__.py +7 -0
  53. framcore/loaders/__init__.py +10 -0
  54. framcore/loaders/loaders.py +405 -0
  55. framcore/metadata/Div.py +73 -0
  56. framcore/metadata/ExprMeta.py +56 -0
  57. framcore/metadata/LevelExprMeta.py +32 -0
  58. framcore/metadata/Member.py +55 -0
  59. framcore/metadata/Meta.py +44 -0
  60. framcore/metadata/__init__.py +15 -0
  61. framcore/populators/Populator.py +108 -0
  62. framcore/populators/__init__.py +7 -0
  63. framcore/querydbs/CacheDB.py +50 -0
  64. framcore/querydbs/ModelDB.py +34 -0
  65. framcore/querydbs/QueryDB.py +45 -0
  66. framcore/querydbs/__init__.py +11 -0
  67. framcore/solvers/Solver.py +63 -0
  68. framcore/solvers/SolverConfig.py +272 -0
  69. framcore/solvers/__init__.py +9 -0
  70. framcore/timeindexes/AverageYearRange.py +27 -0
  71. framcore/timeindexes/ConstantTimeIndex.py +22 -0
  72. framcore/timeindexes/DailyIndex.py +33 -0
  73. framcore/timeindexes/FixedFrequencyTimeIndex.py +814 -0
  74. framcore/timeindexes/HourlyIndex.py +33 -0
  75. framcore/timeindexes/IsoCalendarDay.py +33 -0
  76. framcore/timeindexes/ListTimeIndex.py +277 -0
  77. framcore/timeindexes/ModelYear.py +23 -0
  78. framcore/timeindexes/ModelYears.py +27 -0
  79. framcore/timeindexes/OneYearProfileTimeIndex.py +29 -0
  80. framcore/timeindexes/ProfileTimeIndex.py +43 -0
  81. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  82. framcore/timeindexes/TimeIndex.py +103 -0
  83. framcore/timeindexes/WeeklyIndex.py +33 -0
  84. framcore/timeindexes/__init__.py +36 -0
  85. framcore/timeindexes/_time_vector_operations.py +689 -0
  86. framcore/timevectors/ConstantTimeVector.py +131 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +131 -0
  88. framcore/timevectors/ListTimeVector.py +127 -0
  89. framcore/timevectors/LoadedTimeVector.py +97 -0
  90. framcore/timevectors/ReferencePeriod.py +51 -0
  91. framcore/timevectors/TimeVector.py +108 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +35 -0
  94. framcore/utils/get_regional_volumes.py +387 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +63 -0
  97. framcore/utils/isolate_subnodes.py +172 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +106 -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,33 @@
1
+ from datetime import timedelta
2
+
3
+ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class HourlyIndex(ProfileTimeIndex):
7
+ """
8
+ ProfileTimeIndex with one or more whole years with hourly resolution. Either years with 52 weeks or full iso calendar years.
9
+
10
+ No extrapolation inherited from ProfileTimeIndex.
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ start_year: int,
16
+ num_years: int,
17
+ is_52_week_years: bool = True,
18
+ ) -> None:
19
+ """
20
+ Initialize HourlyIndex over a number of years. Either years with 52 weeks or full iso calendar years.
21
+
22
+ Args:
23
+ start_year (int): First year in the index.
24
+ num_years (int): Number of years in the index.
25
+ is_52_week_years (bool, optional): Whether to use 52-week years. If False, full iso calendar years are used. Defaults to True.
26
+
27
+ """
28
+ super().__init__(
29
+ start_year=start_year,
30
+ num_years=num_years,
31
+ period_duration=timedelta(hours=1),
32
+ is_52_week_years=is_52_week_years,
33
+ )
@@ -0,0 +1,33 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class IsoCalendarDay(SinglePeriodTimeIndex):
7
+ """
8
+ Represents a single ISO calendar day using year, week, and day values.
9
+
10
+ Inherits from SinglePeriodTimeIndex and provides a time index for one day,
11
+ constructed from datetime.fromisocalendar(year, week, day).
12
+
13
+ """
14
+
15
+ def __init__(self, year: int, week: int, day: int) -> None:
16
+ """
17
+ IsoCalendarDay represent a day from datetime.fromisocalendar(year, week, day).
18
+
19
+ No extrapolation and is_52_week_years=False. Useful for testing.
20
+
21
+ Args:
22
+ year (int): The ISO year.
23
+ week (int): The ISO week number (1-53).
24
+ day (int): The ISO weekday (1=Monday, 7=Sunday).
25
+
26
+ """
27
+ super().__init__(
28
+ start_time=datetime.fromisocalendar(year, week, day),
29
+ period_duration=timedelta(days=1),
30
+ is_52_week_years=False,
31
+ extrapolate_first_point=False,
32
+ extrapolate_last_point=False,
33
+ )
@@ -0,0 +1,277 @@
1
+ import functools
2
+ import math
3
+ from datetime import datetime, timedelta, tzinfo
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from framcore.fingerprints import Fingerprint
9
+ from framcore.timeindexes import FixedFrequencyTimeIndex, TimeIndex
10
+ from framcore.timeindexes._time_vector_operations import period_duration
11
+
12
+
13
+ class ListTimeIndex(TimeIndex):
14
+ """
15
+ ListTimeIndex class for TimeIndexes with a list of timestamps. Subclass of TimeIndex.
16
+
17
+ This TimeIndex is defined by a list of timestamps, with possible irregular intervals.The last timestamp is not
18
+ necessarily the end of the time vector, and the first timestamp is not necessarily the start of the time vector
19
+ if extrapolation is enabled.
20
+
21
+ ListTimeIndex is not recommended for large time vectors, as it is less efficient.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ datetime_list: list[datetime],
27
+ is_52_week_years: bool,
28
+ extrapolate_first_point: bool,
29
+ extrapolate_last_point: bool,
30
+ ) -> None:
31
+ """
32
+ Initialize the ListTimeIndex class.
33
+
34
+ Args:
35
+ datetime_list (list[datetime]): List of datetime objects defining the time index. Must be ordered and contain more than one element.
36
+ is_52_week_years (bool): Whether to use 52-week years. If False, full iso calendar years are used.
37
+ extrapolate_first_point (bool): Whether to extrapolate the first point.
38
+ extrapolate_last_point (bool): Whether to extrapolate the last point.
39
+
40
+ Raises:
41
+ ValueError: If datetime_list has less than two elements or is not ordered.
42
+
43
+ """
44
+ dts = datetime_list
45
+ if len(dts) <= 1:
46
+ msg = f"datetime_list must contain more than one element. Got {datetime_list}"
47
+ raise ValueError(msg)
48
+ if not all(dts[i] < dts[i + 1] for i in range(len(dts) - 1)):
49
+ msg = f"All elements of datetime_list must be smaller/lower than the succeeding element. Dates must be ordered. Got {datetime_list}."
50
+ raise ValueError(msg)
51
+ if len(set(dt.tzinfo for dt in dts if dt is not None)) > 1:
52
+ msg = f"Datetime objects in datetime_list have differing time zone information: {set(dt.tzinfo for dt in dts if dt is not None)}"
53
+ raise ValueError(msg)
54
+ if is_52_week_years and any(dts[i].isocalendar().week == 53 for i in range(len(dts))): # noqa: PLR2004
55
+ msg = "When is_52_week_years is True, datetime_list should not contain week 53 datetimes."
56
+ raise ValueError(msg)
57
+
58
+ self._datetime_list = datetime_list
59
+ self._is_52_week_years = is_52_week_years
60
+ self._extrapolate_first_point = extrapolate_first_point
61
+ self._extrapolate_last_point = extrapolate_last_point
62
+
63
+ def __eq__(self, other) -> bool: # noqa: ANN001
64
+ """Check if two ListTimeIndexes are equal."""
65
+ if not isinstance(other, type(self)):
66
+ return False
67
+ return (
68
+ self._datetime_list == other._datetime_list
69
+ and self._extrapolate_first_point == other._extrapolate_first_point
70
+ and self._extrapolate_last_point == other._extrapolate_last_point
71
+ )
72
+
73
+ def __hash__(self) -> int:
74
+ """Return the hash of the ListTimeIndex."""
75
+ return hash(
76
+ (
77
+ tuple(self._datetime_list),
78
+ self._extrapolate_first_point,
79
+ self._extrapolate_last_point,
80
+ ),
81
+ )
82
+
83
+ def __repr__(self) -> str:
84
+ """Return the string representation of the ListTimeIndex."""
85
+ return (
86
+ "ListTimeIndex("
87
+ f"datetimelist={self._datetime_list}, "
88
+ f"extrapolate_first_point={self._extrapolate_first_point}, "
89
+ f"extrapolate_last_point={self._extrapolate_last_point})"
90
+ )
91
+
92
+ def get_fingerprint(self) -> Fingerprint:
93
+ """Get the fingerprint of the ListTimeIndex."""
94
+ fingerprint = Fingerprint()
95
+ fingerprint.add("datetime_list", self._datetime_list)
96
+ fingerprint.add("is_52_week_years", self._is_52_week_years)
97
+ fingerprint.add("extrapolate_first_point", self._extrapolate_first_point)
98
+ fingerprint.add("extrapolate_last_point", self._extrapolate_last_point)
99
+ return fingerprint
100
+
101
+ def get_datetime_list(self) -> list[datetime]:
102
+ """Get a list of all periods (num_periods + 1 datetimes)."""
103
+ return self._datetime_list.copy()
104
+
105
+ def get_timezone(self) -> tzinfo | None:
106
+ """Get the timezone of the TimeIndex."""
107
+ return self._datetime_list[0].tzinfo
108
+
109
+ def get_num_periods(self) -> int:
110
+ """Get the number of periods in the TimeIndex."""
111
+ return len(self._datetime_list) - 1
112
+
113
+ def is_52_week_years(self) -> bool:
114
+ """Check if the TimeIndex is based on 52-week years."""
115
+ return self._is_52_week_years
116
+
117
+ def is_one_year(self) -> bool:
118
+ """Return True if exactly one whole year."""
119
+ if self._extrapolate_first_point or self._extrapolate_last_point:
120
+ return False
121
+ start_time = self._datetime_list[0]
122
+ stop_time = self._datetime_list[-1]
123
+ start_year, start_week, start_weekday = start_time.isocalendar()
124
+
125
+ if not start_weekday == start_week == 1:
126
+ return False
127
+
128
+ if self._is_52_week_years:
129
+ expected_stop_time = start_time + timedelta(weeks=52)
130
+ if expected_stop_time.isocalendar().week == 53: # noqa: PLR2004
131
+ expected_stop_time += timedelta(weeks=1)
132
+ return stop_time == expected_stop_time
133
+
134
+ stop_year, stop_week, stop_weekday = stop_time.isocalendar()
135
+ return (start_year + 1 == stop_year) and (stop_weekday == stop_week == 1)
136
+
137
+ def is_whole_years(self) -> bool:
138
+ """Return True if index covers one or more full years."""
139
+ start_time = self._datetime_list[0]
140
+ _, start_week, start_weekday = start_time.isocalendar()
141
+
142
+ if not start_week == start_weekday == 1:
143
+ return False
144
+
145
+ stop_time = self._datetime_list[-1]
146
+
147
+ if not self.is_52_week_years():
148
+ _, stop_week, stop_weekday = stop_time.isocalendar()
149
+ return stop_week == stop_weekday == 1
150
+
151
+ total_period = self.total_duration()
152
+ total_seconds = int(total_period.total_seconds())
153
+ seconds_per_year = 52 * 7 * 24 * 3600
154
+
155
+ return total_seconds % seconds_per_year == 0
156
+
157
+ def extrapolate_first_point(self) -> bool:
158
+ """Check if the TimeIndex should extrapolate the first point."""
159
+ return self._extrapolate_first_point
160
+
161
+ def extrapolate_last_point(self) -> bool:
162
+ """Check if the TimeIndex should extrapolate the last point."""
163
+ return self._extrapolate_last_point
164
+
165
+ def get_period_average(self, vector: NDArray, start_time: datetime, duration: timedelta, is_52_week_years: bool) -> float:
166
+ """Get the average over the period from the vector."""
167
+ self._check_type(vector, np.ndarray)
168
+ self._check_type(start_time, datetime)
169
+ self._check_type(duration, timedelta)
170
+ self._check_type(is_52_week_years, bool)
171
+
172
+ if vector.shape != (self.get_num_periods(),):
173
+ msg = f"Vector shape {vector.shape} does not match number of periods {self.get_num_periods()} of timeindex ({self})."
174
+ raise ValueError(msg)
175
+
176
+ if not self.extrapolate_first_point():
177
+ if start_time < self._datetime_list[0]:
178
+ msg = f"start_time {start_time} is before start of timeindex {self._datetime_list[0]}, and extrapolate_first_point is False."
179
+ raise ValueError(msg)
180
+ if (start_time + duration) < self._datetime_list[0]:
181
+ msg = f"End time {start_time + duration} is before start of timeindex {self._datetime_list[0]}, and extrapolate_first_point is False."
182
+ raise ValueError(msg)
183
+
184
+ if not self.extrapolate_last_point():
185
+ if (start_time + duration) > self._datetime_list[-1]:
186
+ msg = f"End time {start_time + duration} is after end of timeindex {self._datetime_list[-1]}, and extrapolate_last_point is False."
187
+ raise ValueError(msg)
188
+ if start_time > self._datetime_list[-1]:
189
+ msg = f"start_time {start_time} is after end of timeindex {self._datetime_list[-1]}, and extrapolate_last_point is False."
190
+ raise ValueError(msg)
191
+
192
+ target_timeindex = FixedFrequencyTimeIndex(
193
+ start_time=start_time,
194
+ period_duration=duration,
195
+ num_periods=1,
196
+ is_52_week_years=is_52_week_years,
197
+ extrapolate_first_point=self.extrapolate_first_point(),
198
+ extrapolate_last_point=self.extrapolate_last_point(),
199
+ )
200
+
201
+ target_vector = np.zeros(1, dtype=vector.dtype)
202
+ self.write_into_fixed_frequency(
203
+ target_vector=target_vector,
204
+ target_timeindex=target_timeindex,
205
+ input_vector=vector,
206
+ )
207
+ return target_vector[0]
208
+
209
+ def write_into_fixed_frequency(
210
+ self,
211
+ target_vector: NDArray,
212
+ target_timeindex: FixedFrequencyTimeIndex,
213
+ input_vector: NDArray,
214
+ ) -> None:
215
+ """Write the input vector into the target vector using the target FixedFrequencyTimeIndex."""
216
+ self._check_type(target_vector, np.ndarray)
217
+ self._check_type(target_timeindex, FixedFrequencyTimeIndex)
218
+ self._check_type(input_vector, np.ndarray)
219
+
220
+ dts: list[datetime] = self._datetime_list
221
+
222
+ durations = set(self._microseconds(period_duration(dts[i], dts[i + 1], self._is_52_week_years)) for i in range(len(dts) - 1))
223
+ smallest_common_period_duration = functools.reduce(math.gcd, durations)
224
+
225
+ num_periods_ff = self._microseconds(self.total_duration()) // smallest_common_period_duration
226
+ input_vector_ff = np.zeros(num_periods_ff, dtype=target_vector.dtype)
227
+
228
+ i_start_ff = 0
229
+ for i in range(len(dts) - 1):
230
+ num_periods = self._microseconds(period_duration(dts[i], dts[i + 1], self._is_52_week_years)) // smallest_common_period_duration
231
+ i_stop_ff = i_start_ff + num_periods
232
+ input_vector_ff[i_start_ff:i_stop_ff] = input_vector[i]
233
+ i_start_ff = i_stop_ff
234
+
235
+ input_timeindex_ff = FixedFrequencyTimeIndex(
236
+ start_time=dts[0],
237
+ num_periods=num_periods_ff,
238
+ period_duration=timedelta(microseconds=smallest_common_period_duration),
239
+ is_52_week_years=self.is_52_week_years(),
240
+ extrapolate_first_point=self.extrapolate_first_point(),
241
+ extrapolate_last_point=self.extrapolate_last_point(),
242
+ )
243
+
244
+ input_timeindex_ff.write_into_fixed_frequency(
245
+ target_vector=target_vector,
246
+ target_timeindex=target_timeindex,
247
+ input_vector=input_vector_ff,
248
+ )
249
+
250
+ def total_duration(self) -> timedelta:
251
+ """
252
+ Return the total duration covered by the time index.
253
+
254
+ Returns
255
+ -------
256
+ timedelta
257
+ The duration from the first to the last datetime in the index, skipping all weeks 53 periods if 52-week time format.
258
+
259
+ """
260
+ start_time = self._datetime_list[0]
261
+ end_time = self._datetime_list[-1]
262
+ return period_duration(start_time, end_time, self.is_52_week_years())
263
+
264
+ def _microseconds(self, duration: timedelta) -> int:
265
+ return int(duration.total_seconds() * 1e6)
266
+
267
+ def is_constant(self) -> bool:
268
+ """
269
+ Return True if the time index is constant (single period and both extrapolation flags are True).
270
+
271
+ Returns
272
+ -------
273
+ bool
274
+ True if the time index is constant, False otherwise.
275
+
276
+ """
277
+ return self.get_num_periods() == 1 and self.extrapolate_first_point() == self.extrapolate_last_point() is True
@@ -0,0 +1,23 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class ModelYear(SinglePeriodTimeIndex):
7
+ """ModelYear represent a period of 52 weeks starting from the iso calendar week 1 of a specified year. No extrapolation."""
8
+
9
+ def __init__(self, year: int) -> None:
10
+ """
11
+ Initialize ModelYear to a period of 52 weeks starting from the iso calendar week 1 of the specified year. No extrapolation.
12
+
13
+ Args:
14
+ year (int): Year to represent.
15
+
16
+ """
17
+ super().__init__(
18
+ start_time=datetime.fromisocalendar(year, 1, 1),
19
+ period_duration=timedelta(weeks=52),
20
+ is_52_week_years=True,
21
+ extrapolate_first_point=False,
22
+ extrapolate_last_point=False,
23
+ )
@@ -0,0 +1,27 @@
1
+ from datetime import datetime
2
+
3
+ from framcore.timeindexes.ListTimeIndex import ListTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class ModelYears(ListTimeIndex):
7
+ """ModelYears represents a collection of years as a ListTimeIndex. Extrapolation is enabled and full iso calendar is used."""
8
+
9
+ def __init__(self, years: list[int]) -> None:
10
+ """
11
+ Initialize ModelYears with a list of years.
12
+
13
+ Args:
14
+ years (list[int]): List of years to represent.
15
+
16
+ """
17
+ if not years:
18
+ raise ValueError("At least one year must be provided.")
19
+
20
+ datetime_list = [datetime.fromisocalendar(year, 1, 1) for year in years]
21
+ datetime_list.append(datetime.fromisocalendar(years[-1] + 1, 1, 1))
22
+ super().__init__(
23
+ datetime_list=datetime_list,
24
+ is_52_week_years=False,
25
+ extrapolate_first_point=True,
26
+ extrapolate_last_point=True,
27
+ )
@@ -0,0 +1,29 @@
1
+ from datetime import timedelta
2
+
3
+ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class OneYearProfileTimeIndex(ProfileTimeIndex):
7
+ """
8
+ ProfileTimeIndex with fixed frequency over one year of either 52 or 53 weeks. No extrapolation inherited from ProfileTimeIndex.
9
+
10
+ Attributes:
11
+ period_duration (timedelta): Duration of each period.
12
+ is_52_week_years (bool): Whether to use 52-week years.
13
+
14
+ """
15
+
16
+ def __init__(self, period_duration: timedelta, is_52_week_years: bool) -> None:
17
+ """
18
+ Initialize a ProfileTimeIndex with a fixed frequency over one year.
19
+
20
+ If is_52_week_years is True, the period_duration must divide evenly into 52 weeks. If False, it must divide evenly into 53 weeks.
21
+ We use 1982 for 52-week years and 1981 for 53-week years.
22
+
23
+ Args:
24
+ period_duration (timedelta): Duration of each period.
25
+ is_52_week_years (bool): Whether to use 52-week years.
26
+
27
+ """
28
+ year = 1982 if is_52_week_years else 1981
29
+ super().__init__(year, 1, period_duration, is_52_week_years)
@@ -0,0 +1,43 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from framcore.timeindexes.FixedFrequencyTimeIndex import FixedFrequencyTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class ProfileTimeIndex(FixedFrequencyTimeIndex):
7
+ """ProfileTimeIndex represent one or more whole years with fixed time resolution standard. No extrapolation."""
8
+
9
+ def __init__(
10
+ self,
11
+ start_year: int,
12
+ num_years: int,
13
+ period_duration: timedelta,
14
+ is_52_week_years: bool,
15
+ ) -> None:
16
+ """
17
+ Initialize the ProfileTimeIndex. No extrapolation.
18
+
19
+ Args:
20
+ start_year (int): First year in the index.
21
+ num_years (int): Number of years in the index.
22
+ period_duration (timedelta): Duration of each period in the index.
23
+ is_52_week_years (bool): Whether to use 52-week years. If False, full iso calendar years are used.
24
+
25
+ """
26
+ start_time = datetime.fromisocalendar(start_year, 1, 1)
27
+ if not is_52_week_years:
28
+ stop_time = datetime.fromisocalendar(start_year + num_years, 1, 1)
29
+ num_periods = (stop_time - start_time).total_seconds() / period_duration.total_seconds()
30
+ else:
31
+ num_periods = timedelta(weeks=52 * num_years).total_seconds() / period_duration.total_seconds()
32
+ if not num_periods.is_integer():
33
+ msg = f"Number of periods derived from input arguments must be an integer/whole number. Got {num_periods}."
34
+ raise ValueError(msg)
35
+ num_periods = int(num_periods)
36
+ super().__init__(
37
+ start_time=start_time,
38
+ period_duration=period_duration,
39
+ num_periods=num_periods,
40
+ is_52_week_years=is_52_week_years,
41
+ extrapolate_first_point=False,
42
+ extrapolate_last_point=False,
43
+ )
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timedelta
4
+
5
+ from framcore.timeindexes.FixedFrequencyTimeIndex import FixedFrequencyTimeIndex # NB! full import path needed for inheritance to work
6
+
7
+
8
+ class SinglePeriodTimeIndex(FixedFrequencyTimeIndex):
9
+ """FixedFrequencyTimeIndex with just one single step."""
10
+
11
+ def __init__(
12
+ self,
13
+ start_time: datetime,
14
+ period_duration: timedelta,
15
+ is_52_week_years: bool = False,
16
+ extrapolate_first_point: bool = False,
17
+ extrapolate_last_point: bool = False,
18
+ ) -> None:
19
+ """
20
+ Initialize a SinglePeriodTimeIndex with a single time period.
21
+
22
+ Args:
23
+ start_time (datetime): The start time of the period.
24
+ period_duration (timedelta): The duration of the period.
25
+ is_52_week_years (bool, optional): Whether to use 52-week years. Defaults to False.
26
+ extrapolate_first_point (bool, optional): Whether to extrapolate the first point. Defaults to False.
27
+ extrapolate_last_point (bool, optional): Whether to extrapolate the last point. Defaults to False.
28
+
29
+ """
30
+ super().__init__(
31
+ start_time=start_time,
32
+ period_duration=period_duration,
33
+ num_periods=1,
34
+ is_52_week_years=is_52_week_years,
35
+ extrapolate_first_point=extrapolate_first_point,
36
+ extrapolate_last_point=extrapolate_last_point,
37
+ )
@@ -0,0 +1,103 @@
1
+ # from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from datetime import datetime, timedelta, tzinfo
5
+
6
+ from numpy.typing import NDArray
7
+
8
+ from framcore import Base
9
+ from framcore.fingerprints.fingerprint import Fingerprint
10
+ from framcore.timeindexes import FixedFrequencyTimeIndex
11
+
12
+
13
+ class TimeIndex(Base, ABC):
14
+ """TimeIndex interface for TimeVectors."""
15
+
16
+ @abstractmethod
17
+ def __eq__(self, other) -> bool: # noqa: ANN001
18
+ """Check if two TimeIndexes are equal."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def __hash__(self) -> int:
23
+ """Compute hash value.."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ def get_fingerprint(self) -> Fingerprint:
28
+ """Get the fingerprint of the TimeIndex."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def get_timezone(self) -> tzinfo | None:
33
+ """Get the timezone of the TimeIndex."""
34
+ pass
35
+
36
+ @abstractmethod
37
+ def get_num_periods(self) -> bool:
38
+ """Get the number of periods in the TimeIndex."""
39
+ pass
40
+
41
+ @abstractmethod
42
+ def is_52_week_years(self) -> bool:
43
+ """Check if the TimeIndex is based on 52-week years."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ def is_one_year(self) -> bool:
48
+ """
49
+ Check if the TimeIndex represents a single year.
50
+
51
+ Must be False if extrapolate_first_point and or extrapolate_last_point is True.
52
+
53
+ When True, can be repeted in profiles.
54
+ """
55
+ pass
56
+
57
+ @abstractmethod
58
+ def is_whole_years(self) -> bool:
59
+ """Check if the TimeIndex represents whole years."""
60
+ pass
61
+
62
+ @abstractmethod
63
+ def extrapolate_first_point(self) -> bool:
64
+ """Check if the TimeIndex should extrapolate the first point. Must be False if is_one_year is True."""
65
+ pass
66
+
67
+ @abstractmethod
68
+ def extrapolate_last_point(self) -> bool:
69
+ """Check if the TimeIndex should extrapolate the last point. Must be False if is_one_year is True."""
70
+ pass
71
+
72
+ @abstractmethod
73
+ def get_period_average(self, vector: NDArray, start_time: datetime, duration: timedelta, is_52_week_years: bool) -> float:
74
+ """Get the average over the period from the vector."""
75
+ pass
76
+
77
+ @abstractmethod
78
+ def write_into_fixed_frequency(
79
+ self,
80
+ target_vector: NDArray,
81
+ target_timeindex: FixedFrequencyTimeIndex,
82
+ input_vector: NDArray,
83
+ ) -> None:
84
+ """
85
+ Write the input vector into the target vector based on the target FixedFrequencyTimeIndex.
86
+
87
+ Main functionality in FRAM to extracts data to the correct time period and resolution.
88
+ A conversion of the data into a specific time period and resolution follows these steps:
89
+ - If the TimeIndex is not a FixedFrequencyTimeIndex, convert the TimeIndex and the vector to this format.
90
+ - Then convert the data according to the target TimeIndex.
91
+ - It is easier to efficiently do time series operations between FixedFrequencyTimeIndex
92
+ and we only need to implement all the other conversion functionality once here.
93
+ For example, converting between 52-week and ISO-time TimeVectors, selecting a period, extrapolation or changing the resolution.
94
+ - And when we implement a new TimeIndex, we only need to implement the conversion to FixedFrequencyTimeIndex
95
+ and the rest of the conversion functionality can be reused.
96
+
97
+ """
98
+ pass
99
+
100
+ @abstractmethod
101
+ def is_constant(self) -> bool:
102
+ """Check if the TimeIndex is constant."""
103
+ pass
@@ -0,0 +1,33 @@
1
+ from datetime import timedelta
2
+
3
+ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class WeeklyIndex(ProfileTimeIndex):
7
+ """
8
+ ProfileTimeIndex with one or more whole years with weekly resolution. Either years with 52 weeks or full iso calendar years.
9
+
10
+ No extrapolation inherited from ProfileTimeIndex.
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ start_year: int,
16
+ num_years: int,
17
+ is_52_week_years: bool = True,
18
+ ) -> None:
19
+ """
20
+ Initialize WeeklyIndex with one or more whole years with weekly resolution. Either years with 52 weeks or full iso calendar years.
21
+
22
+ Args:
23
+ start_year (int): First year in the index.
24
+ num_years (int): Number of years in the index.
25
+ is_52_week_years (bool, optional): Whether to use 52-week years. If False, full iso calendar years are used. Defaults to True.
26
+
27
+ """
28
+ super().__init__(
29
+ start_year=start_year,
30
+ num_years=num_years,
31
+ period_duration=timedelta(weeks=1),
32
+ is_52_week_years=is_52_week_years,
33
+ )
@@ -0,0 +1,36 @@
1
+ # framcore/timeindexes/__init__.py
2
+
3
+ """FRAM time indexes package provides functionality for handling time-related data."""
4
+
5
+ from framcore.timeindexes.FixedFrequencyTimeIndex import FixedFrequencyTimeIndex
6
+ from framcore.timeindexes.TimeIndex import TimeIndex
7
+ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex
8
+ from framcore.timeindexes.ListTimeIndex import ListTimeIndex
9
+ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex
10
+ from framcore.timeindexes.AverageYearRange import AverageYearRange
11
+ from framcore.timeindexes.ConstantTimeIndex import ConstantTimeIndex
12
+ from framcore.timeindexes.DailyIndex import DailyIndex
13
+ from framcore.timeindexes.HourlyIndex import HourlyIndex
14
+ from framcore.timeindexes.ModelYear import ModelYear
15
+ from framcore.timeindexes.ModelYears import ModelYears
16
+ from framcore.timeindexes.OneYearProfileTimeIndex import OneYearProfileTimeIndex
17
+ from framcore.timeindexes.WeeklyIndex import WeeklyIndex
18
+ from framcore.timeindexes.IsoCalendarDay import IsoCalendarDay
19
+
20
+
21
+ __all__ = [
22
+ "AverageYearRange",
23
+ "ConstantTimeIndex",
24
+ "DailyIndex",
25
+ "FixedFrequencyTimeIndex",
26
+ "HourlyIndex",
27
+ "IsoCalendarDay",
28
+ "ListTimeIndex",
29
+ "ModelYear",
30
+ "ModelYears",
31
+ "OneYearProfileTimeIndex",
32
+ "ProfileTimeIndex",
33
+ "SinglePeriodTimeIndex",
34
+ "TimeIndex",
35
+ "WeeklyIndex",
36
+ ]