fram-core 0.1.0a2__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fram_core-0.1.0a2.dist-info → fram_core-0.1.1.dist-info}/METADATA +4 -4
- fram_core-0.1.1.dist-info/RECORD +100 -0
- {fram_core-0.1.0a2.dist-info → fram_core-0.1.1.dist-info}/WHEEL +1 -1
- framcore/Base.py +22 -3
- framcore/Model.py +26 -9
- framcore/__init__.py +2 -1
- framcore/aggregators/Aggregator.py +30 -11
- framcore/aggregators/HydroAggregator.py +35 -23
- framcore/aggregators/NodeAggregator.py +65 -30
- framcore/aggregators/WindSolarAggregator.py +22 -30
- framcore/attributes/Arrow.py +6 -4
- framcore/attributes/ElasticDemand.py +13 -13
- framcore/attributes/ReservoirCurve.py +3 -17
- framcore/attributes/SoftBound.py +2 -5
- framcore/attributes/StartUpCost.py +14 -3
- framcore/attributes/Storage.py +17 -5
- framcore/attributes/TargetBound.py +2 -4
- framcore/attributes/__init__.py +2 -4
- framcore/attributes/hydro/HydroBypass.py +9 -2
- framcore/attributes/hydro/HydroGenerator.py +24 -7
- framcore/attributes/hydro/HydroPump.py +32 -10
- framcore/attributes/hydro/HydroReservoir.py +4 -4
- framcore/attributes/level_profile_attributes.py +250 -53
- framcore/components/Component.py +27 -3
- framcore/components/Demand.py +18 -4
- framcore/components/Flow.py +26 -4
- framcore/components/HydroModule.py +45 -4
- framcore/components/Node.py +32 -9
- framcore/components/Thermal.py +12 -8
- framcore/components/Transmission.py +17 -2
- framcore/components/wind_solar.py +25 -10
- framcore/curves/LoadedCurve.py +0 -9
- framcore/expressions/Expr.py +137 -36
- framcore/expressions/__init__.py +3 -1
- framcore/expressions/_get_constant_from_expr.py +14 -20
- framcore/expressions/queries.py +121 -84
- framcore/expressions/units.py +30 -3
- framcore/fingerprints/fingerprint.py +0 -1
- framcore/juliamodels/JuliaModel.py +13 -3
- framcore/loaders/loaders.py +0 -2
- framcore/metadata/ExprMeta.py +13 -7
- framcore/metadata/LevelExprMeta.py +16 -1
- framcore/metadata/Member.py +7 -7
- framcore/metadata/__init__.py +1 -1
- framcore/querydbs/CacheDB.py +1 -1
- framcore/solvers/Solver.py +21 -6
- framcore/solvers/SolverConfig.py +4 -4
- framcore/timeindexes/AverageYearRange.py +9 -2
- framcore/timeindexes/ConstantTimeIndex.py +7 -2
- framcore/timeindexes/DailyIndex.py +14 -2
- framcore/timeindexes/FixedFrequencyTimeIndex.py +105 -53
- framcore/timeindexes/HourlyIndex.py +14 -2
- framcore/timeindexes/IsoCalendarDay.py +5 -3
- framcore/timeindexes/ListTimeIndex.py +103 -23
- framcore/timeindexes/ModelYear.py +8 -2
- framcore/timeindexes/ModelYears.py +11 -2
- framcore/timeindexes/OneYearProfileTimeIndex.py +10 -2
- framcore/timeindexes/ProfileTimeIndex.py +14 -3
- framcore/timeindexes/SinglePeriodTimeIndex.py +1 -1
- framcore/timeindexes/TimeIndex.py +16 -3
- framcore/timeindexes/WeeklyIndex.py +14 -2
- framcore/{expressions → timeindexes}/_time_vector_operations.py +76 -2
- framcore/timevectors/ConstantTimeVector.py +12 -16
- framcore/timevectors/LinearTransformTimeVector.py +20 -3
- framcore/timevectors/ListTimeVector.py +18 -14
- framcore/timevectors/LoadedTimeVector.py +1 -8
- framcore/timevectors/ReferencePeriod.py +13 -3
- framcore/timevectors/TimeVector.py +26 -12
- framcore/utils/__init__.py +0 -1
- framcore/utils/get_regional_volumes.py +21 -3
- framcore/utils/get_supported_components.py +1 -1
- framcore/utils/global_energy_equivalent.py +22 -5
- framcore/utils/isolate_subnodes.py +12 -3
- framcore/utils/loaders.py +7 -7
- framcore/utils/node_flow_utils.py +4 -4
- framcore/utils/storage_subsystems.py +3 -4
- fram_core-0.1.0a2.dist-info/RECORD +0 -100
- {fram_core-0.1.0a2.dist-info → fram_core-0.1.1.dist-info}/licenses/LICENSE.md +0 -0
framcore/solvers/Solver.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Definition of Solver interface."""
|
|
2
|
-
|
|
3
1
|
import pickle
|
|
4
2
|
from abc import ABC, abstractmethod
|
|
5
3
|
from copy import deepcopy
|
|
@@ -10,20 +8,37 @@ from framcore.solvers import SolverConfig
|
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class Solver(Base, ABC):
|
|
13
|
-
"""
|
|
11
|
+
"""
|
|
12
|
+
Solver inteface class.
|
|
13
|
+
|
|
14
|
+
In FRAM we call energy market models for Solvers. They take a populated Model and configurations from a SolverConfig,
|
|
15
|
+
and transfers this to the solver software. Then it solves the energy market model, and writes results back to the Model.
|
|
16
|
+
"""
|
|
14
17
|
|
|
15
18
|
_FILENAME_MODEL = "model.pickle"
|
|
16
19
|
_FILENAME_SOLVER = "solver.pickle"
|
|
17
20
|
|
|
18
21
|
def solve(self, model: Model) -> None:
|
|
19
|
-
"""
|
|
22
|
+
"""
|
|
23
|
+
Inititiate the solve.
|
|
24
|
+
|
|
25
|
+
It takes the populated Model and configurations from self.SolverConfig, and transfers this to the solver software.
|
|
26
|
+
Then it solves the energy market model, and writes results back to the Model.
|
|
27
|
+
|
|
28
|
+
At the end of the solve, the Model (now with results) and the Solver object (with configurations) are pickled to the solve folder.
|
|
29
|
+
- model.pickle can be used to inspect results later.
|
|
30
|
+
- solver.pickle allows reuse of the same solver configurations (with solve_folder set to None to avoid overwriting).
|
|
31
|
+
TODO: Could also pickle the Model before solving, to have a record of the input model.
|
|
32
|
+
|
|
33
|
+
"""
|
|
20
34
|
self._check_type(model, Model)
|
|
21
35
|
|
|
22
36
|
config = self.get_config()
|
|
23
37
|
|
|
24
38
|
folder = config.get_solve_folder()
|
|
25
39
|
|
|
26
|
-
|
|
40
|
+
if folder is None:
|
|
41
|
+
raise ValueError("A folder for the Solver has not been set yet. Use Solver.get_config().set_solve_folder(folder)")
|
|
27
42
|
|
|
28
43
|
Path.mkdir(folder, parents=True, exist_ok=True)
|
|
29
44
|
|
|
@@ -44,5 +59,5 @@ class Solver(Base, ABC):
|
|
|
44
59
|
|
|
45
60
|
@abstractmethod
|
|
46
61
|
def _solve(self, folder: Path, model: Model) -> None:
|
|
47
|
-
"""Solve the model inplace. Write to folder."""
|
|
62
|
+
"""Solve the model inplace. Write to folder. Must be implemented by specific solvers."""
|
|
48
63
|
pass
|
framcore/solvers/SolverConfig.py
CHANGED
|
@@ -149,11 +149,11 @@ class SolverConfig(Base, ABC):
|
|
|
149
149
|
self.send_warning_event(message)
|
|
150
150
|
|
|
151
151
|
def get_num_cpu_cores(self) -> int:
|
|
152
|
-
"""Return number of cpu cores
|
|
152
|
+
"""Return number of cpu cores the Solver can use."""
|
|
153
153
|
return self._num_cpu_cores
|
|
154
154
|
|
|
155
155
|
def set_num_cpu_cores(self, n: int) -> int:
|
|
156
|
-
"""Set number of cpu cores
|
|
156
|
+
"""Set number of cpu cores the Solver can use."""
|
|
157
157
|
self._num_cpu_cores = n
|
|
158
158
|
|
|
159
159
|
def set_currency(self, currency: str) -> None:
|
|
@@ -166,11 +166,11 @@ class SolverConfig(Base, ABC):
|
|
|
166
166
|
return self._currency
|
|
167
167
|
|
|
168
168
|
def set_screen_output_on(self) -> None:
|
|
169
|
-
"""Print output from
|
|
169
|
+
"""Print output from Solver to stdout and logfile."""
|
|
170
170
|
self._show_screen_output = True
|
|
171
171
|
|
|
172
172
|
def set_screen_output_off(self) -> None:
|
|
173
|
-
"""Only print output from
|
|
173
|
+
"""Only print output from Solver to logfile."""
|
|
174
174
|
self._show_screen_output = False
|
|
175
175
|
|
|
176
176
|
def show_screen_output(self) -> bool:
|
|
@@ -4,10 +4,17 @@ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex #
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class AverageYearRange(SinglePeriodTimeIndex):
|
|
7
|
-
"""AverageYearRange represents
|
|
7
|
+
"""AverageYearRange represents a period over a range of years. No extrapolation and represents full iso calendar years."""
|
|
8
8
|
|
|
9
9
|
def __init__(self, start_year: int, num_years: int) -> None:
|
|
10
|
-
"""
|
|
10
|
+
"""
|
|
11
|
+
Initialize AverageYearRange with a year range. No extrapolation and represents full iso calendar years.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
start_year (int): First year in the range.
|
|
15
|
+
num_years (int): Number of years in the range.
|
|
16
|
+
|
|
17
|
+
"""
|
|
11
18
|
start_time = datetime.fromisocalendar(start_year, 1, 1)
|
|
12
19
|
end_time = datetime.fromisocalendar(start_year + num_years, 1, 1)
|
|
13
20
|
period_duration = end_time - start_time
|
|
@@ -4,10 +4,15 @@ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex #
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ConstantTimeIndex(SinglePeriodTimeIndex):
|
|
7
|
-
"""
|
|
7
|
+
"""
|
|
8
|
+
ConstantTimeIndex that is constant over time. For use in ConstantTimeVector.
|
|
9
|
+
|
|
10
|
+
Represents a period of 52 weeks starting from the iso calendar week 1 of 1985. Extrapolates both first and last point.
|
|
11
|
+
|
|
12
|
+
"""
|
|
8
13
|
|
|
9
14
|
def __init__(self) -> None:
|
|
10
|
-
"""
|
|
15
|
+
"""Initialize ConstantTimeIndex."""
|
|
11
16
|
super().__init__(
|
|
12
17
|
start_time=datetime.fromisocalendar(1985, 1, 1),
|
|
13
18
|
period_duration=timedelta(weeks=52),
|
|
@@ -4,7 +4,11 @@ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full i
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class DailyIndex(ProfileTimeIndex):
|
|
7
|
-
"""
|
|
7
|
+
"""
|
|
8
|
+
ProfileTimeIndex with one or more whole years with daily resolution. Either years with 52 weeks or full iso calendar years.
|
|
9
|
+
|
|
10
|
+
No extrapolation inherited from ProfileTimeIndex.
|
|
11
|
+
"""
|
|
8
12
|
|
|
9
13
|
def __init__(
|
|
10
14
|
self,
|
|
@@ -12,7 +16,15 @@ class DailyIndex(ProfileTimeIndex):
|
|
|
12
16
|
num_years: int,
|
|
13
17
|
is_52_week_years: bool = True,
|
|
14
18
|
) -> None:
|
|
15
|
-
"""
|
|
19
|
+
"""
|
|
20
|
+
Initialize DailyIndex 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
|
+
"""
|
|
16
28
|
super().__init__(
|
|
17
29
|
start_year=start_year,
|
|
18
30
|
num_years=num_years,
|
|
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta, tzinfo
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from numpy.typing import NDArray
|
|
8
8
|
|
|
9
|
-
import framcore.
|
|
9
|
+
import framcore.timeindexes._time_vector_operations as v_ops
|
|
10
10
|
from framcore.fingerprints import Fingerprint
|
|
11
11
|
from framcore.timeindexes.TimeIndex import TimeIndex # NB! full import path needed for inheritance to work
|
|
12
12
|
from framcore.timevectors import ReferencePeriod
|
|
@@ -27,32 +27,25 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
27
27
|
"""
|
|
28
28
|
Initialize a FixedFrequencyTimeIndex.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
The
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
The number of periods in the time index.
|
|
38
|
-
is_52_week_years : bool
|
|
39
|
-
Whether to use 52-week years.
|
|
40
|
-
extrapolate_first_point : bool
|
|
41
|
-
Whether to allow extrapolation of the first point.
|
|
42
|
-
extrapolate_last_point : bool
|
|
43
|
-
Whether to allow extrapolation of the last point.
|
|
30
|
+
Args:
|
|
31
|
+
start_time (datetime): The starting datetime of the time index.
|
|
32
|
+
period_duration (timedelta): The duration of each period.
|
|
33
|
+
num_periods (int): The number of periods in the time index. Must be greater than 0.
|
|
34
|
+
is_52_week_years (bool): Whether to use 52-week years.
|
|
35
|
+
extrapolate_first_point (bool): Whether to allow extrapolation of the first point.
|
|
36
|
+
extrapolate_last_point (bool): Whether to allow extrapolation of the last point.
|
|
44
37
|
|
|
45
38
|
"""
|
|
46
|
-
if num_periods
|
|
47
|
-
|
|
48
|
-
raise ValueError(
|
|
39
|
+
if num_periods <= 0:
|
|
40
|
+
msg = f"num_periods must be a positive integer. Got {num_periods}."
|
|
41
|
+
raise ValueError(msg)
|
|
49
42
|
if period_duration < timedelta(seconds=1):
|
|
50
|
-
|
|
51
|
-
raise ValueError(
|
|
43
|
+
msg = f"period_duration must be at least one second. Got {period_duration}."
|
|
44
|
+
raise ValueError(msg)
|
|
52
45
|
if not period_duration.total_seconds().is_integer():
|
|
53
|
-
|
|
54
|
-
raise ValueError(
|
|
55
|
-
if is_52_week_years and start_time.isocalendar().week == 53: #
|
|
46
|
+
msg = f"period_duration must be a whole number of seconds, got {period_duration.total_seconds()} s"
|
|
47
|
+
raise ValueError(msg)
|
|
48
|
+
if is_52_week_years and start_time.isocalendar().week == 53: # noqa: PLR2004
|
|
56
49
|
raise ValueError("Week of start_time must not be 53 when is_52_week_years is True.")
|
|
57
50
|
self._check_type(num_periods, int)
|
|
58
51
|
self._start_time = start_time
|
|
@@ -91,7 +84,7 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
91
84
|
def __repr__(self) -> str:
|
|
92
85
|
"""Return a string representation of the FixedFrequencyTimeIndex."""
|
|
93
86
|
return (
|
|
94
|
-
f"
|
|
87
|
+
f"{type(self).__name__}("
|
|
95
88
|
f"start_time={self._start_time}, "
|
|
96
89
|
f"period_duration={self._period_duration}, "
|
|
97
90
|
f"num_periods={self._num_periods}, "
|
|
@@ -133,7 +126,13 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
133
126
|
return self._num_periods == 1 and self._extrapolate_first_point == self._extrapolate_last_point is True
|
|
134
127
|
|
|
135
128
|
def is_whole_years(self) -> bool:
|
|
136
|
-
"""
|
|
129
|
+
"""
|
|
130
|
+
Return True if index covers one or more full years.
|
|
131
|
+
|
|
132
|
+
The start_time must be the first week and weekday of a year. For real ISO time,
|
|
133
|
+
the stop_time must also be the first week and weekday of a year. For 52-week years,
|
|
134
|
+
the total duration must be an integer number of 52-week years.
|
|
135
|
+
"""
|
|
137
136
|
start_time = self.get_start_time()
|
|
138
137
|
start_year, start_week, start_weekday = start_time.isocalendar()
|
|
139
138
|
if not start_week == start_weekday == 1:
|
|
@@ -144,7 +143,9 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
144
143
|
num_periods = self.get_num_periods()
|
|
145
144
|
stop_time = start_time + num_periods * period_duration
|
|
146
145
|
stop_year, stop_week, stop_weekday = stop_time.isocalendar()
|
|
147
|
-
|
|
146
|
+
if stop_year < start_year:
|
|
147
|
+
msg = f"Stop year must be after start year. Current stop year: {stop_year} and start year: {start_year}"
|
|
148
|
+
raise ValueError(msg)
|
|
148
149
|
return stop_week == stop_weekday == 1
|
|
149
150
|
|
|
150
151
|
period_duration = self.get_period_duration()
|
|
@@ -157,7 +158,6 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
157
158
|
"""Get the reference period (only if is_whole_years() is True)."""
|
|
158
159
|
if self.is_whole_years():
|
|
159
160
|
start_year = self.get_start_time().isocalendar().year
|
|
160
|
-
|
|
161
161
|
if self._is_52_week_years:
|
|
162
162
|
num_years = (self.get_num_periods() * self.get_period_duration()) // timedelta(weeks=52)
|
|
163
163
|
else:
|
|
@@ -202,7 +202,14 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
202
202
|
|
|
203
203
|
def get_period_average(self, vector: NDArray, start_time: datetime, duration: timedelta, is_52_week_years: bool) -> float:
|
|
204
204
|
"""Get the average over the period from the vector."""
|
|
205
|
-
|
|
205
|
+
self._check_type(vector, np.ndarray)
|
|
206
|
+
self._check_type(start_time, datetime)
|
|
207
|
+
self._check_type(duration, timedelta)
|
|
208
|
+
self._check_type(is_52_week_years, bool)
|
|
209
|
+
|
|
210
|
+
if vector.shape != (self.get_num_periods(),):
|
|
211
|
+
msg = f"Vector shape {vector.shape} does not match number of periods {self.get_num_periods()} of timeindex ({self})."
|
|
212
|
+
raise ValueError(msg)
|
|
206
213
|
target_timeindex = FixedFrequencyTimeIndex(
|
|
207
214
|
start_time=start_time,
|
|
208
215
|
period_duration=duration,
|
|
@@ -443,7 +450,14 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
443
450
|
|
|
444
451
|
def get_stop_time(self) -> datetime:
|
|
445
452
|
"""Get the stop time of the TimeIndex."""
|
|
446
|
-
|
|
453
|
+
if not self._is_52_week_years:
|
|
454
|
+
return self._start_time + self._period_duration * self._num_periods
|
|
455
|
+
|
|
456
|
+
return v_ops.calculate_52_week_years_stop_time(
|
|
457
|
+
start_time=self._start_time,
|
|
458
|
+
period_duration=self._period_duration,
|
|
459
|
+
num_periods=self._num_periods,
|
|
460
|
+
)
|
|
447
461
|
|
|
448
462
|
def slice(
|
|
449
463
|
self,
|
|
@@ -469,6 +483,7 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
469
483
|
self._start_time,
|
|
470
484
|
target_index.get_start_time(),
|
|
471
485
|
self._period_duration,
|
|
486
|
+
self._is_52_week_years,
|
|
472
487
|
)
|
|
473
488
|
transformed_timeindex = self.copy_with(
|
|
474
489
|
start_time=target_index.get_start_time(),
|
|
@@ -489,6 +504,7 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
489
504
|
self.get_stop_time(),
|
|
490
505
|
target_index.get_stop_time(),
|
|
491
506
|
self._period_duration,
|
|
507
|
+
self._is_52_week_years,
|
|
492
508
|
)
|
|
493
509
|
transformed_timeindex = self.copy_with(num_periods=self._num_periods - num_periods_to_slice)
|
|
494
510
|
transformed_vector = input_vector[:-num_periods_to_slice]
|
|
@@ -512,7 +528,12 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
512
528
|
if not self._extrapolate_first_point:
|
|
513
529
|
raise ValueError("Cannot extend start without extrapolation.")
|
|
514
530
|
|
|
515
|
-
num_periods_to_extend = self._periods_between(
|
|
531
|
+
num_periods_to_extend = self._periods_between(
|
|
532
|
+
self._start_time,
|
|
533
|
+
target_timeindex.get_start_time(),
|
|
534
|
+
self._period_duration,
|
|
535
|
+
self._is_52_week_years,
|
|
536
|
+
)
|
|
516
537
|
extended_vector = np.concatenate((np.full(num_periods_to_extend, input_vector[0]), input_vector))
|
|
517
538
|
|
|
518
539
|
transformed_timeindex = self.copy_with(
|
|
@@ -534,6 +555,7 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
534
555
|
self.get_stop_time(),
|
|
535
556
|
target_timeindex.get_stop_time(),
|
|
536
557
|
self._period_duration,
|
|
558
|
+
self._is_52_week_years,
|
|
537
559
|
)
|
|
538
560
|
extended_vector = np.concatenate((input_vector, np.full(num_periods_to_extend, input_vector[-1])))
|
|
539
561
|
target_timeindex = self.copy_with(num_periods=self._num_periods + num_periods_to_extend)
|
|
@@ -634,27 +656,25 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
634
656
|
if target_timeindex.get_start_time() < self._start_time:
|
|
635
657
|
if self._extrapolate_first_point:
|
|
636
658
|
return self._extend_start(input_vector, target_timeindex)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
f"Target timeindex: {target_timeindex}"
|
|
644
|
-
),
|
|
659
|
+
msg = (
|
|
660
|
+
"Cannot write into fixed frequency: incompatible time indices. "
|
|
661
|
+
"Start time of the target index is before the start time of the source index "
|
|
662
|
+
"and extrapolate_first_point is False.\n"
|
|
663
|
+
f"Input timeindex: {self}\n"
|
|
664
|
+
f"Target timeindex: {target_timeindex}"
|
|
645
665
|
)
|
|
666
|
+
raise ValueError(msg)
|
|
646
667
|
if target_timeindex.get_stop_time() > self.get_stop_time():
|
|
647
668
|
if self._extrapolate_last_point:
|
|
648
669
|
return self._extend_end(input_vector, target_timeindex)
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
f"Target timeindex: {target_timeindex}"
|
|
656
|
-
),
|
|
670
|
+
msg = (
|
|
671
|
+
"Cannot write into fixed frequency: incompatible time indices. "
|
|
672
|
+
"'stop_time' of the target index is after the 'stop_time' of the source index "
|
|
673
|
+
"and 'extrapolate_last_point' is False.\n"
|
|
674
|
+
f"Input timeindex: {self}\n"
|
|
675
|
+
f"Target timeindex: {target_timeindex}"
|
|
657
676
|
)
|
|
677
|
+
raise ValueError(msg)
|
|
658
678
|
if target_timeindex.get_start_time() > self.get_start_time():
|
|
659
679
|
return self._slice_start(input_vector, target_timeindex)
|
|
660
680
|
|
|
@@ -662,7 +682,7 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
662
682
|
return self._slice_end(input_vector, target_timeindex)
|
|
663
683
|
return target_timeindex, input_vector
|
|
664
684
|
|
|
665
|
-
def _periods_between(self, first_time: datetime, second_time: datetime, period_duration: timedelta) -> int:
|
|
685
|
+
def _periods_between(self, first_time: datetime, second_time: datetime, period_duration: timedelta, is_52_week_years: bool) -> int:
|
|
666
686
|
"""
|
|
667
687
|
Calculate the number of periods between two times.
|
|
668
688
|
|
|
@@ -674,6 +694,8 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
674
694
|
The second time point.
|
|
675
695
|
period_duration : timedelta
|
|
676
696
|
The duration of each period.
|
|
697
|
+
is_52_week_years : bool
|
|
698
|
+
Whether to use 52-week years.
|
|
677
699
|
|
|
678
700
|
Returns
|
|
679
701
|
-------
|
|
@@ -681,7 +703,15 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
681
703
|
The number of periods between the two times.
|
|
682
704
|
|
|
683
705
|
"""
|
|
684
|
-
|
|
706
|
+
start = min(first_time, second_time)
|
|
707
|
+
end = max(first_time, second_time)
|
|
708
|
+
total_period = end - start
|
|
709
|
+
|
|
710
|
+
if is_52_week_years:
|
|
711
|
+
weeks_53 = v_ops._find_all_week_53_periods(start, end) # noqa: SLF001
|
|
712
|
+
total_period -= timedelta(weeks=len(weeks_53))
|
|
713
|
+
|
|
714
|
+
return abs(total_period) // period_duration
|
|
685
715
|
|
|
686
716
|
def copy_with(
|
|
687
717
|
self,
|
|
@@ -740,9 +770,13 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
740
770
|
A new instance with the updated attributes.
|
|
741
771
|
|
|
742
772
|
"""
|
|
773
|
+
if reference_period is None:
|
|
774
|
+
raise ValueError("Cannot copy as reference period when provided reference_period is None.")
|
|
775
|
+
|
|
743
776
|
start_year = reference_period.get_start_year()
|
|
744
777
|
num_years = reference_period.get_num_years()
|
|
745
778
|
start_time = datetime.fromisocalendar(start_year, 1, 1)
|
|
779
|
+
|
|
746
780
|
if self.is_52_week_years():
|
|
747
781
|
period_duration = timedelta(weeks=52 * num_years)
|
|
748
782
|
else:
|
|
@@ -755,8 +789,26 @@ class FixedFrequencyTimeIndex(TimeIndex):
|
|
|
755
789
|
)
|
|
756
790
|
|
|
757
791
|
def get_datetime_list(self) -> list[datetime]:
|
|
758
|
-
"""
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
792
|
+
"""
|
|
793
|
+
Return list of datetime including stop time.
|
|
794
|
+
|
|
795
|
+
Note: When `is_52_week_years` is True, the returned list will skip any datetimes that fall in week 53.
|
|
796
|
+
"""
|
|
797
|
+
start_time = self.get_start_time()
|
|
798
|
+
num_periods = self.get_num_periods()
|
|
799
|
+
period_duration = self.get_period_duration()
|
|
800
|
+
|
|
801
|
+
if not self._is_52_week_years:
|
|
802
|
+
return [start_time + i * period_duration for i in range(num_periods + 1)]
|
|
803
|
+
|
|
804
|
+
datetime_list = []
|
|
805
|
+
i = 0
|
|
806
|
+
count = 0
|
|
807
|
+
while count <= num_periods:
|
|
808
|
+
current = start_time + i * period_duration
|
|
809
|
+
if current.isocalendar().week != 53: # noqa: PLR2004
|
|
810
|
+
datetime_list.append(current)
|
|
811
|
+
count += 1
|
|
812
|
+
i += 1
|
|
813
|
+
|
|
814
|
+
return datetime_list
|
|
@@ -4,7 +4,11 @@ from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full i
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class HourlyIndex(ProfileTimeIndex):
|
|
7
|
-
"""
|
|
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
|
+
"""
|
|
8
12
|
|
|
9
13
|
def __init__(
|
|
10
14
|
self,
|
|
@@ -12,7 +16,15 @@ class HourlyIndex(ProfileTimeIndex):
|
|
|
12
16
|
num_years: int,
|
|
13
17
|
is_52_week_years: bool = True,
|
|
14
18
|
) -> None:
|
|
15
|
-
"""
|
|
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
|
+
"""
|
|
16
28
|
super().__init__(
|
|
17
29
|
start_year=start_year,
|
|
18
30
|
num_years=num_years,
|
|
@@ -16,11 +16,13 @@ class IsoCalendarDay(SinglePeriodTimeIndex):
|
|
|
16
16
|
"""
|
|
17
17
|
IsoCalendarDay represent a day from datetime.fromisocalendar(year, week, day).
|
|
18
18
|
|
|
19
|
-
No extrapolation.
|
|
19
|
+
No extrapolation and is_52_week_years=False. Useful for testing.
|
|
20
20
|
|
|
21
|
-
|
|
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).
|
|
22
25
|
|
|
23
|
-
Useful for testing.
|
|
24
26
|
"""
|
|
25
27
|
super().__init__(
|
|
26
28
|
start_time=datetime.fromisocalendar(year, week, day),
|