flixopt 2.1.1__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
  ):
@@ -86,9 +86,9 @@ class LinearConverter(Component):
86
86
  if self.piecewise_conversion:
87
87
  for flow in self.flows.values():
88
88
  if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
89
- raise PlausibilityError(
90
- f'piecewise_conversion (in {self.label_full}) and variable size '
91
- 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!'
92
92
  )
93
93
 
94
94
  def transform_data(self, flow_system: 'FlowSystem'):
@@ -96,6 +96,7 @@ class LinearConverter(Component):
96
96
  if self.conversion_factors:
97
97
  self.conversion_factors = self._transform_conversion_factors(flow_system)
98
98
  if self.piecewise_conversion:
99
+ self.piecewise_conversion.has_time_dim = True
99
100
  self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion')
100
101
 
101
102
  def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]:
@@ -127,16 +128,17 @@ class Storage(Component):
127
128
  label: str,
128
129
  charging: Flow,
129
130
  discharging: Flow,
130
- capacity_in_flow_hours: Union[Scalar, InvestParameters],
131
- relative_minimum_charge_state: NumericData = 0,
132
- relative_maximum_charge_state: NumericData = 1,
133
- initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0,
134
- minimal_final_charge_state: Optional[Scalar] = None,
135
- maximal_final_charge_state: Optional[Scalar] = None,
136
- eta_charge: NumericData = 1,
137
- eta_discharge: NumericData = 1,
138
- 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,
139
140
  prevent_simultaneous_charge_and_discharge: bool = True,
141
+ balanced: bool = False,
140
142
  meta_data: Optional[Dict] = None,
141
143
  ):
142
144
  """
@@ -162,6 +164,7 @@ class Storage(Component):
162
164
  relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
163
165
  prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
164
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.
165
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.
166
169
  """
167
170
  # TODO: fixed_relative_chargeState implementieren
@@ -176,17 +179,18 @@ class Storage(Component):
176
179
  self.charging = charging
177
180
  self.discharging = discharging
178
181
  self.capacity_in_flow_hours = capacity_in_flow_hours
179
- self.relative_minimum_charge_state: NumericDataTS = relative_minimum_charge_state
180
- 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
181
184
 
182
185
  self.initial_charge_state = initial_charge_state
183
186
  self.minimal_final_charge_state = minimal_final_charge_state
184
187
  self.maximal_final_charge_state = maximal_final_charge_state
185
188
 
186
- self.eta_charge: NumericDataTS = eta_charge
187
- self.eta_discharge: NumericDataTS = eta_discharge
188
- 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
189
192
  self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
193
+ self.balanced = balanced
190
194
 
191
195
  def create_model(self, model: SystemModel) -> 'StorageModel':
192
196
  self._plausibility_checks()
@@ -198,55 +202,83 @@ class Storage(Component):
198
202
  self.relative_minimum_charge_state = flow_system.create_time_series(
199
203
  f'{self.label_full}|relative_minimum_charge_state',
200
204
  self.relative_minimum_charge_state,
201
- needs_extra_timestep=True,
205
+ has_extra_timestep=True,
202
206
  )
203
207
  self.relative_maximum_charge_state = flow_system.create_time_series(
204
208
  f'{self.label_full}|relative_maximum_charge_state',
205
209
  self.relative_maximum_charge_state,
206
- needs_extra_timestep=True,
210
+ has_extra_timestep=True,
207
211
  )
208
212
  self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge)
209
213
  self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge)
210
214
  self.relative_loss_per_hour = flow_system.create_time_series(
211
215
  f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour
212
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
+ )
213
227
  if isinstance(self.capacity_in_flow_hours, InvestParameters):
214
- 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
+ )
215
233
 
216
234
  def _plausibility_checks(self) -> None:
217
235
  """
218
236
  Check for infeasible or uncommon combinations of parameters
219
237
  """
220
238
  super()._plausibility_checks()
221
- if utils.is_number(self.initial_charge_state):
222
- if isinstance(self.capacity_in_flow_hours, InvestParameters):
223
- if self.capacity_in_flow_hours.fixed_size is None:
224
- maximum_capacity = self.capacity_in_flow_hours.maximum_size
225
- minimum_capacity = self.capacity_in_flow_hours.minimum_size
226
- else:
227
- maximum_capacity = self.capacity_in_flow_hours.fixed_size
228
- 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
229
247
  else:
230
- maximum_capacity = self.capacity_in_flow_hours
231
- minimum_capacity = self.capacity_in_flow_hours
232
-
233
- # initial capacity >= allowed min for maximum_size:
234
- minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1)
235
- # initial capacity <= allowed max for minimum_size:
236
- 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
+ )
237
270
 
238
- if self.initial_charge_state > maximum_inital_capacity:
239
- raise ValueError(
240
- f'{self.label_full}: {self.initial_charge_state=} '
241
- f'is above allowed maximum charge_state {maximum_inital_capacity}'
242
- )
243
- if self.initial_charge_state < minimum_inital_capacity:
244
- raise ValueError(
245
- f'{self.label_full}: {self.initial_charge_state=} '
246
- f'is below allowed minimum charge_state {minimum_inital_capacity}'
247
- )
248
- elif self.initial_charge_state != 'lastValueOfSim':
249
- 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=}.')
250
282
 
251
283
 
252
284
  @register_class_for_io
@@ -264,8 +296,8 @@ class Transmission(Component):
264
296
  out1: Flow,
265
297
  in2: Optional[Flow] = None,
266
298
  out2: Optional[Flow] = None,
267
- relative_losses: Optional[NumericDataTS] = None,
268
- absolute_losses: Optional[NumericDataTS] = None,
299
+ relative_losses: Optional[TimestepData] = None,
300
+ absolute_losses: Optional[TimestepData] = None,
269
301
  on_off_parameters: OnOffParameters = None,
270
302
  prevent_simultaneous_flows_in_both_directions: bool = True,
271
303
  meta_data: Optional[Dict] = None,
@@ -348,7 +380,7 @@ class TransmissionModel(ComponentModel):
348
380
  def do_modeling(self):
349
381
  """Initiates all FlowModels"""
350
382
  # Force On Variable if absolute losses are present
351
- 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):
352
384
  for flow in self.element.inputs + self.element.outputs:
353
385
  if flow.on_off_parameters is None:
354
386
  flow.on_off_parameters = OnOffParameters()
@@ -385,14 +417,14 @@ class TransmissionModel(ComponentModel):
385
417
  # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
386
418
  con_transmission = self.add(
387
419
  self._model.add_constraints(
388
- 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),
389
421
  name=f'{self.label_full}|{name}',
390
422
  ),
391
423
  name,
392
424
  )
393
425
 
394
426
  if self.element.absolute_losses is not None:
395
- 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
396
428
 
397
429
  return con_transmission
398
430
 
@@ -420,8 +452,10 @@ class LinearConverterModel(ComponentModel):
420
452
 
421
453
  self.add(
422
454
  self._model.add_constraints(
423
- sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_inputs])
424
- == 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
+ ),
425
459
  name=f'{self.label_full}|conversion_{i}',
426
460
  )
427
461
  )
@@ -461,12 +495,15 @@ class StorageModel(ComponentModel):
461
495
  lb, ub = self.absolute_charge_state_bounds
462
496
  self.charge_state = self.add(
463
497
  self._model.add_variables(
464
- 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',
465
502
  ),
466
503
  'charge_state',
467
504
  )
468
505
  self.netto_discharge = self.add(
469
- 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'),
470
507
  'netto_discharge',
471
508
  )
472
509
  # netto_discharge:
@@ -481,12 +518,12 @@ class StorageModel(ComponentModel):
481
518
  )
482
519
 
483
520
  charge_state = self.charge_state
484
- rel_loss = self.element.relative_loss_per_hour.active_data
521
+ rel_loss = self.element.relative_loss_per_hour.selected_data
485
522
  hours_per_step = self._model.hours_per_step
486
523
  charge_rate = self.element.charging.model.flow_rate
487
524
  discharge_rate = self.element.discharging.model.flow_rate
488
- eff_charge = self.element.eta_charge.active_data
489
- 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
490
527
 
491
528
  self.add(
492
529
  self._model.add_constraints(
@@ -513,29 +550,34 @@ class StorageModel(ComponentModel):
513
550
  # Initial charge state
514
551
  self._initial_and_final_charge_state()
515
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
+
516
562
  def _initial_and_final_charge_state(self):
517
563
  if self.element.initial_charge_state is not None:
518
564
  name_short = 'initial_charge_state'
519
565
  name = f'{self.label_full}|{name_short}'
520
566
 
521
- if utils.is_number(self.element.initial_charge_state):
567
+ if isinstance(self.element.initial_charge_state, str):
522
568
  self.add(
523
569
  self._model.add_constraints(
524
- 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
525
571
  ),
526
572
  name_short,
527
573
  )
528
- elif self.element.initial_charge_state == 'lastValueOfSim':
574
+ else:
529
575
  self.add(
530
576
  self._model.add_constraints(
531
- 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
532
578
  ),
533
579
  name_short,
534
580
  )
535
- else: # TODO: Validation in Storage Class, not in Model
536
- raise PlausibilityError(
537
- f'initial_charge_state has undefined value: {self.element.initial_charge_state}'
538
- )
539
581
 
540
582
  if self.element.maximal_final_charge_state is not None:
541
583
  self.add(
@@ -556,7 +598,7 @@ class StorageModel(ComponentModel):
556
598
  )
557
599
 
558
600
  @property
559
- def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
601
+ def absolute_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
560
602
  relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds
561
603
  if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
562
604
  return (
@@ -570,10 +612,10 @@ class StorageModel(ComponentModel):
570
612
  )
571
613
 
572
614
  @property
573
- def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
615
+ def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
574
616
  return (
575
- self.element.relative_minimum_charge_state.active_data,
576
- self.element.relative_maximum_charge_state.active_data,
617
+ self.element.relative_minimum_charge_state,
618
+ self.element.relative_maximum_charge_state,
577
619
  )
578
620
 
579
621