flixopt 3.2.1__py3-none-any.whl → 3.4.0__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 +105 -39
- flixopt/components.py +16 -0
- flixopt/config.py +120 -0
- flixopt/effects.py +28 -28
- flixopt/elements.py +58 -1
- flixopt/flow_system.py +141 -84
- flixopt/interface.py +23 -2
- flixopt/io.py +506 -4
- flixopt/results.py +52 -24
- flixopt/solvers.py +12 -4
- flixopt/structure.py +369 -49
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/METADATA +3 -2
- flixopt-3.4.0.dist-info/RECORD +26 -0
- flixopt-3.2.1.dist-info/RECORD +0 -26
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/WHEEL +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/top_level.txt +0 -0
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):
|
|
@@ -178,6 +223,8 @@ class Bus(Element):
|
|
|
178
223
|
by the FlowSystem during system setup.
|
|
179
224
|
"""
|
|
180
225
|
|
|
226
|
+
submodel: BusModel | None
|
|
227
|
+
|
|
181
228
|
def __init__(
|
|
182
229
|
self,
|
|
183
230
|
label: str,
|
|
@@ -216,6 +263,10 @@ class Bus(Element):
|
|
|
216
263
|
def with_excess(self) -> bool:
|
|
217
264
|
return False if self.excess_penalty_per_flow_hour is None else True
|
|
218
265
|
|
|
266
|
+
def __repr__(self) -> str:
|
|
267
|
+
"""Return string representation."""
|
|
268
|
+
return super().__repr__() + fx_io.format_flow_details(self)
|
|
269
|
+
|
|
219
270
|
|
|
220
271
|
@register_class_for_io
|
|
221
272
|
class Connection:
|
|
@@ -362,6 +413,8 @@ class Flow(Element):
|
|
|
362
413
|
|
|
363
414
|
"""
|
|
364
415
|
|
|
416
|
+
submodel: FlowModel | None
|
|
417
|
+
|
|
365
418
|
def __init__(
|
|
366
419
|
self,
|
|
367
420
|
label: str,
|
|
@@ -493,6 +546,10 @@ class Flow(Element):
|
|
|
493
546
|
# Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
|
|
494
547
|
return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
|
|
495
548
|
|
|
549
|
+
def _format_invest_params(self, params: InvestParameters) -> str:
|
|
550
|
+
"""Format InvestParameters for display."""
|
|
551
|
+
return f'size: {params.format_for_repr()}'
|
|
552
|
+
|
|
496
553
|
|
|
497
554
|
class FlowModel(ElementModel):
|
|
498
555
|
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,12 +73,78 @@ 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
|
|
|
146
|
+
model: FlowSystemModel | None
|
|
147
|
+
|
|
78
148
|
def __init__(
|
|
79
149
|
self,
|
|
80
150
|
timesteps: pd.DatetimeIndex,
|
|
@@ -104,8 +174,8 @@ class FlowSystem(Interface):
|
|
|
104
174
|
self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep)
|
|
105
175
|
|
|
106
176
|
# Element collections
|
|
107
|
-
self.components:
|
|
108
|
-
self.buses:
|
|
177
|
+
self.components: ElementContainer[Component] = ElementContainer(element_type_name='components')
|
|
178
|
+
self.buses: ElementContainer[Bus] = ElementContainer(element_type_name='buses')
|
|
109
179
|
self.effects: EffectCollection = EffectCollection()
|
|
110
180
|
self.model: FlowSystemModel | None = None
|
|
111
181
|
|
|
@@ -113,6 +183,7 @@ class FlowSystem(Interface):
|
|
|
113
183
|
self._used_in_calculation = False
|
|
114
184
|
|
|
115
185
|
self._network_app = None
|
|
186
|
+
self._flows_cache: ElementContainer[Flow] | None = None
|
|
116
187
|
|
|
117
188
|
# Use properties to validate and store scenario dimension settings
|
|
118
189
|
self.scenario_independent_sizes = scenario_independent_sizes
|
|
@@ -232,7 +303,7 @@ class FlowSystem(Interface):
|
|
|
232
303
|
|
|
233
304
|
# Extract from effects
|
|
234
305
|
effects_structure = {}
|
|
235
|
-
for effect in self.effects:
|
|
306
|
+
for effect in self.effects.values():
|
|
236
307
|
effect_structure, effect_arrays = effect._create_reference_structure()
|
|
237
308
|
all_extracted_arrays.update(effect_arrays)
|
|
238
309
|
effects_structure[effect.label] = effect_structure
|
|
@@ -433,7 +504,7 @@ class FlowSystem(Interface):
|
|
|
433
504
|
self.weights = self.fit_to_model_coords('weights', self.weights, dims=['period', 'scenario'])
|
|
434
505
|
|
|
435
506
|
self._connect_network()
|
|
436
|
-
for element in
|
|
507
|
+
for element in chain(self.components.values(), self.effects.values(), self.buses.values()):
|
|
437
508
|
element.transform_data(self)
|
|
438
509
|
self._connected_and_transformed = True
|
|
439
510
|
|
|
@@ -581,7 +652,7 @@ class FlowSystem(Interface):
|
|
|
581
652
|
'class': 'Bus' if isinstance(node, Bus) else 'Component',
|
|
582
653
|
'infos': node.__str__(),
|
|
583
654
|
}
|
|
584
|
-
for node in
|
|
655
|
+
for node in chain(self.components.values(), self.buses.values())
|
|
585
656
|
}
|
|
586
657
|
|
|
587
658
|
edges = {
|
|
@@ -603,10 +674,8 @@ class FlowSystem(Interface):
|
|
|
603
674
|
Args:
|
|
604
675
|
element: new element to check
|
|
605
676
|
"""
|
|
606
|
-
if element in self.all_elements.values():
|
|
607
|
-
raise ValueError(f'Element {element.label_full} already added to FlowSystem!')
|
|
608
677
|
# check if name is already used:
|
|
609
|
-
if element.label_full in self
|
|
678
|
+
if element.label_full in self:
|
|
610
679
|
raise ValueError(f'Label of Element {element.label_full} already used in another element!')
|
|
611
680
|
|
|
612
681
|
def _add_effects(self, *args: Effect) -> None:
|
|
@@ -616,13 +685,15 @@ class FlowSystem(Interface):
|
|
|
616
685
|
for new_component in list(components):
|
|
617
686
|
logger.info(f'Registered new Component: {new_component.label_full}')
|
|
618
687
|
self._check_if_element_is_unique(new_component) # check if already exists:
|
|
619
|
-
self.components
|
|
688
|
+
self.components.add(new_component) # Add to existing components
|
|
689
|
+
self._flows_cache = None # Invalidate flows cache
|
|
620
690
|
|
|
621
691
|
def _add_buses(self, *buses: Bus):
|
|
622
692
|
for new_bus in list(buses):
|
|
623
693
|
logger.info(f'Registered new Bus: {new_bus.label_full}')
|
|
624
694
|
self._check_if_element_is_unique(new_bus) # check if already exists:
|
|
625
|
-
self.buses
|
|
695
|
+
self.buses.add(new_bus) # Add to existing buses
|
|
696
|
+
self._flows_cache = None # Invalidate flows cache
|
|
626
697
|
|
|
627
698
|
def _connect_network(self):
|
|
628
699
|
"""Connects the network of components and buses. Can be rerun without changes if no elements were added"""
|
|
@@ -632,7 +703,7 @@ class FlowSystem(Interface):
|
|
|
632
703
|
flow.is_input_in_component = True if flow in component.inputs else False
|
|
633
704
|
|
|
634
705
|
# Add Bus if not already added (deprecated)
|
|
635
|
-
if flow._bus_object is not None and flow._bus_object not in self.buses
|
|
706
|
+
if flow._bus_object is not None and flow._bus_object.label_full not in self.buses:
|
|
636
707
|
warnings.warn(
|
|
637
708
|
f'The Bus {flow._bus_object.label_full} was added to the FlowSystem from {flow.label_full}.'
|
|
638
709
|
f'This is deprecated and will be removed in the future. '
|
|
@@ -659,62 +730,40 @@ class FlowSystem(Interface):
|
|
|
659
730
|
)
|
|
660
731
|
|
|
661
732
|
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)'
|
|
733
|
+
"""Return a detailed string representation showing all containers."""
|
|
734
|
+
r = fx_io.format_title_with_underline('FlowSystem', '=')
|
|
681
735
|
|
|
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()}'
|
|
736
|
+
# Timestep info
|
|
737
|
+
time_period = f'{self.timesteps[0].date()} to {self.timesteps[-1].date()}'
|
|
687
738
|
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
|
-
]
|
|
739
|
+
r += f'Timesteps: {len(self.timesteps)} ({freq_str}) [{time_period}]\n'
|
|
692
740
|
|
|
693
741
|
# Add periods if present
|
|
694
742
|
if self.periods is not None:
|
|
695
743
|
period_names = ', '.join(str(p) for p in self.periods[:3])
|
|
696
744
|
if len(self.periods) > 3:
|
|
697
745
|
period_names += f' ... (+{len(self.periods) - 3} more)'
|
|
698
|
-
|
|
746
|
+
r += f'Periods: {len(self.periods)} ({period_names})\n'
|
|
747
|
+
else:
|
|
748
|
+
r += 'Periods: None\n'
|
|
699
749
|
|
|
700
750
|
# Add scenarios if present
|
|
701
751
|
if self.scenarios is not None:
|
|
702
752
|
scenario_names = ', '.join(str(s) for s in self.scenarios[:3])
|
|
703
753
|
if len(self.scenarios) > 3:
|
|
704
754
|
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
|
|
755
|
+
r += f'Scenarios: {len(self.scenarios)} ({scenario_names})\n'
|
|
756
|
+
else:
|
|
757
|
+
r += 'Scenarios: None\n'
|
|
758
|
+
|
|
759
|
+
# Add status
|
|
760
|
+
status = '✓' if self.connected_and_transformed else '⚠'
|
|
761
|
+
r += f'Status: {status}\n'
|
|
762
|
+
|
|
763
|
+
# Add grouped container view
|
|
764
|
+
r += '\n' + self._format_grouped_containers()
|
|
716
765
|
|
|
717
|
-
return
|
|
766
|
+
return r
|
|
718
767
|
|
|
719
768
|
def __eq__(self, other: FlowSystem):
|
|
720
769
|
"""Check if two FlowSystems are equal by comparing their dataset representations."""
|
|
@@ -734,38 +783,46 @@ class FlowSystem(Interface):
|
|
|
734
783
|
|
|
735
784
|
return True
|
|
736
785
|
|
|
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())
|
|
786
|
+
def _get_container_groups(self) -> dict[str, ElementContainer]:
|
|
787
|
+
"""Return ordered container groups for CompositeContainerMixin."""
|
|
788
|
+
return {
|
|
789
|
+
'Components': self.components,
|
|
790
|
+
'Buses': self.buses,
|
|
791
|
+
'Effects': self.effects,
|
|
792
|
+
'Flows': self.flows,
|
|
793
|
+
}
|
|
760
794
|
|
|
761
795
|
@property
|
|
762
|
-
def flows(self) ->
|
|
763
|
-
|
|
764
|
-
|
|
796
|
+
def flows(self) -> ElementContainer[Flow]:
|
|
797
|
+
if self._flows_cache is None:
|
|
798
|
+
flows = [f for c in self.components.values() for f in c.inputs + c.outputs]
|
|
799
|
+
# Deduplicate by id and sort for reproducibility
|
|
800
|
+
flows = sorted({id(f): f for f in flows}.values(), key=lambda f: f.label_full.lower())
|
|
801
|
+
self._flows_cache = ElementContainer(flows, element_type_name='flows')
|
|
802
|
+
return self._flows_cache
|
|
765
803
|
|
|
766
804
|
@property
|
|
767
805
|
def all_elements(self) -> dict[str, Element]:
|
|
768
|
-
|
|
806
|
+
"""
|
|
807
|
+
Get all elements as a dictionary.
|
|
808
|
+
|
|
809
|
+
.. deprecated:: 3.2.0
|
|
810
|
+
Use dict-like interface instead: `flow_system['element']`, `'element' in flow_system`,
|
|
811
|
+
`flow_system.keys()`, `flow_system.values()`, or `flow_system.items()`.
|
|
812
|
+
This property will be removed in v4.0.0.
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
Dictionary mapping element labels to element objects.
|
|
816
|
+
"""
|
|
817
|
+
warnings.warn(
|
|
818
|
+
"The 'all_elements' property is deprecated. Use dict-like interface instead: "
|
|
819
|
+
"flow_system['element'], 'element' in flow_system, flow_system.keys(), "
|
|
820
|
+
'flow_system.values(), or flow_system.items(). '
|
|
821
|
+
'This property will be removed in v4.0.0.',
|
|
822
|
+
DeprecationWarning,
|
|
823
|
+
stacklevel=2,
|
|
824
|
+
)
|
|
825
|
+
return {**self.components, **self.effects, **self.flows, **self.buses}
|
|
769
826
|
|
|
770
827
|
@property
|
|
771
828
|
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,
|