flixopt 2.2.0b0__py3-none-any.whl → 2.2.0rc2__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.
- docs/examples/00-Minimal Example.md +1 -1
- docs/examples/01-Basic Example.md +1 -1
- docs/examples/02-Complex Example.md +1 -1
- docs/examples/index.md +1 -1
- docs/faq/contribute.md +26 -14
- docs/faq/index.md +1 -1
- docs/javascripts/mathjax.js +1 -1
- docs/user-guide/Mathematical Notation/Bus.md +1 -1
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +13 -13
- docs/user-guide/Mathematical Notation/Flow.md +1 -1
- docs/user-guide/Mathematical Notation/LinearConverter.md +2 -2
- docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
- docs/user-guide/Mathematical Notation/Storage.md +1 -1
- docs/user-guide/Mathematical Notation/index.md +1 -1
- docs/user-guide/Mathematical Notation/others.md +1 -1
- docs/user-guide/index.md +2 -2
- flixopt/__init__.py +5 -0
- flixopt/aggregation.py +0 -1
- flixopt/calculation.py +40 -72
- flixopt/commons.py +10 -1
- flixopt/components.py +326 -154
- flixopt/core.py +459 -966
- flixopt/effects.py +67 -270
- flixopt/elements.py +76 -84
- flixopt/features.py +172 -154
- flixopt/flow_system.py +70 -99
- flixopt/interface.py +315 -147
- flixopt/io.py +27 -56
- flixopt/linear_converters.py +3 -3
- flixopt/network_app.py +755 -0
- flixopt/plotting.py +16 -34
- flixopt/results.py +108 -806
- flixopt/structure.py +11 -67
- flixopt/utils.py +9 -6
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/METADATA +63 -42
- flixopt-2.2.0rc2.dist-info/RECORD +54 -0
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/WHEEL +1 -1
- scripts/extract_release_notes.py +45 -0
- docs/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- docs/user-guide/Mathematical Notation/Investment.md +0 -115
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/top_level.txt +0 -0
flixopt/flow_system.py
CHANGED
|
@@ -16,17 +16,10 @@ from rich.console import Console
|
|
|
16
16
|
from rich.pretty import Pretty
|
|
17
17
|
|
|
18
18
|
from . import io as fx_io
|
|
19
|
-
from .core import
|
|
20
|
-
from .effects import
|
|
21
|
-
Effect,
|
|
22
|
-
EffectCollection,
|
|
23
|
-
EffectTimeSeries,
|
|
24
|
-
EffectValuesDict,
|
|
25
|
-
EffectValuesUserScenario,
|
|
26
|
-
EffectValuesUserTimestep,
|
|
27
|
-
)
|
|
19
|
+
from .core import NumericData, NumericDataTS, TimeSeries, TimeSeriesCollection, TimeSeriesData
|
|
20
|
+
from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser
|
|
28
21
|
from .elements import Bus, Component, Flow
|
|
29
|
-
from .structure import CLASS_REGISTRY, Element, SystemModel
|
|
22
|
+
from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation
|
|
30
23
|
|
|
31
24
|
if TYPE_CHECKING:
|
|
32
25
|
import pyvis
|
|
@@ -42,31 +35,23 @@ class FlowSystem:
|
|
|
42
35
|
def __init__(
|
|
43
36
|
self,
|
|
44
37
|
timesteps: pd.DatetimeIndex,
|
|
45
|
-
scenarios: Optional[pd.Index] = None,
|
|
46
38
|
hours_of_last_timestep: Optional[float] = None,
|
|
47
39
|
hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None,
|
|
48
|
-
scenario_weights: Optional[ScenarioData] = None,
|
|
49
40
|
):
|
|
50
41
|
"""
|
|
51
42
|
Args:
|
|
52
43
|
timesteps: The timesteps of the model.
|
|
53
|
-
scenarios: The scenarios of the model.
|
|
54
44
|
hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified
|
|
55
45
|
hours_of_previous_timesteps: The duration of previous timesteps.
|
|
56
46
|
If None, the first time increment of time_series is used.
|
|
57
47
|
This is needed to calculate previous durations (for example consecutive_on_hours).
|
|
58
48
|
If you use an array, take care that its long enough to cover all previous values!
|
|
59
|
-
scenario_weights: The weights of the scenarios. If None, all scenarios have the same weight. All weights are normalized to 1.
|
|
60
49
|
"""
|
|
61
50
|
self.time_series_collection = TimeSeriesCollection(
|
|
62
51
|
timesteps=timesteps,
|
|
63
|
-
scenarios=scenarios,
|
|
64
52
|
hours_of_last_timestep=hours_of_last_timestep,
|
|
65
53
|
hours_of_previous_timesteps=hours_of_previous_timesteps,
|
|
66
54
|
)
|
|
67
|
-
self.scenario_weights = self.create_time_series(
|
|
68
|
-
'scenario_weights', scenario_weights, has_time_dim=False, has_scenario_dim=True
|
|
69
|
-
)
|
|
70
55
|
|
|
71
56
|
# defaults:
|
|
72
57
|
self.components: Dict[str, Component] = {}
|
|
@@ -76,20 +61,17 @@ class FlowSystem:
|
|
|
76
61
|
|
|
77
62
|
self._connected = False
|
|
78
63
|
|
|
64
|
+
self._network_app = None
|
|
65
|
+
|
|
79
66
|
@classmethod
|
|
80
67
|
def from_dataset(cls, ds: xr.Dataset):
|
|
81
68
|
timesteps_extra = pd.DatetimeIndex(ds.attrs['timesteps_extra'], name='time')
|
|
82
69
|
hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item()
|
|
83
70
|
|
|
84
|
-
scenarios = pd.Index(ds.attrs['scenarios'], name='scenario') if ds.attrs.get('scenarios') is not None else None
|
|
85
|
-
scenario_weights = fx_io.insert_dataarray(ds.attrs['scenario_weights'], ds)
|
|
86
|
-
|
|
87
71
|
flow_system = FlowSystem(
|
|
88
72
|
timesteps=timesteps_extra[:-1],
|
|
89
73
|
hours_of_last_timestep=hours_of_last_timestep,
|
|
90
74
|
hours_of_previous_timesteps=ds.attrs['hours_of_previous_timesteps'],
|
|
91
|
-
scenarios=scenarios,
|
|
92
|
-
scenario_weights=scenario_weights,
|
|
93
75
|
)
|
|
94
76
|
|
|
95
77
|
structure = fx_io.insert_dataarray({key: ds.attrs[key] for key in ['components', 'buses', 'effects']}, ds)
|
|
@@ -110,15 +92,11 @@ class FlowSystem:
|
|
|
110
92
|
"""
|
|
111
93
|
timesteps_extra = pd.DatetimeIndex(data['timesteps_extra'], name='time')
|
|
112
94
|
hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item()
|
|
113
|
-
scenarios = pd.Index(data['scenarios'], name='scenario') if data.get('scenarios') is not None else None
|
|
114
|
-
scenario_weights = data.get('scenario_weights').selected_data if data.get('scenario_weights') is not None else None
|
|
115
95
|
|
|
116
96
|
flow_system = FlowSystem(
|
|
117
97
|
timesteps=timesteps_extra[:-1],
|
|
118
98
|
hours_of_last_timestep=hours_of_last_timestep,
|
|
119
99
|
hours_of_previous_timesteps=data['hours_of_previous_timesteps'],
|
|
120
|
-
scenarios=scenarios,
|
|
121
|
-
scenario_weights=scenario_weights,
|
|
122
100
|
)
|
|
123
101
|
|
|
124
102
|
flow_system.add_elements(*[Bus.from_dict(bus) for bus in data['buses'].values()])
|
|
@@ -194,8 +172,6 @@ class FlowSystem:
|
|
|
194
172
|
},
|
|
195
173
|
'timesteps_extra': [date.isoformat() for date in self.time_series_collection.timesteps_extra],
|
|
196
174
|
'hours_of_previous_timesteps': self.time_series_collection.hours_of_previous_timesteps,
|
|
197
|
-
'scenarios': self.time_series_collection.scenarios.tolist() if self.time_series_collection.scenarios is not None else None,
|
|
198
|
-
'scenario_weights': self.scenario_weights,
|
|
199
175
|
}
|
|
200
176
|
if data_mode == 'data':
|
|
201
177
|
return fx_io.replace_timeseries(data, 'data')
|
|
@@ -210,7 +186,7 @@ class FlowSystem:
|
|
|
210
186
|
Args:
|
|
211
187
|
constants_in_dataset: If True, constants are included as Dataset variables.
|
|
212
188
|
"""
|
|
213
|
-
ds = self.time_series_collection.
|
|
189
|
+
ds = self.time_series_collection.to_dataset(include_constants=constants_in_dataset)
|
|
214
190
|
ds.attrs = self.as_dict(data_mode='name')
|
|
215
191
|
return ds
|
|
216
192
|
|
|
@@ -267,6 +243,58 @@ class FlowSystem:
|
|
|
267
243
|
node_infos, edge_infos = self.network_infos()
|
|
268
244
|
return plotting.plot_network(node_infos, edge_infos, path, controls, show)
|
|
269
245
|
|
|
246
|
+
def start_network_app(self):
|
|
247
|
+
"""Visualizes the network structure of a FlowSystem using Dash, Cytoscape, and networkx.
|
|
248
|
+
Requires optional dependencies: dash, dash-cytoscape, networkx, werkzeug.
|
|
249
|
+
"""
|
|
250
|
+
from .network_app import DASH_CYTOSCAPE_AVAILABLE, VISUALIZATION_ERROR, flow_graph, shownetwork
|
|
251
|
+
|
|
252
|
+
warnings.warn(
|
|
253
|
+
'The network visualization is still experimental and might change in the future.',
|
|
254
|
+
stacklevel=2,
|
|
255
|
+
category=UserWarning,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
259
|
+
raise ImportError(
|
|
260
|
+
f'Network visualization requires optional dependencies. '
|
|
261
|
+
f'Install with: pip install flixopt[viz], flixopt[full] or pip install dash dash_cytoscape networkx werkzeug. '
|
|
262
|
+
f'Original error: {VISUALIZATION_ERROR}'
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if not self._connected:
|
|
266
|
+
self._connect_network()
|
|
267
|
+
|
|
268
|
+
if self._network_app is not None:
|
|
269
|
+
logger.warning('The network app is already running. Restarting it.')
|
|
270
|
+
self.stop_network_app()
|
|
271
|
+
|
|
272
|
+
self._network_app = shownetwork(flow_graph(self))
|
|
273
|
+
|
|
274
|
+
def stop_network_app(self):
|
|
275
|
+
"""Stop the network visualization server."""
|
|
276
|
+
from .network_app import DASH_CYTOSCAPE_AVAILABLE, VISUALIZATION_ERROR
|
|
277
|
+
|
|
278
|
+
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
279
|
+
raise ImportError(
|
|
280
|
+
f'Network visualization requires optional dependencies. '
|
|
281
|
+
f'Install with: pip install flixopt[viz]. '
|
|
282
|
+
f'Original error: {VISUALIZATION_ERROR}'
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if self._network_app is None:
|
|
286
|
+
logger.warning('No network app is currently running. Cant stop it')
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
logger.info('Stopping network visualization server...')
|
|
291
|
+
self._network_app.server_instance.shutdown()
|
|
292
|
+
logger.info('Network visualization stopped.')
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logger.error(f'Failed to stop the network visualization app: {e}')
|
|
295
|
+
finally:
|
|
296
|
+
self._network_app = None
|
|
297
|
+
|
|
270
298
|
def network_infos(self) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]:
|
|
271
299
|
if not self._connected:
|
|
272
300
|
self._connect_network()
|
|
@@ -294,80 +322,41 @@ class FlowSystem:
|
|
|
294
322
|
def transform_data(self):
|
|
295
323
|
if not self._connected:
|
|
296
324
|
self._connect_network()
|
|
297
|
-
self.scenario_weights = self.create_time_series(
|
|
298
|
-
'scenario_weights', self.scenario_weights, has_time_dim=False, has_scenario_dim=True
|
|
299
|
-
)
|
|
300
325
|
for element in self.all_elements.values():
|
|
301
326
|
element.transform_data(self)
|
|
302
327
|
|
|
303
328
|
def create_time_series(
|
|
304
329
|
self,
|
|
305
330
|
name: str,
|
|
306
|
-
data: Optional[Union[
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
has_extra_timestep: bool = False,
|
|
310
|
-
) -> Optional[Union[Scalar, TimeSeries]]:
|
|
331
|
+
data: Optional[Union[NumericData, TimeSeriesData, TimeSeries]],
|
|
332
|
+
needs_extra_timestep: bool = False,
|
|
333
|
+
) -> Optional[TimeSeries]:
|
|
311
334
|
"""
|
|
312
|
-
Tries to create a TimeSeries from
|
|
335
|
+
Tries to create a TimeSeries from NumericData Data and adds it to the time_series_collection
|
|
313
336
|
If the data already is a TimeSeries, nothing happens and the TimeSeries gets reset and returned
|
|
314
337
|
If the data is a TimeSeriesData, it is converted to a TimeSeries, and the aggregation weights are applied.
|
|
315
338
|
If the data is None, nothing happens.
|
|
316
|
-
|
|
317
|
-
Args:
|
|
318
|
-
name: The name of the TimeSeries
|
|
319
|
-
data: The data to create a TimeSeries from
|
|
320
|
-
has_time_dim: Whether the data has a time dimension
|
|
321
|
-
has_scenario_dim: Whether the data has a scenario dimension
|
|
322
|
-
has_extra_timestep: Whether the data has an extra timestep
|
|
323
339
|
"""
|
|
324
|
-
if not has_time_dim and not has_scenario_dim:
|
|
325
|
-
raise ValueError('At least one of the dimensions must be present')
|
|
326
340
|
|
|
327
341
|
if data is None:
|
|
328
342
|
return None
|
|
329
|
-
|
|
330
|
-
if not has_time_dim and self.time_series_collection.scenarios is None:
|
|
331
|
-
return data
|
|
332
|
-
|
|
333
|
-
if isinstance(data, TimeSeries):
|
|
343
|
+
elif isinstance(data, TimeSeries):
|
|
334
344
|
data.restore_data()
|
|
335
345
|
if data in self.time_series_collection:
|
|
336
346
|
return data
|
|
337
|
-
return self.time_series_collection.
|
|
338
|
-
data=data.
|
|
339
|
-
name=name,
|
|
340
|
-
has_time_dim=has_time_dim,
|
|
341
|
-
has_scenario_dim=has_scenario_dim,
|
|
342
|
-
has_extra_timestep=has_extra_timestep,
|
|
343
|
-
)
|
|
344
|
-
elif isinstance(data, TimeSeriesData):
|
|
345
|
-
data.label = name
|
|
346
|
-
return self.time_series_collection.add_time_series(
|
|
347
|
-
data=data.data,
|
|
348
|
-
name=name,
|
|
349
|
-
has_time_dim=has_time_dim,
|
|
350
|
-
has_scenario_dim=has_scenario_dim,
|
|
351
|
-
has_extra_timestep=has_extra_timestep,
|
|
352
|
-
aggregation_weight=data.agg_weight,
|
|
353
|
-
aggregation_group=data.agg_group,
|
|
347
|
+
return self.time_series_collection.create_time_series(
|
|
348
|
+
data=data.active_data, name=name, needs_extra_timestep=needs_extra_timestep
|
|
354
349
|
)
|
|
355
|
-
return self.time_series_collection.
|
|
356
|
-
data=data,
|
|
357
|
-
name=name,
|
|
358
|
-
has_time_dim=has_time_dim,
|
|
359
|
-
has_scenario_dim=has_scenario_dim,
|
|
360
|
-
has_extra_timestep=has_extra_timestep,
|
|
350
|
+
return self.time_series_collection.create_time_series(
|
|
351
|
+
data=data, name=name, needs_extra_timestep=needs_extra_timestep
|
|
361
352
|
)
|
|
362
353
|
|
|
363
354
|
def create_effect_time_series(
|
|
364
355
|
self,
|
|
365
356
|
label_prefix: Optional[str],
|
|
366
|
-
effect_values:
|
|
357
|
+
effect_values: EffectValuesUser,
|
|
367
358
|
label_suffix: Optional[str] = None,
|
|
368
|
-
|
|
369
|
-
has_scenario_dim: bool = True,
|
|
370
|
-
) -> Optional[Union[EffectTimeSeries, EffectValuesDict]]:
|
|
359
|
+
) -> Optional[EffectTimeSeries]:
|
|
371
360
|
"""
|
|
372
361
|
Transform EffectValues to EffectTimeSeries.
|
|
373
362
|
Creates a TimeSeries for each key in the nested_values dictionary, using the value as the data.
|
|
@@ -375,31 +364,13 @@ class FlowSystem:
|
|
|
375
364
|
The resulting label of the TimeSeries is the label of the parent_element,
|
|
376
365
|
followed by the label of the Effect in the nested_values and the label_suffix.
|
|
377
366
|
If the key in the EffectValues is None, the alias 'Standard_Effect' is used
|
|
378
|
-
|
|
379
|
-
Args:
|
|
380
|
-
label_prefix: Prefix for the TimeSeries name
|
|
381
|
-
effect_values: Dictionary of EffectValues
|
|
382
|
-
label_suffix: Suffix for the TimeSeries name
|
|
383
|
-
has_time_dim: Whether the data has a time dimension
|
|
384
|
-
has_scenario_dim: Whether the data has a scenario dimension
|
|
385
367
|
"""
|
|
386
|
-
if not has_time_dim and not has_scenario_dim:
|
|
387
|
-
raise ValueError('At least one of the dimensions must be present')
|
|
388
|
-
|
|
389
368
|
effect_values: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values)
|
|
390
369
|
if effect_values is None:
|
|
391
370
|
return None
|
|
392
371
|
|
|
393
|
-
if not has_time_dim and self.time_series_collection.scenarios is None:
|
|
394
|
-
return effect_values
|
|
395
|
-
|
|
396
372
|
return {
|
|
397
|
-
effect: self.create_time_series(
|
|
398
|
-
name='|'.join(filter(None, [label_prefix, effect, label_suffix])),
|
|
399
|
-
data=value,
|
|
400
|
-
has_time_dim=has_time_dim,
|
|
401
|
-
has_scenario_dim=has_scenario_dim,
|
|
402
|
-
)
|
|
373
|
+
effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value)
|
|
403
374
|
for effect, value in effect_values.items()
|
|
404
375
|
}
|
|
405
376
|
|