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,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