flixopt 1.0.12__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/effects.py ADDED
@@ -0,0 +1,410 @@
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
+ from typing import Dict, Literal, Optional, Union
10
+
11
+ import numpy as np
12
+
13
+ from .core import Numeric, Numeric_TS, Skalar, TimeSeries
14
+ from .features import ShareAllocationModel
15
+ from .math_modeling import Equation, Variable
16
+ from .structure import Element, ElementModel, SystemModel, _create_time_series
17
+
18
+ logger = logging.getLogger('flixOpt')
19
+
20
+
21
+ class Effect(Element):
22
+ """
23
+ Effect, i.g. costs, CO2 emissions, area, ...
24
+ Components, FLows, and so on can contribute to an Effect. One Effect is chosen as the Objective of the Optimization
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ label: str,
30
+ unit: str,
31
+ description: str,
32
+ meta_data: Optional[Dict] = None,
33
+ is_standard: bool = False,
34
+ is_objective: bool = False,
35
+ specific_share_to_other_effects_operation: 'EffectValues' = None,
36
+ specific_share_to_other_effects_invest: 'EffectValuesInvest' = None,
37
+ minimum_operation: Optional[Skalar] = None,
38
+ maximum_operation: Optional[Skalar] = None,
39
+ minimum_invest: Optional[Skalar] = None,
40
+ maximum_invest: Optional[Skalar] = None,
41
+ minimum_operation_per_hour: Optional[Numeric_TS] = None,
42
+ maximum_operation_per_hour: Optional[Numeric_TS] = None,
43
+ minimum_total: Optional[Skalar] = None,
44
+ maximum_total: Optional[Skalar] = None,
45
+ ):
46
+ """
47
+ Parameters
48
+ ----------
49
+ label : str
50
+ name
51
+ unit : str
52
+ unit of effect, i.g. €, kg_CO2, kWh_primaryEnergy
53
+ description : str
54
+ long name
55
+ meta_data : Optional[Dict]
56
+ used to store more information about the element. Is not used internally, but saved in the results
57
+ is_standard : boolean, optional
58
+ true, if Standard-Effect (for direct input of value without effect (alternatively to dict)) , else false
59
+ is_objective : boolean, optional
60
+ true, if optimization target
61
+ specific_share_to_other_effects_operation : {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
62
+ share to other effects (only operation)
63
+ specific_share_to_other_effects_invest : {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
64
+ share to other effects (only invest).
65
+ minimum_operation : scalar, optional
66
+ minimal sum (only operation) of the effect
67
+ maximum_operation : scalar, optional
68
+ maximal sum (nur operation) of the effect.
69
+ minimum_operation_per_hour : scalar or TS
70
+ maximum value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
71
+ maximum_operation_per_hour : scalar or TS
72
+ minimum value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
73
+ minimum_invest : scalar, optional
74
+ minimal sum (only invest) of the effect
75
+ maximum_invest : scalar, optional
76
+ maximal sum (only invest) of the effect
77
+ minimum_total : sclalar, optional
78
+ min sum of effect (invest+operation).
79
+ maximum_total : scalar, optional
80
+ max sum of effect (invest+operation).
81
+ **kwargs : TYPE
82
+ DESCRIPTION.
83
+
84
+ Returns
85
+ -------
86
+ None.
87
+
88
+ """
89
+ super().__init__(label, meta_data=meta_data)
90
+ self.label = label
91
+ self.unit = unit
92
+ self.description = description
93
+ self.is_standard = is_standard
94
+ self.is_objective = is_objective
95
+ self.specific_share_to_other_effects_operation: Union[EffectValues, EffectTimeSeries] = (
96
+ specific_share_to_other_effects_operation or {}
97
+ )
98
+ self.specific_share_to_other_effects_invest: Union[EffectValuesInvest, EffectDictInvest] = (
99
+ specific_share_to_other_effects_invest or {}
100
+ )
101
+ self.minimum_operation = minimum_operation
102
+ self.maximum_operation = maximum_operation
103
+ self.minimum_operation_per_hour: Numeric_TS = minimum_operation_per_hour
104
+ self.maximum_operation_per_hour: Numeric_TS = maximum_operation_per_hour
105
+ self.minimum_invest = minimum_invest
106
+ self.maximum_invest = maximum_invest
107
+ self.minimum_total = minimum_total
108
+ self.maximum_total = maximum_total
109
+
110
+ self._plausibility_checks()
111
+
112
+ def _plausibility_checks(self) -> None:
113
+ # Check circular loops in effects: (Effekte fügen sich gegenseitig Shares hinzu):
114
+ # TODO: Improve checks!! Only most basic case covered...
115
+
116
+ def error_str(effect_label: str, share_ffect_label: str):
117
+ return (
118
+ f' {effect_label} -> has share in: {share_ffect_label}\n'
119
+ f' {share_ffect_label} -> has share in: {effect_label}'
120
+ )
121
+
122
+ # Effekt darf nicht selber als Share in seinen ShareEffekten auftauchen:
123
+ # operation:
124
+ for target_effect in self.specific_share_to_other_effects_operation.keys():
125
+ assert self not in target_effect.specific_share_to_other_effects_operation.keys(), (
126
+ f'Error: circular operation-shares \n{error_str(target_effect.label, target_effect.label)}'
127
+ )
128
+ # invest:
129
+ for target_effect in self.specific_share_to_other_effects_invest.keys():
130
+ assert self not in target_effect.specific_share_to_other_effects_invest.keys(), (
131
+ f'Error: circular invest-shares \n{error_str(target_effect.label, target_effect.label)}'
132
+ )
133
+
134
+ def transform_data(self):
135
+ self.minimum_operation_per_hour = _create_time_series(
136
+ 'minimum_operation_per_hour', self.minimum_operation_per_hour, self
137
+ )
138
+ self.maximum_operation_per_hour = _create_time_series(
139
+ 'maximum_operation_per_hour', self.maximum_operation_per_hour, self
140
+ )
141
+
142
+ self.specific_share_to_other_effects_operation = effect_values_to_time_series(
143
+ 'specific_share_to_other_effects_operation', self.specific_share_to_other_effects_operation, self
144
+ )
145
+
146
+ def create_model(self) -> 'EffectModel':
147
+ self.model = EffectModel(self)
148
+ return self.model
149
+
150
+
151
+ class EffectModel(ElementModel):
152
+ def __init__(self, element: Effect):
153
+ super().__init__(element)
154
+ self.element: Effect
155
+ self.invest = ShareAllocationModel(
156
+ self.element, 'invest', False, total_max=self.element.maximum_invest, total_min=self.element.minimum_invest
157
+ )
158
+ self.operation = ShareAllocationModel(
159
+ self.element,
160
+ 'operation',
161
+ True,
162
+ total_max=self.element.maximum_operation,
163
+ total_min=self.element.minimum_operation,
164
+ min_per_hour=self.element.minimum_operation_per_hour.active_data
165
+ if self.element.minimum_operation_per_hour is not None
166
+ else None,
167
+ max_per_hour=self.element.maximum_operation_per_hour.active_data
168
+ if self.element.maximum_operation_per_hour is not None
169
+ else None,
170
+ )
171
+ self.all = ShareAllocationModel(
172
+ self.element, 'all', False, total_max=self.element.maximum_total, total_min=self.element.minimum_total
173
+ )
174
+ self.sub_models.extend([self.invest, self.operation, self.all])
175
+
176
+ def do_modeling(self, system_model: SystemModel):
177
+ for model in self.sub_models:
178
+ model.do_modeling(system_model)
179
+
180
+ self.all.add_share(system_model, 'operation', self.operation.sum, 1)
181
+ self.all.add_share(system_model, 'invest', self.invest.sum, 1)
182
+
183
+
184
+ EffectDict = Dict[Optional['Effect'], Numeric]
185
+ EffectDictInvest = Dict[Optional['Effect'], Skalar]
186
+
187
+ EffectValues = Optional[Union[Numeric_TS, EffectDict]] # Datatype for User Input
188
+ EffectValuesInvest = Optional[Union[Skalar, EffectDictInvest]] # Datatype for User Input
189
+
190
+ EffectTimeSeries = Dict[Optional['Effect'], TimeSeries] # Final Internal Data Structure
191
+ ElementTimeSeries = Dict[Optional[Element], TimeSeries] # Final Internal Data Structure
192
+
193
+
194
+ def nested_values_to_time_series(
195
+ nested_values: Dict[Element, Numeric_TS], label_suffix: str, parent_element: Element
196
+ ) -> ElementTimeSeries:
197
+ """
198
+ Creates TimeSeries from nested values, which are a Dict of Elements to values.
199
+ The resulting label of the TimeSeries is the label of the parent_element, followed by the label of the element in
200
+ the nested_values and the label_suffix.
201
+ """
202
+ return {
203
+ element: _create_time_series(f'{element.label}_{label_suffix}', value, parent_element)
204
+ for element, value in nested_values.items()
205
+ if element is not None
206
+ }
207
+
208
+
209
+ def effect_values_to_time_series(
210
+ label_suffix: str, nested_values: EffectValues, parent_element: Element
211
+ ) -> Optional[EffectTimeSeries]:
212
+ """
213
+ Creates TimeSeries from EffectValues. The resulting label of the TimeSeries is the label of the parent_element,
214
+ followed by the label of the Effect in the nested_values and the label_suffix.
215
+ If the key in the EffectValues is None, the alias 'Standart_Effect' is used
216
+ """
217
+ nested_values = as_effect_dict(nested_values)
218
+ if nested_values is None:
219
+ return None
220
+ else:
221
+ standard_value = nested_values.pop(None, None)
222
+ transformed_values = nested_values_to_time_series(nested_values, label_suffix, parent_element)
223
+ if standard_value is not None:
224
+ transformed_values[None] = _create_time_series(
225
+ f'Standard_Effect_{label_suffix}', standard_value, parent_element
226
+ )
227
+ return transformed_values
228
+
229
+
230
+ def as_effect_dict(effect_values: EffectValues) -> Optional[EffectDict]:
231
+ """
232
+ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.
233
+
234
+ Examples
235
+ --------
236
+ costs = 20 -> {None: 20}
237
+ costs = None -> None
238
+ costs = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3}
239
+
240
+ Parameters
241
+ ----------
242
+ effect_values : None, int, float, TimeSeries, or dict
243
+ The effect values to convert, either a scalar, TimeSeries, or a dictionary.
244
+
245
+ Returns
246
+ -------
247
+ dict or None
248
+ A dictionary with None or Effect as the key, or None if input is None.
249
+ """
250
+ return (
251
+ effect_values
252
+ if isinstance(effect_values, dict)
253
+ else {None: effect_values}
254
+ if effect_values is not None
255
+ else None
256
+ )
257
+
258
+
259
+ def effect_values_from_effect_time_series(effect_time_series: EffectTimeSeries) -> Dict[Optional[Effect], Numeric]:
260
+ return {effect: time_series.active_data for effect, time_series in effect_time_series.items()}
261
+
262
+
263
+ class EffectCollection:
264
+ """
265
+ Handling all Effects
266
+ """
267
+
268
+ def __init__(self, label: str):
269
+ self.label = label
270
+ self.model: Optional[EffectCollectionModel] = None
271
+ self.effects: Dict[str, Effect] = {}
272
+
273
+ def create_model(self, system_model: SystemModel) -> 'EffectCollectionModel':
274
+ self.model = EffectCollectionModel(self, system_model)
275
+ return self.model
276
+
277
+ def add_effect(self, effect: 'Effect') -> None:
278
+ if effect.is_standard and self.standard_effect is not None:
279
+ raise Exception(f'A standard-effect already exists! ({self.standard_effect.label=})')
280
+ if effect.is_objective and self.objective_effect is not None:
281
+ raise Exception(f'A objective-effect already exists! ({self.objective_effect.label=})')
282
+ if effect in self.effects.values():
283
+ raise Exception(f'Effect already added! ({effect.label=})')
284
+ if effect.label in self.effects:
285
+ raise Exception(f'Effect with label "{effect.label=}" already added!')
286
+ self.effects[effect.label] = effect
287
+
288
+ @property
289
+ def standard_effect(self) -> Optional[Effect]:
290
+ for effect in self.effects.values():
291
+ if effect.is_standard:
292
+ return effect
293
+
294
+ @property
295
+ def objective_effect(self) -> Optional[Effect]:
296
+ for effect in self.effects.values():
297
+ if effect.is_objective:
298
+ return effect
299
+
300
+ @property
301
+ def label_full(self):
302
+ return self.label
303
+
304
+
305
+ class EffectCollectionModel(ElementModel):
306
+ # TODO: Maybe all EffectModels should be sub_models of this Model? Including Objective and Penalty?
307
+ def __init__(self, element: EffectCollection, system_model: SystemModel):
308
+ super().__init__(element)
309
+ self.element = element
310
+ self._system_model = system_model
311
+ self._effect_models: Dict[Effect, EffectModel] = {}
312
+ self.penalty: Optional[ShareAllocationModel] = None
313
+ self.objective: Optional[Equation] = None
314
+
315
+ def do_modeling(self, system_model: SystemModel):
316
+ self._effect_models = {effect: effect.create_model() for effect in self.element.effects.values()}
317
+ self.penalty = ShareAllocationModel(self.element, 'penalty', False)
318
+ self.sub_models.extend(list(self._effect_models.values()) + [self.penalty])
319
+ for model in self.sub_models:
320
+ model.do_modeling(system_model)
321
+
322
+ self.add_share_between_effects()
323
+
324
+ self.objective = Equation('OBJECTIVE', 'OBJECTIVE', is_objective=True)
325
+ self.objective.add_summand(self._objective_effect_model.operation.sum, 1)
326
+ self.objective.add_summand(self._objective_effect_model.invest.sum, 1)
327
+ self.objective.add_summand(self.penalty.sum, 1)
328
+
329
+ @property
330
+ def _objective_effect_model(self) -> EffectModel:
331
+ return self._effect_models[self.element.objective_effect]
332
+
333
+ def _add_share_to_effects(
334
+ self,
335
+ name: str,
336
+ element: Element,
337
+ target: Literal['operation', 'invest'],
338
+ effect_values: EffectDict,
339
+ factor: Numeric,
340
+ variable: Optional[Variable] = None,
341
+ ) -> None:
342
+ # an alle Effects, die einen Wert haben, anhängen:
343
+ for effect, value in effect_values.items():
344
+ if effect is None: # Falls None, dann Standard-effekt nutzen:
345
+ effect = self.element.standard_effect
346
+ assert effect in self.element.effects.values(), f'Effect {effect.label} was used but not added to model!'
347
+
348
+ if target == 'operation':
349
+ model = self._effect_models[effect].operation
350
+ elif target == 'invest':
351
+ model = self._effect_models[effect].invest
352
+ else:
353
+ raise ValueError(f'Target {target} not supported!')
354
+
355
+ name_of_share = f'{element.label_full}__{name}'
356
+ total_factor = np.multiply(value, factor)
357
+ model.add_share(self._system_model, name_of_share, variable, total_factor)
358
+
359
+ def add_share_to_invest(
360
+ self,
361
+ name: str,
362
+ element: Element,
363
+ effect_values: EffectDictInvest,
364
+ factor: Numeric,
365
+ variable: Optional[Variable] = None,
366
+ ) -> None:
367
+ # TODO: Add checks
368
+ self._add_share_to_effects(name, element, 'invest', effect_values, factor, variable)
369
+
370
+ def add_share_to_operation(
371
+ self,
372
+ name: str,
373
+ element: Element,
374
+ effect_values: EffectTimeSeries,
375
+ factor: Numeric,
376
+ variable: Optional[Variable] = None,
377
+ ) -> None:
378
+ # TODO: Add checks
379
+ self._add_share_to_effects(
380
+ name, element, 'operation', effect_values_from_effect_time_series(effect_values), factor, variable
381
+ )
382
+
383
+ def add_share_to_penalty(
384
+ self,
385
+ name: Optional[str],
386
+ variable: Variable,
387
+ factor: Numeric,
388
+ ) -> None:
389
+ assert variable is not None, 'A Variable must be passed to add a share to penalty! Else its a constant Penalty!'
390
+ self.penalty.add_share(self._system_model, name, variable, factor, True)
391
+
392
+ def add_share_between_effects(self):
393
+ for origin_effect in self.element.effects.values():
394
+ # 1. operation: -> hier sind es Zeitreihen (share_TS)
395
+ for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items():
396
+ target_model = self._effect_models[target_effect].operation
397
+ origin_model = self._effect_models[origin_effect].operation
398
+ target_model.add_share(
399
+ self._system_model,
400
+ f'{origin_effect.label_full}_operation',
401
+ origin_model.sum_TS,
402
+ time_series.active_data,
403
+ )
404
+ # 2. invest: -> hier ist es Skalar (share)
405
+ for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items():
406
+ target_model = self._effect_models[target_effect].invest
407
+ origin_model = self._effect_models[origin_effect].invest
408
+ target_model.add_share(
409
+ self._system_model, f'{origin_effect.label_full}_invest', origin_model.sum, factor
410
+ )