flixopt 2.1.0__py3-none-any.whl → 2.2.0b0__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/components.py CHANGED
@@ -9,7 +9,7 @@ import linopy
9
9
  import numpy as np
10
10
 
11
11
  from . import utils
12
- from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeries
12
+ from .core import PlausibilityError, Scalar, ScenarioData, TimeSeries, TimestepData
13
13
  from .elements import Component, ComponentModel, Flow
14
14
  from .features import InvestmentModel, OnOffModel, PiecewiseModel
15
15
  from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
@@ -34,7 +34,7 @@ class LinearConverter(Component):
34
34
  inputs: List[Flow],
35
35
  outputs: List[Flow],
36
36
  on_off_parameters: OnOffParameters = None,
37
- conversion_factors: List[Dict[str, NumericDataTS]] = None,
37
+ conversion_factors: List[Dict[str, TimestepData]] = None,
38
38
  piecewise_conversion: Optional[PiecewiseConversion] = None,
39
39
  meta_data: Optional[Dict] = None,
40
40
  ):
@@ -43,7 +43,10 @@ class LinearConverter(Component):
43
43
  label: The label of the Element. Used to identify it in the FlowSystem
44
44
  inputs: The input Flows
45
45
  outputs: The output Flows
46
- on_off_parameters: Information about on and off states. See class OnOffParameters.
46
+ on_off_parameters: Information about on and off state of LinearConverter.
47
+ Component is On/Off, if all connected Flows are On/Off. This induces an On-Variable (binary) in all Flows!
48
+ If possible, use OnOffParameters in a single Flow instead to keep the number of binary variables low.
49
+ See class OnOffParameters.
47
50
  conversion_factors: linear relation between flows.
48
51
  Either 'conversion_factors' or 'piecewise_conversion' can be used!
49
52
  piecewise_conversion: Define a piecewise linear relation between flow rates of different flows.
@@ -83,9 +86,9 @@ class LinearConverter(Component):
83
86
  if self.piecewise_conversion:
84
87
  for flow in self.flows.values():
85
88
  if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
86
- raise PlausibilityError(
87
- f'piecewise_conversion (in {self.label_full}) and variable size '
88
- f'(in flow {flow.label_full}) do not make sense together!'
89
+ logger.warning(
90
+ f'Using a FLow with a fixed size ({flow.label_full}) AND a piecewise_conversion '
91
+ f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!'
89
92
  )
90
93
 
91
94
  def transform_data(self, flow_system: 'FlowSystem'):
@@ -93,6 +96,7 @@ class LinearConverter(Component):
93
96
  if self.conversion_factors:
94
97
  self.conversion_factors = self._transform_conversion_factors(flow_system)
95
98
  if self.piecewise_conversion:
99
+ self.piecewise_conversion.has_time_dim = True
96
100
  self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion')
97
101
 
98
102
  def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]:
@@ -124,16 +128,17 @@ class Storage(Component):
124
128
  label: str,
125
129
  charging: Flow,
126
130
  discharging: Flow,
127
- capacity_in_flow_hours: Union[Scalar, InvestParameters],
128
- relative_minimum_charge_state: NumericData = 0,
129
- relative_maximum_charge_state: NumericData = 1,
130
- initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0,
131
- minimal_final_charge_state: Optional[Scalar] = None,
132
- maximal_final_charge_state: Optional[Scalar] = None,
133
- eta_charge: NumericData = 1,
134
- eta_discharge: NumericData = 1,
135
- relative_loss_per_hour: NumericData = 0,
131
+ capacity_in_flow_hours: Union[ScenarioData, InvestParameters],
132
+ relative_minimum_charge_state: TimestepData = 0,
133
+ relative_maximum_charge_state: TimestepData = 1,
134
+ initial_charge_state: Union[ScenarioData, Literal['lastValueOfSim']] = 0,
135
+ minimal_final_charge_state: Optional[ScenarioData] = None,
136
+ maximal_final_charge_state: Optional[ScenarioData] = None,
137
+ eta_charge: TimestepData = 1,
138
+ eta_discharge: TimestepData = 1,
139
+ relative_loss_per_hour: TimestepData = 0,
136
140
  prevent_simultaneous_charge_and_discharge: bool = True,
141
+ balanced: bool = False,
137
142
  meta_data: Optional[Dict] = None,
138
143
  ):
139
144
  """
@@ -159,6 +164,7 @@ class Storage(Component):
159
164
  relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
160
165
  prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
161
166
  Increases the number of binary variables, but is recommended for easier evaluation. The default is True.
167
+ balanced: Wether to equate the size of the charging and discharging flow. Only if not fixed.
162
168
  meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
163
169
  """
164
170
  # TODO: fixed_relative_chargeState implementieren
@@ -173,17 +179,18 @@ class Storage(Component):
173
179
  self.charging = charging
174
180
  self.discharging = discharging
175
181
  self.capacity_in_flow_hours = capacity_in_flow_hours
176
- self.relative_minimum_charge_state: NumericDataTS = relative_minimum_charge_state
177
- self.relative_maximum_charge_state: NumericDataTS = relative_maximum_charge_state
182
+ self.relative_minimum_charge_state: TimestepData = relative_minimum_charge_state
183
+ self.relative_maximum_charge_state: TimestepData = relative_maximum_charge_state
178
184
 
179
185
  self.initial_charge_state = initial_charge_state
180
186
  self.minimal_final_charge_state = minimal_final_charge_state
181
187
  self.maximal_final_charge_state = maximal_final_charge_state
182
188
 
183
- self.eta_charge: NumericDataTS = eta_charge
184
- self.eta_discharge: NumericDataTS = eta_discharge
185
- self.relative_loss_per_hour: NumericDataTS = relative_loss_per_hour
189
+ self.eta_charge: TimestepData = eta_charge
190
+ self.eta_discharge: TimestepData = eta_discharge
191
+ self.relative_loss_per_hour: TimestepData = relative_loss_per_hour
186
192
  self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
193
+ self.balanced = balanced
187
194
 
188
195
  def create_model(self, model: SystemModel) -> 'StorageModel':
189
196
  self._plausibility_checks()
@@ -195,55 +202,83 @@ class Storage(Component):
195
202
  self.relative_minimum_charge_state = flow_system.create_time_series(
196
203
  f'{self.label_full}|relative_minimum_charge_state',
197
204
  self.relative_minimum_charge_state,
198
- needs_extra_timestep=True,
205
+ has_extra_timestep=True,
199
206
  )
200
207
  self.relative_maximum_charge_state = flow_system.create_time_series(
201
208
  f'{self.label_full}|relative_maximum_charge_state',
202
209
  self.relative_maximum_charge_state,
203
- needs_extra_timestep=True,
210
+ has_extra_timestep=True,
204
211
  )
205
212
  self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge)
206
213
  self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge)
207
214
  self.relative_loss_per_hour = flow_system.create_time_series(
208
215
  f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour
209
216
  )
217
+ if not isinstance(self.initial_charge_state, str):
218
+ self.initial_charge_state = flow_system.create_time_series(
219
+ f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False
220
+ )
221
+ self.minimal_final_charge_state = flow_system.create_time_series(
222
+ f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, has_time_dim=False
223
+ )
224
+ self.maximal_final_charge_state = flow_system.create_time_series(
225
+ f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False
226
+ )
210
227
  if isinstance(self.capacity_in_flow_hours, InvestParameters):
211
- self.capacity_in_flow_hours.transform_data(flow_system)
228
+ self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters')
229
+ else:
230
+ self.capacity_in_flow_hours = flow_system.create_time_series(
231
+ f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, has_time_dim=False
232
+ )
212
233
 
213
234
  def _plausibility_checks(self) -> None:
214
235
  """
215
236
  Check for infeasible or uncommon combinations of parameters
216
237
  """
217
238
  super()._plausibility_checks()
218
- if utils.is_number(self.initial_charge_state):
219
- if isinstance(self.capacity_in_flow_hours, InvestParameters):
220
- if self.capacity_in_flow_hours.fixed_size is None:
221
- maximum_capacity = self.capacity_in_flow_hours.maximum_size
222
- minimum_capacity = self.capacity_in_flow_hours.minimum_size
223
- else:
224
- maximum_capacity = self.capacity_in_flow_hours.fixed_size
225
- minimum_capacity = self.capacity_in_flow_hours.fixed_size
239
+ if isinstance(self.initial_charge_state, str):
240
+ if self.initial_charge_state != 'lastValueOfSim':
241
+ raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}')
242
+ return
243
+ if isinstance(self.capacity_in_flow_hours, InvestParameters):
244
+ if self.capacity_in_flow_hours.fixed_size is None:
245
+ maximum_capacity = self.capacity_in_flow_hours.maximum_size
246
+ minimum_capacity = self.capacity_in_flow_hours.minimum_size
226
247
  else:
227
- maximum_capacity = self.capacity_in_flow_hours
228
- minimum_capacity = self.capacity_in_flow_hours
229
-
230
- # initial capacity >= allowed min for maximum_size:
231
- minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1)
232
- # initial capacity <= allowed max for minimum_size:
233
- maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1)
248
+ maximum_capacity = self.capacity_in_flow_hours.fixed_size
249
+ minimum_capacity = self.capacity_in_flow_hours.fixed_size
250
+ else:
251
+ maximum_capacity = self.capacity_in_flow_hours
252
+ minimum_capacity = self.capacity_in_flow_hours
253
+
254
+ # initial capacity >= allowed min for maximum_size:
255
+ minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0)
256
+ # initial capacity <= allowed max for minimum_size:
257
+ maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0)
258
+ # TODO: index=1 ??? I think index 0
259
+
260
+ if (self.initial_charge_state > maximum_inital_capacity).any():
261
+ raise ValueError(
262
+ f'{self.label_full}: {self.initial_charge_state=} '
263
+ f'is above allowed maximum charge_state {maximum_inital_capacity}'
264
+ )
265
+ if (self.initial_charge_state < minimum_inital_capacity).any():
266
+ raise ValueError(
267
+ f'{self.label_full}: {self.initial_charge_state=} '
268
+ f'is below allowed minimum charge_state {minimum_inital_capacity}'
269
+ )
234
270
 
235
- if self.initial_charge_state > maximum_inital_capacity:
236
- raise ValueError(
237
- f'{self.label_full}: {self.initial_charge_state=} '
238
- f'is above allowed maximum charge_state {maximum_inital_capacity}'
239
- )
240
- if self.initial_charge_state < minimum_inital_capacity:
241
- raise ValueError(
242
- f'{self.label_full}: {self.initial_charge_state=} '
243
- f'is below allowed minimum charge_state {minimum_inital_capacity}'
244
- )
245
- elif self.initial_charge_state != 'lastValueOfSim':
246
- raise ValueError(f'{self.label_full}: {self.initial_charge_state=} has an invalid value')
271
+ if self.balanced:
272
+ if not isinstance(self.charging.size, InvestParameters) or not isinstance(self.discharging.size, InvestParameters):
273
+ raise PlausibilityError(
274
+ f'Balancing charging and discharging Flows in {self.label_full} '
275
+ f'is only possible with Investments.')
276
+ if (self.charging.size.minimum_size > self.discharging.size.maximum_size or
277
+ self.charging.size.maximum_size < self.discharging.size.minimum_size):
278
+ raise PlausibilityError(
279
+ f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.'
280
+ f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and '
281
+ f'{self.charging.size.minimum_size=}, {self.charging.size.maximum_size=}.')
247
282
 
248
283
 
249
284
  @register_class_for_io
@@ -261,8 +296,8 @@ class Transmission(Component):
261
296
  out1: Flow,
262
297
  in2: Optional[Flow] = None,
263
298
  out2: Optional[Flow] = None,
264
- relative_losses: Optional[NumericDataTS] = None,
265
- absolute_losses: Optional[NumericDataTS] = None,
299
+ relative_losses: Optional[TimestepData] = None,
300
+ absolute_losses: Optional[TimestepData] = None,
266
301
  on_off_parameters: OnOffParameters = None,
267
302
  prevent_simultaneous_flows_in_both_directions: bool = True,
268
303
  meta_data: Optional[Dict] = None,
@@ -345,7 +380,7 @@ class TransmissionModel(ComponentModel):
345
380
  def do_modeling(self):
346
381
  """Initiates all FlowModels"""
347
382
  # Force On Variable if absolute losses are present
348
- if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.active_data != 0):
383
+ if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.selected_data != 0):
349
384
  for flow in self.element.inputs + self.element.outputs:
350
385
  if flow.on_off_parameters is None:
351
386
  flow.on_off_parameters = OnOffParameters()
@@ -382,14 +417,14 @@ class TransmissionModel(ComponentModel):
382
417
  # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
383
418
  con_transmission = self.add(
384
419
  self._model.add_constraints(
385
- out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.active_data - 1),
420
+ out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.selected_data - 1),
386
421
  name=f'{self.label_full}|{name}',
387
422
  ),
388
423
  name,
389
424
  )
390
425
 
391
426
  if self.element.absolute_losses is not None:
392
- con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.active_data
427
+ con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.selected_data
393
428
 
394
429
  return con_transmission
395
430
 
@@ -417,8 +452,10 @@ class LinearConverterModel(ComponentModel):
417
452
 
418
453
  self.add(
419
454
  self._model.add_constraints(
420
- sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_inputs])
421
- == sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_outputs]),
455
+ sum([flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_inputs])
456
+ == sum(
457
+ [flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_outputs]
458
+ ),
422
459
  name=f'{self.label_full}|conversion_{i}',
423
460
  )
424
461
  )
@@ -458,12 +495,15 @@ class StorageModel(ComponentModel):
458
495
  lb, ub = self.absolute_charge_state_bounds
459
496
  self.charge_state = self.add(
460
497
  self._model.add_variables(
461
- lower=lb, upper=ub, coords=self._model.coords_extra, name=f'{self.label_full}|charge_state'
498
+ lower=lb,
499
+ upper=ub,
500
+ coords=self._model.get_coords(extra_timestep=True),
501
+ name=f'{self.label_full}|charge_state',
462
502
  ),
463
503
  'charge_state',
464
504
  )
465
505
  self.netto_discharge = self.add(
466
- self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|netto_discharge'),
506
+ self._model.add_variables(coords=self._model.get_coords(), name=f'{self.label_full}|netto_discharge'),
467
507
  'netto_discharge',
468
508
  )
469
509
  # netto_discharge:
@@ -478,12 +518,12 @@ class StorageModel(ComponentModel):
478
518
  )
479
519
 
480
520
  charge_state = self.charge_state
481
- rel_loss = self.element.relative_loss_per_hour.active_data
521
+ rel_loss = self.element.relative_loss_per_hour.selected_data
482
522
  hours_per_step = self._model.hours_per_step
483
523
  charge_rate = self.element.charging.model.flow_rate
484
524
  discharge_rate = self.element.discharging.model.flow_rate
485
- eff_charge = self.element.eta_charge.active_data
486
- eff_discharge = self.element.eta_discharge.active_data
525
+ eff_charge = self.element.eta_charge.selected_data
526
+ eff_discharge = self.element.eta_discharge.selected_data
487
527
 
488
528
  self.add(
489
529
  self._model.add_constraints(
@@ -510,29 +550,34 @@ class StorageModel(ComponentModel):
510
550
  # Initial charge state
511
551
  self._initial_and_final_charge_state()
512
552
 
553
+ if self.element.balanced:
554
+ self.add(
555
+ self._model.add_constraints(
556
+ self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1,
557
+ name=f'{self.label_full}|balanced_sizes',
558
+ ),
559
+ 'balanced_sizes'
560
+ )
561
+
513
562
  def _initial_and_final_charge_state(self):
514
563
  if self.element.initial_charge_state is not None:
515
564
  name_short = 'initial_charge_state'
516
565
  name = f'{self.label_full}|{name_short}'
517
566
 
518
- if utils.is_number(self.element.initial_charge_state):
567
+ if isinstance(self.element.initial_charge_state, str):
519
568
  self.add(
520
569
  self._model.add_constraints(
521
- self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
570
+ self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
522
571
  ),
523
572
  name_short,
524
573
  )
525
- elif self.element.initial_charge_state == 'lastValueOfSim':
574
+ else:
526
575
  self.add(
527
576
  self._model.add_constraints(
528
- self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
577
+ self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
529
578
  ),
530
579
  name_short,
531
580
  )
532
- else: # TODO: Validation in Storage Class, not in Model
533
- raise PlausibilityError(
534
- f'initial_charge_state has undefined value: {self.element.initial_charge_state}'
535
- )
536
581
 
537
582
  if self.element.maximal_final_charge_state is not None:
538
583
  self.add(
@@ -553,7 +598,7 @@ class StorageModel(ComponentModel):
553
598
  )
554
599
 
555
600
  @property
556
- def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
601
+ def absolute_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
557
602
  relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds
558
603
  if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
559
604
  return (
@@ -567,10 +612,10 @@ class StorageModel(ComponentModel):
567
612
  )
568
613
 
569
614
  @property
570
- def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
615
+ def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
571
616
  return (
572
- self.element.relative_minimum_charge_state.active_data,
573
- self.element.relative_maximum_charge_state.active_data,
617
+ self.element.relative_minimum_charge_state,
618
+ self.element.relative_maximum_charge_state,
574
619
  )
575
620
 
576
621