fram-core 0.1.0a1__py3-none-any.whl → 0.1.1__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.0a1.dist-info → fram_core-0.1.1.dist-info}/METADATA +6 -5
- fram_core-0.1.1.dist-info/RECORD +100 -0
- {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/WHEEL +1 -1
- framcore/Base.py +22 -3
- framcore/Model.py +26 -9
- framcore/__init__.py +2 -1
- framcore/aggregators/Aggregator.py +30 -11
- framcore/aggregators/HydroAggregator.py +37 -25
- framcore/aggregators/NodeAggregator.py +65 -30
- framcore/aggregators/WindSolarAggregator.py +22 -30
- framcore/attributes/Arrow.py +6 -4
- framcore/attributes/ElasticDemand.py +13 -13
- framcore/attributes/ReservoirCurve.py +3 -17
- framcore/attributes/SoftBound.py +2 -5
- framcore/attributes/StartUpCost.py +14 -3
- framcore/attributes/Storage.py +17 -5
- framcore/attributes/TargetBound.py +2 -4
- framcore/attributes/__init__.py +2 -4
- framcore/attributes/hydro/HydroBypass.py +9 -2
- framcore/attributes/hydro/HydroGenerator.py +24 -7
- framcore/attributes/hydro/HydroPump.py +32 -10
- framcore/attributes/hydro/HydroReservoir.py +4 -4
- framcore/attributes/level_profile_attributes.py +250 -53
- framcore/components/Component.py +27 -3
- framcore/components/Demand.py +18 -4
- framcore/components/Flow.py +26 -4
- framcore/components/HydroModule.py +45 -4
- framcore/components/Node.py +32 -9
- framcore/components/Thermal.py +12 -8
- framcore/components/Transmission.py +17 -2
- framcore/components/wind_solar.py +25 -10
- framcore/curves/LoadedCurve.py +0 -9
- framcore/expressions/Expr.py +137 -36
- framcore/expressions/__init__.py +3 -1
- framcore/expressions/_get_constant_from_expr.py +14 -20
- framcore/expressions/queries.py +121 -84
- framcore/expressions/units.py +30 -3
- framcore/fingerprints/fingerprint.py +0 -1
- framcore/juliamodels/JuliaModel.py +13 -3
- framcore/loaders/loaders.py +0 -2
- framcore/metadata/ExprMeta.py +13 -7
- framcore/metadata/LevelExprMeta.py +16 -1
- framcore/metadata/Member.py +7 -7
- framcore/metadata/__init__.py +1 -1
- framcore/querydbs/CacheDB.py +1 -1
- framcore/solvers/Solver.py +21 -6
- framcore/solvers/SolverConfig.py +4 -4
- framcore/timeindexes/AverageYearRange.py +9 -2
- framcore/timeindexes/ConstantTimeIndex.py +7 -2
- framcore/timeindexes/DailyIndex.py +14 -2
- framcore/timeindexes/FixedFrequencyTimeIndex.py +105 -53
- framcore/timeindexes/HourlyIndex.py +14 -2
- framcore/timeindexes/IsoCalendarDay.py +5 -3
- framcore/timeindexes/ListTimeIndex.py +103 -23
- framcore/timeindexes/ModelYear.py +8 -2
- framcore/timeindexes/ModelYears.py +11 -2
- framcore/timeindexes/OneYearProfileTimeIndex.py +10 -2
- framcore/timeindexes/ProfileTimeIndex.py +14 -3
- framcore/timeindexes/SinglePeriodTimeIndex.py +1 -1
- framcore/timeindexes/TimeIndex.py +16 -3
- framcore/timeindexes/WeeklyIndex.py +14 -2
- framcore/{expressions → timeindexes}/_time_vector_operations.py +76 -2
- framcore/timevectors/ConstantTimeVector.py +12 -16
- framcore/timevectors/LinearTransformTimeVector.py +20 -3
- framcore/timevectors/ListTimeVector.py +18 -14
- framcore/timevectors/LoadedTimeVector.py +1 -8
- framcore/timevectors/ReferencePeriod.py +13 -3
- framcore/timevectors/TimeVector.py +26 -12
- framcore/utils/__init__.py +0 -1
- framcore/utils/get_regional_volumes.py +21 -3
- framcore/utils/get_supported_components.py +1 -1
- framcore/utils/global_energy_equivalent.py +22 -5
- framcore/utils/isolate_subnodes.py +12 -3
- framcore/utils/loaders.py +7 -7
- framcore/utils/node_flow_utils.py +4 -4
- framcore/utils/storage_subsystems.py +3 -4
- fram_core-0.1.0a1.dist-info/RECORD +0 -100
- {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -6,8 +6,8 @@ import numpy as np
|
|
|
6
6
|
from numpy.typing import NDArray
|
|
7
7
|
|
|
8
8
|
from framcore.fingerprints import Fingerprint
|
|
9
|
-
from framcore.timeindexes import FixedFrequencyTimeIndex
|
|
10
|
-
from framcore.timeindexes.
|
|
9
|
+
from framcore.timeindexes import FixedFrequencyTimeIndex, TimeIndex
|
|
10
|
+
from framcore.timeindexes._time_vector_operations import period_duration
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class ListTimeIndex(TimeIndex):
|
|
@@ -28,15 +28,33 @@ class ListTimeIndex(TimeIndex):
|
|
|
28
28
|
extrapolate_first_point: bool,
|
|
29
29
|
extrapolate_last_point: bool,
|
|
30
30
|
) -> None:
|
|
31
|
-
"""
|
|
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
|
+
"""
|
|
32
44
|
dts = datetime_list
|
|
33
45
|
if len(dts) <= 1:
|
|
34
|
-
|
|
35
|
-
raise ValueError(
|
|
46
|
+
msg = f"datetime_list must contain more than one element. Got {datetime_list}"
|
|
47
|
+
raise ValueError(msg)
|
|
36
48
|
if not all(dts[i] < dts[i + 1] for i in range(len(dts) - 1)):
|
|
37
|
-
|
|
38
|
-
raise ValueError(
|
|
39
|
-
|
|
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
|
+
|
|
40
58
|
self._datetime_list = datetime_list
|
|
41
59
|
self._is_52_week_years = is_52_week_years
|
|
42
60
|
self._extrapolate_first_point = extrapolate_first_point
|
|
@@ -103,27 +121,38 @@ class ListTimeIndex(TimeIndex):
|
|
|
103
121
|
start_time = self._datetime_list[0]
|
|
104
122
|
stop_time = self._datetime_list[-1]
|
|
105
123
|
start_year, start_week, start_weekday = start_time.isocalendar()
|
|
124
|
+
|
|
125
|
+
if not start_weekday == start_week == 1:
|
|
126
|
+
return False
|
|
127
|
+
|
|
106
128
|
if self._is_52_week_years:
|
|
107
|
-
|
|
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
|
+
|
|
108
134
|
stop_year, stop_week, stop_weekday = stop_time.isocalendar()
|
|
109
|
-
return (start_year + 1 == stop_year) and (
|
|
135
|
+
return (start_year + 1 == stop_year) and (stop_weekday == stop_week == 1)
|
|
110
136
|
|
|
111
137
|
def is_whole_years(self) -> bool:
|
|
112
138
|
"""Return True if index covers one or more full years."""
|
|
113
139
|
start_time = self._datetime_list[0]
|
|
114
|
-
|
|
140
|
+
_, start_week, start_weekday = start_time.isocalendar()
|
|
141
|
+
|
|
115
142
|
if not start_week == start_weekday == 1:
|
|
116
143
|
return False
|
|
117
144
|
|
|
118
145
|
stop_time = self._datetime_list[-1]
|
|
146
|
+
|
|
119
147
|
if not self.is_52_week_years():
|
|
120
|
-
|
|
121
|
-
assert stop_year >= start_year
|
|
148
|
+
_, stop_week, stop_weekday = stop_time.isocalendar()
|
|
122
149
|
return stop_week == stop_weekday == 1
|
|
123
150
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
156
|
|
|
128
157
|
def extrapolate_first_point(self) -> bool:
|
|
129
158
|
"""Check if the TimeIndex should extrapolate the first point."""
|
|
@@ -135,7 +164,31 @@ class ListTimeIndex(TimeIndex):
|
|
|
135
164
|
|
|
136
165
|
def get_period_average(self, vector: NDArray, start_time: datetime, duration: timedelta, is_52_week_years: bool) -> float:
|
|
137
166
|
"""Get the average over the period from the vector."""
|
|
138
|
-
|
|
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
|
+
|
|
139
192
|
target_timeindex = FixedFrequencyTimeIndex(
|
|
140
193
|
start_time=start_time,
|
|
141
194
|
period_duration=duration,
|
|
@@ -144,6 +197,7 @@ class ListTimeIndex(TimeIndex):
|
|
|
144
197
|
extrapolate_first_point=self.extrapolate_first_point(),
|
|
145
198
|
extrapolate_last_point=self.extrapolate_last_point(),
|
|
146
199
|
)
|
|
200
|
+
|
|
147
201
|
target_vector = np.zeros(1, dtype=vector.dtype)
|
|
148
202
|
self.write_into_fixed_frequency(
|
|
149
203
|
target_vector=target_vector,
|
|
@@ -158,18 +212,22 @@ class ListTimeIndex(TimeIndex):
|
|
|
158
212
|
target_timeindex: FixedFrequencyTimeIndex,
|
|
159
213
|
input_vector: NDArray,
|
|
160
214
|
) -> None:
|
|
161
|
-
"""Write the input vector into the target vector using the target
|
|
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
|
+
|
|
162
220
|
dts: list[datetime] = self._datetime_list
|
|
163
221
|
|
|
164
|
-
durations = set(self._microseconds(dts[i + 1]
|
|
222
|
+
durations = set(self._microseconds(period_duration(dts[i], dts[i + 1], self._is_52_week_years)) for i in range(len(dts) - 1))
|
|
165
223
|
smallest_common_period_duration = functools.reduce(math.gcd, durations)
|
|
166
224
|
|
|
167
|
-
num_periods_ff = self._microseconds(
|
|
225
|
+
num_periods_ff = self._microseconds(self.total_duration()) // smallest_common_period_duration
|
|
168
226
|
input_vector_ff = np.zeros(num_periods_ff, dtype=target_vector.dtype)
|
|
169
227
|
|
|
170
228
|
i_start_ff = 0
|
|
171
229
|
for i in range(len(dts) - 1):
|
|
172
|
-
num_periods = self._microseconds(dts[i + 1]
|
|
230
|
+
num_periods = self._microseconds(period_duration(dts[i], dts[i + 1], self._is_52_week_years)) // smallest_common_period_duration
|
|
173
231
|
i_stop_ff = i_start_ff + num_periods
|
|
174
232
|
input_vector_ff[i_start_ff:i_stop_ff] = input_vector[i]
|
|
175
233
|
i_start_ff = i_stop_ff
|
|
@@ -189,9 +247,31 @@ class ListTimeIndex(TimeIndex):
|
|
|
189
247
|
input_vector=input_vector_ff,
|
|
190
248
|
)
|
|
191
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
|
+
|
|
192
264
|
def _microseconds(self, duration: timedelta) -> int:
|
|
193
265
|
return int(duration.total_seconds() * 1e6)
|
|
194
266
|
|
|
195
267
|
def is_constant(self) -> bool:
|
|
196
|
-
"""
|
|
197
|
-
|
|
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
|
|
@@ -4,10 +4,16 @@ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex #
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ModelYear(SinglePeriodTimeIndex):
|
|
7
|
-
"""ModelYear represent
|
|
7
|
+
"""ModelYear represent a period of 52 weeks starting from the iso calendar week 1 of a specified year. No extrapolation."""
|
|
8
8
|
|
|
9
9
|
def __init__(self, year: int) -> None:
|
|
10
|
-
"""
|
|
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
|
+
"""
|
|
11
17
|
super().__init__(
|
|
12
18
|
start_time=datetime.fromisocalendar(year, 1, 1),
|
|
13
19
|
period_duration=timedelta(weeks=52),
|
|
@@ -4,10 +4,19 @@ from framcore.timeindexes.ListTimeIndex import ListTimeIndex # NB! full import
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ModelYears(ListTimeIndex):
|
|
7
|
-
"""ModelYears represents a collection of years as a ListTimeIndex."""
|
|
7
|
+
"""ModelYears represents a collection of years as a ListTimeIndex. Extrapolation is enabled and full iso calendar is used."""
|
|
8
8
|
|
|
9
9
|
def __init__(self, years: list[int]) -> None:
|
|
10
|
-
"""
|
|
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
|
+
|
|
11
20
|
datetime_list = [datetime.fromisocalendar(year, 1, 1) for year in years]
|
|
12
21
|
datetime_list.append(datetime.fromisocalendar(years[-1] + 1, 1, 1))
|
|
13
22
|
super().__init__(
|
|
@@ -4,12 +4,20 @@ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full i
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class OneYearProfileTimeIndex(ProfileTimeIndex):
|
|
7
|
-
"""
|
|
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
|
+
"""
|
|
8
15
|
|
|
9
16
|
def __init__(self, period_duration: timedelta, is_52_week_years: bool) -> None:
|
|
10
17
|
"""
|
|
11
|
-
Initialize a
|
|
18
|
+
Initialize a ProfileTimeIndex with a fixed frequency over one year.
|
|
12
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.
|
|
13
21
|
We use 1982 for 52-week years and 1981 for 53-week years.
|
|
14
22
|
|
|
15
23
|
Args:
|
|
@@ -4,7 +4,7 @@ from framcore.timeindexes.FixedFrequencyTimeIndex import FixedFrequencyTimeIndex
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ProfileTimeIndex(FixedFrequencyTimeIndex):
|
|
7
|
-
"""ProfileTimeIndex represent one or more whole years with fixed time resolution standard."""
|
|
7
|
+
"""ProfileTimeIndex represent one or more whole years with fixed time resolution standard. No extrapolation."""
|
|
8
8
|
|
|
9
9
|
def __init__(
|
|
10
10
|
self,
|
|
@@ -13,14 +13,25 @@ class ProfileTimeIndex(FixedFrequencyTimeIndex):
|
|
|
13
13
|
period_duration: timedelta,
|
|
14
14
|
is_52_week_years: bool,
|
|
15
15
|
) -> None:
|
|
16
|
-
"""
|
|
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
|
+
"""
|
|
17
26
|
start_time = datetime.fromisocalendar(start_year, 1, 1)
|
|
18
27
|
if not is_52_week_years:
|
|
19
28
|
stop_time = datetime.fromisocalendar(start_year + num_years, 1, 1)
|
|
20
29
|
num_periods = (stop_time - start_time).total_seconds() / period_duration.total_seconds()
|
|
21
30
|
else:
|
|
22
31
|
num_periods = timedelta(weeks=52 * num_years).total_seconds() / period_duration.total_seconds()
|
|
23
|
-
|
|
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)
|
|
24
35
|
num_periods = int(num_periods)
|
|
25
36
|
super().__init__(
|
|
26
37
|
start_time=start_time,
|
|
@@ -6,7 +6,7 @@ from framcore.timeindexes.FixedFrequencyTimeIndex import FixedFrequencyTimeIndex
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class SinglePeriodTimeIndex(FixedFrequencyTimeIndex):
|
|
9
|
-
"""
|
|
9
|
+
"""FixedFrequencyTimeIndex with just one single step."""
|
|
10
10
|
|
|
11
11
|
def __init__(
|
|
12
12
|
self,
|
|
@@ -48,8 +48,7 @@ class TimeIndex(Base, ABC):
|
|
|
48
48
|
"""
|
|
49
49
|
Check if the TimeIndex represents a single year.
|
|
50
50
|
|
|
51
|
-
Must be False if
|
|
52
|
-
extrapolate_first_point and or extrapolate_last_point is True.
|
|
51
|
+
Must be False if extrapolate_first_point and or extrapolate_last_point is True.
|
|
53
52
|
|
|
54
53
|
When True, can be repeted in profiles.
|
|
55
54
|
"""
|
|
@@ -57,6 +56,7 @@ class TimeIndex(Base, ABC):
|
|
|
57
56
|
|
|
58
57
|
@abstractmethod
|
|
59
58
|
def is_whole_years(self) -> bool:
|
|
59
|
+
"""Check if the TimeIndex represents whole years."""
|
|
60
60
|
pass
|
|
61
61
|
|
|
62
62
|
@abstractmethod
|
|
@@ -81,7 +81,20 @@ class TimeIndex(Base, ABC):
|
|
|
81
81
|
target_timeindex: FixedFrequencyTimeIndex,
|
|
82
82
|
input_vector: NDArray,
|
|
83
83
|
) -> None:
|
|
84
|
-
"""
|
|
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
|
+
"""
|
|
85
98
|
pass
|
|
86
99
|
|
|
87
100
|
@abstractmethod
|
|
@@ -4,7 +4,11 @@ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full i
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class WeeklyIndex(ProfileTimeIndex):
|
|
7
|
-
"""
|
|
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
|
+
"""
|
|
8
12
|
|
|
9
13
|
def __init__(
|
|
10
14
|
self,
|
|
@@ -12,7 +16,15 @@ class WeeklyIndex(ProfileTimeIndex):
|
|
|
12
16
|
num_years: int,
|
|
13
17
|
is_52_week_years: bool = True,
|
|
14
18
|
) -> None:
|
|
15
|
-
"""
|
|
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
|
+
"""
|
|
16
28
|
super().__init__(
|
|
17
29
|
start_year=start_year,
|
|
18
30
|
num_years=num_years,
|
|
@@ -80,7 +80,11 @@ def convert_to_modeltime(input_vector: NDArray, startdate: datetime, period_dura
|
|
|
80
80
|
# check if the remaining period is compatible with the target period duration
|
|
81
81
|
if remaining_period % period_duration != timedelta(0):
|
|
82
82
|
suggested_period_duration = _common_compatible_period_duration(whole_duration, remaining_period)
|
|
83
|
-
err_message =
|
|
83
|
+
err_message = (
|
|
84
|
+
f"Incompatible period duration detected! The resulting vector would be incompatible with period duration of {period_duration} "
|
|
85
|
+
f"after week 53 data is removed. Solution: use period duration that is compatible with both input and resulting vectors. "
|
|
86
|
+
f"Suggested period duration: {suggested_period_duration}."
|
|
87
|
+
)
|
|
84
88
|
raise ValueError(err_message)
|
|
85
89
|
|
|
86
90
|
sub_periods = _find_all_sub_periods(startdate, end_date, week_53_periods)
|
|
@@ -199,7 +203,12 @@ def convert_to_isotime(
|
|
|
199
203
|
# check if the extended period is compatible with the target period duration
|
|
200
204
|
if extended_total_duration % period_duration != timedelta(0):
|
|
201
205
|
suggested_period_duration = _common_compatible_period_duration(total_duration, extended_total_duration)
|
|
202
|
-
err_message =
|
|
206
|
+
err_message = (
|
|
207
|
+
f"Incompatible period duration detected when converting to ISO-time! "
|
|
208
|
+
f"The resulting vector would be incompatible with period duration of {period_duration} "
|
|
209
|
+
f"after week 53 data is added. Solution: use period duration that is compatible with both "
|
|
210
|
+
f"input and resulting vectors. Suggested period duration: {suggested_period_duration}."
|
|
211
|
+
)
|
|
203
212
|
raise ValueError(err_message)
|
|
204
213
|
|
|
205
214
|
sub_periods = _find_all_sub_periods(startdate, end_date, week_53_periods)
|
|
@@ -613,3 +622,68 @@ def _find_all_week_53_periods(startdate: datetime, enddate: datetime) -> list[tu
|
|
|
613
622
|
if start < end:
|
|
614
623
|
week_53_periods.append((start, end))
|
|
615
624
|
return week_53_periods
|
|
625
|
+
|
|
626
|
+
def calculate_52_week_years_stop_time(
|
|
627
|
+
start_time: datetime,
|
|
628
|
+
period_duration: timedelta,
|
|
629
|
+
num_periods: int,
|
|
630
|
+
) -> datetime:
|
|
631
|
+
"""
|
|
632
|
+
Calculate the stop time of an isotime time series.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
start_time (datetime): The start date of the time series.
|
|
636
|
+
period_duration (timedelta): The duration of each period in the time series.
|
|
637
|
+
num_periods (int): The number of periods in the time series.
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
datetime: The calculated stop time of the time series.
|
|
641
|
+
|
|
642
|
+
"""
|
|
643
|
+
assert isinstance(start_time, datetime)
|
|
644
|
+
assert isinstance(period_duration, timedelta)
|
|
645
|
+
assert period_duration.total_seconds() > 0, "Period duration must be greater than zero."
|
|
646
|
+
assert isinstance(num_periods, int)
|
|
647
|
+
assert num_periods > 0, "Number of periods must be greater than zero."
|
|
648
|
+
|
|
649
|
+
stop_time = start_time + period_duration * num_periods
|
|
650
|
+
week_53_periods = _find_all_week_53_periods(startdate=start_time, enddate=stop_time)
|
|
651
|
+
|
|
652
|
+
if week_53_periods:
|
|
653
|
+
stop_time += timedelta(weeks=len(week_53_periods))
|
|
654
|
+
|
|
655
|
+
if stop_time.isocalendar().week == 53:
|
|
656
|
+
stop_time += timedelta(weeks=1)
|
|
657
|
+
|
|
658
|
+
return stop_time
|
|
659
|
+
|
|
660
|
+
def period_duration(start_time: datetime, end_time: datetime, is_52_week_years: bool) -> timedelta:
|
|
661
|
+
if not is_52_week_years:
|
|
662
|
+
return end_time - start_time
|
|
663
|
+
|
|
664
|
+
return _period_duration_excluded_weeks_53(start_time, end_time)
|
|
665
|
+
|
|
666
|
+
def _period_duration_excluded_weeks_53(start_time: datetime, end_time: datetime) -> timedelta:
|
|
667
|
+
"""
|
|
668
|
+
Calculate the period duration excluding all week 53 periods.
|
|
669
|
+
|
|
670
|
+
Parameters
|
|
671
|
+
----------
|
|
672
|
+
start_time : datetime
|
|
673
|
+
The start datetime.
|
|
674
|
+
end_time : datetime
|
|
675
|
+
The end datetime.
|
|
676
|
+
|
|
677
|
+
Returns
|
|
678
|
+
-------
|
|
679
|
+
timedelta
|
|
680
|
+
The period duration excluding all week 53 periods.
|
|
681
|
+
|
|
682
|
+
"""
|
|
683
|
+
if end_time < start_time:
|
|
684
|
+
raise ValueError("end_time must be after or equal to start_time")
|
|
685
|
+
|
|
686
|
+
week_53_periods = _find_all_week_53_periods(startdate=start_time, enddate=end_time)
|
|
687
|
+
excluded_duration = _total_duration(week_53_periods)
|
|
688
|
+
total_duration = end_time - start_time
|
|
689
|
+
return total_duration - excluded_duration
|
|
@@ -19,14 +19,14 @@ class ConstantTimeVector(TimeVector):
|
|
|
19
19
|
reference_period: ReferencePeriod | None = None,
|
|
20
20
|
) -> None:
|
|
21
21
|
"""
|
|
22
|
-
Initialize the
|
|
22
|
+
Initialize the ConstantTimeVector class.
|
|
23
23
|
|
|
24
24
|
Args:
|
|
25
|
-
scalar (float): Constant float value of the
|
|
25
|
+
scalar (float): Constant float value of the TimeVector.
|
|
26
26
|
unit (str | None): Unit of the value in the vector.
|
|
27
27
|
is_max_level (bool | None): Whether the vector represents the maximum level, average level given a
|
|
28
28
|
reference period, or not a level at all.
|
|
29
|
-
is_zero_one_profile (bool | None): Whether the vector represents
|
|
29
|
+
is_zero_one_profile (bool | None): Whether the vector represents a profile with values between 0 and 1, a
|
|
30
30
|
profile with values averaging to 1 over a given reference period, or is
|
|
31
31
|
not a profile.
|
|
32
32
|
reference_period (ReferencePeriod | None, optional): Given reference period if the vector represents average
|
|
@@ -39,14 +39,6 @@ class ConstantTimeVector(TimeVector):
|
|
|
39
39
|
"""
|
|
40
40
|
self._scalar = float(scalar)
|
|
41
41
|
self._unit = unit
|
|
42
|
-
|
|
43
|
-
if (is_max_level is not None and is_zero_one_profile is not None) or (is_max_level is None and is_zero_one_profile is None):
|
|
44
|
-
message = (
|
|
45
|
-
f"Input arguments for {self}: Must have exactly one 'non-None'"
|
|
46
|
-
"value for is_max_level and is_zero_one_profile. "
|
|
47
|
-
"A TimeVector is either a level or a profile."
|
|
48
|
-
)
|
|
49
|
-
raise ValueError(message)
|
|
50
42
|
self._is_max_level = is_max_level
|
|
51
43
|
self._is_zero_one_profile = is_zero_one_profile
|
|
52
44
|
self._reference_period = reference_period
|
|
@@ -57,13 +49,14 @@ class ConstantTimeVector(TimeVector):
|
|
|
57
49
|
self._check_type(is_zero_one_profile, (bool, type(None)))
|
|
58
50
|
self._check_type(reference_period, (ReferencePeriod, type(None)))
|
|
59
51
|
|
|
52
|
+
self._check_is_level_or_profile()
|
|
53
|
+
|
|
60
54
|
def __repr__(self) -> str:
|
|
61
55
|
"""Return the string representation of the ConstantTimeVector."""
|
|
62
56
|
ref_period = None
|
|
63
57
|
if self._reference_period is not None:
|
|
64
58
|
start_year = self._reference_period.get_start_year()
|
|
65
59
|
num_years = self._reference_period.get_num_years()
|
|
66
|
-
assert num_years > 0
|
|
67
60
|
ref_period = f"{start_year}-{start_year + num_years - 1}"
|
|
68
61
|
unit = f", unit={self._unit}" if self._unit is not None else ""
|
|
69
62
|
ref_period = f", reference_period={ref_period}" if ref_period is not None else ""
|
|
@@ -73,7 +66,7 @@ class ConstantTimeVector(TimeVector):
|
|
|
73
66
|
def __eq__(self, other: object) -> bool:
|
|
74
67
|
"""Check equality between two ConstantTimeVector objects."""
|
|
75
68
|
if not isinstance(other, ConstantTimeVector):
|
|
76
|
-
return
|
|
69
|
+
return False
|
|
77
70
|
return (
|
|
78
71
|
self._scalar == other._scalar
|
|
79
72
|
and self._unit == other._unit
|
|
@@ -88,7 +81,10 @@ class ConstantTimeVector(TimeVector):
|
|
|
88
81
|
|
|
89
82
|
def get_expr_str(self) -> str:
|
|
90
83
|
"""Simpler representation of self to show in Expr."""
|
|
91
|
-
|
|
84
|
+
if self._unit:
|
|
85
|
+
return f"{self._scalar} {self._unit}"
|
|
86
|
+
|
|
87
|
+
return f"{self._scalar}"
|
|
92
88
|
|
|
93
89
|
def get_vector(self, is_float32: bool) -> NDArray:
|
|
94
90
|
"""Get the values of the TimeVector."""
|
|
@@ -105,11 +101,11 @@ class ConstantTimeVector(TimeVector):
|
|
|
105
101
|
"""Check if the TimeVector is constant."""
|
|
106
102
|
return True
|
|
107
103
|
|
|
108
|
-
def is_max_level(self) -> bool:
|
|
104
|
+
def is_max_level(self) -> bool | None:
|
|
109
105
|
"""Check if TimeVector is a level representing maximum Volume/Capacity."""
|
|
110
106
|
return self._is_max_level
|
|
111
107
|
|
|
112
|
-
def is_zero_one_profile(self) -> bool:
|
|
108
|
+
def is_zero_one_profile(self) -> bool | None:
|
|
113
109
|
"""Check if TimeVector is a profile with values between zero and one."""
|
|
114
110
|
return self._is_zero_one_profile
|
|
115
111
|
|
|
@@ -9,7 +9,7 @@ from framcore.timevectors.TimeVector import TimeVector # NB! full import path n
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class LinearTransformTimeVector(TimeVector):
|
|
12
|
-
"""LinearTransformTimeVector
|
|
12
|
+
"""LinearTransformTimeVector represents a TimeVector as scale * timevector + shift. Immutable."""
|
|
13
13
|
|
|
14
14
|
def __init__(
|
|
15
15
|
self,
|
|
@@ -22,9 +22,24 @@ class LinearTransformTimeVector(TimeVector):
|
|
|
22
22
|
reference_period: ReferencePeriod | None = None,
|
|
23
23
|
) -> None:
|
|
24
24
|
"""
|
|
25
|
-
|
|
25
|
+
Initialize LinearTransformTimeVector with a TimeVector, scale and shift.
|
|
26
|
+
|
|
27
|
+
May also override unit, is_max_level, is_zero_one_profile and reference_period of the original timevector.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
timevector (TimeVector): TimeVector.
|
|
31
|
+
scale (float): Scale factor.
|
|
32
|
+
shift (float): Shift value.
|
|
33
|
+
unit (str | None): Unit of the values in the transformed vector.
|
|
34
|
+
is_max_level (bool | None, optional): Whether the transformed vector represents the maximum level,
|
|
35
|
+
average level given a reference period, or not a level at all.
|
|
36
|
+
Defaults to None.
|
|
37
|
+
is_zero_one_profile (bool | None, optional): Whether the transformed vector represents a profile with values
|
|
38
|
+
between 0 and 1, a profile with values averaging to 1 over a given
|
|
39
|
+
reference period, or is not a profile. Defaults to None.
|
|
40
|
+
reference_period (ReferencePeriod | None, optional): Given reference period if the transformed vector
|
|
41
|
+
represents average level or mean one profile. Defaults to None.
|
|
26
42
|
|
|
27
|
-
May also override unit, is_max_level, is_zero_one_profile and reference_period.
|
|
28
43
|
"""
|
|
29
44
|
self._check_type(timevector, TimeVector)
|
|
30
45
|
self._check_type(scale, float)
|
|
@@ -41,6 +56,8 @@ class LinearTransformTimeVector(TimeVector):
|
|
|
41
56
|
self._is_zero_one_profile = is_zero_one_profile
|
|
42
57
|
self._reference_period = reference_period
|
|
43
58
|
|
|
59
|
+
self._check_is_level_or_profile()
|
|
60
|
+
|
|
44
61
|
def get_vector(self, is_float32: bool) -> NDArray:
|
|
45
62
|
"""Get the values of the TimeVector."""
|
|
46
63
|
vector = self._timevector.get_vector(is_float32)
|