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.
- fram_core-0.1.0a2.dist-info/METADATA +42 -0
- fram_core-0.1.0a2.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0a2.dist-info}/WHEEL +1 -2
- fram_core-0.1.0a2.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,714 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from framcore import Base
|
|
10
|
+
from framcore.expressions import (
|
|
11
|
+
Expr,
|
|
12
|
+
ensure_expr,
|
|
13
|
+
get_level_value,
|
|
14
|
+
get_profile_vector,
|
|
15
|
+
get_timeindexes_from_expr,
|
|
16
|
+
get_units_from_expr,
|
|
17
|
+
)
|
|
18
|
+
from framcore.expressions._get_constant_from_expr import _get_constant_from_expr
|
|
19
|
+
from framcore.querydbs import QueryDB
|
|
20
|
+
from framcore.timeindexes import FixedFrequencyTimeIndex, SinglePeriodTimeIndex, TimeIndex
|
|
21
|
+
from framcore.timevectors import ConstantTimeVector, ReferencePeriod, TimeVector
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from framcore import Model
|
|
25
|
+
from framcore.loaders import Loader
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# TODO: Name all abstract classes Abstract[clsname]
|
|
29
|
+
class LevelProfile(Base, ABC):
|
|
30
|
+
"""Attributes representing data the form level * profile + intercept."""
|
|
31
|
+
|
|
32
|
+
# must be overwritten by subclass when otherwise
|
|
33
|
+
# don't change the defaults
|
|
34
|
+
_IS_ABSTRACT: bool = True
|
|
35
|
+
_IS_STOCK: bool = False
|
|
36
|
+
_IS_FLOW: bool = False
|
|
37
|
+
_IS_NOT_NEGATIVE: bool = True
|
|
38
|
+
_IS_MAX_AND_ZERO_ONE: bool = False
|
|
39
|
+
|
|
40
|
+
# must be set by subclass when applicable
|
|
41
|
+
_IS_INGOING: bool | None = None
|
|
42
|
+
_IS_COST: bool | None = None
|
|
43
|
+
_IS_UNITLESS: bool | None = None
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
level: Expr | TimeVector | str | None = None,
|
|
48
|
+
profile: Expr | TimeVector | str | None = None,
|
|
49
|
+
value: float | int | None = None, # To support Price(value=20, unit="EUR/MWh")
|
|
50
|
+
unit: str | None = None,
|
|
51
|
+
level_shift: Expr | None = None,
|
|
52
|
+
intercept: Expr | None = None,
|
|
53
|
+
scale: Expr | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Validate all Expr fields."""
|
|
56
|
+
self._assert_invariants()
|
|
57
|
+
|
|
58
|
+
self._check_type(value, (float, int, type(None)))
|
|
59
|
+
self._check_type(unit, (str, type(None)))
|
|
60
|
+
self._check_type(level, (Expr, TimeVector, str, type(None)))
|
|
61
|
+
self._check_type(profile, (Expr, TimeVector, str, type(None)))
|
|
62
|
+
self._check_type(level_shift, (Expr, type(None)))
|
|
63
|
+
self._check_type(intercept, (Expr, type(None)))
|
|
64
|
+
self._check_type(scale, (Expr, type(None)))
|
|
65
|
+
self._level = self._ensure_level_expr(level, value, unit)
|
|
66
|
+
self._profile = self._ensure_profile_expr(profile)
|
|
67
|
+
self._level_shift: Expr | None = None
|
|
68
|
+
self._intercept: Expr | None = None
|
|
69
|
+
self._scale: Expr | None = None
|
|
70
|
+
|
|
71
|
+
def _assert_invariants(self) -> None:
|
|
72
|
+
abstract = self._IS_ABSTRACT
|
|
73
|
+
max_level_profile = self._IS_MAX_AND_ZERO_ONE
|
|
74
|
+
stock = self._IS_STOCK
|
|
75
|
+
flow = self._IS_FLOW
|
|
76
|
+
unitless = self._IS_UNITLESS
|
|
77
|
+
ingoing = self._IS_INGOING
|
|
78
|
+
cost = self._IS_COST
|
|
79
|
+
not_negative = self._IS_NOT_NEGATIVE
|
|
80
|
+
|
|
81
|
+
assert not abstract, "Abstract types should only be used for type hints and checks."
|
|
82
|
+
assert isinstance(max_level_profile, bool)
|
|
83
|
+
assert isinstance(stock, bool)
|
|
84
|
+
assert isinstance(flow, bool)
|
|
85
|
+
assert isinstance(not_negative, bool)
|
|
86
|
+
assert isinstance(ingoing, bool | type(None))
|
|
87
|
+
assert isinstance(unitless, bool | type(None))
|
|
88
|
+
assert isinstance(cost, bool | type(None))
|
|
89
|
+
assert not (flow and stock)
|
|
90
|
+
if flow or stock:
|
|
91
|
+
assert not unitless, "flow and stock must have unit that is not None."
|
|
92
|
+
assert not_negative, "flow and stock cannot have negative values."
|
|
93
|
+
if ingoing is True:
|
|
94
|
+
assert cost is None, "cost must be None when ingoing is True."
|
|
95
|
+
if cost is True:
|
|
96
|
+
assert ingoing is None, "ingoing must be None when cost is True."
|
|
97
|
+
|
|
98
|
+
parent = super()
|
|
99
|
+
if isinstance(parent, LevelProfile) and not parent._IS_ABSTRACT: # noqa: SLF001
|
|
100
|
+
self._assert_same_behaviour(parent)
|
|
101
|
+
|
|
102
|
+
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
103
|
+
"""Add all loaders stored in expressions to loaders."""
|
|
104
|
+
from framcore.utils import add_loaders_if # noqa: PLC0415
|
|
105
|
+
|
|
106
|
+
add_loaders_if(loaders, self.get_level())
|
|
107
|
+
add_loaders_if(loaders, self.get_profile())
|
|
108
|
+
|
|
109
|
+
def clear(self) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Set all internal fields to None.
|
|
112
|
+
|
|
113
|
+
You may want to use this to get exogenous flow to use capacities instead of volume.
|
|
114
|
+
"""
|
|
115
|
+
self._level = None
|
|
116
|
+
self._profile = None
|
|
117
|
+
self._level_shift = None
|
|
118
|
+
self._intercept = None
|
|
119
|
+
self._scale = None
|
|
120
|
+
|
|
121
|
+
def is_stock(self) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Return True if attribute is a stock variable.
|
|
124
|
+
|
|
125
|
+
Return False if attribute is not a stock variable.
|
|
126
|
+
"""
|
|
127
|
+
return self._IS_STOCK
|
|
128
|
+
|
|
129
|
+
def is_flow(self) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Return True if attribute is a flow variable.
|
|
132
|
+
|
|
133
|
+
Return False if attribute is not a flow variable.
|
|
134
|
+
"""
|
|
135
|
+
return self._IS_FLOW
|
|
136
|
+
|
|
137
|
+
def is_not_negative(self) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Return True if attribute is not allowed to have negative values.
|
|
140
|
+
|
|
141
|
+
Return False if attribute can have both positive and negative values.
|
|
142
|
+
"""
|
|
143
|
+
return self._IS_NOT_NEGATIVE
|
|
144
|
+
|
|
145
|
+
def is_max_and_zero_one(self) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
When True level should be max (not average) and corresponding profile should be zero_one (not mean_one).
|
|
148
|
+
|
|
149
|
+
When False level should be average (not max) and corresponding profile should be mean_one (not zero_one).
|
|
150
|
+
"""
|
|
151
|
+
return self._IS_MAX_AND_ZERO_ONE
|
|
152
|
+
|
|
153
|
+
def is_ingoing(self) -> bool | None:
|
|
154
|
+
"""
|
|
155
|
+
Return True if attribute is ingoing.
|
|
156
|
+
|
|
157
|
+
Return True if attribute is outgoing.
|
|
158
|
+
|
|
159
|
+
Return None if not applicable.
|
|
160
|
+
"""
|
|
161
|
+
return self._IS_INGOING
|
|
162
|
+
|
|
163
|
+
def is_cost(self) -> bool | None:
|
|
164
|
+
"""
|
|
165
|
+
Return True if attribute is objective function cost coefficient.
|
|
166
|
+
|
|
167
|
+
Return False if attribute is objective function revenue coefficient.
|
|
168
|
+
|
|
169
|
+
Return None if not applicable.
|
|
170
|
+
"""
|
|
171
|
+
return self._IS_COST
|
|
172
|
+
|
|
173
|
+
def is_unitless(self) -> bool | None:
|
|
174
|
+
"""
|
|
175
|
+
Return True if attribute is known to be unitless.
|
|
176
|
+
|
|
177
|
+
Return False if attribute is known to have a unit that is not None.
|
|
178
|
+
|
|
179
|
+
Return None if not applicable.
|
|
180
|
+
"""
|
|
181
|
+
return self._IS_UNITLESS
|
|
182
|
+
|
|
183
|
+
def has_level(self) -> bool:
|
|
184
|
+
"""Return True if get_level will return value not None."""
|
|
185
|
+
return (self._level is not None) or (self._level_shift is not None)
|
|
186
|
+
|
|
187
|
+
def has_profile(self) -> bool:
|
|
188
|
+
"""Return True if get_profile will return value not None."""
|
|
189
|
+
return self._profile is not None
|
|
190
|
+
|
|
191
|
+
def has_intercept(self) -> bool:
|
|
192
|
+
"""Return True if get_intercept will return value not None."""
|
|
193
|
+
return self._intercept is not None
|
|
194
|
+
|
|
195
|
+
def copy_from(self, other: LevelProfile) -> None:
|
|
196
|
+
"""Copy fields from other."""
|
|
197
|
+
self._check_type(other, LevelProfile)
|
|
198
|
+
self._assert_same_behaviour(other)
|
|
199
|
+
self._level = other._level
|
|
200
|
+
self._profile = other._profile
|
|
201
|
+
self._level_shift = other._level_shift
|
|
202
|
+
self._intercept = other._intercept
|
|
203
|
+
self._scale = other._scale
|
|
204
|
+
|
|
205
|
+
def get_level(self) -> Expr | None:
|
|
206
|
+
"""Get level part of (level * profile + intercept)."""
|
|
207
|
+
level = self._level
|
|
208
|
+
|
|
209
|
+
if level is None:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
if level.is_leaf():
|
|
213
|
+
level = Expr(
|
|
214
|
+
src=level.get_src(),
|
|
215
|
+
operations=level.get_operations(expect_ops=False, copy_list=True),
|
|
216
|
+
is_stock=level.is_stock(),
|
|
217
|
+
is_flow=level.is_flow(),
|
|
218
|
+
is_level=True,
|
|
219
|
+
is_profile=False,
|
|
220
|
+
profile=self._profile, # TODO: not always?
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if self._level_shift is not None:
|
|
224
|
+
level += self._level_shift
|
|
225
|
+
|
|
226
|
+
if self._scale is not None:
|
|
227
|
+
level *= self._scale
|
|
228
|
+
|
|
229
|
+
return level
|
|
230
|
+
|
|
231
|
+
def set_level(self, level: Expr | TimeVector | str | None) -> None:
|
|
232
|
+
"""Set level part of (level * profile + intercept)."""
|
|
233
|
+
self._check_type(level, (Expr, TimeVector, str, type(None)))
|
|
234
|
+
self._level = self._ensure_level_expr(level)
|
|
235
|
+
|
|
236
|
+
def get_profile(self) -> Expr | None:
|
|
237
|
+
"""Get profile part of (level * profile + intercept)."""
|
|
238
|
+
return self._profile
|
|
239
|
+
|
|
240
|
+
def set_profile(self, profile: Expr | TimeVector | str | None) -> None:
|
|
241
|
+
"""Set profile part of (level * profile + intercept)."""
|
|
242
|
+
self._check_type(profile, (Expr, TimeVector, str, type(None)))
|
|
243
|
+
self._profile = self._ensure_profile_expr(profile)
|
|
244
|
+
|
|
245
|
+
def get_intercept(self) -> Expr | None:
|
|
246
|
+
"""Get intercept part of (level * profile + intercept)."""
|
|
247
|
+
intercept = self._intercept
|
|
248
|
+
if self._scale is not None:
|
|
249
|
+
intercept *= self._scale
|
|
250
|
+
return intercept
|
|
251
|
+
|
|
252
|
+
def set_intercept(self, value: Expr | None) -> None:
|
|
253
|
+
"""Set intercept part of (level * profile + intercept)."""
|
|
254
|
+
self._check_type(value, (Expr, type(None)))
|
|
255
|
+
if value is not None:
|
|
256
|
+
self._check_level_expr(value)
|
|
257
|
+
self._intercept = value
|
|
258
|
+
|
|
259
|
+
def get_level_unit_set(
|
|
260
|
+
self,
|
|
261
|
+
db: QueryDB | Model,
|
|
262
|
+
) -> set[TimeIndex]:
|
|
263
|
+
"""
|
|
264
|
+
Return set with all units behind level expression.
|
|
265
|
+
|
|
266
|
+
Useful for discovering valid unit input to get_level_value.
|
|
267
|
+
"""
|
|
268
|
+
if not self.has_level():
|
|
269
|
+
return set()
|
|
270
|
+
return get_units_from_expr(db, self.get_level())
|
|
271
|
+
|
|
272
|
+
def get_profile_timeindex_set(
|
|
273
|
+
self,
|
|
274
|
+
db: QueryDB | Model,
|
|
275
|
+
) -> set[TimeIndex]:
|
|
276
|
+
"""
|
|
277
|
+
Return set with all TimeIndex behind profile expression.
|
|
278
|
+
|
|
279
|
+
Can be used to run optimized queries, i.e. not asking for
|
|
280
|
+
finer time resolutions than necessary.
|
|
281
|
+
"""
|
|
282
|
+
if not self.has_profile():
|
|
283
|
+
return set()
|
|
284
|
+
return get_timeindexes_from_expr(db, self.get_profile())
|
|
285
|
+
|
|
286
|
+
def get_scenario_vector(
|
|
287
|
+
self,
|
|
288
|
+
db: QueryDB | Model,
|
|
289
|
+
scenario_horizon: FixedFrequencyTimeIndex,
|
|
290
|
+
level_period: SinglePeriodTimeIndex,
|
|
291
|
+
unit: str | None,
|
|
292
|
+
is_float32: bool = True,
|
|
293
|
+
) -> NDArray:
|
|
294
|
+
"""Return vector with values along the given scenario horizon using level over level_period."""
|
|
295
|
+
return self._get_scenario_vector(db, scenario_horizon, level_period, unit, is_float32)
|
|
296
|
+
|
|
297
|
+
def get_data_value(
|
|
298
|
+
self,
|
|
299
|
+
db: QueryDB | Model,
|
|
300
|
+
scenario_horizon: FixedFrequencyTimeIndex,
|
|
301
|
+
level_period: SinglePeriodTimeIndex,
|
|
302
|
+
unit: str | None,
|
|
303
|
+
is_max_level: bool | None = None,
|
|
304
|
+
) -> float:
|
|
305
|
+
"""Return float for level_period."""
|
|
306
|
+
return self._get_data_value(db, scenario_horizon, level_period, unit, is_max_level)
|
|
307
|
+
|
|
308
|
+
def shift_intercept(self, value: float, unit: str | None) -> None:
|
|
309
|
+
"""Modify the intercept part of (level*profile + intercept) of an attribute by adding a constant value."""
|
|
310
|
+
expr = ensure_expr(
|
|
311
|
+
ConstantTimeVector(self._ensure_float(value), unit=unit, is_max_level=False),
|
|
312
|
+
is_level=True,
|
|
313
|
+
is_profile=False,
|
|
314
|
+
is_stock=self._IS_STOCK,
|
|
315
|
+
is_flow=self._IS_FLOW,
|
|
316
|
+
profile=None,
|
|
317
|
+
)
|
|
318
|
+
if self._intercept is None:
|
|
319
|
+
self._intercept = expr
|
|
320
|
+
else:
|
|
321
|
+
self._intercept += expr
|
|
322
|
+
|
|
323
|
+
def shift_level(
|
|
324
|
+
self,
|
|
325
|
+
value: float | int,
|
|
326
|
+
unit: str | None = None,
|
|
327
|
+
reference_period: ReferencePeriod | None = None,
|
|
328
|
+
is_max_level: bool | None = None,
|
|
329
|
+
use_profile: bool = False,
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Modify the level part of (level*profile + intercept) of an attribute by adding a constant value."""
|
|
332
|
+
self._check_type(value, (float, int))
|
|
333
|
+
self._check_type(unit, (str, type(None)))
|
|
334
|
+
self._check_type(reference_period, (ReferencePeriod, type(None)))
|
|
335
|
+
self._check_type(is_max_level, (bool, type(None)))
|
|
336
|
+
self._check_type(use_profile, bool)
|
|
337
|
+
|
|
338
|
+
if is_max_level is None:
|
|
339
|
+
is_max_level = self._IS_MAX_AND_ZERO_ONE
|
|
340
|
+
|
|
341
|
+
expr = ensure_expr(
|
|
342
|
+
ConstantTimeVector(
|
|
343
|
+
self._ensure_float(value),
|
|
344
|
+
unit=unit,
|
|
345
|
+
is_max_level=is_max_level,
|
|
346
|
+
reference_period=reference_period,
|
|
347
|
+
),
|
|
348
|
+
is_level=True,
|
|
349
|
+
is_profile=False,
|
|
350
|
+
is_stock=self._IS_STOCK,
|
|
351
|
+
is_flow=self._IS_FLOW,
|
|
352
|
+
profile=self._profile if use_profile else None,
|
|
353
|
+
)
|
|
354
|
+
if self._level_shift is None:
|
|
355
|
+
self._level_shift = expr
|
|
356
|
+
else:
|
|
357
|
+
self._level_shift += expr
|
|
358
|
+
|
|
359
|
+
def scale(self, value: float | int) -> None:
|
|
360
|
+
"""Modify the value (level*profile + intercept) of an attribute by multiplying with a constant value."""
|
|
361
|
+
expr = ensure_expr(
|
|
362
|
+
ConstantTimeVector(self._ensure_float(value), unit=None, is_max_level=False),
|
|
363
|
+
is_level=True,
|
|
364
|
+
is_profile=False,
|
|
365
|
+
profile=None,
|
|
366
|
+
)
|
|
367
|
+
if self._scale is None:
|
|
368
|
+
self._scale = expr
|
|
369
|
+
else:
|
|
370
|
+
self._scale *= expr
|
|
371
|
+
|
|
372
|
+
def _ensure_level_expr(
|
|
373
|
+
self,
|
|
374
|
+
level: Expr | str | TimeVector | None,
|
|
375
|
+
value: float | int | None = None,
|
|
376
|
+
unit: str | None = None,
|
|
377
|
+
reference_period: ReferencePeriod | None = None,
|
|
378
|
+
) -> Expr | None:
|
|
379
|
+
if value is not None:
|
|
380
|
+
level = ConstantTimeVector(
|
|
381
|
+
scalar=float(value),
|
|
382
|
+
unit=unit,
|
|
383
|
+
is_max_level=self._IS_MAX_AND_ZERO_ONE,
|
|
384
|
+
is_zero_one_profile=None,
|
|
385
|
+
reference_period=reference_period,
|
|
386
|
+
)
|
|
387
|
+
if level is None:
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
if isinstance(level, Expr):
|
|
391
|
+
self._check_level_expr(level)
|
|
392
|
+
return level
|
|
393
|
+
|
|
394
|
+
return Expr(
|
|
395
|
+
src=level,
|
|
396
|
+
is_flow=self._IS_FLOW,
|
|
397
|
+
is_stock=self._IS_STOCK,
|
|
398
|
+
is_level=True,
|
|
399
|
+
is_profile=False,
|
|
400
|
+
profile=None,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def _check_level_expr(self, expr: Expr) -> None:
|
|
404
|
+
assert expr.is_stock() == self._IS_STOCK
|
|
405
|
+
assert expr.is_flow() == self._IS_FLOW
|
|
406
|
+
assert expr.is_level() is True
|
|
407
|
+
assert expr.is_profile() is False
|
|
408
|
+
|
|
409
|
+
def _check_profile_expr(self, expr: Expr) -> None:
|
|
410
|
+
assert expr.is_stock() is False
|
|
411
|
+
assert expr.is_flow() is False
|
|
412
|
+
assert expr.is_level() is False
|
|
413
|
+
assert expr.is_profile() is True
|
|
414
|
+
|
|
415
|
+
def _ensure_profile_expr(
|
|
416
|
+
self,
|
|
417
|
+
value: Expr | str | TimeVector | None,
|
|
418
|
+
) -> Expr | None:
|
|
419
|
+
if value is None:
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
if isinstance(value, Expr):
|
|
423
|
+
self._check_profile_expr(value)
|
|
424
|
+
return value
|
|
425
|
+
|
|
426
|
+
return Expr(
|
|
427
|
+
src=value,
|
|
428
|
+
is_flow=False,
|
|
429
|
+
is_stock=False,
|
|
430
|
+
is_level=False,
|
|
431
|
+
is_profile=True,
|
|
432
|
+
profile=None,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def _get_data_value(
|
|
436
|
+
self,
|
|
437
|
+
db: QueryDB,
|
|
438
|
+
scenario_horizon: FixedFrequencyTimeIndex,
|
|
439
|
+
level_period: SinglePeriodTimeIndex,
|
|
440
|
+
unit: str | None,
|
|
441
|
+
is_max_level: bool | None,
|
|
442
|
+
) -> float:
|
|
443
|
+
# NB! don't type check db, as this is done in get_level_value and get_profile_vector
|
|
444
|
+
self._check_type(scenario_horizon, FixedFrequencyTimeIndex)
|
|
445
|
+
self._check_type(level_period, SinglePeriodTimeIndex)
|
|
446
|
+
self._check_type(unit, (str, type(None)))
|
|
447
|
+
self._check_type(is_max_level, (bool, type(None)))
|
|
448
|
+
|
|
449
|
+
level_expr = self.get_level()
|
|
450
|
+
|
|
451
|
+
if is_max_level is None:
|
|
452
|
+
is_max_level = self._IS_MAX_AND_ZERO_ONE
|
|
453
|
+
|
|
454
|
+
self._check_type(level_expr, (Expr, type(None)))
|
|
455
|
+
assert isinstance(level_expr, Expr), "Attribute level Expr is None. Have you called Solver.solve yet?"
|
|
456
|
+
|
|
457
|
+
level_value = get_level_value(
|
|
458
|
+
expr=level_expr,
|
|
459
|
+
db=db,
|
|
460
|
+
scen_dim=scenario_horizon,
|
|
461
|
+
data_dim=level_period,
|
|
462
|
+
unit=unit,
|
|
463
|
+
is_max=is_max_level,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
intercept = None
|
|
467
|
+
if self._intercept is not None:
|
|
468
|
+
intercept = _get_constant_from_expr(
|
|
469
|
+
self._intercept,
|
|
470
|
+
db,
|
|
471
|
+
unit=unit,
|
|
472
|
+
data_dim=level_period,
|
|
473
|
+
scen_dim=scenario_horizon,
|
|
474
|
+
is_max=is_max_level,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if intercept is None:
|
|
478
|
+
return level_value
|
|
479
|
+
|
|
480
|
+
return level_value + intercept
|
|
481
|
+
|
|
482
|
+
def _get_scenario_vector(
|
|
483
|
+
self,
|
|
484
|
+
db: QueryDB | Model,
|
|
485
|
+
scenario_horizon: FixedFrequencyTimeIndex,
|
|
486
|
+
level_period: SinglePeriodTimeIndex,
|
|
487
|
+
unit: str | None,
|
|
488
|
+
is_float32: bool = True,
|
|
489
|
+
) -> NDArray:
|
|
490
|
+
"""Return vector with values along the given scenario horizon using level over level_period."""
|
|
491
|
+
# NB! don't type check db, as this is done in get_level_value and get_profile_vector
|
|
492
|
+
self._check_type(scenario_horizon, FixedFrequencyTimeIndex)
|
|
493
|
+
self._check_type(level_period, SinglePeriodTimeIndex)
|
|
494
|
+
self._check_type(unit, (str, type(None)))
|
|
495
|
+
self._check_type(is_float32, bool)
|
|
496
|
+
|
|
497
|
+
level_expr = self.get_level()
|
|
498
|
+
|
|
499
|
+
self._check_type(level_expr, (Expr, type(None)))
|
|
500
|
+
assert isinstance(level_expr, Expr), "Attribute level Expr is None. Have you called Solver.solve yet?"
|
|
501
|
+
|
|
502
|
+
level_value = get_level_value(
|
|
503
|
+
expr=level_expr,
|
|
504
|
+
db=db,
|
|
505
|
+
scen_dim=scenario_horizon,
|
|
506
|
+
data_dim=level_period,
|
|
507
|
+
unit=unit,
|
|
508
|
+
is_max=self._IS_MAX_AND_ZERO_ONE,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
profile_expr = self.get_profile()
|
|
512
|
+
|
|
513
|
+
if profile_expr is None:
|
|
514
|
+
profile_vector = np.ones(
|
|
515
|
+
scenario_horizon.get_num_periods(),
|
|
516
|
+
dtype=np.float32 if is_float32 else np.float64,
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
profile_vector = get_profile_vector(
|
|
520
|
+
expr=profile_expr,
|
|
521
|
+
db=db,
|
|
522
|
+
scen_dim=scenario_horizon,
|
|
523
|
+
data_dim=level_period,
|
|
524
|
+
is_zero_one=self._IS_MAX_AND_ZERO_ONE,
|
|
525
|
+
is_float32=is_float32,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
intercept = None
|
|
529
|
+
if self._intercept is not None:
|
|
530
|
+
intercept = _get_constant_from_expr(
|
|
531
|
+
self._intercept,
|
|
532
|
+
db,
|
|
533
|
+
unit=unit,
|
|
534
|
+
data_dim=level_period,
|
|
535
|
+
scen_dim=scenario_horizon,
|
|
536
|
+
is_max=self._IS_MAX_AND_ZERO_ONE,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if intercept is None:
|
|
540
|
+
return level_value * profile_vector
|
|
541
|
+
|
|
542
|
+
return level_value * profile_vector + intercept
|
|
543
|
+
|
|
544
|
+
def _has_same_behaviour(self, other: LevelProfile) -> bool:
|
|
545
|
+
return all(
|
|
546
|
+
(
|
|
547
|
+
self._IS_FLOW == other._IS_FLOW,
|
|
548
|
+
self._IS_STOCK == other._IS_STOCK,
|
|
549
|
+
self._IS_NOT_NEGATIVE == other._IS_NOT_NEGATIVE,
|
|
550
|
+
self._IS_MAX_AND_ZERO_ONE == other._IS_MAX_AND_ZERO_ONE,
|
|
551
|
+
self._IS_INGOING == other._IS_INGOING,
|
|
552
|
+
self._IS_COST == other._IS_COST,
|
|
553
|
+
self._IS_UNITLESS == other._IS_UNITLESS,
|
|
554
|
+
),
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def _assert_same_behaviour(self, other: LevelProfile) -> None:
|
|
558
|
+
if not self._has_same_behaviour(other):
|
|
559
|
+
message = f"Not same behaviour for {self} and {other}"
|
|
560
|
+
raise ValueError(message)
|
|
561
|
+
|
|
562
|
+
def __eq__(self, other) -> bool: # noqa: ANN001
|
|
563
|
+
"""Return True if other is equal to self."""
|
|
564
|
+
if not isinstance(other, LevelProfile):
|
|
565
|
+
return False
|
|
566
|
+
if not self._has_same_behaviour(other):
|
|
567
|
+
return False
|
|
568
|
+
return all(
|
|
569
|
+
(
|
|
570
|
+
self._level == other._level,
|
|
571
|
+
self._profile == other._profile,
|
|
572
|
+
self._level_shift == other._level_shift,
|
|
573
|
+
self._intercept == other._intercept,
|
|
574
|
+
self._scale == other._scale,
|
|
575
|
+
),
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
def __hash__(self) -> int:
|
|
579
|
+
"""Compute hash of self."""
|
|
580
|
+
return hash(
|
|
581
|
+
(
|
|
582
|
+
type(self).__name__,
|
|
583
|
+
self._level,
|
|
584
|
+
self._profile,
|
|
585
|
+
self._level_shift,
|
|
586
|
+
self._intercept,
|
|
587
|
+
self._scale,
|
|
588
|
+
),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# Abstract subclasses intended type hints and checks
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class FlowVolume(LevelProfile):
|
|
596
|
+
"""Represents a flow volume attribute, indicating that the attribute is a flow variable."""
|
|
597
|
+
|
|
598
|
+
_IS_FLOW = True
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class Coefficient(LevelProfile):
|
|
602
|
+
"""Represents a coefficient attribute, used as a base class for various coefficient types."""
|
|
603
|
+
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class ArrowCoefficient(Coefficient):
|
|
608
|
+
"""Represents an arrow coefficient attribute, used for efficiency, loss, and conversion coefficients."""
|
|
609
|
+
|
|
610
|
+
pass
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class ShaddowPrice(Coefficient):
|
|
614
|
+
"""Represents a shadow price attribute, indicating that the attribute has unit might be negative."""
|
|
615
|
+
|
|
616
|
+
_IS_UNITLESS = False
|
|
617
|
+
_IS_NOT_NEGATIVE = False
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class ObjectiveCoefficient(Coefficient):
|
|
621
|
+
"""Represents an objective coefficient attribute, indicating cost or revenue coefficients in the objective function."""
|
|
622
|
+
|
|
623
|
+
_IS_UNITLESS = False
|
|
624
|
+
_IS_NOT_NEGATIVE = False
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# concrete subclasses intended for final use
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class Price(ShaddowPrice):
|
|
631
|
+
"""Represents a price attribute, inheriting from ShaddowPrice."""
|
|
632
|
+
|
|
633
|
+
_IS_ABSTRACT = False
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class WaterValue(ShaddowPrice):
|
|
637
|
+
"""Represents a water value attribute, inheriting from ShaddowPrice."""
|
|
638
|
+
|
|
639
|
+
_IS_ABSTRACT = False
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class Cost(ObjectiveCoefficient):
|
|
643
|
+
"""Represents a cost attribute, indicating cost coefficients in the objective function."""
|
|
644
|
+
|
|
645
|
+
_IS_ABSTRACT = False
|
|
646
|
+
_IS_COST = True
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
class ReservePrice(ObjectiveCoefficient):
|
|
650
|
+
"""Represents a reserve price attribute, indicating revenue coefficients in the objective function."""
|
|
651
|
+
|
|
652
|
+
_IS_ABSTRACT = False
|
|
653
|
+
_IS_COST = False
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
class Elasticity(Coefficient): # TODO: How do this work?
|
|
657
|
+
"""Represents an elasticity coefficient attribute, indicating a unitless coefficient."""
|
|
658
|
+
|
|
659
|
+
_IS_ABSTRACT = False
|
|
660
|
+
_IS_UNITLESS = True
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class Proportion(Coefficient): # TODO: How do this work?
|
|
664
|
+
"""Represents a proportion coefficient attribute, indicating a unitless coefficient."""
|
|
665
|
+
|
|
666
|
+
_IS_ABSTRACT = False
|
|
667
|
+
_IS_UNITLESS = True
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class Hours(Coefficient): # TODO: How do this work?
|
|
671
|
+
"""Represents an hours coefficient attribute, indicating a time-related coefficient."""
|
|
672
|
+
|
|
673
|
+
_IS_ABSTRACT = False
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class Efficiency(ArrowCoefficient):
|
|
677
|
+
"""Represents an efficiency coefficient attribute, indicating a unitless coefficient."""
|
|
678
|
+
|
|
679
|
+
_IS_ABSTRACT = False
|
|
680
|
+
_IS_UNITLESS = True
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class Loss(ArrowCoefficient):
|
|
684
|
+
"""Represents a loss coefficient attribute, indicating a unitless coefficient."""
|
|
685
|
+
|
|
686
|
+
_IS_ABSTRACT = False
|
|
687
|
+
_IS_UNITLESS = True
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class Conversion(ArrowCoefficient):
|
|
691
|
+
"""Represents a conversion coefficient attribute, used for conversion factors in the model."""
|
|
692
|
+
|
|
693
|
+
_IS_ABSTRACT = False
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class AvgFlowVolume(FlowVolume):
|
|
697
|
+
"""Represents an average flow volume attribute, indicating a flow variable with average values."""
|
|
698
|
+
|
|
699
|
+
_IS_ABSTRACT = False
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class MaxFlowVolume(FlowVolume):
|
|
703
|
+
"""Represents a maximum flow volume attribute, indicating a flow variable with maximum values."""
|
|
704
|
+
|
|
705
|
+
_IS_ABSTRACT = False
|
|
706
|
+
_IS_MAX_AND_ZERO_ONE = True
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class StockVolume(LevelProfile):
|
|
710
|
+
"""Represents a stock volume attribute, indicating a stock variable with maximum values."""
|
|
711
|
+
|
|
712
|
+
_IS_ABSTRACT = False
|
|
713
|
+
_IS_STOCK = True
|
|
714
|
+
_IS_MAX_AND_ZERO_ONE = True
|