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

Files changed (73) 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/release-notes/v2.0.1.md +12 -0
  16. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  17. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  18. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  19. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  20. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  21. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  22. docs/user-guide/Mathematical Notation/index.md +22 -0
  23. docs/user-guide/Mathematical Notation/others.md +3 -0
  24. docs/user-guide/index.md +124 -0
  25. {flixOpt → flixopt}/__init__.py +5 -2
  26. {flixOpt → flixopt}/aggregation.py +113 -140
  27. flixopt/calculation.py +455 -0
  28. {flixOpt → flixopt}/commons.py +7 -4
  29. flixopt/components.py +630 -0
  30. {flixOpt → flixopt}/config.py +9 -8
  31. {flixOpt → flixopt}/config.yaml +3 -3
  32. flixopt/core.py +970 -0
  33. flixopt/effects.py +386 -0
  34. flixopt/elements.py +534 -0
  35. flixopt/features.py +1042 -0
  36. flixopt/flow_system.py +409 -0
  37. flixopt/interface.py +265 -0
  38. flixopt/io.py +308 -0
  39. flixopt/linear_converters.py +331 -0
  40. flixopt/plotting.py +1340 -0
  41. flixopt/results.py +898 -0
  42. flixopt/solvers.py +77 -0
  43. flixopt/structure.py +630 -0
  44. flixopt/utils.py +62 -0
  45. flixopt-2.0.1.dist-info/METADATA +145 -0
  46. flixopt-2.0.1.dist-info/RECORD +57 -0
  47. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
  48. flixopt-2.0.1.dist-info/top_level.txt +6 -0
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixopt-icon.svg +1 -0
  52. pics/pics.pptx +0 -0
  53. scripts/gen_ref_pages.py +54 -0
  54. site/release-notes/_template.txt +32 -0
  55. flixOpt/calculation.py +0 -629
  56. flixOpt/components.py +0 -614
  57. flixOpt/core.py +0 -182
  58. flixOpt/effects.py +0 -410
  59. flixOpt/elements.py +0 -489
  60. flixOpt/features.py +0 -942
  61. flixOpt/flow_system.py +0 -351
  62. flixOpt/interface.py +0 -203
  63. flixOpt/linear_converters.py +0 -325
  64. flixOpt/math_modeling.py +0 -1145
  65. flixOpt/plotting.py +0 -712
  66. flixOpt/results.py +0 -563
  67. flixOpt/solvers.py +0 -21
  68. flixOpt/structure.py +0 -733
  69. flixOpt/utils.py +0 -134
  70. flixopt-1.0.12.dist-info/METADATA +0 -174
  71. flixopt-1.0.12.dist-info/RECORD +0 -29
  72. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  73. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info/licenses}/LICENSE +0 -0
flixopt/effects.py ADDED
@@ -0,0 +1,386 @@
1
+ """
2
+ This module contains the effects of the flixopt framework.
3
+ Furthermore, it contains the EffectCollection, which is used to collect all effects of a system.
4
+ Different Datatypes are used to represent the effects with assigned values by the user,
5
+ which are then transformed into the internal data structure.
6
+ """
7
+
8
+ import logging
9
+ import warnings
10
+ from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union
11
+
12
+ import linopy
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ from .core import NumericData, NumericDataTS, Scalar, TimeSeries, TimeSeriesCollection
17
+ from .features import ShareAllocationModel
18
+ from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io
19
+
20
+ if TYPE_CHECKING:
21
+ from .flow_system import FlowSystem
22
+
23
+ logger = logging.getLogger('flixopt')
24
+
25
+
26
+ @register_class_for_io
27
+ class Effect(Element):
28
+ """
29
+ Effect, i.g. costs, CO2 emissions, area, ...
30
+ Components, FLows, and so on can contribute to an Effect. One Effect is chosen as the Objective of the Optimization
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ label: str,
36
+ unit: str,
37
+ description: str,
38
+ meta_data: Optional[Dict] = None,
39
+ is_standard: bool = False,
40
+ is_objective: bool = False,
41
+ specific_share_to_other_effects_operation: Optional['EffectValuesUser'] = None,
42
+ specific_share_to_other_effects_invest: Optional['EffectValuesUser'] = None,
43
+ minimum_operation: Optional[Scalar] = None,
44
+ maximum_operation: Optional[Scalar] = None,
45
+ minimum_invest: Optional[Scalar] = None,
46
+ maximum_invest: Optional[Scalar] = None,
47
+ minimum_operation_per_hour: Optional[NumericDataTS] = None,
48
+ maximum_operation_per_hour: Optional[NumericDataTS] = None,
49
+ minimum_total: Optional[Scalar] = None,
50
+ maximum_total: Optional[Scalar] = None,
51
+ ):
52
+ """
53
+ Args:
54
+ label: The label of the Element. Used to identify it in the FlowSystem
55
+ unit: The unit of effect, i.g. €, kg_CO2, kWh_primaryEnergy
56
+ description: The long name
57
+ is_standard: true, if Standard-Effect (for direct input of value without effect (alternatively to dict)) , else false
58
+ is_objective: true, if optimization target
59
+ specific_share_to_other_effects_operation: {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
60
+ share to other effects (only operation)
61
+ specific_share_to_other_effects_invest: {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
62
+ share to other effects (only invest).
63
+ minimum_operation: minimal sum (only operation) of the effect.
64
+ maximum_operation: maximal sum (nur operation) of the effect.
65
+ minimum_operation_per_hour: max. value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
66
+ maximum_operation_per_hour: min. value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
67
+ minimum_invest: minimal sum (only invest) of the effect
68
+ maximum_invest: maximal sum (only invest) of the effect
69
+ minimum_total: min sum of effect (invest+operation).
70
+ maximum_total: max sum of effect (invest+operation).
71
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
72
+ """
73
+ super().__init__(label, meta_data=meta_data)
74
+ self.label = label
75
+ self.unit = unit
76
+ self.description = description
77
+ self.is_standard = is_standard
78
+ self.is_objective = is_objective
79
+ self.specific_share_to_other_effects_operation: EffectValuesUser = (
80
+ specific_share_to_other_effects_operation or {}
81
+ )
82
+ self.specific_share_to_other_effects_invest: EffectValuesUser = specific_share_to_other_effects_invest or {}
83
+ self.minimum_operation = minimum_operation
84
+ self.maximum_operation = maximum_operation
85
+ self.minimum_operation_per_hour: NumericDataTS = minimum_operation_per_hour
86
+ self.maximum_operation_per_hour: NumericDataTS = maximum_operation_per_hour
87
+ self.minimum_invest = minimum_invest
88
+ self.maximum_invest = maximum_invest
89
+ self.minimum_total = minimum_total
90
+ self.maximum_total = maximum_total
91
+
92
+ def transform_data(self, flow_system: 'FlowSystem'):
93
+ self.minimum_operation_per_hour = flow_system.create_time_series(
94
+ f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour
95
+ )
96
+ self.maximum_operation_per_hour = flow_system.create_time_series(
97
+ f'{self.label_full}|maximum_operation_per_hour', self.maximum_operation_per_hour, flow_system
98
+ )
99
+
100
+ self.specific_share_to_other_effects_operation = flow_system.create_effect_time_series(
101
+ f'{self.label_full}|operation->', self.specific_share_to_other_effects_operation, 'operation'
102
+ )
103
+
104
+ def create_model(self, model: SystemModel) -> 'EffectModel':
105
+ self._plausibility_checks()
106
+ self.model = EffectModel(model, self)
107
+ return self.model
108
+
109
+ def _plausibility_checks(self) -> None:
110
+ # TODO: Check for plausibility
111
+ pass
112
+
113
+
114
+ class EffectModel(ElementModel):
115
+ def __init__(self, model: SystemModel, element: Effect):
116
+ super().__init__(model, element)
117
+ self.element: Effect = element
118
+ self.total: Optional[linopy.Variable] = None
119
+ self.invest: ShareAllocationModel = self.add(
120
+ ShareAllocationModel(
121
+ self._model,
122
+ False,
123
+ self.label_of_element,
124
+ 'invest',
125
+ label_full=f'{self.label_full}(invest)',
126
+ total_max=self.element.maximum_invest,
127
+ total_min=self.element.minimum_invest,
128
+ )
129
+ )
130
+
131
+ self.operation: ShareAllocationModel = self.add(
132
+ ShareAllocationModel(
133
+ self._model,
134
+ True,
135
+ self.label_of_element,
136
+ 'operation',
137
+ label_full=f'{self.label_full}(operation)',
138
+ total_max=self.element.maximum_operation,
139
+ total_min=self.element.minimum_operation,
140
+ min_per_hour=self.element.minimum_operation_per_hour.active_data
141
+ if self.element.minimum_operation_per_hour is not None
142
+ else None,
143
+ max_per_hour=self.element.maximum_operation_per_hour.active_data
144
+ if self.element.maximum_operation_per_hour is not None
145
+ else None,
146
+ )
147
+ )
148
+
149
+ def do_modeling(self):
150
+ for model in self.sub_models:
151
+ model.do_modeling()
152
+
153
+ self.total = self.add(
154
+ self._model.add_variables(
155
+ lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf,
156
+ upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf,
157
+ coords=None,
158
+ name=f'{self.label_full}|total',
159
+ ),
160
+ 'total',
161
+ )
162
+
163
+ self.add(
164
+ self._model.add_constraints(
165
+ self.total == self.operation.total.sum() + self.invest.total.sum(), name=f'{self.label_full}|total'
166
+ ),
167
+ 'total',
168
+ )
169
+
170
+
171
+ EffectValuesExpr = Dict[str, linopy.LinearExpression] # Used to create Shares
172
+ EffectTimeSeries = Dict[str, TimeSeries] # Used internally to index values
173
+ EffectValuesDict = Dict[str, NumericDataTS] # How effect values are stored
174
+ EffectValuesUser = Union[NumericDataTS, Dict[str, NumericDataTS]] # User-specified Shares to Effects
175
+ """ This datatype is used to define the share to an effect by a certain attribute. """
176
+
177
+ EffectValuesUserScalar = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects
178
+ """ This datatype is used to define the share to an effect by a certain attribute. Only scalars are allowed. """
179
+
180
+
181
+ class EffectCollection:
182
+ """
183
+ Handling all Effects
184
+ """
185
+
186
+ def __init__(self, *effects: List[Effect]):
187
+ self._effects = {}
188
+ self._standard_effect: Optional[Effect] = None
189
+ self._objective_effect: Optional[Effect] = None
190
+
191
+ self.model: Optional[EffectCollectionModel] = None
192
+ self.add_effects(*effects)
193
+
194
+ def create_model(self, model: SystemModel) -> 'EffectCollectionModel':
195
+ self._plausibility_checks()
196
+ self.model = EffectCollectionModel(model, self)
197
+ return self.model
198
+
199
+ def add_effects(self, *effects: Effect) -> None:
200
+ for effect in list(effects):
201
+ if effect in self:
202
+ raise ValueError(f'Effect with label "{effect.label=}" already added!')
203
+ if effect.is_standard:
204
+ self.standard_effect = effect
205
+ if effect.is_objective:
206
+ self.objective_effect = effect
207
+ self._effects[effect.label] = effect
208
+ logger.info(f'Registered new Effect: {effect.label}')
209
+
210
+ def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> Optional[EffectValuesDict]:
211
+ """
212
+ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.
213
+
214
+ Examples
215
+ --------
216
+ effect_values_user = 20 -> {None: 20}
217
+ effect_values_user = None -> None
218
+ effect_values_user = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3}
219
+
220
+ Returns
221
+ -------
222
+ dict or None
223
+ A dictionary with None or Effect as the key, or None if input is None.
224
+ """
225
+
226
+ def get_effect_label(eff: Union[Effect, str]) -> str:
227
+ """Temporary function to get the label of an effect and warn for deprecation"""
228
+ if isinstance(eff, Effect):
229
+ warnings.warn(
230
+ f'The use of effect objects when specifying EffectValues is deprecated. '
231
+ f'Use the label of the effect instead. Used effect: {eff.label_full}',
232
+ UserWarning,
233
+ stacklevel=2,
234
+ )
235
+ return eff.label_full
236
+ else:
237
+ return eff
238
+
239
+ if effect_values_user is None:
240
+ return None
241
+ if isinstance(effect_values_user, dict):
242
+ return {get_effect_label(effect): value for effect, value in effect_values_user.items()}
243
+ return {self.standard_effect.label_full: effect_values_user}
244
+
245
+ def _plausibility_checks(self) -> None:
246
+ # Check circular loops in effects:
247
+ # TODO: Improve checks!! Only most basic case covered...
248
+
249
+ def error_str(effect_label: str, share_ffect_label: str):
250
+ return (
251
+ f' {effect_label} -> has share in: {share_ffect_label}\n'
252
+ f' {share_ffect_label} -> has share in: {effect_label}'
253
+ )
254
+
255
+ for effect in self.effects.values():
256
+ # Effekt darf nicht selber als Share in seinen ShareEffekten auftauchen:
257
+ # operation:
258
+ for target_effect in effect.specific_share_to_other_effects_operation.keys():
259
+ assert effect not in self[target_effect].specific_share_to_other_effects_operation.keys(), (
260
+ f'Error: circular operation-shares \n{error_str(target_effect.label, target_effect.label)}'
261
+ )
262
+ # invest:
263
+ for target_effect in effect.specific_share_to_other_effects_invest.keys():
264
+ assert effect not in self[target_effect].specific_share_to_other_effects_invest.keys(), (
265
+ f'Error: circular invest-shares \n{error_str(target_effect.label, target_effect.label)}'
266
+ )
267
+
268
+ def __getitem__(self, effect: Union[str, Effect]) -> 'Effect':
269
+ """
270
+ Get an effect by label, or return the standard effect if None is passed
271
+
272
+ Raises:
273
+ KeyError: If no effect with the given label is found.
274
+ KeyError: If no standard effect is specified.
275
+ """
276
+ if effect is None:
277
+ return self.standard_effect
278
+ if isinstance(effect, Effect):
279
+ if effect in self:
280
+ return effect
281
+ else:
282
+ raise KeyError(f'Effect {effect} not found!')
283
+ try:
284
+ return self.effects[effect]
285
+ except KeyError as e:
286
+ raise KeyError(f'Effect "{effect}" not found! Add it to the FlowSystem first!') from e
287
+
288
+ def __iter__(self) -> Iterator[Effect]:
289
+ return iter(self._effects.values())
290
+
291
+ def __len__(self) -> int:
292
+ return len(self._effects)
293
+
294
+ def __contains__(self, item: Union[str, 'Effect']) -> bool:
295
+ """Check if the effect exists. Checks for label or object"""
296
+ if isinstance(item, str):
297
+ return item in self.effects # Check if the label exists
298
+ elif isinstance(item, Effect):
299
+ return item in self.effects.values() # Check if the object exists
300
+ return False
301
+
302
+ @property
303
+ def effects(self) -> Dict[str, Effect]:
304
+ return self._effects
305
+
306
+ @property
307
+ def standard_effect(self) -> Effect:
308
+ if self._standard_effect is None:
309
+ raise KeyError('No standard-effect specified!')
310
+ return self._standard_effect
311
+
312
+ @standard_effect.setter
313
+ def standard_effect(self, value: Effect) -> None:
314
+ if self._standard_effect is not None:
315
+ raise ValueError(f'A standard-effect already exists! ({self._standard_effect.label=})')
316
+ self._standard_effect = value
317
+
318
+ @property
319
+ def objective_effect(self) -> Effect:
320
+ if self._objective_effect is None:
321
+ raise KeyError('No objective-effect specified!')
322
+ return self._objective_effect
323
+
324
+ @objective_effect.setter
325
+ def objective_effect(self, value: Effect) -> None:
326
+ if self._objective_effect is not None:
327
+ raise ValueError(f'An objective-effect already exists! ({self._objective_effect.label=})')
328
+ self._objective_effect = value
329
+
330
+
331
+ class EffectCollectionModel(Model):
332
+ """
333
+ Handling all Effects
334
+ """
335
+
336
+ def __init__(self, model: SystemModel, effects: EffectCollection):
337
+ super().__init__(model, label_of_element='Effects')
338
+ self.effects = effects
339
+ self.penalty: Optional[ShareAllocationModel] = None
340
+
341
+ def add_share_to_effects(
342
+ self,
343
+ name: str,
344
+ expressions: EffectValuesExpr,
345
+ target: Literal['operation', 'invest'],
346
+ ) -> None:
347
+ for effect, expression in expressions.items():
348
+ if target == 'operation':
349
+ self.effects[effect].model.operation.add_share(name, expression)
350
+ elif target == 'invest':
351
+ self.effects[effect].model.invest.add_share(name, expression)
352
+ else:
353
+ raise ValueError(f'Target {target} not supported!')
354
+
355
+ def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) -> None:
356
+ if expression.ndim != 0:
357
+ raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})')
358
+ self.penalty.add_share(name, expression)
359
+
360
+ def do_modeling(self):
361
+ for effect in self.effects:
362
+ effect.create_model(self._model)
363
+ self.penalty = self.add(
364
+ ShareAllocationModel(self._model, shares_are_time_series=False, label_of_element='Penalty')
365
+ )
366
+ for model in [effect.model for effect in self.effects] + [self.penalty]:
367
+ model.do_modeling()
368
+
369
+ self._add_share_between_effects()
370
+
371
+ self._model.add_objective(self.effects.objective_effect.model.total + self.penalty.total)
372
+
373
+ def _add_share_between_effects(self):
374
+ for origin_effect in self.effects:
375
+ # 1. operation: -> hier sind es Zeitreihen (share_TS)
376
+ for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items():
377
+ self.effects[target_effect].model.operation.add_share(
378
+ origin_effect.model.operation.label_full,
379
+ origin_effect.model.operation.total_per_timestep * time_series.active_data,
380
+ )
381
+ # 2. invest: -> hier ist es Scalar (share)
382
+ for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items():
383
+ self.effects[target_effect].model.invest.add_share(
384
+ origin_effect.model.invest.label_full,
385
+ origin_effect.model.invest.total * factor,
386
+ )