Skip to content

Commit 67d1716

Browse files
authored
Scenarios/fixes (#255)
* Fix indexing issue with only one scenario * Bugfix Cooling Tower * Add option for balanced Storage Flows (equalize size of charging and discharging) * Add option for balanced Storage Flows * Change error to warning (non-fixed size with piecewise conversion AND fixed_flow_rate with OnOff) * Bugfix in DataConverter * BUGFIX: Typo (total_max/total_min in Effect) * Bugfix in node_balance() (negating did not work when using flow_hours mode
1 parent 16fd74c commit 67d1716

6 files changed

Lines changed: 44 additions & 21 deletions

File tree

flixopt/components.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ def _plausibility_checks(self) -> None:
8686
if self.piecewise_conversion:
8787
for flow in self.flows.values():
8888
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 '
89+
logger.warning(
90+
f'Piecewise_conversion (in {self.label_full}) and variable size '
9191
f'(in flow {flow.label_full}) do not make sense together!'
9292
)
9393

@@ -138,6 +138,7 @@ def __init__(
138138
eta_discharge: TimestepData = 1,
139139
relative_loss_per_hour: TimestepData = 0,
140140
prevent_simultaneous_charge_and_discharge: bool = True,
141+
balanced: bool = False,
141142
meta_data: Optional[Dict] = None,
142143
):
143144
"""
@@ -163,6 +164,7 @@ def __init__(
163164
relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
164165
prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
165166
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.
166168
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
167169
"""
168170
# TODO: fixed_relative_chargeState implementieren
@@ -188,6 +190,7 @@ def __init__(
188190
self.eta_discharge: TimestepData = eta_discharge
189191
self.relative_loss_per_hour: TimestepData = relative_loss_per_hour
190192
self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
193+
self.balanced = balanced
191194

192195
def create_model(self, model: SystemModel) -> 'StorageModel':
193196
self._plausibility_checks()
@@ -261,6 +264,18 @@ def _plausibility_checks(self) -> None:
261264
f'is below allowed minimum charge_state {minimum_inital_capacity}'
262265
)
263266

267+
if self.balanced:
268+
if not isinstance(self.charging.size, InvestParameters) or not isinstance(self.discharging.size, InvestParameters):
269+
raise PlausibilityError(
270+
f'Balancing charging and discharging Flows in {self.label_full} '
271+
f'is only possible with Investments.')
272+
if (self.charging.size.minimum_size > self.discharging.size.maximum_size or
273+
self.charging.size.maximum_size < self.discharging.size.minimum_size):
274+
raise PlausibilityError(
275+
f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.'
276+
f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and '
277+
f'{self.charging.size.minimum_size=}, {self.charging.size.maximum_size=}.')
278+
264279

265280
@register_class_for_io
266281
class Transmission(Component):
@@ -531,6 +546,15 @@ def do_modeling(self):
531546
# Initial charge state
532547
self._initial_and_final_charge_state()
533548

549+
if self.element.balanced:
550+
self.add(
551+
self._model.add_constraints(
552+
self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1,
553+
name=f'{self.label_full}|balanced_sizes',
554+
),
555+
'balanced_sizes'
556+
)
557+
534558
def _initial_and_final_charge_state(self):
535559
if self.element.initial_charge_state is not None:
536560
name_short = 'initial_charge_state'

flixopt/core.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,11 @@ def _broadcast_time_to_scenarios(
251251
if not np.array_equal(data.coords['time'].values, coords['time'].values):
252252
raise ConversionError("Source time coordinates don't match target time coordinates")
253253

254+
if len(coords['scenario']) <= 1:
255+
return data.copy(deep=True)
256+
254257
# Broadcast values
255-
values = np.tile(data.values, (len(coords['scenario']), 1))
258+
values = np.tile(data.values, (len(coords['scenario']), 1)).T # Tile seems to be faster than repeat()
256259
return xr.DataArray(values.copy(), coords=coords, dims=dims)
257260

258261
@staticmethod

flixopt/elements.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,9 @@ def _plausibility_checks(self) -> None:
292292
)
293293

294294
if self.fixed_relative_profile is not None and self.on_off_parameters is not None:
295-
raise ValueError(
296-
f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters. This is not supported. '
297-
f'Use relative_minimum and relative_maximum instead, '
298-
f'if you want to allow flows to be switched on and off.'
295+
logger.warning(
296+
f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters.'
297+
f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.'
299298
)
300299

301300
if (self.relative_minimum > 0).any() and self.on_off_parameters is None:

flixopt/features.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,6 @@ def _create_bounds_for_defining_variable(self):
154154
),
155155
f'fix_{variable.name}',
156156
)
157-
if self._on_variable is not None:
158-
raise ValueError(
159-
f'Flow {self.label_full} has a fixed relative flow rate and an on_variable.'
160-
f'This combination is currently not supported.'
161-
)
162157
return
163158

164159
# eq: defining_variable(t) <= size * upper_bound(t)
@@ -988,7 +983,7 @@ def __init__(
988983
# Parameters
989984
self._has_time_dim = has_time_dim
990985
self._has_scenario_dim = has_scenario_dim
991-
self._total_max = total_max if total_min is not None else np.inf
986+
self._total_max = total_max if total_max is not None else np.inf
992987
self._total_min = total_min if total_min is not None else -np.inf
993988
self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf
994989
self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf

flixopt/linear_converters.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def __init__(
165165
label,
166166
inputs=[P_el, Q_th],
167167
outputs=[],
168-
conversion_factors=[{P_el.label: 1, Q_th.label: -specific_electricity_demand}],
168+
conversion_factors=[{P_el.label: -1, Q_th.label: specific_electricity_demand}],
169169
on_off_parameters=on_off_parameters,
170170
meta_data=meta_data,
171171
)
@@ -177,12 +177,12 @@ def __init__(
177177

178178
@property
179179
def specific_electricity_demand(self):
180-
return -self.conversion_factors[0][self.Q_th.label]
180+
return self.conversion_factors[0][self.Q_th.label]
181181

182182
@specific_electricity_demand.setter
183183
def specific_electricity_demand(self, value):
184184
check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1)
185-
self.conversion_factors[0][self.Q_th.label] = -value
185+
self.conversion_factors[0][self.Q_th.label] = value
186186

187187

188188
@register_class_for_io

flixopt/results.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,7 @@ def node_balance(
631631
"""
632632
ds = self.solution[self.inputs + self.outputs]
633633

634-
if mode == 'flow_hours':
635-
ds = ds * self._calculation_results.hours_per_timestep
636-
ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars})
637-
638-
return sanitize_dataset(
634+
ds = sanitize_dataset(
639635
ds=ds,
640636
threshold=threshold,
641637
timesteps=self._calculation_results.timesteps_extra if with_last_timestep else None,
@@ -651,6 +647,12 @@ def node_balance(
651647
drop_suffix='|' if drop_suffix else None,
652648
)
653649

650+
if mode == 'flow_hours':
651+
ds = ds * self._calculation_results.hours_per_timestep
652+
ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars})
653+
654+
return ds
655+
654656

655657
class BusResults(_NodeResults):
656658
"""Results for a Bus"""

0 commit comments

Comments
 (0)