flixopt 3.2.1__py3-none-any.whl → 3.3.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.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- flixopt/calculation.py +1 -1
- flixopt/components.py +10 -0
- flixopt/effects.py +23 -27
- flixopt/elements.py +54 -1
- flixopt/flow_system.py +139 -84
- flixopt/interface.py +23 -2
- flixopt/io.py +396 -12
- flixopt/results.py +48 -22
- flixopt/structure.py +366 -48
- {flixopt-3.2.1.dist-info → flixopt-3.3.1.dist-info}/METADATA +1 -1
- {flixopt-3.2.1.dist-info → flixopt-3.3.1.dist-info}/RECORD +14 -14
- {flixopt-3.2.1.dist-info → flixopt-3.3.1.dist-info}/WHEEL +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.3.1.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.3.1.dist-info}/top_level.txt +0 -0
flixopt/calculation.py
CHANGED
|
@@ -112,7 +112,7 @@ class Calculation:
|
|
|
112
112
|
'periodic': effect.submodel.periodic.total.solution.values,
|
|
113
113
|
'total': effect.submodel.total.solution.values,
|
|
114
114
|
}
|
|
115
|
-
for effect in self.flow_system.effects
|
|
115
|
+
for effect in sorted(self.flow_system.effects.values(), key=lambda e: e.label_full.upper())
|
|
116
116
|
},
|
|
117
117
|
'Invest-Decisions': {
|
|
118
118
|
'Invested': {
|
flixopt/components.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Literal
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import xarray as xr
|
|
13
13
|
|
|
14
|
+
from . import io as fx_io
|
|
14
15
|
from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser
|
|
15
16
|
from .elements import Component, ComponentModel, Flow
|
|
16
17
|
from .features import InvestmentModel, PiecewiseModel
|
|
@@ -528,6 +529,15 @@ class Storage(Component):
|
|
|
528
529
|
f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.'
|
|
529
530
|
)
|
|
530
531
|
|
|
532
|
+
def __repr__(self) -> str:
|
|
533
|
+
"""Return string representation."""
|
|
534
|
+
# Use build_repr_from_init directly to exclude charging and discharging
|
|
535
|
+
return fx_io.build_repr_from_init(
|
|
536
|
+
self,
|
|
537
|
+
excluded_params={'self', 'label', 'charging', 'discharging', 'kwargs'},
|
|
538
|
+
skip_default_size=True,
|
|
539
|
+
) + fx_io.format_flow_details(self)
|
|
540
|
+
|
|
531
541
|
|
|
532
542
|
@register_class_for_io
|
|
533
543
|
class Transmission(Component):
|
flixopt/effects.py
CHANGED
|
@@ -16,9 +16,10 @@ import linopy
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
import xarray as xr
|
|
18
18
|
|
|
19
|
+
from . import io as fx_io
|
|
19
20
|
from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser
|
|
20
21
|
from .features import ShareAllocationModel
|
|
21
|
-
from .structure import Element, ElementModel, FlowSystemModel, Submodel, register_class_for_io
|
|
22
|
+
from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
24
25
|
from collections.abc import Iterator
|
|
@@ -448,13 +449,13 @@ PeriodicEffects = dict[str, Scalar]
|
|
|
448
449
|
EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares
|
|
449
450
|
|
|
450
451
|
|
|
451
|
-
class EffectCollection:
|
|
452
|
+
class EffectCollection(ElementContainer[Effect]):
|
|
452
453
|
"""
|
|
453
454
|
Handling all Effects
|
|
454
455
|
"""
|
|
455
456
|
|
|
456
457
|
def __init__(self, *effects: Effect):
|
|
457
|
-
|
|
458
|
+
super().__init__(element_type_name='effects')
|
|
458
459
|
self._standard_effect: Effect | None = None
|
|
459
460
|
self._objective_effect: Effect | None = None
|
|
460
461
|
|
|
@@ -474,7 +475,7 @@ class EffectCollection:
|
|
|
474
475
|
self.standard_effect = effect
|
|
475
476
|
if effect.is_objective:
|
|
476
477
|
self.objective_effect = effect
|
|
477
|
-
self.
|
|
478
|
+
self.add(effect) # Use the inherited add() method from ElementContainer
|
|
478
479
|
logger.info(f'Registered new Effect: {effect.label}')
|
|
479
480
|
|
|
480
481
|
def create_effect_values_dict(
|
|
@@ -520,10 +521,13 @@ class EffectCollection:
|
|
|
520
521
|
# Check circular loops in effects:
|
|
521
522
|
temporal, periodic = self.calculate_effect_share_factors()
|
|
522
523
|
|
|
523
|
-
# Validate all referenced sources exist
|
|
524
|
-
|
|
524
|
+
# Validate all referenced effects (both sources and targets) exist
|
|
525
|
+
edges = list(temporal.keys()) + list(periodic.keys())
|
|
526
|
+
unknown_sources = {src for src, _ in edges if src not in self}
|
|
527
|
+
unknown_targets = {tgt for _, tgt in edges if tgt not in self}
|
|
528
|
+
unknown = unknown_sources | unknown_targets
|
|
525
529
|
if unknown:
|
|
526
|
-
raise KeyError(f'Unknown effects used in
|
|
530
|
+
raise KeyError(f'Unknown effects used in effect share mappings: {sorted(unknown)}')
|
|
527
531
|
|
|
528
532
|
temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal]))
|
|
529
533
|
periodic_cycles = detect_cycles(tuples_to_adjacency_list([key for key in periodic]))
|
|
@@ -552,31 +556,23 @@ class EffectCollection:
|
|
|
552
556
|
else:
|
|
553
557
|
raise KeyError(f'Effect {effect} not found!')
|
|
554
558
|
try:
|
|
555
|
-
return
|
|
559
|
+
return super().__getitem__(effect) # Leverage ContainerMixin suggestions
|
|
556
560
|
except KeyError as e:
|
|
557
|
-
|
|
561
|
+
# Extract the original message and append context for cleaner output
|
|
562
|
+
original_msg = str(e).strip('\'"')
|
|
563
|
+
raise KeyError(f'{original_msg} Add the effect to the FlowSystem first.') from None
|
|
558
564
|
|
|
559
|
-
def __iter__(self) -> Iterator[
|
|
560
|
-
return iter(self.
|
|
561
|
-
|
|
562
|
-
def __len__(self) -> int:
|
|
563
|
-
return len(self._effects)
|
|
565
|
+
def __iter__(self) -> Iterator[str]:
|
|
566
|
+
return iter(self.keys()) # Iterate over keys like a normal dict
|
|
564
567
|
|
|
565
568
|
def __contains__(self, item: str | Effect) -> bool:
|
|
566
569
|
"""Check if the effect exists. Checks for label or object"""
|
|
567
570
|
if isinstance(item, str):
|
|
568
|
-
return item
|
|
571
|
+
return super().__contains__(item) # Check if the label exists
|
|
569
572
|
elif isinstance(item, Effect):
|
|
570
|
-
|
|
571
|
-
return True
|
|
572
|
-
if item in self.effects.values(): # Check if the object exists
|
|
573
|
-
return True
|
|
573
|
+
return item.label_full in self and self[item.label_full] is item
|
|
574
574
|
return False
|
|
575
575
|
|
|
576
|
-
@property
|
|
577
|
-
def effects(self) -> dict[str, Effect]:
|
|
578
|
-
return self._effects
|
|
579
|
-
|
|
580
576
|
@property
|
|
581
577
|
def standard_effect(self) -> Effect:
|
|
582
578
|
if self._standard_effect is None:
|
|
@@ -611,7 +607,7 @@ class EffectCollection:
|
|
|
611
607
|
dict[tuple[str, str], xr.DataArray],
|
|
612
608
|
]:
|
|
613
609
|
shares_periodic = {}
|
|
614
|
-
for name, effect in self.
|
|
610
|
+
for name, effect in self.items():
|
|
615
611
|
if effect.share_from_periodic:
|
|
616
612
|
for source, data in effect.share_from_periodic.items():
|
|
617
613
|
if source not in shares_periodic:
|
|
@@ -620,7 +616,7 @@ class EffectCollection:
|
|
|
620
616
|
shares_periodic = calculate_all_conversion_paths(shares_periodic)
|
|
621
617
|
|
|
622
618
|
shares_temporal = {}
|
|
623
|
-
for name, effect in self.
|
|
619
|
+
for name, effect in self.items():
|
|
624
620
|
if effect.share_from_temporal:
|
|
625
621
|
for source, data in effect.share_from_temporal.items():
|
|
626
622
|
if source not in shares_temporal:
|
|
@@ -670,7 +666,7 @@ class EffectCollectionModel(Submodel):
|
|
|
670
666
|
|
|
671
667
|
def _do_modeling(self):
|
|
672
668
|
super()._do_modeling()
|
|
673
|
-
for effect in self.effects:
|
|
669
|
+
for effect in self.effects.values():
|
|
674
670
|
effect.create_model(self._model)
|
|
675
671
|
self.penalty = self.add_submodels(
|
|
676
672
|
ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'),
|
|
@@ -684,7 +680,7 @@ class EffectCollectionModel(Submodel):
|
|
|
684
680
|
)
|
|
685
681
|
|
|
686
682
|
def _add_share_between_effects(self):
|
|
687
|
-
for target_effect in self.effects:
|
|
683
|
+
for target_effect in self.effects.values():
|
|
688
684
|
# 1. temporal: <- receiving temporal shares from other effects
|
|
689
685
|
for source_effect, time_series in target_effect.share_from_temporal.items():
|
|
690
686
|
target_effect.submodel.temporal.add_share(
|
flixopt/elements.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import xarray as xr
|
|
13
13
|
|
|
14
|
+
from . import io as fx_io
|
|
14
15
|
from .config import CONFIG
|
|
15
16
|
from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser
|
|
16
17
|
from .features import InvestmentModel, OnOffModel
|
|
@@ -86,10 +87,12 @@ class Component(Element):
|
|
|
86
87
|
super().__init__(label, meta_data=meta_data)
|
|
87
88
|
self.inputs: list[Flow] = inputs or []
|
|
88
89
|
self.outputs: list[Flow] = outputs or []
|
|
89
|
-
self._check_unique_flow_labels()
|
|
90
90
|
self.on_off_parameters = on_off_parameters
|
|
91
91
|
self.prevent_simultaneous_flows: list[Flow] = prevent_simultaneous_flows or []
|
|
92
92
|
|
|
93
|
+
self._check_unique_flow_labels()
|
|
94
|
+
self._connect_flows()
|
|
95
|
+
|
|
93
96
|
self.flows: dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs}
|
|
94
97
|
|
|
95
98
|
def create_model(self, model: FlowSystemModel) -> ComponentModel:
|
|
@@ -115,6 +118,48 @@ class Component(Element):
|
|
|
115
118
|
def _plausibility_checks(self) -> None:
|
|
116
119
|
self._check_unique_flow_labels()
|
|
117
120
|
|
|
121
|
+
def _connect_flows(self):
|
|
122
|
+
# Inputs
|
|
123
|
+
for flow in self.inputs:
|
|
124
|
+
if flow.component not in ('UnknownComponent', self.label_full):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f'Flow "{flow.label}" already assigned to component "{flow.component}". '
|
|
127
|
+
f'Cannot attach to "{self.label_full}".'
|
|
128
|
+
)
|
|
129
|
+
flow.component = self.label_full
|
|
130
|
+
flow.is_input_in_component = True
|
|
131
|
+
# Outputs
|
|
132
|
+
for flow in self.outputs:
|
|
133
|
+
if flow.component not in ('UnknownComponent', self.label_full):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f'Flow "{flow.label}" already assigned to component "{flow.component}". '
|
|
136
|
+
f'Cannot attach to "{self.label_full}".'
|
|
137
|
+
)
|
|
138
|
+
flow.component = self.label_full
|
|
139
|
+
flow.is_input_in_component = False
|
|
140
|
+
|
|
141
|
+
# Validate prevent_simultaneous_flows: only allow local flows
|
|
142
|
+
if self.prevent_simultaneous_flows:
|
|
143
|
+
# Deduplicate while preserving order
|
|
144
|
+
seen = set()
|
|
145
|
+
self.prevent_simultaneous_flows = [
|
|
146
|
+
f for f in self.prevent_simultaneous_flows if id(f) not in seen and not seen.add(id(f))
|
|
147
|
+
]
|
|
148
|
+
local = set(self.inputs + self.outputs)
|
|
149
|
+
foreign = [f for f in self.prevent_simultaneous_flows if f not in local]
|
|
150
|
+
if foreign:
|
|
151
|
+
names = ', '.join(f.label_full for f in foreign)
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f'prevent_simultaneous_flows for "{self.label_full}" must reference its own flows. '
|
|
154
|
+
f'Foreign flows detected: {names}'
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def __repr__(self) -> str:
|
|
158
|
+
"""Return string representation with flow information."""
|
|
159
|
+
return fx_io.build_repr_from_init(
|
|
160
|
+
self, excluded_params={'self', 'label', 'inputs', 'outputs', 'kwargs'}, skip_default_size=True
|
|
161
|
+
) + fx_io.format_flow_details(self)
|
|
162
|
+
|
|
118
163
|
|
|
119
164
|
@register_class_for_io
|
|
120
165
|
class Bus(Element):
|
|
@@ -216,6 +261,10 @@ class Bus(Element):
|
|
|
216
261
|
def with_excess(self) -> bool:
|
|
217
262
|
return False if self.excess_penalty_per_flow_hour is None else True
|
|
218
263
|
|
|
264
|
+
def __repr__(self) -> str:
|
|
265
|
+
"""Return string representation."""
|
|
266
|
+
return super().__repr__() + fx_io.format_flow_details(self)
|
|
267
|
+
|
|
219
268
|
|
|
220
269
|
@register_class_for_io
|
|
221
270
|
class Connection:
|
|
@@ -493,6 +542,10 @@ class Flow(Element):
|
|
|
493
542
|
# Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
|
|
494
543
|
return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
|
|
495
544
|
|
|
545
|
+
def _format_invest_params(self, params: InvestParameters) -> str:
|
|
546
|
+
"""Format InvestParameters for display."""
|
|
547
|
+
return f'size: {params.format_for_repr()}'
|
|
548
|
+
|
|
496
549
|
|
|
497
550
|
class FlowModel(ElementModel):
|
|
498
551
|
element: Flow # Type hint
|
flixopt/flow_system.py
CHANGED
|
@@ -6,12 +6,14 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
import warnings
|
|
9
|
+
from itertools import chain
|
|
9
10
|
from typing import TYPE_CHECKING, Any, Literal, Optional
|
|
10
11
|
|
|
11
12
|
import numpy as np
|
|
12
13
|
import pandas as pd
|
|
13
14
|
import xarray as xr
|
|
14
15
|
|
|
16
|
+
from . import io as fx_io
|
|
15
17
|
from .config import CONFIG
|
|
16
18
|
from .core import (
|
|
17
19
|
ConversionError,
|
|
@@ -32,7 +34,7 @@ from .effects import (
|
|
|
32
34
|
TemporalEffectsUser,
|
|
33
35
|
)
|
|
34
36
|
from .elements import Bus, Component, Flow
|
|
35
|
-
from .structure import Element, FlowSystemModel, Interface
|
|
37
|
+
from .structure import CompositeContainerMixin, Element, ElementContainer, FlowSystemModel, Interface
|
|
36
38
|
|
|
37
39
|
if TYPE_CHECKING:
|
|
38
40
|
import pathlib
|
|
@@ -43,11 +45,13 @@ if TYPE_CHECKING:
|
|
|
43
45
|
logger = logging.getLogger('flixopt')
|
|
44
46
|
|
|
45
47
|
|
|
46
|
-
class FlowSystem(Interface):
|
|
48
|
+
class FlowSystem(Interface, CompositeContainerMixin[Element]):
|
|
47
49
|
"""
|
|
48
|
-
A FlowSystem organizes the high level Elements (Components, Buses &
|
|
50
|
+
A FlowSystem organizes the high level Elements (Components, Buses, Effects & Flows).
|
|
49
51
|
|
|
50
|
-
This is the main container class that users work with to build and manage their
|
|
52
|
+
This is the main container class that users work with to build and manage their energy or material flow system.
|
|
53
|
+
FlowSystem provides both direct container access (via .components, .buses, .effects, .flows) and a unified
|
|
54
|
+
dict-like interface for accessing any element by label across all container types.
|
|
51
55
|
|
|
52
56
|
Args:
|
|
53
57
|
timesteps: The timesteps of the model.
|
|
@@ -69,10 +73,74 @@ class FlowSystem(Interface):
|
|
|
69
73
|
- False: All flow rates are optimized separately per scenario
|
|
70
74
|
- list[str]: Only specified flows (by label_full) are equalized across scenarios
|
|
71
75
|
|
|
76
|
+
Examples:
|
|
77
|
+
Creating a FlowSystem and accessing elements:
|
|
78
|
+
|
|
79
|
+
>>> import flixopt as fx
|
|
80
|
+
>>> import pandas as pd
|
|
81
|
+
>>> timesteps = pd.date_range('2023-01-01', periods=24, freq='h')
|
|
82
|
+
>>> flow_system = fx.FlowSystem(timesteps)
|
|
83
|
+
>>>
|
|
84
|
+
>>> # Add elements to the system
|
|
85
|
+
>>> boiler = fx.Component('Boiler', inputs=[heat_flow], on_off_parameters=...)
|
|
86
|
+
>>> heat_bus = fx.Bus('Heat', excess_penalty_per_flow_hour=1e4)
|
|
87
|
+
>>> costs = fx.Effect('costs', is_objective=True, is_standard=True)
|
|
88
|
+
>>> flow_system.add_elements(boiler, heat_bus, costs)
|
|
89
|
+
|
|
90
|
+
Unified dict-like access (recommended for most cases):
|
|
91
|
+
|
|
92
|
+
>>> # Access any element by label, regardless of type
|
|
93
|
+
>>> boiler = flow_system['Boiler'] # Returns Component
|
|
94
|
+
>>> heat_bus = flow_system['Heat'] # Returns Bus
|
|
95
|
+
>>> costs = flow_system['costs'] # Returns Effect
|
|
96
|
+
>>>
|
|
97
|
+
>>> # Check if element exists
|
|
98
|
+
>>> if 'Boiler' in flow_system:
|
|
99
|
+
... print('Boiler found in system')
|
|
100
|
+
>>>
|
|
101
|
+
>>> # Iterate over all elements
|
|
102
|
+
>>> for label in flow_system.keys():
|
|
103
|
+
... element = flow_system[label]
|
|
104
|
+
... print(f'{label}: {type(element).__name__}')
|
|
105
|
+
>>>
|
|
106
|
+
>>> # Get all element labels and objects
|
|
107
|
+
>>> all_labels = list(flow_system.keys())
|
|
108
|
+
>>> all_elements = list(flow_system.values())
|
|
109
|
+
>>> for label, element in flow_system.items():
|
|
110
|
+
... print(f'{label}: {element}')
|
|
111
|
+
|
|
112
|
+
Direct container access for type-specific operations:
|
|
113
|
+
|
|
114
|
+
>>> # Access specific container when you need type filtering
|
|
115
|
+
>>> for component in flow_system.components.values():
|
|
116
|
+
... print(f'{component.label}: {len(component.inputs)} inputs')
|
|
117
|
+
>>>
|
|
118
|
+
>>> # Access buses directly
|
|
119
|
+
>>> for bus in flow_system.buses.values():
|
|
120
|
+
... print(f'{bus.label}')
|
|
121
|
+
>>>
|
|
122
|
+
>>> # Flows are automatically collected from all components
|
|
123
|
+
>>> for flow in flow_system.flows.values():
|
|
124
|
+
... print(f'{flow.label_full}: {flow.size}')
|
|
125
|
+
>>>
|
|
126
|
+
>>> # Access effects
|
|
127
|
+
>>> for effect in flow_system.effects.values():
|
|
128
|
+
... print(f'{effect.label}')
|
|
129
|
+
|
|
72
130
|
Notes:
|
|
131
|
+
- The dict-like interface (`flow_system['element']`) searches across all containers
|
|
132
|
+
(components, buses, effects, flows) to find the element with the matching label.
|
|
133
|
+
- Element labels must be unique across all container types. Attempting to add
|
|
134
|
+
elements with duplicate labels will raise an error, ensuring each label maps to exactly one element.
|
|
135
|
+
- The `.all_elements` property is deprecated. Use the dict-like interface instead:
|
|
136
|
+
`flow_system['element']`, `'element' in flow_system`, `flow_system.keys()`,
|
|
137
|
+
`flow_system.values()`, or `flow_system.items()`.
|
|
138
|
+
- Direct container access (`.components`, `.buses`, `.effects`, `.flows`) is useful
|
|
139
|
+
when you need type-specific filtering or operations.
|
|
140
|
+
- The `.flows` container is automatically populated from all component inputs and outputs.
|
|
73
141
|
- Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel.
|
|
74
142
|
- The instance starts disconnected (self._connected_and_transformed == False) and will be
|
|
75
|
-
|
|
143
|
+
connected_and_transformed automatically when trying to solve a calculation.
|
|
76
144
|
"""
|
|
77
145
|
|
|
78
146
|
def __init__(
|
|
@@ -104,8 +172,8 @@ class FlowSystem(Interface):
|
|
|
104
172
|
self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep)
|
|
105
173
|
|
|
106
174
|
# Element collections
|
|
107
|
-
self.components:
|
|
108
|
-
self.buses:
|
|
175
|
+
self.components: ElementContainer[Component] = ElementContainer(element_type_name='components')
|
|
176
|
+
self.buses: ElementContainer[Bus] = ElementContainer(element_type_name='buses')
|
|
109
177
|
self.effects: EffectCollection = EffectCollection()
|
|
110
178
|
self.model: FlowSystemModel | None = None
|
|
111
179
|
|
|
@@ -113,6 +181,7 @@ class FlowSystem(Interface):
|
|
|
113
181
|
self._used_in_calculation = False
|
|
114
182
|
|
|
115
183
|
self._network_app = None
|
|
184
|
+
self._flows_cache: ElementContainer[Flow] | None = None
|
|
116
185
|
|
|
117
186
|
# Use properties to validate and store scenario dimension settings
|
|
118
187
|
self.scenario_independent_sizes = scenario_independent_sizes
|
|
@@ -232,7 +301,7 @@ class FlowSystem(Interface):
|
|
|
232
301
|
|
|
233
302
|
# Extract from effects
|
|
234
303
|
effects_structure = {}
|
|
235
|
-
for effect in self.effects:
|
|
304
|
+
for effect in self.effects.values():
|
|
236
305
|
effect_structure, effect_arrays = effect._create_reference_structure()
|
|
237
306
|
all_extracted_arrays.update(effect_arrays)
|
|
238
307
|
effects_structure[effect.label] = effect_structure
|
|
@@ -433,7 +502,7 @@ class FlowSystem(Interface):
|
|
|
433
502
|
self.weights = self.fit_to_model_coords('weights', self.weights, dims=['period', 'scenario'])
|
|
434
503
|
|
|
435
504
|
self._connect_network()
|
|
436
|
-
for element in
|
|
505
|
+
for element in chain(self.components.values(), self.effects.values(), self.buses.values()):
|
|
437
506
|
element.transform_data(self)
|
|
438
507
|
self._connected_and_transformed = True
|
|
439
508
|
|
|
@@ -581,7 +650,7 @@ class FlowSystem(Interface):
|
|
|
581
650
|
'class': 'Bus' if isinstance(node, Bus) else 'Component',
|
|
582
651
|
'infos': node.__str__(),
|
|
583
652
|
}
|
|
584
|
-
for node in
|
|
653
|
+
for node in chain(self.components.values(), self.buses.values())
|
|
585
654
|
}
|
|
586
655
|
|
|
587
656
|
edges = {
|
|
@@ -603,10 +672,8 @@ class FlowSystem(Interface):
|
|
|
603
672
|
Args:
|
|
604
673
|
element: new element to check
|
|
605
674
|
"""
|
|
606
|
-
if element in self.all_elements.values():
|
|
607
|
-
raise ValueError(f'Element {element.label_full} already added to FlowSystem!')
|
|
608
675
|
# check if name is already used:
|
|
609
|
-
if element.label_full in self
|
|
676
|
+
if element.label_full in self:
|
|
610
677
|
raise ValueError(f'Label of Element {element.label_full} already used in another element!')
|
|
611
678
|
|
|
612
679
|
def _add_effects(self, *args: Effect) -> None:
|
|
@@ -616,13 +683,15 @@ class FlowSystem(Interface):
|
|
|
616
683
|
for new_component in list(components):
|
|
617
684
|
logger.info(f'Registered new Component: {new_component.label_full}')
|
|
618
685
|
self._check_if_element_is_unique(new_component) # check if already exists:
|
|
619
|
-
self.components
|
|
686
|
+
self.components.add(new_component) # Add to existing components
|
|
687
|
+
self._flows_cache = None # Invalidate flows cache
|
|
620
688
|
|
|
621
689
|
def _add_buses(self, *buses: Bus):
|
|
622
690
|
for new_bus in list(buses):
|
|
623
691
|
logger.info(f'Registered new Bus: {new_bus.label_full}')
|
|
624
692
|
self._check_if_element_is_unique(new_bus) # check if already exists:
|
|
625
|
-
self.buses
|
|
693
|
+
self.buses.add(new_bus) # Add to existing buses
|
|
694
|
+
self._flows_cache = None # Invalidate flows cache
|
|
626
695
|
|
|
627
696
|
def _connect_network(self):
|
|
628
697
|
"""Connects the network of components and buses. Can be rerun without changes if no elements were added"""
|
|
@@ -632,7 +701,7 @@ class FlowSystem(Interface):
|
|
|
632
701
|
flow.is_input_in_component = True if flow in component.inputs else False
|
|
633
702
|
|
|
634
703
|
# Add Bus if not already added (deprecated)
|
|
635
|
-
if flow._bus_object is not None and flow._bus_object not in self.buses
|
|
704
|
+
if flow._bus_object is not None and flow._bus_object.label_full not in self.buses:
|
|
636
705
|
warnings.warn(
|
|
637
706
|
f'The Bus {flow._bus_object.label_full} was added to the FlowSystem from {flow.label_full}.'
|
|
638
707
|
f'This is deprecated and will be removed in the future. '
|
|
@@ -659,62 +728,40 @@ class FlowSystem(Interface):
|
|
|
659
728
|
)
|
|
660
729
|
|
|
661
730
|
def __repr__(self) -> str:
|
|
662
|
-
"""
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
# Build dimension info
|
|
666
|
-
dims = f'{len(self.timesteps)} timesteps [{self.timesteps[0].strftime("%Y-%m-%d")} to {self.timesteps[-1].strftime("%Y-%m-%d")}]'
|
|
667
|
-
if self.periods is not None:
|
|
668
|
-
dims += f', {len(self.periods)} periods'
|
|
669
|
-
if self.scenarios is not None:
|
|
670
|
-
dims += f', {len(self.scenarios)} scenarios'
|
|
671
|
-
|
|
672
|
-
return f'FlowSystem({dims}, {len(self.components)} Components, {len(self.buses)} Buses, {len(self.effects)} Effects, {status})'
|
|
673
|
-
|
|
674
|
-
def __str__(self) -> str:
|
|
675
|
-
"""Structured summary for users."""
|
|
676
|
-
|
|
677
|
-
def format_elements(element_names: list, label: str, alignment: int = 12):
|
|
678
|
-
name_list = ', '.join(element_names[:3])
|
|
679
|
-
if len(element_names) > 3:
|
|
680
|
-
name_list += f' ... (+{len(element_names) - 3} more)'
|
|
731
|
+
"""Return a detailed string representation showing all containers."""
|
|
732
|
+
r = fx_io.format_title_with_underline('FlowSystem', '=')
|
|
681
733
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
return f'{label}:{"":<{padding}} {len(element_names)}{suffix}'
|
|
685
|
-
|
|
686
|
-
time_period = f'Time period: {self.timesteps[0].date()} to {self.timesteps[-1].date()}'
|
|
734
|
+
# Timestep info
|
|
735
|
+
time_period = f'{self.timesteps[0].date()} to {self.timesteps[-1].date()}'
|
|
687
736
|
freq_str = str(self.timesteps.freq).replace('<', '').replace('>', '') if self.timesteps.freq else 'irregular'
|
|
688
|
-
|
|
689
|
-
lines = [
|
|
690
|
-
f'Timesteps: {len(self.timesteps)} ({freq_str}) [{time_period}]',
|
|
691
|
-
]
|
|
737
|
+
r += f'Timesteps: {len(self.timesteps)} ({freq_str}) [{time_period}]\n'
|
|
692
738
|
|
|
693
739
|
# Add periods if present
|
|
694
740
|
if self.periods is not None:
|
|
695
741
|
period_names = ', '.join(str(p) for p in self.periods[:3])
|
|
696
742
|
if len(self.periods) > 3:
|
|
697
743
|
period_names += f' ... (+{len(self.periods) - 3} more)'
|
|
698
|
-
|
|
744
|
+
r += f'Periods: {len(self.periods)} ({period_names})\n'
|
|
745
|
+
else:
|
|
746
|
+
r += 'Periods: None\n'
|
|
699
747
|
|
|
700
748
|
# Add scenarios if present
|
|
701
749
|
if self.scenarios is not None:
|
|
702
750
|
scenario_names = ', '.join(str(s) for s in self.scenarios[:3])
|
|
703
751
|
if len(self.scenarios) > 3:
|
|
704
752
|
scenario_names += f' ... (+{len(self.scenarios) - 3} more)'
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
)
|
|
715
|
-
lines = ['FlowSystem:', f'{"─" * max(len(line) for line in lines)}'] + lines
|
|
753
|
+
r += f'Scenarios: {len(self.scenarios)} ({scenario_names})\n'
|
|
754
|
+
else:
|
|
755
|
+
r += 'Scenarios: None\n'
|
|
756
|
+
|
|
757
|
+
# Add status
|
|
758
|
+
status = '✓' if self.connected_and_transformed else '⚠'
|
|
759
|
+
r += f'Status: {status}\n'
|
|
760
|
+
|
|
761
|
+
# Add grouped container view
|
|
762
|
+
r += '\n' + self._format_grouped_containers()
|
|
716
763
|
|
|
717
|
-
return
|
|
764
|
+
return r
|
|
718
765
|
|
|
719
766
|
def __eq__(self, other: FlowSystem):
|
|
720
767
|
"""Check if two FlowSystems are equal by comparing their dataset representations."""
|
|
@@ -734,38 +781,46 @@ class FlowSystem(Interface):
|
|
|
734
781
|
|
|
735
782
|
return True
|
|
736
783
|
|
|
737
|
-
def
|
|
738
|
-
"""
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
suggestions = get_close_matches(item, self.all_elements.keys(), n=3, cutoff=0.6)
|
|
746
|
-
|
|
747
|
-
if suggestions:
|
|
748
|
-
suggestion_str = ', '.join(f"'{s}'" for s in suggestions)
|
|
749
|
-
raise KeyError(f"Element '{item}' not found. Did you mean: {suggestion_str}?")
|
|
750
|
-
else:
|
|
751
|
-
raise KeyError(f"Element '{item}' not found in FlowSystem")
|
|
752
|
-
|
|
753
|
-
def __contains__(self, item: str) -> bool:
|
|
754
|
-
"""Check if element exists in the FlowSystem."""
|
|
755
|
-
return item in self.all_elements
|
|
756
|
-
|
|
757
|
-
def __iter__(self):
|
|
758
|
-
"""Iterate over element labels."""
|
|
759
|
-
return iter(self.all_elements.keys())
|
|
784
|
+
def _get_container_groups(self) -> dict[str, ElementContainer]:
|
|
785
|
+
"""Return ordered container groups for CompositeContainerMixin."""
|
|
786
|
+
return {
|
|
787
|
+
'Components': self.components,
|
|
788
|
+
'Buses': self.buses,
|
|
789
|
+
'Effects': self.effects,
|
|
790
|
+
'Flows': self.flows,
|
|
791
|
+
}
|
|
760
792
|
|
|
761
793
|
@property
|
|
762
|
-
def flows(self) ->
|
|
763
|
-
|
|
764
|
-
|
|
794
|
+
def flows(self) -> ElementContainer[Flow]:
|
|
795
|
+
if self._flows_cache is None:
|
|
796
|
+
flows = [f for c in self.components.values() for f in c.inputs + c.outputs]
|
|
797
|
+
# Deduplicate by id and sort for reproducibility
|
|
798
|
+
flows = sorted({id(f): f for f in flows}.values(), key=lambda f: f.label_full.lower())
|
|
799
|
+
self._flows_cache = ElementContainer(flows, element_type_name='flows')
|
|
800
|
+
return self._flows_cache
|
|
765
801
|
|
|
766
802
|
@property
|
|
767
803
|
def all_elements(self) -> dict[str, Element]:
|
|
768
|
-
|
|
804
|
+
"""
|
|
805
|
+
Get all elements as a dictionary.
|
|
806
|
+
|
|
807
|
+
.. deprecated:: 3.2.0
|
|
808
|
+
Use dict-like interface instead: `flow_system['element']`, `'element' in flow_system`,
|
|
809
|
+
`flow_system.keys()`, `flow_system.values()`, or `flow_system.items()`.
|
|
810
|
+
This property will be removed in v4.0.0.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Dictionary mapping element labels to element objects.
|
|
814
|
+
"""
|
|
815
|
+
warnings.warn(
|
|
816
|
+
"The 'all_elements' property is deprecated. Use dict-like interface instead: "
|
|
817
|
+
"flow_system['element'], 'element' in flow_system, flow_system.keys(), "
|
|
818
|
+
'flow_system.values(), or flow_system.items(). '
|
|
819
|
+
'This property will be removed in v4.0.0.',
|
|
820
|
+
DeprecationWarning,
|
|
821
|
+
stacklevel=2,
|
|
822
|
+
)
|
|
823
|
+
return {**self.components, **self.effects, **self.flows, **self.buses}
|
|
769
824
|
|
|
770
825
|
@property
|
|
771
826
|
def coords(self) -> dict[FlowSystemDimensions, pd.Index]:
|
flixopt/interface.py
CHANGED
|
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import warnings
|
|
10
|
-
from typing import TYPE_CHECKING,
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
12
|
import numpy as np
|
|
13
13
|
import pandas as pd
|
|
@@ -1051,6 +1051,27 @@ class InvestParameters(Interface):
|
|
|
1051
1051
|
def maximum_or_fixed_size(self) -> PeriodicData:
|
|
1052
1052
|
return self.fixed_size if self.fixed_size is not None else self.maximum_size
|
|
1053
1053
|
|
|
1054
|
+
def format_for_repr(self) -> str:
|
|
1055
|
+
"""Format InvestParameters for display in repr methods.
|
|
1056
|
+
|
|
1057
|
+
Returns:
|
|
1058
|
+
Formatted string showing size information
|
|
1059
|
+
"""
|
|
1060
|
+
from .io import numeric_to_str_for_repr
|
|
1061
|
+
|
|
1062
|
+
if self.fixed_size is not None:
|
|
1063
|
+
val = numeric_to_str_for_repr(self.fixed_size)
|
|
1064
|
+
status = 'mandatory' if self.mandatory else 'optional'
|
|
1065
|
+
return f'{val} ({status})'
|
|
1066
|
+
|
|
1067
|
+
# Show range if available
|
|
1068
|
+
parts = []
|
|
1069
|
+
if self.minimum_size is not None:
|
|
1070
|
+
parts.append(f'min: {numeric_to_str_for_repr(self.minimum_size)}')
|
|
1071
|
+
if self.maximum_size is not None:
|
|
1072
|
+
parts.append(f'max: {numeric_to_str_for_repr(self.maximum_size)}')
|
|
1073
|
+
return ', '.join(parts) if parts else 'invest'
|
|
1074
|
+
|
|
1054
1075
|
@staticmethod
|
|
1055
1076
|
def compute_linked_periods(first_period: int, last_period: int, periods: pd.Index | list[int]) -> xr.DataArray:
|
|
1056
1077
|
return xr.DataArray(
|
|
@@ -1324,7 +1345,7 @@ class OnOffParameters(Interface):
|
|
|
1324
1345
|
return True
|
|
1325
1346
|
|
|
1326
1347
|
return any(
|
|
1327
|
-
param
|
|
1348
|
+
self._has_value(param)
|
|
1328
1349
|
for param in [
|
|
1329
1350
|
self.effects_per_switch_on,
|
|
1330
1351
|
self.switch_on_total_max,
|