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.

Files changed (48) hide show
  1. docs/examples/00-Minimal Example.md +1 -1
  2. docs/examples/01-Basic Example.md +1 -1
  3. docs/examples/02-Complex Example.md +1 -1
  4. docs/examples/index.md +1 -1
  5. docs/faq/contribute.md +26 -14
  6. docs/faq/index.md +1 -1
  7. docs/javascripts/mathjax.js +1 -1
  8. docs/user-guide/Mathematical Notation/Bus.md +1 -1
  9. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +13 -13
  10. docs/user-guide/Mathematical Notation/Flow.md +1 -1
  11. docs/user-guide/Mathematical Notation/LinearConverter.md +2 -2
  12. docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
  13. docs/user-guide/Mathematical Notation/Storage.md +1 -1
  14. docs/user-guide/Mathematical Notation/index.md +1 -1
  15. docs/user-guide/Mathematical Notation/others.md +1 -1
  16. docs/user-guide/index.md +2 -2
  17. flixopt/__init__.py +5 -0
  18. flixopt/aggregation.py +0 -1
  19. flixopt/calculation.py +40 -72
  20. flixopt/commons.py +10 -1
  21. flixopt/components.py +326 -154
  22. flixopt/core.py +459 -966
  23. flixopt/effects.py +67 -270
  24. flixopt/elements.py +76 -84
  25. flixopt/features.py +172 -154
  26. flixopt/flow_system.py +70 -99
  27. flixopt/interface.py +315 -147
  28. flixopt/io.py +27 -56
  29. flixopt/linear_converters.py +3 -3
  30. flixopt/network_app.py +755 -0
  31. flixopt/plotting.py +16 -34
  32. flixopt/results.py +108 -806
  33. flixopt/structure.py +11 -67
  34. flixopt/utils.py +9 -6
  35. {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/METADATA +63 -42
  36. flixopt-2.2.0rc2.dist-info/RECORD +54 -0
  37. {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/WHEEL +1 -1
  38. scripts/extract_release_notes.py +45 -0
  39. docs/release-notes/_template.txt +0 -32
  40. docs/release-notes/index.md +0 -7
  41. docs/release-notes/v2.0.0.md +0 -93
  42. docs/release-notes/v2.0.1.md +0 -12
  43. docs/release-notes/v2.1.0.md +0 -31
  44. docs/release-notes/v2.2.0.md +0 -55
  45. docs/user-guide/Mathematical Notation/Investment.md +0 -115
  46. flixopt-2.2.0b0.dist-info/RECORD +0 -59
  47. {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/licenses/LICENSE +0 -0
  48. {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 Scalar, ScenarioData, TimeSeries, TimeSeriesCollection, TimeSeriesData, TimestepData
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.as_dataset()
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[TimestepData, TimeSeriesData, TimeSeries]],
307
- has_time_dim: bool = True,
308
- has_scenario_dim: bool = True,
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 TimestepData and adds it to the time_series_collection
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.add_time_series(
338
- data=data.selected_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.add_time_series(
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: Union[EffectValuesUserScenario, EffectValuesUserTimestep],
357
+ effect_values: EffectValuesUser,
367
358
  label_suffix: Optional[str] = None,
368
- has_time_dim: bool = True,
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