flixopt 1.0.12__py3-none-any.whl → 2.0.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.

Files changed (72) hide show
  1. docs/examples/00-Minimal Example.md +5 -0
  2. docs/examples/01-Basic Example.md +5 -0
  3. docs/examples/02-Complex Example.md +10 -0
  4. docs/examples/03-Calculation Modes.md +5 -0
  5. docs/examples/index.md +5 -0
  6. docs/faq/contribute.md +49 -0
  7. docs/faq/index.md +3 -0
  8. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  9. docs/images/architecture_flixOpt.png +0 -0
  10. docs/images/flixopt-icon.svg +1 -0
  11. docs/javascripts/mathjax.js +18 -0
  12. docs/release-notes/_template.txt +32 -0
  13. docs/release-notes/index.md +7 -0
  14. docs/release-notes/v2.0.0.md +93 -0
  15. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  16. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  17. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  18. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  19. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  20. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  21. docs/user-guide/Mathematical Notation/index.md +22 -0
  22. docs/user-guide/Mathematical Notation/others.md +3 -0
  23. docs/user-guide/index.md +124 -0
  24. {flixOpt → flixopt}/__init__.py +5 -2
  25. {flixOpt → flixopt}/aggregation.py +113 -140
  26. flixopt/calculation.py +455 -0
  27. {flixOpt → flixopt}/commons.py +7 -4
  28. flixopt/components.py +630 -0
  29. {flixOpt → flixopt}/config.py +9 -8
  30. {flixOpt → flixopt}/config.yaml +3 -3
  31. flixopt/core.py +914 -0
  32. flixopt/effects.py +386 -0
  33. flixopt/elements.py +529 -0
  34. flixopt/features.py +1042 -0
  35. flixopt/flow_system.py +409 -0
  36. flixopt/interface.py +265 -0
  37. flixopt/io.py +308 -0
  38. flixopt/linear_converters.py +331 -0
  39. flixopt/plotting.py +1337 -0
  40. flixopt/results.py +898 -0
  41. flixopt/solvers.py +77 -0
  42. flixopt/structure.py +630 -0
  43. flixopt/utils.py +62 -0
  44. flixopt-2.0.0.dist-info/METADATA +145 -0
  45. flixopt-2.0.0.dist-info/RECORD +56 -0
  46. {flixopt-1.0.12.dist-info → flixopt-2.0.0.dist-info}/WHEEL +1 -1
  47. flixopt-2.0.0.dist-info/top_level.txt +6 -0
  48. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  49. pics/architecture_flixOpt.png +0 -0
  50. pics/flixopt-icon.svg +1 -0
  51. pics/pics.pptx +0 -0
  52. scripts/gen_ref_pages.py +54 -0
  53. site/release-notes/_template.txt +32 -0
  54. flixOpt/calculation.py +0 -629
  55. flixOpt/components.py +0 -614
  56. flixOpt/core.py +0 -182
  57. flixOpt/effects.py +0 -410
  58. flixOpt/elements.py +0 -489
  59. flixOpt/features.py +0 -942
  60. flixOpt/flow_system.py +0 -351
  61. flixOpt/interface.py +0 -203
  62. flixOpt/linear_converters.py +0 -325
  63. flixOpt/math_modeling.py +0 -1145
  64. flixOpt/plotting.py +0 -712
  65. flixOpt/results.py +0 -563
  66. flixOpt/solvers.py +0 -21
  67. flixOpt/structure.py +0 -733
  68. flixOpt/utils.py +0 -134
  69. flixopt-1.0.12.dist-info/METADATA +0 -174
  70. flixopt-1.0.12.dist-info/RECORD +0 -29
  71. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  72. {flixopt-1.0.12.dist-info → flixopt-2.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,42 +1,41 @@
1
1
  """
2
- This module contains the Aggregation functionality for the flixOpt framework.
2
+ This module contains the Aggregation functionality for the flixopt framework.
3
3
  Through this, aggregating TimeSeriesData is possible.
4
4
  """
5
5
 
6
6
  import copy
7
7
  import logging
8
+ import pathlib
8
9
  import timeit
9
10
  import warnings
10
- from collections import Counter
11
- from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
11
+ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
12
12
 
13
+ import linopy
13
14
  import numpy as np
14
15
  import pandas as pd
15
16
 
16
17
  try:
17
18
  import tsam.timeseriesaggregation as tsam
19
+
18
20
  TSAM_AVAILABLE = True
19
21
  except ImportError:
20
22
  TSAM_AVAILABLE = False
21
23
 
22
24
  from .components import Storage
23
- from .core import Skalar, TimeSeries, TimeSeriesData
25
+ from .core import Scalar, TimeSeriesData
24
26
  from .elements import Component
25
27
  from .flow_system import FlowSystem
26
- from .math_modeling import Equation, Variable, VariableTS
27
28
  from .structure import (
28
29
  Element,
29
- ElementModel,
30
+ Model,
30
31
  SystemModel,
31
- create_equation,
32
- create_variable,
33
32
  )
34
33
 
35
34
  if TYPE_CHECKING:
36
35
  import plotly.graph_objects as go
37
36
 
38
37
  warnings.filterwarnings('ignore', category=DeprecationWarning)
39
- logger = logging.getLogger('flixOpt')
38
+ logger = logging.getLogger('flixopt')
40
39
 
41
40
 
42
41
  class Aggregation:
@@ -47,24 +46,27 @@ class Aggregation:
47
46
  def __init__(
48
47
  self,
49
48
  original_data: pd.DataFrame,
50
- hours_per_time_step: Skalar,
51
- hours_per_period: Skalar,
49
+ hours_per_time_step: Scalar,
50
+ hours_per_period: Scalar,
52
51
  nr_of_periods: int = 8,
53
52
  weights: Dict[str, float] = None,
54
53
  time_series_for_high_peaks: List[str] = None,
55
54
  time_series_for_low_peaks: List[str] = None,
56
55
  ):
57
56
  """
58
- Write a docstring please
59
-
60
- Parameters
61
- ----------
62
- timeseries: pd.DataFrame
63
- timeseries of the data with a datetime index
57
+ Args:
58
+ original_data: The original data to aggregate
59
+ hours_per_time_step: The duration of each timestep in hours.
60
+ hours_per_period: The duration of each period in hours.
61
+ nr_of_periods: The number of typical periods to use in the aggregation.
62
+ weights: The weights for aggregation. If None, all time series are equally weighted.
63
+ time_series_for_high_peaks: List of time series to use for explicitly selecting periods with high values.
64
+ time_series_for_low_peaks: List of time series to use for explicitly selecting periods with low values.
64
65
  """
65
66
  if not TSAM_AVAILABLE:
66
- raise ImportError("The 'tsam' package is required for clustering functionality. "
67
- "Install it with 'pip install tsam'.")
67
+ raise ImportError(
68
+ "The 'tsam' package is required for clustering functionality. Install it with 'pip install tsam'."
69
+ )
68
70
  self.original_data = copy.deepcopy(original_data)
69
71
  self.hours_per_time_step = hours_per_time_step
70
72
  self.hours_per_period = hours_per_period
@@ -93,7 +95,7 @@ class Aggregation:
93
95
  extremePeriodMethod='new_cluster_center'
94
96
  if self.use_extreme_periods
95
97
  else 'None', # Wenn Extremperioden eingebunden werden sollen, nutze die Methode 'new_cluster_center' aus tsam
96
- weightDict=self.weights,
98
+ weightDict={name: weight for name, weight in self.weights.items() if name in self.original_data.columns},
97
99
  addPeakMax=self.time_series_for_high_peaks,
98
100
  addPeakMin=self.time_series_for_low_peaks,
99
101
  )
@@ -139,7 +141,7 @@ class Aggregation:
139
141
  def use_extreme_periods(self):
140
142
  return self.time_series_for_high_peaks or self.time_series_for_low_peaks
141
143
 
142
- def plot(self, colormap: str = 'viridis', show: bool = True) -> 'go.Figure':
144
+ def plot(self, colormap: str = 'viridis', show: bool = True, save: Optional[pathlib.Path] = None) -> 'go.Figure':
143
145
  from . import plotting
144
146
 
145
147
  df_org = self.original_data.copy().rename(
@@ -151,11 +153,21 @@ class Aggregation:
151
153
  fig = plotting.with_plotly(df_org, 'line', colors=colormap)
152
154
  for trace in fig.data:
153
155
  trace.update(dict(line=dict(dash='dash')))
154
- fig = plotting.with_plotly(df_agg, 'line', colors=colormap, show=show, fig=fig)
156
+ fig = plotting.with_plotly(df_agg, 'line', colors=colormap, fig=fig)
155
157
 
156
158
  fig.update_layout(
157
159
  title='Original vs Aggregated Data (original = ---)', xaxis_title='Index', yaxis_title='Value'
158
160
  )
161
+
162
+ plotting.export_figure(
163
+ figure_like=fig,
164
+ default_path=pathlib.Path('aggregated data.html'),
165
+ default_filetype='.html',
166
+ user_path=None if isinstance(save, bool) else pathlib.Path(save),
167
+ show=show,
168
+ save=True if save else False,
169
+ )
170
+
159
171
  return fig
160
172
 
161
173
  def get_cluster_indices(self) -> Dict[str, List[np.ndarray]]:
@@ -217,60 +229,6 @@ class Aggregation:
217
229
  return np.array(idx_var1), np.array(idx_var2)
218
230
 
219
231
 
220
- class TimeSeriesCollection:
221
- def __init__(self, time_series_list: List[TimeSeries]):
222
- self.time_series_list = time_series_list
223
- self.group_weights: Dict[str, float] = {}
224
- self._unique_labels()
225
- self._calculate_aggregation_weigths()
226
- self.weights: Dict[str, float] = {
227
- time_series.label: time_series.aggregation_weight for time_series in self.time_series_list
228
- }
229
- self.data: Dict[str, np.ndarray] = {
230
- time_series.label: time_series.active_data for time_series in self.time_series_list
231
- }
232
-
233
- if np.all(np.isclose(list(self.weights.values()), 1, atol=1e-6)):
234
- logger.info('All Aggregation weights were set to 1')
235
-
236
- def _calculate_aggregation_weigths(self):
237
- """Calculates the aggergation weights of all TimeSeries. Necessary to use groups"""
238
- groups = [
239
- time_series.aggregation_group
240
- for time_series in self.time_series_list
241
- if time_series.aggregation_group is not None
242
- ]
243
- group_size = dict(Counter(groups))
244
- self.group_weights = {group: 1 / size for group, size in group_size.items()}
245
- for time_series in self.time_series_list:
246
- time_series.aggregation_weight = self.group_weights.get(
247
- time_series.aggregation_group, time_series.aggregation_weight or 1
248
- )
249
-
250
- def _unique_labels(self):
251
- """Makes sure every label of the TimeSeries in time_series_list is unique"""
252
- label_counts = Counter([time_series.label for time_series in self.time_series_list])
253
- duplicates = [label for label, count in label_counts.items() if count > 1]
254
- assert duplicates == [], 'Duplicate TimeSeries labels found: {}.'.format(', '.join(duplicates))
255
-
256
- def insert_data(self, data: Dict[str, np.ndarray]):
257
- for time_series in self.time_series_list:
258
- if time_series.label in data:
259
- time_series.aggregated_data = data[time_series.label]
260
- logger.debug(f'Inserted data for {time_series.label}')
261
-
262
- def description(self) -> str:
263
- # TODO:
264
- result = f'{len(self.time_series_list)} TimeSeries used for aggregation:\n'
265
- for time_series in self.time_series_list:
266
- result += f' -> {time_series.label} (weight: {time_series.aggregation_weight:.4f}; group: "{time_series.aggregation_group}")\n'
267
- if self.group_weights:
268
- result += f'Aggregation_Groups: {list(self.group_weights.keys())}\n'
269
- else:
270
- result += 'Warning!: no agg_types defined, i.e. all TS have weight 1 (or explicitly given weight)!\n'
271
- return result
272
-
273
-
274
232
  class AggregationParameters:
275
233
  def __init__(
276
234
  self,
@@ -286,29 +244,20 @@ class AggregationParameters:
286
244
  """
287
245
  Initializes aggregation parameters for time series data
288
246
 
289
- Parameters
290
- ----------
291
- hours_per_period : float
292
- Duration of each period in hours.
293
- nr_of_periods : int
294
- Number of typical periods to use in the aggregation.
295
- fix_storage_flows : bool
296
- Whether to aggregate storage flows (load/unload); if other flows
297
- are fixed, fixing storage flows is usually not required.
298
- aggregate_data_and_fix_non_binary_vars : bool
299
- Whether to aggregate all time series data, which allows to fix all time series variables (like flow_rate),
300
- or only fix binary variables. If False non time_series data is changed!! If True, the mathematical Problem
301
- is simplified even further.
302
- percentage_of_period_freedom : float, optional
303
- Specifies the maximum percentage (0–100) of binary values within each period
304
- that can deviate as "free variables", chosen by the solver (default is 0).
305
- This allows binary variables to be 'partly equated' between aggregated periods.
306
- penalty_of_period_freedom : float, optional
307
- The penalty associated with each "free variable"; defaults to 0. Added to Penalty
308
- time_series_for_high_peaks : list of TimeSeriesData
309
- List of time series to use for explicitly selecting periods with high values.
310
- time_series_for_low_peaks : list of TimeSeriesData
311
- List of time series to use for explicitly selecting periods with low values.
247
+ Args:
248
+ hours_per_period: Duration of each period in hours.
249
+ nr_of_periods: Number of typical periods to use in the aggregation.
250
+ fix_storage_flows: Whether to aggregate storage flows (load/unload); if other flows
251
+ are fixed, fixing storage flows is usually not required.
252
+ aggregate_data_and_fix_non_binary_vars: Whether to aggregate all time series data, which allows to fix all time series variables (like flow_rate),
253
+ or only fix binary variables. If False non time_series data is changed!! If True, the mathematical Problem
254
+ is simplified even further.
255
+ percentage_of_period_freedom: Specifies the maximum percentage (0–100) of binary values within each period
256
+ that can deviate as "free variables", chosen by the solver (default is 0).
257
+ This allows binary variables to be 'partly equated' between aggregated periods.
258
+ penalty_of_period_freedom: The penalty associated with each "free variable"; defaults to 0. Added to Penalty
259
+ time_series_for_high_peaks: List of TimeSeriesData to use for explicitly selecting periods with high values.
260
+ time_series_for_low_peaks: List of TimeSeriesData to use for explicitly selecting periods with low values.
312
261
  """
313
262
  self.hours_per_period = hours_per_period
314
263
  self.nr_of_periods = nr_of_periods
@@ -336,13 +285,14 @@ class AggregationParameters:
336
285
  return self.time_series_for_low_peaks is not None
337
286
 
338
287
 
339
- class AggregationModel(ElementModel):
288
+ class AggregationModel(Model):
340
289
  """The AggregationModel holds equations and variables related to the Aggregation of a FLowSystem.
341
290
  It creates Equations that equates indices of variables, and introduces penalties related to binary variables, that
342
291
  escape the equation to their related binaries in other periods"""
343
292
 
344
293
  def __init__(
345
294
  self,
295
+ model: SystemModel,
346
296
  aggregation_parameters: AggregationParameters,
347
297
  flow_system: FlowSystem,
348
298
  aggregation_data: Aggregation,
@@ -351,13 +301,13 @@ class AggregationModel(ElementModel):
351
301
  """
352
302
  Modeling-Element for "index-equating"-equations
353
303
  """
354
- super().__init__(Element('Aggregation'), 'Model')
304
+ super().__init__(model, label_of_element='Aggregation', label_full='Aggregation')
355
305
  self.flow_system = flow_system
356
306
  self.aggregation_parameters = aggregation_parameters
357
307
  self.aggregation_data = aggregation_data
358
308
  self.components_to_clusterize = components_to_clusterize
359
309
 
360
- def do_modeling(self, system_model: SystemModel):
310
+ def do_modeling(self):
361
311
  if not self.components_to_clusterize:
362
312
  components = self.flow_system.components.values()
363
313
  else:
@@ -365,66 +315,89 @@ class AggregationModel(ElementModel):
365
315
 
366
316
  indices = self.aggregation_data.get_equation_indices(skip_first_index_of_period=True)
367
317
 
318
+ time_variables: Set[str] = {k for k, v in self._model.variables.data.items() if 'time' in v.indexes}
319
+ binary_variables: Set[str] = {k for k, v in self._model.variables.data.items() if k in self._model.binaries}
320
+ binary_time_variables: Set[str] = time_variables & binary_variables
321
+
368
322
  for component in components:
369
323
  if isinstance(component, Storage) and not self.aggregation_parameters.fix_storage_flows:
370
324
  continue # Fix Nothing in The Storage
371
325
 
372
- all_variables_of_component = component.model.all_variables
326
+ all_variables_of_component = set(component.model.variables)
327
+
373
328
  if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
374
- all_relevant_variables = [v for v in all_variables_of_component.values() if isinstance(v, VariableTS)]
329
+ relevant_variables = component.model.variables[all_variables_of_component & time_variables]
375
330
  else:
376
- all_relevant_variables = [
377
- v for v in all_variables_of_component.values() if isinstance(v, VariableTS) and v.is_binary
378
- ]
379
- for variable in all_relevant_variables:
380
- self.equate_indices(variable, indices, system_model)
331
+ relevant_variables = component.model.variables[all_variables_of_component & binary_time_variables]
332
+ for variable in relevant_variables:
333
+ self._equate_indices(component.model.variables[variable], indices)
381
334
 
382
335
  penalty = self.aggregation_parameters.penalty_of_period_freedom
383
336
  if (self.aggregation_parameters.percentage_of_period_freedom > 0) and penalty != 0:
384
- for label, variable in self.variables.items():
385
- system_model.effect_collection_model.add_share_to_penalty(
386
- f'Aggregation_penalty__{label}', variable, penalty
387
- )
388
-
389
- def equate_indices(
390
- self, variable: Variable, indices: Tuple[np.ndarray, np.ndarray], system_model: SystemModel
391
- ) -> Equation:
392
- # Gleichung:
393
- # eq1: x(p1,t) - x(p3,t) = 0 # wobei p1 und p3 im gleichen Cluster sind und t = 0..N_p
394
- length = len(indices[0])
337
+ for variable in self.variables_direct.values():
338
+ self._model.effects.add_share_to_penalty('Aggregation', variable * penalty)
339
+
340
+ def _equate_indices(self, variable: linopy.Variable, indices: Tuple[np.ndarray, np.ndarray]) -> None:
395
341
  assert len(indices[0]) == len(indices[1]), 'The length of the indices must match!!'
342
+ length = len(indices[0])
396
343
 
397
- eq = create_equation(f'Equate_indices_of_{variable.label}', self)
398
- eq.add_summand(variable, 1, indices_of_variable=indices[0])
399
- eq.add_summand(variable, -1, indices_of_variable=indices[1])
344
+ # Gleichung:
345
+ # eq1: x(p1,t) - x(p3,t) = 0 # wobei p1 und p3 im gleichen Cluster sind und t = 0..N_p
346
+ con = self.add(
347
+ self._model.add_constraints(
348
+ variable.isel(time=indices[0]) - variable.isel(time=indices[1]) == 0,
349
+ name=f'{self.label_full}|equate_indices|{variable.name}',
350
+ ),
351
+ f'equate_indices|{variable.name}',
352
+ )
400
353
 
401
354
  # Korrektur: (bisher nur für Binärvariablen:)
402
- if variable.is_binary and self.aggregation_parameters.percentage_of_period_freedom > 0:
403
- # correction-vars (so viele wie Indexe in eq:)
404
- var_k1 = create_variable(f'Korr1_{variable.label}', self, length, is_binary=True)
405
- var_k0 = create_variable(f'Korr0_{variable.label}', self, length, is_binary=True)
355
+ if (
356
+ variable.name in self._model.variables.binaries
357
+ and self.aggregation_parameters.percentage_of_period_freedom > 0
358
+ ):
359
+ var_k1 = self.add(
360
+ self._model.add_variables(
361
+ binary=True,
362
+ coords={'time': variable.isel(time=indices[0]).indexes['time']},
363
+ name=f'{self.label_full}|correction1|{variable.name}',
364
+ ),
365
+ f'correction1|{variable.name}',
366
+ )
367
+
368
+ var_k0 = self.add(
369
+ self._model.add_variables(
370
+ binary=True,
371
+ coords={'time': variable.isel(time=indices[0]).indexes['time']},
372
+ name=f'{self.label_full}|correction0|{variable.name}',
373
+ ),
374
+ f'correction0|{variable.name}',
375
+ )
376
+
406
377
  # equation extends ...
407
378
  # --> On(p3) can be 0/1 independent of On(p1,t)!
408
379
  # eq1: On(p1,t) - On(p3,t) + K1(p3,t) - K0(p3,t) = 0
409
380
  # --> correction On(p3) can be:
410
381
  # On(p1,t) = 1 -> On(p3) can be 0 -> K0=1 (,K1=0)
411
382
  # On(p1,t) = 0 -> On(p3) can be 1 -> K1=1 (,K0=1)
412
- eq.add_summand(var_k1, +1)
413
- eq.add_summand(var_k0, -1)
383
+ con.lhs += 1 * var_k1 - 1 * var_k0
414
384
 
415
385
  # interlock var_k1 and var_K2:
416
386
  # eq: var_k0(t)+var_k1(t) <= 1.1
417
- eq_lock = create_equation(f'lock_K0andK1_{variable.label}', self, eq_type='ineq')
418
- eq_lock.add_summand(var_k0, 1)
419
- eq_lock.add_summand(var_k1, 1)
420
- eq_lock.add_constant(1.1)
387
+ self.add(
388
+ self._model.add_constraints(
389
+ var_k0 + var_k1 <= 1.1, name=f'{self.label_full}|lock_k0_and_k1|{variable.name}'
390
+ ),
391
+ f'lock_k0_and_k1|{variable.name}',
392
+ )
421
393
 
422
394
  # Begrenzung der Korrektur-Anzahl:
423
395
  # eq: sum(K) <= n_Corr_max
424
- eq_max = create_equation(f'Nr_of_Corrections_{variable.label}', self, eq_type='ineq')
425
- eq_max.add_summand(var_k1, 1, as_sum=True)
426
- eq_max.add_summand(var_k0, 1, as_sum=True)
427
- eq_max.add_constant(
428
- round(self.aggregation_parameters.percentage_of_period_freedom / 100 * var_k1.length)
429
- ) # Maximum
430
- return eq
396
+ self.add(
397
+ self._model.add_constraints(
398
+ sum(var_k0) + sum(var_k1)
399
+ <= round(self.aggregation_parameters.percentage_of_period_freedom / 100 * length),
400
+ name=f'{self.label_full}|limit_corrections|{variable.name}',
401
+ ),
402
+ f'limit_corrections|{variable.name}',
403
+ )