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.
- docs/release-notes/v2.2.0.md +55 -0
- docs/user-guide/Mathematical Notation/Investment.md +115 -0
- flixopt/calculation.py +65 -37
- flixopt/components.py +119 -74
- flixopt/core.py +966 -451
- flixopt/effects.py +269 -65
- flixopt/elements.py +83 -52
- flixopt/features.py +134 -85
- flixopt/flow_system.py +99 -16
- flixopt/interface.py +142 -51
- flixopt/io.py +56 -27
- flixopt/linear_converters.py +3 -3
- flixopt/plotting.py +34 -16
- flixopt/results.py +807 -109
- flixopt/structure.py +64 -10
- flixopt/utils.py +6 -9
- {flixopt-2.1.0.dist-info → flixopt-2.2.0b0.dist-info}/METADATA +1 -1
- {flixopt-2.1.0.dist-info → flixopt-2.2.0b0.dist-info}/RECORD +21 -20
- {flixopt-2.1.0.dist-info → flixopt-2.2.0b0.dist-info}/WHEEL +1 -1
- {flixopt-2.1.0.dist-info → flixopt-2.2.0b0.dist-info}/top_level.txt +0 -1
- site/release-notes/_template.txt +0 -32
- {flixopt-2.1.0.dist-info → flixopt-2.2.0b0.dist-info}/licenses/LICENSE +0 -0
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
87
|
-
f'
|
|
88
|
-
f'(in
|
|
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[
|
|
128
|
-
relative_minimum_charge_state:
|
|
129
|
-
relative_maximum_charge_state:
|
|
130
|
-
initial_charge_state: Union[
|
|
131
|
-
minimal_final_charge_state: Optional[
|
|
132
|
-
maximal_final_charge_state: Optional[
|
|
133
|
-
eta_charge:
|
|
134
|
-
eta_discharge:
|
|
135
|
-
relative_loss_per_hour:
|
|
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:
|
|
177
|
-
self.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:
|
|
184
|
-
self.eta_discharge:
|
|
185
|
-
self.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
|
-
|
|
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
|
-
|
|
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
|
|
219
|
-
if
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
f'
|
|
239
|
-
|
|
240
|
-
if self.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
f'
|
|
244
|
-
|
|
245
|
-
|
|
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[
|
|
265
|
-
absolute_losses: Optional[
|
|
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.
|
|
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.
|
|
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.
|
|
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].
|
|
421
|
-
== sum(
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
486
|
-
eff_discharge = self.element.eta_discharge.
|
|
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
|
|
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.
|
|
570
|
+
self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
|
|
522
571
|
),
|
|
523
572
|
name_short,
|
|
524
573
|
)
|
|
525
|
-
|
|
574
|
+
else:
|
|
526
575
|
self.add(
|
|
527
576
|
self._model.add_constraints(
|
|
528
|
-
self.charge_state.isel(time=0) == self.
|
|
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[
|
|
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[
|
|
615
|
+
def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
|
|
571
616
|
return (
|
|
572
|
-
self.element.relative_minimum_charge_state
|
|
573
|
-
self.element.relative_maximum_charge_state
|
|
617
|
+
self.element.relative_minimum_charge_state,
|
|
618
|
+
self.element.relative_maximum_charge_state,
|
|
574
619
|
)
|
|
575
620
|
|
|
576
621
|
|