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.
Files changed (78) hide show
  1. {fram_core-0.1.0a2.dist-info → fram_core-0.1.1.dist-info}/METADATA +4 -4
  2. fram_core-0.1.1.dist-info/RECORD +100 -0
  3. {fram_core-0.1.0a2.dist-info → fram_core-0.1.1.dist-info}/WHEEL +1 -1
  4. framcore/Base.py +22 -3
  5. framcore/Model.py +26 -9
  6. framcore/__init__.py +2 -1
  7. framcore/aggregators/Aggregator.py +30 -11
  8. framcore/aggregators/HydroAggregator.py +35 -23
  9. framcore/aggregators/NodeAggregator.py +65 -30
  10. framcore/aggregators/WindSolarAggregator.py +22 -30
  11. framcore/attributes/Arrow.py +6 -4
  12. framcore/attributes/ElasticDemand.py +13 -13
  13. framcore/attributes/ReservoirCurve.py +3 -17
  14. framcore/attributes/SoftBound.py +2 -5
  15. framcore/attributes/StartUpCost.py +14 -3
  16. framcore/attributes/Storage.py +17 -5
  17. framcore/attributes/TargetBound.py +2 -4
  18. framcore/attributes/__init__.py +2 -4
  19. framcore/attributes/hydro/HydroBypass.py +9 -2
  20. framcore/attributes/hydro/HydroGenerator.py +24 -7
  21. framcore/attributes/hydro/HydroPump.py +32 -10
  22. framcore/attributes/hydro/HydroReservoir.py +4 -4
  23. framcore/attributes/level_profile_attributes.py +250 -53
  24. framcore/components/Component.py +27 -3
  25. framcore/components/Demand.py +18 -4
  26. framcore/components/Flow.py +26 -4
  27. framcore/components/HydroModule.py +45 -4
  28. framcore/components/Node.py +32 -9
  29. framcore/components/Thermal.py +12 -8
  30. framcore/components/Transmission.py +17 -2
  31. framcore/components/wind_solar.py +25 -10
  32. framcore/curves/LoadedCurve.py +0 -9
  33. framcore/expressions/Expr.py +137 -36
  34. framcore/expressions/__init__.py +3 -1
  35. framcore/expressions/_get_constant_from_expr.py +14 -20
  36. framcore/expressions/queries.py +121 -84
  37. framcore/expressions/units.py +30 -3
  38. framcore/fingerprints/fingerprint.py +0 -1
  39. framcore/juliamodels/JuliaModel.py +13 -3
  40. framcore/loaders/loaders.py +0 -2
  41. framcore/metadata/ExprMeta.py +13 -7
  42. framcore/metadata/LevelExprMeta.py +16 -1
  43. framcore/metadata/Member.py +7 -7
  44. framcore/metadata/__init__.py +1 -1
  45. framcore/querydbs/CacheDB.py +1 -1
  46. framcore/solvers/Solver.py +21 -6
  47. framcore/solvers/SolverConfig.py +4 -4
  48. framcore/timeindexes/AverageYearRange.py +9 -2
  49. framcore/timeindexes/ConstantTimeIndex.py +7 -2
  50. framcore/timeindexes/DailyIndex.py +14 -2
  51. framcore/timeindexes/FixedFrequencyTimeIndex.py +105 -53
  52. framcore/timeindexes/HourlyIndex.py +14 -2
  53. framcore/timeindexes/IsoCalendarDay.py +5 -3
  54. framcore/timeindexes/ListTimeIndex.py +103 -23
  55. framcore/timeindexes/ModelYear.py +8 -2
  56. framcore/timeindexes/ModelYears.py +11 -2
  57. framcore/timeindexes/OneYearProfileTimeIndex.py +10 -2
  58. framcore/timeindexes/ProfileTimeIndex.py +14 -3
  59. framcore/timeindexes/SinglePeriodTimeIndex.py +1 -1
  60. framcore/timeindexes/TimeIndex.py +16 -3
  61. framcore/timeindexes/WeeklyIndex.py +14 -2
  62. framcore/{expressions → timeindexes}/_time_vector_operations.py +76 -2
  63. framcore/timevectors/ConstantTimeVector.py +12 -16
  64. framcore/timevectors/LinearTransformTimeVector.py +20 -3
  65. framcore/timevectors/ListTimeVector.py +18 -14
  66. framcore/timevectors/LoadedTimeVector.py +1 -8
  67. framcore/timevectors/ReferencePeriod.py +13 -3
  68. framcore/timevectors/TimeVector.py +26 -12
  69. framcore/utils/__init__.py +0 -1
  70. framcore/utils/get_regional_volumes.py +21 -3
  71. framcore/utils/get_supported_components.py +1 -1
  72. framcore/utils/global_energy_equivalent.py +22 -5
  73. framcore/utils/isolate_subnodes.py +12 -3
  74. framcore/utils/loaders.py +7 -7
  75. framcore/utils/node_flow_utils.py +4 -4
  76. framcore/utils/storage_subsystems.py +3 -4
  77. fram_core-0.1.0a2.dist-info/RECORD +0 -100
  78. {fram_core-0.1.0a2.dist-info → fram_core-0.1.1.dist-info}/licenses/LICENSE.md +0 -0
@@ -10,7 +10,9 @@ from framcore import Base
10
10
  from framcore.expressions import (
11
11
  Expr,
12
12
  ensure_expr,
13
+ get_leaf_profiles,
13
14
  get_level_value,
15
+ get_profile_exprs_from_leaf_levels,
14
16
  get_profile_vector,
15
17
  get_timeindexes_from_expr,
16
18
  get_units_from_expr,
@@ -27,7 +29,45 @@ if TYPE_CHECKING:
27
29
 
28
30
  # TODO: Name all abstract classes Abstract[clsname]
29
31
  class LevelProfile(Base, ABC):
30
- """Attributes representing data the form level * profile + intercept."""
32
+ """
33
+ Attributes representing timeseries data for Components. Mostly as Level * Profile, where both Level and Profile are Expr (expressions).
34
+
35
+ Level and Profile represent two distinct dimensions of time. This is because we want to simulate future system states with historical weather patterns.
36
+ Therefore, Level represents the system state at a given time (data_dim), while Profile represents the scenario dimension (scen_dim).
37
+ A Level would for example represent the installed capacity of solar plants towards 2030,
38
+ while the Profile would represent the historical variation between 1991-2020.
39
+
40
+ Level and Profile can have two main formats: A maximum Level with a Profile that varies between 0-1,
41
+ and an average Level with a Profile with a mean of 1 (the latter can have a ReferencePeriod).
42
+ The max format is, for example, used for capacities, while the mean format can be used for prices and flows.
43
+ The system needs to be able to convert between the two formats. This is especially important for aggregations
44
+ (for example weighted averages) where all the TimeVectors need to be on the same format for a correct result.
45
+ One simple example of conversion is pairing a max Level of 100 MW with a mean_one Profile [0, 1, 2].
46
+ Asking for this on the max format will return the series 100*[0, 0.5, 1] MW, while on the avg format it will return 50*[0, 1, 2] MW.
47
+
48
+ Queries to LevelProfile need to provide a database, the desired target TimeIndex for both dimensions, the target unit and the desired format.
49
+ At the moment we support these queries for LevelProfile:
50
+ - self.get_data_value(db, scen_dim, data_dim, unit, is_max_level)
51
+ - self.get_scenario_vector(db, scen_dim, data_dim, unit, is_float32)
52
+
53
+ In addition, we have the possibility to shift, scale, and change the intercept of the LevelProfiles.
54
+ Then we get the full representation: Scale * (Level + Level_shift) * Profile + Intercept.
55
+ - Level_shift adds a constant value to Level, has the same Profile as Level.
56
+ - Scale multiplies (Level + Level_shift) by a constant value.
57
+ - Intercept adds a constant value to LevelProfile, ignoring Level and Profile. **This is the only way of supporting a timeseries that crosses zero
58
+ in our system. This functionality is under development and has not been properly tested.**
59
+
60
+ LevelProfiles also have additional properties that describes their behaviour. These can be used for initialization, validation,
61
+ and to simplify queries. The properties are:
62
+ - is_stock: True if attribute is a stock variable. Level Expr should also have is_stock=True. See Expr for details.
63
+ - is_flow: True if attribute is a flow variable. Level Expr should also have is_flow=True. See Expr for details.
64
+ - is_not_negative: True if attribute is not allowed to have negative values. Level Expr should also have only non-negative values.
65
+ - is_max_and_zero_one: Preferred format of Level and Profile. Used for initialization and queries.
66
+ - is_ingoing: True if attribute is ingoing, False if outgoing, None if neither.
67
+ - is_cost: True if attribute is objective function cost coefficient. Else None.
68
+ - is_unitless: True if attribute is known to be unitless. False if known to have a unit that is not None. Else None.
69
+
70
+ """
31
71
 
32
72
  # must be overwritten by subclass when otherwise
33
73
  # don't change the defaults
@@ -52,7 +92,24 @@ class LevelProfile(Base, ABC):
52
92
  intercept: Expr | None = None,
53
93
  scale: Expr | None = None,
54
94
  ) -> None:
55
- """Validate all Expr fields."""
95
+ """
96
+ Initialize LevelProfile.
97
+
98
+ See the LevelProfile class docstring for details. A complete LevelProfile is represented as:
99
+ Scale * (Level + Level_shift) * Profile + Intercept. Normally only Level and Profile are used.
100
+
101
+ Either give level and profile, or value and unit.
102
+
103
+ Args:
104
+ level (Expr | TimeVector | str | None, optional): Level Expr. Defaults to None.
105
+ profile (Expr | TimeVector | str | None, optional): Profile Expr. Defaults to None.
106
+ value (float | int | None, optional): A constant value to initialize Level. Defaults to None.
107
+ unit (str | None, optional): Unit of the constant value to initialize Level. Defaults to None.
108
+ level_shift (Expr | None, optional): Level_shift Expr. Defaults to None.
109
+ intercept (Expr | None, optional): Intercept Expr. Defaults to None.
110
+ scale (Expr | None, optional): Scale Expr. Defaults to None.
111
+
112
+ """
56
113
  self._assert_invariants()
57
114
 
58
115
  self._check_type(value, (float, int, type(None)))
@@ -62,11 +119,18 @@ class LevelProfile(Base, ABC):
62
119
  self._check_type(level_shift, (Expr, type(None)))
63
120
  self._check_type(intercept, (Expr, type(None)))
64
121
  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
122
+ level = self._ensure_level_expr(level, value, unit)
123
+ profile = self._ensure_profile_expr(profile)
124
+ self._ensure_compatible_level_profile_combo(level, profile)
125
+ self._ensure_compatible_level_profile_combo(level_shift, profile)
126
+ self._level: Expr | None = level
127
+ self._profile: Expr | None = profile
128
+ self._level_shift: Expr | None = level_shift
129
+ self._intercept: Expr | None = intercept
130
+ self._scale: Expr | None = scale
131
+ # TODO: Validate that profiles are equal in level and level_shift.
132
+ # TODO: Validate that level_shift, scale and intercept only consist of Exprs with ConstantTimeVectors
133
+ # TODO: Validate that level_shift, level_scale and intercept have correct Expr properties
70
134
 
71
135
  def _assert_invariants(self) -> None:
72
136
  abstract = self._IS_ABSTRACT
@@ -101,7 +165,7 @@ class LevelProfile(Base, ABC):
101
165
 
102
166
  def add_loaders(self, loaders: set[Loader]) -> None:
103
167
  """Add all loaders stored in expressions to loaders."""
104
- from framcore.utils import add_loaders_if # noqa: PLC0415
168
+ from framcore.utils import add_loaders_if
105
169
 
106
170
  add_loaders_if(loaders, self.get_level())
107
171
  add_loaders_if(loaders, self.get_profile())
@@ -217,7 +281,7 @@ class LevelProfile(Base, ABC):
217
281
  is_flow=level.is_flow(),
218
282
  is_level=True,
219
283
  is_profile=False,
220
- profile=self._profile, # TODO: not always?
284
+ profile=self._profile,
221
285
  )
222
286
 
223
287
  if self._level_shift is not None:
@@ -229,18 +293,22 @@ class LevelProfile(Base, ABC):
229
293
  return level
230
294
 
231
295
  def set_level(self, level: Expr | TimeVector | str | None) -> None:
232
- """Set level part of (level * profile + intercept)."""
296
+ """Set level part of (scale * (level + level_shift) * profile + intercept)."""
233
297
  self._check_type(level, (Expr, TimeVector, str, type(None)))
234
- self._level = self._ensure_level_expr(level)
298
+ level = self._ensure_level_expr(level)
299
+ self._ensure_compatible_level_profile_combo(level, self._profile)
300
+ self._level = level
235
301
 
236
302
  def get_profile(self) -> Expr | None:
237
303
  """Get profile part of (level * profile + intercept)."""
238
304
  return self._profile
239
305
 
240
306
  def set_profile(self, profile: Expr | TimeVector | str | None) -> None:
241
- """Set profile part of (level * profile + intercept)."""
307
+ """Set profile part of (scale * (level + level_shift) * profile + intercept)."""
242
308
  self._check_type(profile, (Expr, TimeVector, str, type(None)))
243
- self._profile = self._ensure_profile_expr(profile)
309
+ profile = self._ensure_profile_expr(profile)
310
+ self._ensure_compatible_level_profile_combo(self._level, profile)
311
+ self._profile = profile
244
312
 
245
313
  def get_intercept(self) -> Expr | None:
246
314
  """Get intercept part of (level * profile + intercept)."""
@@ -291,7 +359,22 @@ class LevelProfile(Base, ABC):
291
359
  unit: str | None,
292
360
  is_float32: bool = True,
293
361
  ) -> NDArray:
294
- """Return vector with values along the given scenario horizon using level over level_period."""
362
+ """
363
+ Evaluate LevelProfile over the periods in scenario dimension, and at the level period of the data dimension.
364
+
365
+ Underlying profiles are evalutated over the scenario dimension,
366
+ and levels are evalutated to scalars over level_period in the data dimension.
367
+
368
+ Args:
369
+ db (QueryDB | Model): The database or model instance used to fetch the required data.
370
+ scenario_horizon (FixedFrequencyTimeIndex): TimeIndex of the scenario dimension to evaluate profiles.
371
+ level_period (SinglePeriodTimeIndex): TimeIndex of the data dimension to evaluate levels.
372
+ unit (str | None): The unit to convert the resulting values into (e.g., MW, GWh). If None,
373
+ the expression should be unitless.
374
+ is_float32 (bool, optional): Whether to return the vector as a NumPy array with `float32`
375
+ precision. Defaults to True.
376
+
377
+ """
295
378
  return self._get_scenario_vector(db, scenario_horizon, level_period, unit, is_float32)
296
379
 
297
380
  def get_data_value(
@@ -302,11 +385,23 @@ class LevelProfile(Base, ABC):
302
385
  unit: str | None,
303
386
  is_max_level: bool | None = None,
304
387
  ) -> float:
305
- """Return float for level_period."""
388
+ """
389
+ Evaluate LevelProfile to a scalar at the level period of the data dimension, and as an average over the scenario horizon.
390
+
391
+ Args:
392
+ db (QueryDB | Model): The database or model instance used to fetch the required data.
393
+ scenario_horizon (FixedFrequencyTimeIndex): TimeIndex of the scenario dimension to evaluate profiles.
394
+ level_period (SinglePeriodTimeIndex): TimeIndex of the data dimension to evaluate levels.
395
+ unit (str | None): The unit to convert the resulting values into (e.g., MW, GWh). If None,
396
+ the expression should be unitless.
397
+ is_max_level (bool | None, optional): Whether to evaluate the expression as a maximum level (with a zero_one profile)
398
+ or as an average level (with a mean_one profile). If None, the default format of the attribute is used.
399
+
400
+ """
306
401
  return self._get_data_value(db, scenario_horizon, level_period, unit, is_max_level)
307
402
 
308
403
  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."""
404
+ """Modify the intercept part of (level * profile + intercept) of an attribute by adding a constant value."""
310
405
  expr = ensure_expr(
311
406
  ConstantTimeVector(self._ensure_float(value), unit=unit, is_max_level=False),
312
407
  is_level=True,
@@ -326,9 +421,10 @@ class LevelProfile(Base, ABC):
326
421
  unit: str | None = None,
327
422
  reference_period: ReferencePeriod | None = None,
328
423
  is_max_level: bool | None = None,
329
- use_profile: bool = False,
424
+ use_profile: bool = True, # TODO: Remove. Should always use profile. If has profile validate that it is equal to the profile of Level.
330
425
  ) -> None:
331
- """Modify the level part of (level*profile + intercept) of an attribute by adding a constant value."""
426
+ """Modify the level_shift part of (scale * (level + level_shift) * profile + intercept) of an attribute by adding a constant value."""
427
+ # TODO: Not allowed to shift if there is intercept?
332
428
  self._check_type(value, (float, int))
333
429
  self._check_type(unit, (str, type(None)))
334
430
  self._check_type(reference_period, (ReferencePeriod, type(None)))
@@ -357,7 +453,8 @@ class LevelProfile(Base, ABC):
357
453
  self._level_shift += expr
358
454
 
359
455
  def scale(self, value: float | int) -> None:
360
- """Modify the value (level*profile + intercept) of an attribute by multiplying with a constant value."""
456
+ """Modify the scale part of (scale * (level + level_shift) * profile + intercept) of an attribute by multiplying with a constant value."""
457
+ # TODO: Not allowed to scale if there is intercept?
361
458
  expr = ensure_expr(
362
459
  ConstantTimeVector(self._ensure_float(value), unit=None, is_max_level=False),
363
460
  is_level=True,
@@ -400,17 +497,43 @@ class LevelProfile(Base, ABC):
400
497
  profile=None,
401
498
  )
402
499
 
500
+ def _ensure_compatible_level_profile_combo(self, level: Expr | None, profile: Expr | None) -> None:
501
+ """Check that all profiles in leaf levels (in level) also exist in profile."""
502
+ if level is None or profile is None:
503
+ return
504
+
505
+ leaf_level_profiles = get_profile_exprs_from_leaf_levels(level)
506
+ leaf_profile_profiles = get_leaf_profiles(profile)
507
+
508
+ for p in leaf_level_profiles:
509
+ if p not in leaf_profile_profiles:
510
+ message = (
511
+ f"Incompatible level/profile combination because all profiles in leaf levels (in level) does not exist in profile. "
512
+ f"Profile expression {p} found in level {level} but not in profile."
513
+ )
514
+ raise ValueError(message)
515
+
403
516
  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
517
+ msg = f"{self} requires {expr} to be "
518
+ if expr.is_stock() != self._IS_STOCK:
519
+ raise ValueError(msg + f"is_stock={self._IS_STOCK}")
520
+ if expr.is_flow() != self._IS_FLOW:
521
+ raise ValueError(msg + f"is_flow={self._IS_STOCK}")
522
+ if expr.is_level() is False:
523
+ raise ValueError(msg + "is_level=True")
524
+ if expr.is_profile() is True:
525
+ raise ValueError(msg + "is_profile=False")
408
526
 
409
527
  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
528
+ msg = f"{self} requires {expr} to be "
529
+ if expr.is_stock() is True:
530
+ raise ValueError(msg + "is_stock=False")
531
+ if expr.is_flow() is True:
532
+ raise ValueError(msg + "is_flow=False")
533
+ if expr.is_level() is True:
534
+ raise ValueError(msg + "is_level=False")
535
+ if expr.is_profile() is False:
536
+ raise ValueError(msg + "is_profile=True")
414
537
 
415
538
  def _ensure_profile_expr(
416
539
  self,
@@ -452,7 +575,8 @@ class LevelProfile(Base, ABC):
452
575
  is_max_level = self._IS_MAX_AND_ZERO_ONE
453
576
 
454
577
  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?"
578
+ if not isinstance(level_expr, Expr):
579
+ raise ValueError("Attribute level Expr is None. Have you called Solver.solve yet?")
456
580
 
457
581
  level_value = get_level_value(
458
582
  expr=level_expr,
@@ -497,7 +621,8 @@ class LevelProfile(Base, ABC):
497
621
  level_expr = self.get_level()
498
622
 
499
623
  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?"
624
+ if not isinstance(level_expr, Expr):
625
+ raise ValueError("Attribute level Expr is None. Have you called Solver.solve yet?")
501
626
 
502
627
  level_value = get_level_value(
503
628
  expr=level_expr,
@@ -593,121 +718,193 @@ class LevelProfile(Base, ABC):
593
718
 
594
719
 
595
720
  class FlowVolume(LevelProfile):
596
- """Represents a flow volume attribute, indicating that the attribute is a flow variable."""
721
+ """
722
+ Abstract class representing a flow volume attribute, indicating that the attribute is a flow variable.
723
+
724
+ Subclass of LevelProfile. See LevelProfile for details.
725
+ """
597
726
 
598
727
  _IS_FLOW = True
599
728
 
600
729
 
601
730
  class Coefficient(LevelProfile):
602
- """Represents a coefficient attribute, used as a base class for various coefficient types."""
731
+ """
732
+ Abstract class representing a coefficient attribute, used as a base class for various coefficient types.
733
+
734
+ Subclass of LevelProfile. See LevelProfile for details.
735
+ """
603
736
 
604
737
  pass
605
738
 
606
739
 
607
740
  class ArrowCoefficient(Coefficient):
608
- """Represents an arrow coefficient attribute, used for efficiency, loss, and conversion coefficients."""
741
+ """
742
+ Abstract class representing an arrow coefficient attribute, used for efficiency, loss, and conversion coefficients.
743
+
744
+ Subclass of Coefficient < LevelProfile. See LevelProfile for details.
745
+ """
609
746
 
610
747
  pass
611
748
 
612
749
 
613
- class ShaddowPrice(Coefficient):
614
- """Represents a shadow price attribute, indicating that the attribute has unit might be negative."""
750
+ class ShadowPrice(Coefficient):
751
+ """
752
+ Abstract class representing a shadow price attribute, indicating that the attribute has a unit and might be negative.
753
+
754
+ Subclass of Coefficient < LevelProfile. See LevelProfile for details.
755
+ """
615
756
 
616
757
  _IS_UNITLESS = False
617
758
  _IS_NOT_NEGATIVE = False
618
759
 
619
760
 
620
761
  class ObjectiveCoefficient(Coefficient):
621
- """Represents an objective coefficient attribute, indicating cost or revenue coefficients in the objective function."""
762
+ """
763
+ Abstract class representing an objective coefficient attribute, indicating cost or revenue coefficients in the objective function.
764
+
765
+ Subclass of Coefficient < LevelProfile. See LevelProfile for details.
766
+ """
622
767
 
623
768
  _IS_UNITLESS = False
624
769
  _IS_NOT_NEGATIVE = False
625
770
 
626
771
 
627
- # concrete subclasses intended for final use
772
+ # Concrete subclasses intended for final use
628
773
 
629
774
 
630
- class Price(ShaddowPrice):
631
- """Represents a price attribute, inheriting from ShaddowPrice."""
775
+ class Price(ShadowPrice):
776
+ """
777
+ Concrete class representing a price attribute, indicating the price of a commodity at a specific node.
778
+
779
+ Subclass of ShadowPrice < Coefficient < LevelProfile. See LevelProfile for details.
780
+ """
632
781
 
633
782
  _IS_ABSTRACT = False
634
783
 
635
784
 
636
- class WaterValue(ShaddowPrice):
637
- """Represents a water value attribute, inheriting from ShaddowPrice."""
785
+ class WaterValue(ShadowPrice):
786
+ """
787
+ Concrete class representing a water value attribute, indicating the value of water in the system.
788
+
789
+ Subclass of ShadowPrice < Coefficient < LevelProfile. See LevelProfile for details.
790
+ """
638
791
 
639
792
  _IS_ABSTRACT = False
640
793
 
641
794
 
642
795
  class Cost(ObjectiveCoefficient):
643
- """Represents a cost attribute, indicating cost coefficients in the objective function."""
796
+ """
797
+ Concrete class representing a cost attribute, indicating cost coefficients in the objective function.
798
+
799
+ Subclass of ObjectiveCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
800
+ """
644
801
 
645
802
  _IS_ABSTRACT = False
646
803
  _IS_COST = True
647
804
 
648
805
 
649
806
  class ReservePrice(ObjectiveCoefficient):
650
- """Represents a reserve price attribute, indicating revenue coefficients in the objective function."""
807
+ """
808
+ Concrete class representing a reserve price attribute, indicating revenue coefficients in the objective function.
809
+
810
+ Subclass of ObjectiveCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
811
+ """
651
812
 
652
813
  _IS_ABSTRACT = False
653
814
  _IS_COST = False
654
815
 
655
816
 
656
817
  class Elasticity(Coefficient): # TODO: How do this work?
657
- """Represents an elasticity coefficient attribute, indicating a unitless coefficient."""
818
+ """
819
+ Concrete class representing an elasticity coefficient attribute, indicating a unitless coefficient.
820
+
821
+ Subclass of Coefficient < LevelProfile. See LevelProfile for details.
822
+ """
658
823
 
659
824
  _IS_ABSTRACT = False
660
825
  _IS_UNITLESS = True
661
826
 
662
827
 
663
- class Proportion(Coefficient): # TODO: How do this work?
664
- """Represents a proportion coefficient attribute, indicating a unitless coefficient."""
828
+ class Proportion(Coefficient):
829
+ """
830
+ Concrete class representing a proportion coefficient attribute, indicating a unitless coefficient between 0 and 1.
831
+
832
+ Subclass of Coefficient < LevelProfile. See LevelProfile for details.
833
+ """
665
834
 
666
835
  _IS_ABSTRACT = False
667
836
  _IS_UNITLESS = True
668
837
 
669
838
 
670
839
  class Hours(Coefficient): # TODO: How do this work?
671
- """Represents an hours coefficient attribute, indicating a time-related coefficient."""
840
+ """
841
+ Concrete class representing an hours coefficient attribute, indicating a time-related coefficient.
842
+
843
+ Subclass of Coefficient < LevelProfile. See LevelProfile for details.
844
+ """
672
845
 
673
846
  _IS_ABSTRACT = False
674
847
 
675
848
 
676
849
  class Efficiency(ArrowCoefficient):
677
- """Represents an efficiency coefficient attribute, indicating a unitless coefficient."""
850
+ """
851
+ Concrete class representing an efficiency coefficient attribute, indicating a unitless coefficient.
852
+
853
+ Subclass of ArrowCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
854
+ """
678
855
 
679
856
  _IS_ABSTRACT = False
680
857
  _IS_UNITLESS = True
681
858
 
682
859
 
683
- class Loss(ArrowCoefficient):
684
- """Represents a loss coefficient attribute, indicating a unitless coefficient."""
860
+ class Loss(ArrowCoefficient): # TODO: Make a loss for storage that is percentage per time
861
+ """
862
+ Concrete class representing a loss coefficient attribute, indicating a unitless coefficient.
863
+
864
+ Subclass of ArrowCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
865
+ """
685
866
 
686
867
  _IS_ABSTRACT = False
687
868
  _IS_UNITLESS = True
688
869
 
689
870
 
690
871
  class Conversion(ArrowCoefficient):
691
- """Represents a conversion coefficient attribute, used for conversion factors in the model."""
872
+ """
873
+ Concrete class representing a conversion coefficient attribute, used for conversion factors in the model.
874
+
875
+ Subclass of ArrowCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
876
+ """
692
877
 
693
878
  _IS_ABSTRACT = False
694
879
 
695
880
 
696
881
  class AvgFlowVolume(FlowVolume):
697
- """Represents an average flow volume attribute, indicating a flow variable with average values."""
882
+ """
883
+ Concrete class representing an average flow volume attribute, indicating a flow variable with average values.
884
+
885
+ Subclass of FlowVolume < LevelProfile. See LevelProfile for details.
886
+ """
698
887
 
699
888
  _IS_ABSTRACT = False
700
889
 
701
890
 
702
891
  class MaxFlowVolume(FlowVolume):
703
- """Represents a maximum flow volume attribute, indicating a flow variable with maximum values."""
892
+ """
893
+ Concrete class representing a maximum flow volume attribute, indicating a flow variable with maximum values.
894
+
895
+ Subclass of FlowVolume < LevelProfile. See LevelProfile for details.
896
+ """
704
897
 
705
898
  _IS_ABSTRACT = False
706
899
  _IS_MAX_AND_ZERO_ONE = True
707
900
 
708
901
 
709
902
  class StockVolume(LevelProfile):
710
- """Represents a stock volume attribute, indicating a stock variable with maximum values."""
903
+ """
904
+ Concrete class representing a stock volume attribute, indicating a stock variable with maximum values.
905
+
906
+ Subclass of LevelProfile. See LevelProfile for details.
907
+ """
711
908
 
712
909
  _IS_ABSTRACT = False
713
910
  _IS_STOCK = True
@@ -8,7 +8,31 @@ from framcore.metadata import Meta
8
8
 
9
9
 
10
10
  class Component(Base, ABC):
11
- """Component interface class."""
11
+ """
12
+ Components describe the main elements in the energy system. Can have additional Attributes and Metadata.
13
+
14
+ We have high-level and low-level Components. High-level Components, such as a HydroModule,
15
+ can be decomposed into low-level Components like Flows and Nodes. The high-level description lets
16
+ analysts work with recognizable domain objects, while the low-level descriptions enable generic algorithms
17
+ that minimize code duplication and simplify data manipulation.
18
+
19
+ Some energy market models like JulES, SpineOpt and PyPSA also have a generic description of the system,
20
+ so this two-tier system can be used to easier adapt the dataset to their required formats.
21
+
22
+ The method Component.get_simpler_components() is used to decompose high-level Components into low-level
23
+ Components. This can also be used together with the utility function get_supported_components() to transform
24
+ a set of Components into a set that only contains supported Component types.
25
+
26
+ Result attributes are initialized in the high-level Components. When they are transferred to low-level Components,
27
+ and the results are set by a model like JulES, the results will also appear in the high-level Components.
28
+
29
+ Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
30
+ Node represent a point where a commodity can possibly be traded, stored or pass through.
31
+ Movement between Nodes is represented by Flows and Arrows. Flows represent a commodity flow,
32
+ and can have Arrows that each describe contribution of the Flow into a Node.
33
+ The Arrows have direction to determine input or output, and parameters for the contribution of the
34
+ Flow to the Node (conversion, efficiency and loss).
35
+ """
12
36
 
13
37
  def __init__(self) -> None:
14
38
  """Set mandatory private variables."""
@@ -46,7 +70,7 @@ class Component(Base, ABC):
46
70
  """
47
71
  self._check_type(base_name, str)
48
72
  components = self._get_simpler_components(base_name)
49
- assert base_name not in components, f"base_name: {base_name}\ncomponent: {self}"
73
+ assert base_name not in components, f"base_name: {base_name} should not be in \ncomponent: {self}"
50
74
  components: dict[str, Component]
51
75
  self._check_type(components, dict)
52
76
  for name, c in components.items():
@@ -85,7 +109,7 @@ class Component(Base, ABC):
85
109
  return parents[-1]
86
110
 
87
111
  def replace_node(self, old: str, new: str) -> None:
88
- """Replace old node with new. Not error if no match."""
112
+ """Replace old Node with new. Not error if no match."""
89
113
  self._check_type(old, str)
90
114
  self._check_type(new, str)
91
115
  self._replace_node(old, new)
@@ -18,7 +18,21 @@ class Demand(Component):
18
18
  temperature_profile: Expr | str | TimeVector | None = None,
19
19
  consumption: AvgFlowVolume | None = None,
20
20
  ) -> None:
21
- """Initialize the Demand class."""
21
+ """
22
+ Initialize the Demand class.
23
+
24
+ Args:
25
+ node (str): Node which this Demand consumes power on.
26
+ capacity (FlowVolume | None, optional): Maximum consumption capacity. Defaults to None.
27
+ reserve_price (ReservePrice | None, optional): Price in node at which the Demand will stop consumption. Defaults to None.
28
+ elastic_demand (ElasticDemand | None, optional): Describe changes in consumption based on commodity price in node. Defaults to None.
29
+ temperature_profile (Expr | str | TimeVector | None, optional): Describe changes in consumption based on temperatures. Defaults to None.
30
+ consumption (AvgFlowVolume | None, optional): Actual calculated consumption. Defaults to None.
31
+
32
+ Raises:
33
+ ValueError: When both reserve_price and elastic_demand is passed as arguments. This is ambiguous.
34
+
35
+ """
22
36
  super().__init__()
23
37
  self._check_type(node, str)
24
38
  self._check_type(capacity, (FlowVolume, type(None)))
@@ -62,9 +76,9 @@ class Demand(Component):
62
76
  """Get the reserve price level of the demand component."""
63
77
  return self._reserve_price
64
78
 
65
- def set_reserve_price_level(self, reserve_price: ReservePrice | None) -> None: # TODO: Update
79
+ def set_reserve_price(self, reserve_price: ReservePrice | None) -> None:
66
80
  """Set the reserve price level of the demand component."""
67
- self._check_type(reserve_price, (Expr, type(None)))
81
+ self._check_type(reserve_price, (ReservePrice, type(None)))
68
82
  if self._elastic_demand and reserve_price:
69
83
  message = "Cannot set reserve_price when elastic_demand is not None."
70
84
  raise ValueError(message)
@@ -78,7 +92,7 @@ class Demand(Component):
78
92
  """Set the elastic demand of the demand component."""
79
93
  self._check_type(elastic_demand, (ElasticDemand, type(None)))
80
94
  if self._reserve_price is not None and elastic_demand is not None:
81
- message = "Cannot set elastic_demand when reserve_price_level is not None."
95
+ message = "Cannot set elastic_demand when reserve_price is not None."
82
96
  raise ValueError(message)
83
97
  self._elastic_demand = elastic_demand
84
98
 
@@ -12,7 +12,15 @@ if TYPE_CHECKING:
12
12
 
13
13
 
14
14
  class Flow(Component):
15
- """Represents a commodity flow in or out of one or more nodes."""
15
+ """
16
+ Represents a commodity flow in or out of one or more nodes. Can have Attributes and Metadata.
17
+
18
+ Main attributes are arrows, main_node, max_capacity, min_capacity, startupcost and if it is exogenous.
19
+
20
+ Arrows describes contribution of a Flow into a Node. Has direction to determine input or output,
21
+ and parameters for the contribution of the Flow to the Node (conversion, efficiency, loss).
22
+ Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
23
+ """
16
24
 
17
25
  def __init__(
18
26
  self,
@@ -24,7 +32,21 @@ class Flow(Component):
24
32
  arrow_volumes: dict[Arrow, AvgFlowVolume] | None = None,
25
33
  is_exogenous: bool = False,
26
34
  ) -> None:
27
- """Initialize Flow with main node, capacity, and startup cost."""
35
+ """
36
+ Initialize Flow with main node, capacity, and startup cost.
37
+
38
+ Args:
39
+ main_node (str): Node which the Flow is primarily associated with.
40
+ max_capacity (FlowVolume | None, optional): Maximum capacity of the Flow. Defaults to None.
41
+ min_capacity (FlowVolume | None, optional): Minimum capacity of the Flow. Defaults to None.
42
+ startupcost (StartUpCost | None, optional): Costs associated with starting up this Flow. Defaults to None.
43
+ volume (AvgFlowVolume | None, optional): The actual volume carried by this Flow at a given moment. Defaults to None.
44
+ arrow_volumes (dict[Arrow, AvgFlowVolume] | None, optional): Possibility to store a version of volume for each Arrow. Can account for conversion,
45
+ efficiency and loss to represent the result for different commodities and units. Defaults to None.
46
+ is_exogenous (bool, optional): Flag denoting if a Solver should calculate the volumes associated with this flow or use its predefined volume.
47
+ Defaults to False.
48
+
49
+ """
28
50
  super().__init__()
29
51
  self._check_type(main_node, str)
30
52
  self._check_type(max_capacity, (FlowVolume, type(None)))
@@ -54,7 +76,7 @@ class Flow(Component):
54
76
 
55
77
  def set_exogenous(self) -> None:
56
78
  """
57
- Treat flow as rhs term.
79
+ Treat flow as fixed variable.
58
80
 
59
81
  Use volume if it exists.
60
82
  If no volume, then try to use
@@ -132,7 +154,7 @@ class Flow(Component):
132
154
 
133
155
  def add_loaders(self, loaders: set[Loader]) -> None:
134
156
  """Add loaders stored in attributes to loaders."""
135
- from framcore.utils import add_loaders_if # noqa: PLC0415
157
+ from framcore.utils import add_loaders_if
136
158
 
137
159
  add_loaders_if(loaders, self.get_volume())
138
160
  add_loaders_if(loaders, self.get_max_capacity())