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.
- fram_core-0.1.0.dist-info/METADATA +42 -0
- fram_core-0.1.0.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0.dist-info}/WHEEL +1 -2
- fram_core-0.1.0.dist-info/licenses/LICENSE.md +8 -0
- framcore/Base.py +161 -0
- framcore/Model.py +90 -0
- framcore/__init__.py +10 -0
- framcore/aggregators/Aggregator.py +172 -0
- framcore/aggregators/HydroAggregator.py +849 -0
- framcore/aggregators/NodeAggregator.py +530 -0
- framcore/aggregators/WindSolarAggregator.py +315 -0
- framcore/aggregators/__init__.py +13 -0
- framcore/aggregators/_utils.py +184 -0
- framcore/attributes/Arrow.py +307 -0
- framcore/attributes/ElasticDemand.py +90 -0
- framcore/attributes/ReservoirCurve.py +23 -0
- framcore/attributes/SoftBound.py +16 -0
- framcore/attributes/StartUpCost.py +65 -0
- framcore/attributes/Storage.py +158 -0
- framcore/attributes/TargetBound.py +16 -0
- framcore/attributes/__init__.py +63 -0
- framcore/attributes/hydro/HydroBypass.py +49 -0
- framcore/attributes/hydro/HydroGenerator.py +100 -0
- framcore/attributes/hydro/HydroPump.py +178 -0
- framcore/attributes/hydro/HydroReservoir.py +27 -0
- framcore/attributes/hydro/__init__.py +13 -0
- framcore/attributes/level_profile_attributes.py +911 -0
- framcore/components/Component.py +136 -0
- framcore/components/Demand.py +144 -0
- framcore/components/Flow.py +189 -0
- framcore/components/HydroModule.py +371 -0
- framcore/components/Node.py +99 -0
- framcore/components/Thermal.py +208 -0
- framcore/components/Transmission.py +198 -0
- framcore/components/_PowerPlant.py +81 -0
- framcore/components/__init__.py +22 -0
- framcore/components/wind_solar.py +82 -0
- framcore/curves/Curve.py +44 -0
- framcore/curves/LoadedCurve.py +146 -0
- framcore/curves/__init__.py +9 -0
- framcore/events/__init__.py +21 -0
- framcore/events/events.py +51 -0
- framcore/expressions/Expr.py +591 -0
- framcore/expressions/__init__.py +30 -0
- framcore/expressions/_get_constant_from_expr.py +477 -0
- framcore/expressions/_utils.py +73 -0
- framcore/expressions/queries.py +416 -0
- framcore/expressions/units.py +227 -0
- framcore/fingerprints/__init__.py +11 -0
- framcore/fingerprints/fingerprint.py +292 -0
- framcore/juliamodels/JuliaModel.py +171 -0
- framcore/juliamodels/__init__.py +7 -0
- framcore/loaders/__init__.py +10 -0
- framcore/loaders/loaders.py +405 -0
- framcore/metadata/Div.py +73 -0
- framcore/metadata/ExprMeta.py +56 -0
- framcore/metadata/LevelExprMeta.py +32 -0
- framcore/metadata/Member.py +55 -0
- framcore/metadata/Meta.py +44 -0
- framcore/metadata/__init__.py +15 -0
- framcore/populators/Populator.py +108 -0
- framcore/populators/__init__.py +7 -0
- framcore/querydbs/CacheDB.py +50 -0
- framcore/querydbs/ModelDB.py +34 -0
- framcore/querydbs/QueryDB.py +45 -0
- framcore/querydbs/__init__.py +11 -0
- framcore/solvers/Solver.py +63 -0
- framcore/solvers/SolverConfig.py +272 -0
- framcore/solvers/__init__.py +9 -0
- framcore/timeindexes/AverageYearRange.py +27 -0
- framcore/timeindexes/ConstantTimeIndex.py +22 -0
- framcore/timeindexes/DailyIndex.py +33 -0
- framcore/timeindexes/FixedFrequencyTimeIndex.py +814 -0
- framcore/timeindexes/HourlyIndex.py +33 -0
- framcore/timeindexes/IsoCalendarDay.py +33 -0
- framcore/timeindexes/ListTimeIndex.py +277 -0
- framcore/timeindexes/ModelYear.py +23 -0
- framcore/timeindexes/ModelYears.py +27 -0
- framcore/timeindexes/OneYearProfileTimeIndex.py +29 -0
- framcore/timeindexes/ProfileTimeIndex.py +43 -0
- framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
- framcore/timeindexes/TimeIndex.py +103 -0
- framcore/timeindexes/WeeklyIndex.py +33 -0
- framcore/timeindexes/__init__.py +36 -0
- framcore/timeindexes/_time_vector_operations.py +689 -0
- framcore/timevectors/ConstantTimeVector.py +131 -0
- framcore/timevectors/LinearTransformTimeVector.py +131 -0
- framcore/timevectors/ListTimeVector.py +127 -0
- framcore/timevectors/LoadedTimeVector.py +97 -0
- framcore/timevectors/ReferencePeriod.py +51 -0
- framcore/timevectors/TimeVector.py +108 -0
- framcore/timevectors/__init__.py +17 -0
- framcore/utils/__init__.py +35 -0
- framcore/utils/get_regional_volumes.py +387 -0
- framcore/utils/get_supported_components.py +60 -0
- framcore/utils/global_energy_equivalent.py +63 -0
- framcore/utils/isolate_subnodes.py +172 -0
- framcore/utils/loaders.py +97 -0
- framcore/utils/node_flow_utils.py +236 -0
- framcore/utils/storage_subsystems.py +106 -0
- fram_core-0.0.0.dist-info/METADATA +0 -5
- fram_core-0.0.0.dist-info/RECORD +0 -4
- fram_core-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,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
|
+
]
|