fram-core 0.0.0__py3-none-any.whl → 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fram_core-0.1.0a1.dist-info/METADATA +41 -0
- fram_core-0.1.0a1.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
- fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
- framcore/Base.py +142 -0
- framcore/Model.py +73 -0
- framcore/__init__.py +9 -0
- framcore/aggregators/Aggregator.py +153 -0
- framcore/aggregators/HydroAggregator.py +837 -0
- framcore/aggregators/NodeAggregator.py +495 -0
- framcore/aggregators/WindSolarAggregator.py +323 -0
- framcore/aggregators/__init__.py +13 -0
- framcore/aggregators/_utils.py +184 -0
- framcore/attributes/Arrow.py +305 -0
- framcore/attributes/ElasticDemand.py +90 -0
- framcore/attributes/ReservoirCurve.py +37 -0
- framcore/attributes/SoftBound.py +19 -0
- framcore/attributes/StartUpCost.py +54 -0
- framcore/attributes/Storage.py +146 -0
- framcore/attributes/TargetBound.py +18 -0
- framcore/attributes/__init__.py +65 -0
- framcore/attributes/hydro/HydroBypass.py +42 -0
- framcore/attributes/hydro/HydroGenerator.py +83 -0
- framcore/attributes/hydro/HydroPump.py +156 -0
- framcore/attributes/hydro/HydroReservoir.py +27 -0
- framcore/attributes/hydro/__init__.py +13 -0
- framcore/attributes/level_profile_attributes.py +714 -0
- framcore/components/Component.py +112 -0
- framcore/components/Demand.py +130 -0
- framcore/components/Flow.py +167 -0
- framcore/components/HydroModule.py +330 -0
- framcore/components/Node.py +76 -0
- framcore/components/Thermal.py +204 -0
- framcore/components/Transmission.py +183 -0
- framcore/components/_PowerPlant.py +81 -0
- framcore/components/__init__.py +22 -0
- framcore/components/wind_solar.py +67 -0
- framcore/curves/Curve.py +44 -0
- framcore/curves/LoadedCurve.py +155 -0
- framcore/curves/__init__.py +9 -0
- framcore/events/__init__.py +21 -0
- framcore/events/events.py +51 -0
- framcore/expressions/Expr.py +490 -0
- framcore/expressions/__init__.py +28 -0
- framcore/expressions/_get_constant_from_expr.py +483 -0
- framcore/expressions/_time_vector_operations.py +615 -0
- framcore/expressions/_utils.py +73 -0
- framcore/expressions/queries.py +423 -0
- framcore/expressions/units.py +207 -0
- framcore/fingerprints/__init__.py +11 -0
- framcore/fingerprints/fingerprint.py +293 -0
- framcore/juliamodels/JuliaModel.py +161 -0
- framcore/juliamodels/__init__.py +7 -0
- framcore/loaders/__init__.py +10 -0
- framcore/loaders/loaders.py +407 -0
- framcore/metadata/Div.py +73 -0
- framcore/metadata/ExprMeta.py +50 -0
- framcore/metadata/LevelExprMeta.py +17 -0
- framcore/metadata/Member.py +55 -0
- framcore/metadata/Meta.py +44 -0
- framcore/metadata/__init__.py +15 -0
- framcore/populators/Populator.py +108 -0
- framcore/populators/__init__.py +7 -0
- framcore/querydbs/CacheDB.py +50 -0
- framcore/querydbs/ModelDB.py +34 -0
- framcore/querydbs/QueryDB.py +45 -0
- framcore/querydbs/__init__.py +11 -0
- framcore/solvers/Solver.py +48 -0
- framcore/solvers/SolverConfig.py +272 -0
- framcore/solvers/__init__.py +9 -0
- framcore/timeindexes/AverageYearRange.py +20 -0
- framcore/timeindexes/ConstantTimeIndex.py +17 -0
- framcore/timeindexes/DailyIndex.py +21 -0
- framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
- framcore/timeindexes/HourlyIndex.py +21 -0
- framcore/timeindexes/IsoCalendarDay.py +31 -0
- framcore/timeindexes/ListTimeIndex.py +197 -0
- framcore/timeindexes/ModelYear.py +17 -0
- framcore/timeindexes/ModelYears.py +18 -0
- framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
- framcore/timeindexes/ProfileTimeIndex.py +32 -0
- framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
- framcore/timeindexes/TimeIndex.py +90 -0
- framcore/timeindexes/WeeklyIndex.py +21 -0
- framcore/timeindexes/__init__.py +36 -0
- framcore/timevectors/ConstantTimeVector.py +135 -0
- framcore/timevectors/LinearTransformTimeVector.py +114 -0
- framcore/timevectors/ListTimeVector.py +123 -0
- framcore/timevectors/LoadedTimeVector.py +104 -0
- framcore/timevectors/ReferencePeriod.py +41 -0
- framcore/timevectors/TimeVector.py +94 -0
- framcore/timevectors/__init__.py +17 -0
- framcore/utils/__init__.py +36 -0
- framcore/utils/get_regional_volumes.py +369 -0
- framcore/utils/get_supported_components.py +60 -0
- framcore/utils/global_energy_equivalent.py +46 -0
- framcore/utils/isolate_subnodes.py +163 -0
- framcore/utils/loaders.py +97 -0
- framcore/utils/node_flow_utils.py +236 -0
- framcore/utils/storage_subsystems.py +107 -0
- fram_core-0.0.0.dist-info/METADATA +0 -5
- fram_core-0.0.0.dist-info/RECORD +0 -4
- fram_core-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
"""One or more whole years with hourly resolution."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
start_year: int,
|
|
12
|
+
num_years: int,
|
|
13
|
+
is_52_week_years: bool = True,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""One or more whole years with hourly resolution."""
|
|
16
|
+
super().__init__(
|
|
17
|
+
start_year=start_year,
|
|
18
|
+
num_years=num_years,
|
|
19
|
+
period_duration=timedelta(hours=1),
|
|
20
|
+
is_52_week_years=is_52_week_years,
|
|
21
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
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.
|
|
20
|
+
|
|
21
|
+
is_52_week_years=False
|
|
22
|
+
|
|
23
|
+
Useful for testing.
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(
|
|
26
|
+
start_time=datetime.fromisocalendar(year, week, day),
|
|
27
|
+
period_duration=timedelta(days=1),
|
|
28
|
+
is_52_week_years=False,
|
|
29
|
+
extrapolate_first_point=False,
|
|
30
|
+
extrapolate_last_point=False,
|
|
31
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
|
10
|
+
from framcore.timeindexes.TimeIndex import TimeIndex # NB! full import path needed for inheritance to work
|
|
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
|
+
"""Initialize the ListTimeIndex class."""
|
|
32
|
+
dts = datetime_list
|
|
33
|
+
if len(dts) <= 1:
|
|
34
|
+
message = f"datetime_list must contain more than one element. Got {datetime_list}"
|
|
35
|
+
raise ValueError(message)
|
|
36
|
+
if not all(dts[i] < dts[i + 1] for i in range(len(dts) - 1)):
|
|
37
|
+
message = f"All elements of datetime_list must be smaller/lower than the succeeding element. Dates must be ordered. Got {datetime_list}."
|
|
38
|
+
raise ValueError(message)
|
|
39
|
+
assert len(set(dt.tzinfo for dt in dts if dt is not None)) <= 1
|
|
40
|
+
self._datetime_list = datetime_list
|
|
41
|
+
self._is_52_week_years = is_52_week_years
|
|
42
|
+
self._extrapolate_first_point = extrapolate_first_point
|
|
43
|
+
self._extrapolate_last_point = extrapolate_last_point
|
|
44
|
+
|
|
45
|
+
def __eq__(self, other) -> bool: # noqa: ANN001
|
|
46
|
+
"""Check if two ListTimeIndexes are equal."""
|
|
47
|
+
if not isinstance(other, type(self)):
|
|
48
|
+
return False
|
|
49
|
+
return (
|
|
50
|
+
self._datetime_list == other._datetime_list
|
|
51
|
+
and self._extrapolate_first_point == other._extrapolate_first_point
|
|
52
|
+
and self._extrapolate_last_point == other._extrapolate_last_point
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __hash__(self) -> int:
|
|
56
|
+
"""Return the hash of the ListTimeIndex."""
|
|
57
|
+
return hash(
|
|
58
|
+
(
|
|
59
|
+
tuple(self._datetime_list),
|
|
60
|
+
self._extrapolate_first_point,
|
|
61
|
+
self._extrapolate_last_point,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
"""Return the string representation of the ListTimeIndex."""
|
|
67
|
+
return (
|
|
68
|
+
"ListTimeIndex("
|
|
69
|
+
f"datetimelist={self._datetime_list}, "
|
|
70
|
+
f"extrapolate_first_point={self._extrapolate_first_point}, "
|
|
71
|
+
f"extrapolate_last_point={self._extrapolate_last_point})"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def get_fingerprint(self) -> Fingerprint:
|
|
75
|
+
"""Get the fingerprint of the ListTimeIndex."""
|
|
76
|
+
fingerprint = Fingerprint()
|
|
77
|
+
fingerprint.add("datetime_list", self._datetime_list)
|
|
78
|
+
fingerprint.add("is_52_week_years", self._is_52_week_years)
|
|
79
|
+
fingerprint.add("extrapolate_first_point", self._extrapolate_first_point)
|
|
80
|
+
fingerprint.add("extrapolate_last_point", self._extrapolate_last_point)
|
|
81
|
+
return fingerprint
|
|
82
|
+
|
|
83
|
+
def get_datetime_list(self) -> list[datetime]:
|
|
84
|
+
"""Get a list of all periods (num_periods + 1 datetimes)."""
|
|
85
|
+
return self._datetime_list.copy()
|
|
86
|
+
|
|
87
|
+
def get_timezone(self) -> tzinfo | None:
|
|
88
|
+
"""Get the timezone of the TimeIndex."""
|
|
89
|
+
return self._datetime_list[0].tzinfo
|
|
90
|
+
|
|
91
|
+
def get_num_periods(self) -> int:
|
|
92
|
+
"""Get the number of periods in the TimeIndex."""
|
|
93
|
+
return len(self._datetime_list) - 1
|
|
94
|
+
|
|
95
|
+
def is_52_week_years(self) -> bool:
|
|
96
|
+
"""Check if the TimeIndex is based on 52-week years."""
|
|
97
|
+
return self._is_52_week_years
|
|
98
|
+
|
|
99
|
+
def is_one_year(self) -> bool:
|
|
100
|
+
"""Return True if exactly one whole year."""
|
|
101
|
+
if self._extrapolate_first_point or self._extrapolate_last_point:
|
|
102
|
+
return False
|
|
103
|
+
start_time = self._datetime_list[0]
|
|
104
|
+
stop_time = self._datetime_list[-1]
|
|
105
|
+
start_year, start_week, start_weekday = start_time.isocalendar()
|
|
106
|
+
if self._is_52_week_years:
|
|
107
|
+
return (start_weekday == 1) and (start_week == 1) and (stop_time == start_time + timedelta(weeks=52))
|
|
108
|
+
stop_year, stop_week, stop_weekday = stop_time.isocalendar()
|
|
109
|
+
return (start_year + 1 == stop_year) and (start_weekday == stop_weekday == 1) and (start_week == stop_week == 1)
|
|
110
|
+
|
|
111
|
+
def is_whole_years(self) -> bool:
|
|
112
|
+
"""Return True if index covers one or more full years."""
|
|
113
|
+
start_time = self._datetime_list[0]
|
|
114
|
+
start_year, start_week, start_weekday = start_time.isocalendar()
|
|
115
|
+
if not start_week == start_weekday == 1:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
stop_time = self._datetime_list[-1]
|
|
119
|
+
if not self.is_52_week_years():
|
|
120
|
+
stop_year, stop_week, stop_weekday = stop_time.isocalendar()
|
|
121
|
+
assert stop_year >= start_year
|
|
122
|
+
return stop_week == stop_weekday == 1
|
|
123
|
+
|
|
124
|
+
seconds_52_week_year = 52 * 168 * 3600
|
|
125
|
+
num_years = (stop_time - start_time).total_seconds() / seconds_52_week_year
|
|
126
|
+
return num_years.is_integer()
|
|
127
|
+
|
|
128
|
+
def extrapolate_first_point(self) -> bool:
|
|
129
|
+
"""Check if the TimeIndex should extrapolate the first point."""
|
|
130
|
+
return self._extrapolate_first_point
|
|
131
|
+
|
|
132
|
+
def extrapolate_last_point(self) -> bool:
|
|
133
|
+
"""Check if the TimeIndex should extrapolate the last point."""
|
|
134
|
+
return self._extrapolate_last_point
|
|
135
|
+
|
|
136
|
+
def get_period_average(self, vector: NDArray, start_time: datetime, duration: timedelta, is_52_week_years: bool) -> float:
|
|
137
|
+
"""Get the average over the period from the vector."""
|
|
138
|
+
# assert vector.shape == (self.get_num_periods(),), f"Vector shape {vector.shape} does not match timeindex {self}"
|
|
139
|
+
target_timeindex = FixedFrequencyTimeIndex(
|
|
140
|
+
start_time=start_time,
|
|
141
|
+
period_duration=duration,
|
|
142
|
+
num_periods=1,
|
|
143
|
+
is_52_week_years=is_52_week_years,
|
|
144
|
+
extrapolate_first_point=self.extrapolate_first_point(),
|
|
145
|
+
extrapolate_last_point=self.extrapolate_last_point(),
|
|
146
|
+
)
|
|
147
|
+
target_vector = np.zeros(1, dtype=vector.dtype)
|
|
148
|
+
self.write_into_fixed_frequency(
|
|
149
|
+
target_vector=target_vector,
|
|
150
|
+
target_timeindex=target_timeindex,
|
|
151
|
+
input_vector=vector,
|
|
152
|
+
)
|
|
153
|
+
return target_vector[0]
|
|
154
|
+
|
|
155
|
+
def write_into_fixed_frequency(
|
|
156
|
+
self,
|
|
157
|
+
target_vector: NDArray,
|
|
158
|
+
target_timeindex: FixedFrequencyTimeIndex,
|
|
159
|
+
input_vector: NDArray,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Write the input vector into the target vector using the target timeindex."""
|
|
162
|
+
dts: list[datetime] = self._datetime_list
|
|
163
|
+
|
|
164
|
+
durations = set(self._microseconds(dts[i + 1] - dts[i]) for i in range(len(dts) - 1))
|
|
165
|
+
smallest_common_period_duration = functools.reduce(math.gcd, durations)
|
|
166
|
+
|
|
167
|
+
num_periods_ff = self._microseconds(dts[-1] - dts[0]) // smallest_common_period_duration
|
|
168
|
+
input_vector_ff = np.zeros(num_periods_ff, dtype=target_vector.dtype)
|
|
169
|
+
|
|
170
|
+
i_start_ff = 0
|
|
171
|
+
for i in range(len(dts) - 1):
|
|
172
|
+
num_periods = self._microseconds(dts[i + 1] - dts[i]) // smallest_common_period_duration
|
|
173
|
+
i_stop_ff = i_start_ff + num_periods
|
|
174
|
+
input_vector_ff[i_start_ff:i_stop_ff] = input_vector[i]
|
|
175
|
+
i_start_ff = i_stop_ff
|
|
176
|
+
|
|
177
|
+
input_timeindex_ff = FixedFrequencyTimeIndex(
|
|
178
|
+
start_time=dts[0],
|
|
179
|
+
num_periods=num_periods_ff,
|
|
180
|
+
period_duration=timedelta(microseconds=smallest_common_period_duration),
|
|
181
|
+
is_52_week_years=self.is_52_week_years(),
|
|
182
|
+
extrapolate_first_point=self.extrapolate_first_point(),
|
|
183
|
+
extrapolate_last_point=self.extrapolate_last_point(),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
input_timeindex_ff.write_into_fixed_frequency(
|
|
187
|
+
target_vector=target_vector,
|
|
188
|
+
target_timeindex=target_timeindex,
|
|
189
|
+
input_vector=input_vector_ff,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _microseconds(self, duration: timedelta) -> int:
|
|
193
|
+
return int(duration.total_seconds() * 1e6)
|
|
194
|
+
|
|
195
|
+
def is_constant(self) -> bool:
|
|
196
|
+
"""Check if the time index is constant."""
|
|
197
|
+
return super().is_constant()
|
|
@@ -0,0 +1,17 @@
|
|
|
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 one 52-week-year. No extrapolation."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, year: int) -> None:
|
|
10
|
+
"""Represent a specified year. Use 52-week-year starting on monday in week 1. No extrapolation."""
|
|
11
|
+
super().__init__(
|
|
12
|
+
start_time=datetime.fromisocalendar(year, 1, 1),
|
|
13
|
+
period_duration=timedelta(weeks=52),
|
|
14
|
+
is_52_week_years=True,
|
|
15
|
+
extrapolate_first_point=False,
|
|
16
|
+
extrapolate_last_point=False,
|
|
17
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
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."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, years: list[int]) -> None:
|
|
10
|
+
"""Initialize ModelYears with a list of years."""
|
|
11
|
+
datetime_list = [datetime.fromisocalendar(year, 1, 1) for year in years]
|
|
12
|
+
datetime_list.append(datetime.fromisocalendar(years[-1] + 1, 1, 1))
|
|
13
|
+
super().__init__(
|
|
14
|
+
datetime_list=datetime_list,
|
|
15
|
+
is_52_week_years=False,
|
|
16
|
+
extrapolate_first_point=True,
|
|
17
|
+
extrapolate_last_point=True,
|
|
18
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
"""Fixed frequency over one year."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, period_duration: timedelta, is_52_week_years: bool) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Initialize a OneYearProfileTimeIndex with a fixed frequency over one year.
|
|
12
|
+
|
|
13
|
+
We use 1982 for 52-week years and 1981 for 53-week years.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
period_duration (timedelta): Duration of each period.
|
|
17
|
+
is_52_week_years (bool): Whether to use 52-week years.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
year = 1982 if is_52_week_years else 1981
|
|
21
|
+
super().__init__(year, 1, period_duration, is_52_week_years)
|
|
@@ -0,0 +1,32 @@
|
|
|
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."""
|
|
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
|
+
"""Initialize the ProfileTimeIndex."""
|
|
17
|
+
start_time = datetime.fromisocalendar(start_year, 1, 1)
|
|
18
|
+
if not is_52_week_years:
|
|
19
|
+
stop_time = datetime.fromisocalendar(start_year + num_years, 1, 1)
|
|
20
|
+
num_periods = (stop_time - start_time).total_seconds() / period_duration.total_seconds()
|
|
21
|
+
else:
|
|
22
|
+
num_periods = timedelta(weeks=52 * num_years).total_seconds() / period_duration.total_seconds()
|
|
23
|
+
assert num_periods.is_integer()
|
|
24
|
+
num_periods = int(num_periods)
|
|
25
|
+
super().__init__(
|
|
26
|
+
start_time=start_time,
|
|
27
|
+
period_duration=period_duration,
|
|
28
|
+
num_periods=num_periods,
|
|
29
|
+
is_52_week_years=is_52_week_years,
|
|
30
|
+
extrapolate_first_point=False,
|
|
31
|
+
extrapolate_last_point=False,
|
|
32
|
+
)
|
|
@@ -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
|
+
"""FrequencyTimeIndex 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,90 @@
|
|
|
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
|
|
52
|
+
extrapolate_first_point and or extrapolate_last_point is True.
|
|
53
|
+
|
|
54
|
+
When True, can be repeted in profiles.
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def is_whole_years(self) -> bool:
|
|
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
|
+
"""Write the input vector into the target vector based on the target FixedFrequencyTimeIndex."""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def is_constant(self) -> bool:
|
|
89
|
+
"""Check if the TimeIndex is constant."""
|
|
90
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
"""One or more whole years with weekly resolution."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
start_year: int,
|
|
12
|
+
num_years: int,
|
|
13
|
+
is_52_week_years: bool = True,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""One or more whole years with weekly resolution."""
|
|
16
|
+
super().__init__(
|
|
17
|
+
start_year=start_year,
|
|
18
|
+
num_years=num_years,
|
|
19
|
+
period_duration=timedelta(weeks=1),
|
|
20
|
+
is_52_week_years=is_52_week_years,
|
|
21
|
+
)
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numpy.typing import NDArray
|
|
3
|
+
|
|
4
|
+
from framcore.fingerprints import Fingerprint
|
|
5
|
+
from framcore.timeindexes import ConstantTimeIndex
|
|
6
|
+
from framcore.timevectors import ReferencePeriod
|
|
7
|
+
from framcore.timevectors.TimeVector import TimeVector # NB! full import path needed for inheritance to work
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConstantTimeVector(TimeVector):
|
|
11
|
+
"""ConstantTimeVector class for TimeVectors that are constant over time. Subclass of TimeVector."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
scalar: float,
|
|
16
|
+
unit: str | None = None,
|
|
17
|
+
is_max_level: bool | None = None,
|
|
18
|
+
is_zero_one_profile: bool | None = None,
|
|
19
|
+
reference_period: ReferencePeriod | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initialize the ListTimeVector class.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
scalar (float): Constant float value of the timevector.
|
|
26
|
+
unit (str | None): Unit of the value in the vector.
|
|
27
|
+
is_max_level (bool | None): Whether the vector represents the maximum level, average level given a
|
|
28
|
+
reference period, or not a level at all.
|
|
29
|
+
is_zero_one_profile (bool | None): Whether the vector represents aprofile with values between 0 and 1, a
|
|
30
|
+
profile with values averaging to 1 over a given reference period, or is
|
|
31
|
+
not a profile.
|
|
32
|
+
reference_period (ReferencePeriod | None, optional): Given reference period if the vector represents average
|
|
33
|
+
level or mean one profile. Defaults to None.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: When both is_max_level and is_zero_one_profile is not None. This would mean the TimeVector
|
|
37
|
+
represents both a level and a profile, which is not allowed.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
self._scalar = float(scalar)
|
|
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
|
+
self._is_max_level = is_max_level
|
|
51
|
+
self._is_zero_one_profile = is_zero_one_profile
|
|
52
|
+
self._reference_period = reference_period
|
|
53
|
+
|
|
54
|
+
self._check_type(scalar, (float, np.float32)) # TODO: Accept np.float32 elsewhere aswell
|
|
55
|
+
self._check_type(unit, (str, type(None)))
|
|
56
|
+
self._check_type(is_max_level, (bool, type(None)))
|
|
57
|
+
self._check_type(is_zero_one_profile, (bool, type(None)))
|
|
58
|
+
self._check_type(reference_period, (ReferencePeriod, type(None)))
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
"""Return the string representation of the ConstantTimeVector."""
|
|
62
|
+
ref_period = None
|
|
63
|
+
if self._reference_period is not None:
|
|
64
|
+
start_year = self._reference_period.get_start_year()
|
|
65
|
+
num_years = self._reference_period.get_num_years()
|
|
66
|
+
assert num_years > 0
|
|
67
|
+
ref_period = f"{start_year}-{start_year + num_years - 1}"
|
|
68
|
+
unit = f", unit={self._unit}" if self._unit is not None else ""
|
|
69
|
+
ref_period = f", reference_period={ref_period}" if ref_period is not None else ""
|
|
70
|
+
is_max_level = f", is_max_level={self._is_max_level}"
|
|
71
|
+
return f"ConstantTimeVector({self._scalar}{unit}{ref_period}{is_max_level})"
|
|
72
|
+
|
|
73
|
+
def __eq__(self, other: object) -> bool:
|
|
74
|
+
"""Check equality between two ConstantTimeVector objects."""
|
|
75
|
+
if not isinstance(other, ConstantTimeVector):
|
|
76
|
+
return NotImplemented
|
|
77
|
+
return (
|
|
78
|
+
self._scalar == other._scalar
|
|
79
|
+
and self._unit == other._unit
|
|
80
|
+
and self._is_max_level == other._is_max_level
|
|
81
|
+
and self._is_zero_one_profile == other._is_zero_one_profile
|
|
82
|
+
and self._reference_period == other._reference_period
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def __hash__(self) -> int:
|
|
86
|
+
"""Compute the hash of the ConstantTimeVector."""
|
|
87
|
+
return hash((self._scalar, self._unit, self._is_max_level, self._is_zero_one_profile, self._reference_period))
|
|
88
|
+
|
|
89
|
+
def get_expr_str(self) -> str:
|
|
90
|
+
"""Simpler representation of self to show in Expr."""
|
|
91
|
+
return f"{self._scalar} {self._unit if self._unit else ''}"
|
|
92
|
+
|
|
93
|
+
def get_vector(self, is_float32: bool) -> NDArray:
|
|
94
|
+
"""Get the values of the TimeVector."""
|
|
95
|
+
dtype = np.float32 if is_float32 else np.float64
|
|
96
|
+
out = np.zeros(1, dtype=dtype)
|
|
97
|
+
out[0] = self._scalar
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
def get_timeindex(self) -> ConstantTimeIndex:
|
|
101
|
+
"""Get the TimeIndex of the TimeVector."""
|
|
102
|
+
return ConstantTimeIndex()
|
|
103
|
+
|
|
104
|
+
def is_constant(self) -> bool:
|
|
105
|
+
"""Check if the TimeVector is constant."""
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def is_max_level(self) -> bool:
|
|
109
|
+
"""Check if TimeVector is a level representing maximum Volume/Capacity."""
|
|
110
|
+
return self._is_max_level
|
|
111
|
+
|
|
112
|
+
def is_zero_one_profile(self) -> bool:
|
|
113
|
+
"""Check if TimeVector is a profile with values between zero and one."""
|
|
114
|
+
return self._is_zero_one_profile
|
|
115
|
+
|
|
116
|
+
def get_unit(self) -> str | None:
|
|
117
|
+
"""Get the unit of the TimeVector."""
|
|
118
|
+
return self._unit
|
|
119
|
+
|
|
120
|
+
def get_reference_period(self) -> ReferencePeriod | None:
|
|
121
|
+
"""Get the reference period of the TimeVector."""
|
|
122
|
+
if self._reference_period is not None:
|
|
123
|
+
return self._reference_period
|
|
124
|
+
if self.is_zero_one_profile() is False:
|
|
125
|
+
timeindex = self.get_timeindex()
|
|
126
|
+
return timeindex.get_reference_period()
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def get_fingerprint(self) -> Fingerprint:
|
|
130
|
+
"""Get the Fingerprint of the TimeVector."""
|
|
131
|
+
return self.get_fingerprint_default()
|
|
132
|
+
|
|
133
|
+
def get_loader(self) -> None:
|
|
134
|
+
"""Interface method Not applicable for this type. Return None."""
|
|
135
|
+
return
|