fram-core 0.0.0__py3-none-any.whl → 0.1.0a2__py3-none-any.whl

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