diff --git a/flixopt/components.py b/flixopt/components.py index c5913c8e2..267c144af 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -265,7 +265,9 @@ class Storage(Component): charging: Incoming flow for loading the storage. discharging: Outgoing flow for unloading the storage. capacity_in_flow_hours: Storage capacity in flow-hours (kWh, m³, kg). - Scalar for fixed size or InvestParameters for optimization. + Scalar for fixed size, InvestParameters for optimization, or None (unbounded). + Default: None (unbounded capacity). When using InvestParameters, + maximum_size (or fixed_size) must be explicitly set for proper model scaling. relative_minimum_charge_state: Minimum charge state (0-1). Default: 0. relative_maximum_charge_state: Maximum charge state (0-1). Default: 1. initial_charge_state: Charge at start. Numeric or 'equals_final'. Default: 0. @@ -366,6 +368,11 @@ class Storage(Component): variables enforce mutual exclusivity, increasing solution time but preventing unrealistic simultaneous charging and discharging. + **Unbounded capacity**: When capacity_in_flow_hours is None (default), the storage has + unlimited capacity. Note that prevent_simultaneous_charge_and_discharge requires the + charging and discharging flows to have explicit sizes. Use prevent_simultaneous_charge_and_discharge=False + with unbounded storages, or set flow sizes explicitly. + **Units**: Flow rates and charge states are related by the concept of 'flow hours' (=flow_rate * time). With flow rates in kW, the charge state is therefore (usually) kWh. With flow rates in m3/h, the charge state is therefore in m3. @@ -378,7 +385,7 @@ def __init__( label: str, charging: Flow, discharging: Flow, - capacity_in_flow_hours: Numeric_PS | InvestParameters, + capacity_in_flow_hours: Numeric_PS | InvestParameters | None = None, relative_minimum_charge_state: Numeric_TPS = 0, relative_maximum_charge_state: Numeric_TPS = 1, initial_charge_state: Numeric_PS | Literal['equals_final'] = 0, @@ -485,31 +492,58 @@ def _plausibility_checks(self) -> None: raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') initial_equals_final = True - # Use new InvestParameters methods to get capacity bounds - if isinstance(self.capacity_in_flow_hours, InvestParameters): - minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size - maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size - else: - maximum_capacity = self.capacity_in_flow_hours - minimum_capacity = self.capacity_in_flow_hours - - # Initial capacity should not constraint investment decision - minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) - maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) - - # Only perform numeric comparisons if not using 'equals_final' - if not initial_equals_final: - if (self.initial_charge_state > maximum_initial_capacity).any(): + # Capacity is required when using non-default relative bounds + if self.capacity_in_flow_hours is None: + if np.any(self.relative_minimum_charge_state > 0): + raise PlausibilityError( + f'Storage "{self.label_full}" has relative_minimum_charge_state > 0 but no capacity_in_flow_hours. ' + f'A capacity is required because the lower bound is capacity * relative_minimum_charge_state.' + ) + if np.any(self.relative_maximum_charge_state < 1): + raise PlausibilityError( + f'Storage "{self.label_full}" has relative_maximum_charge_state < 1 but no capacity_in_flow_hours. ' + f'A capacity is required because the upper bound is capacity * relative_maximum_charge_state.' + ) + if self.relative_minimum_final_charge_state is not None: raise PlausibilityError( - f'{self.label_full}: {self.initial_charge_state=} ' - f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}' + f'Storage "{self.label_full}" has relative_minimum_final_charge_state but no capacity_in_flow_hours. ' + f'A capacity is required for relative final charge state constraints.' ) - if (self.initial_charge_state < minimum_initial_capacity).any(): + if self.relative_maximum_final_charge_state is not None: raise PlausibilityError( - f'{self.label_full}: {self.initial_charge_state=} ' - f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}' + f'Storage "{self.label_full}" has relative_maximum_final_charge_state but no capacity_in_flow_hours. ' + f'A capacity is required for relative final charge state constraints.' ) + # Skip capacity-related checks if capacity is None (unbounded) + if self.capacity_in_flow_hours is not None: + # Use new InvestParameters methods to get capacity bounds + if isinstance(self.capacity_in_flow_hours, InvestParameters): + minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size + maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size + else: + maximum_capacity = self.capacity_in_flow_hours + minimum_capacity = self.capacity_in_flow_hours + + # Initial charge state should not constrain investment decision + # If initial > (min_cap * rel_max), investment is forced to increase capacity + # If initial < (max_cap * rel_min), investment is forced to decrease capacity + min_initial_at_max_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) + max_initial_at_min_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) + + # Only perform numeric comparisons if not using 'equals_final' + if not initial_equals_final: + if (self.initial_charge_state > max_initial_at_min_capacity).any(): + raise PlausibilityError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is constraining the investment decision. Choose a value <= {max_initial_at_min_capacity}.' + ) + if (self.initial_charge_state < min_initial_at_max_capacity).any(): + raise PlausibilityError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is constraining the investment decision. Choose a value >= {min_initial_at_max_capacity}.' + ) + if self.balanced: if not isinstance(self.charging.size, InvestParameters) or not isinstance( self.discharging.size, InvestParameters @@ -518,13 +552,13 @@ def _plausibility_checks(self) -> None: f'Balancing charging and discharging Flows in {self.label_full} is only possible with Investments.' ) - if (self.charging.size.minimum_size > self.discharging.size.maximum_size).any() or ( - self.charging.size.maximum_size < self.discharging.size.minimum_size + if (self.charging.size.minimum_or_fixed_size > self.discharging.size.maximum_or_fixed_size).any() or ( + self.charging.size.maximum_or_fixed_size < self.discharging.size.minimum_or_fixed_size ).any(): raise PlausibilityError( f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.' - f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and ' - f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.' + f'Got: {self.charging.size.minimum_or_fixed_size=}, {self.charging.size.maximum_or_fixed_size=} and ' + f'{self.discharging.size.minimum_or_fixed_size=}, {self.discharging.size.maximum_or_fixed_size=}.' ) def __repr__(self) -> str: @@ -705,8 +739,8 @@ def _plausibility_checks(self): ).any(): raise ValueError( f'Balanced Transmission needs compatible minimum and maximum sizes.' - f'Got: {self.in1.size.minimum_size=}, {self.in1.size.maximum_size=}, {self.in1.size.fixed_size=} and ' - f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.' + f'Got: {self.in1.size.minimum_or_fixed_size=}, {self.in1.size.maximum_or_fixed_size=} and ' + f'{self.in2.size.minimum_or_fixed_size=}, {self.in2.size.maximum_or_fixed_size=}.' ) def create_model(self, model) -> TransmissionModel: @@ -938,15 +972,18 @@ def _initial_and_final_charge_state(self): @property def _absolute_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds - if not isinstance(self.element.capacity_in_flow_hours, InvestParameters): + if self.element.capacity_in_flow_hours is None: + # Unbounded storage: lower bound is 0, upper bound is infinite + return (0, np.inf) + elif isinstance(self.element.capacity_in_flow_hours, InvestParameters): return ( - relative_lower_bound * self.element.capacity_in_flow_hours, - relative_upper_bound * self.element.capacity_in_flow_hours, + relative_lower_bound * self.element.capacity_in_flow_hours.minimum_or_fixed_size, + relative_upper_bound * self.element.capacity_in_flow_hours.maximum_or_fixed_size, ) else: return ( - relative_lower_bound * self.element.capacity_in_flow_hours.minimum_size, - relative_upper_bound * self.element.capacity_in_flow_hours.maximum_size, + relative_lower_bound * self.element.capacity_in_flow_hours, + relative_upper_bound * self.element.capacity_in_flow_hours, ) @property diff --git a/flixopt/elements.py b/flixopt/elements.py index d97ff3ffb..2933eb95a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -138,6 +138,17 @@ def _check_unique_flow_labels(self): def _plausibility_checks(self) -> None: self._check_unique_flow_labels() + # Component with status_parameters requires all flows to have sizes set + # (status_parameters are propagated to flows in _do_modeling, which need sizes for big-M constraints) + if self.status_parameters is not None: + flows_without_size = [flow.label for flow in self.inputs + self.outputs if flow.size is None] + if flows_without_size: + raise PlausibilityError( + f'Component "{self.label_full}" has status_parameters, but the following flows have no size: ' + f'{flows_without_size}. All flows need explicit sizes when the component uses status_parameters ' + f'(required for big-M constraints).' + ) + def _connect_flows(self): # Inputs for flow in self.inputs: @@ -344,7 +355,7 @@ class Flow(Element): Args: label: Unique flow identifier within its component. bus: Bus label this flow connects to. - size: Flow capacity. Scalar, InvestParameters, or None (uses CONFIG.Modeling.big). + size: Flow capacity. Scalar, InvestParameters, or None (unbounded). relative_minimum: Minimum flow rate as fraction of size (0-1). Default: 0. relative_maximum: Maximum flow rate as fraction of size. Default: 1. load_factor_min: Minimum average utilization (0-1). Default: 0. @@ -445,7 +456,8 @@ class Flow(Element): `relative_maximum` for upper bounds on optimization variables. Notes: - - Default size (CONFIG.Modeling.big) is used when size=None + - size=None means unbounded (no capacity constraint) + - size must be set when using status_parameters or fixed_relative_profile - list inputs for previous_flow_rate are converted to NumPy arrays - Flow direction is determined by component input/output designation @@ -460,7 +472,7 @@ def __init__( self, label: str, bus: str, - size: Numeric_PS | InvestParameters = None, + size: Numeric_PS | InvestParameters | None = None, fixed_relative_profile: Numeric_TPS | None = None, relative_minimum: Numeric_TPS = 0, relative_maximum: Numeric_TPS = 1, @@ -476,7 +488,7 @@ def __init__( meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) - self.size = CONFIG.Modeling.big if size is None else size + self.size = size self.relative_minimum = relative_minimum self.relative_maximum = relative_maximum self.fixed_relative_profile = fixed_relative_profile @@ -549,7 +561,7 @@ def transform_data(self) -> None: self.status_parameters.transform_data() if isinstance(self.size, InvestParameters): self.size.transform_data() - else: + elif self.size is not None: self.size = self._fit_coords(f'{self.prefix}|size', self.size, dims=['period', 'scenario']) def _plausibility_checks(self) -> None: @@ -557,13 +569,43 @@ def _plausibility_checks(self) -> None: if (self.relative_minimum > self.relative_maximum).any(): raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') - if not isinstance(self.size, InvestParameters) and ( - np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None - ): # Default Size --> Most likely by accident - logger.warning( - f'Flow "{self.label_full}" has no size assigned, but a "fixed_relative_profile". ' - f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", ' - f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.' + # Size is required when using StatusParameters (for big-M constraints) + if self.status_parameters is not None and self.size is None: + raise PlausibilityError( + f'Flow "{self.label_full}" has status_parameters but no size defined. ' + f'A size is required when using status_parameters to bound the flow rate.' + ) + + if self.size is None and self.fixed_relative_profile is not None: + raise PlausibilityError( + f'Flow "{self.label_full}" has a fixed_relative_profile but no size defined. ' + f'A size is required because flow_rate = size * fixed_relative_profile.' + ) + + # Size is required when using non-default relative bounds (flow_rate = size * relative_bound) + if self.size is None and np.any(self.relative_minimum > 0): + raise PlausibilityError( + f'Flow "{self.label_full}" has relative_minimum > 0 but no size defined. ' + f'A size is required because the lower bound is size * relative_minimum.' + ) + + if self.size is None and np.any(self.relative_maximum < 1): + raise PlausibilityError( + f'Flow "{self.label_full}" has relative_maximum != 1 but no size defined. ' + f'A size is required because the upper bound is size * relative_maximum.' + ) + + # Size is required for load factor constraints (total_flow_hours / size) + if self.size is None and self.load_factor_min is not None: + raise PlausibilityError( + f'Flow "{self.label_full}" has load_factor_min but no size defined. ' + f'A size is required because the constraint is total_flow_hours >= size * load_factor_min * hours.' + ) + + if self.size is None and self.load_factor_max is not None: + raise PlausibilityError( + f'Flow "{self.label_full}" has load_factor_max but no size defined. ' + f'A size is required because the constraint is total_flow_hours <= size * load_factor_max * hours.' ) if self.fixed_relative_profile is not None and self.status_parameters is not None: @@ -829,15 +871,18 @@ def absolute_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: if not self.with_status: if not self.with_investment: # Basic case without investment and without Status - lb = lb_relative * self.element.size + if self.element.size is not None: + lb = lb_relative * self.element.size elif self.with_investment and self.element.size.mandatory: # With mandatory Investment lb = lb_relative * self.element.size.minimum_or_fixed_size if self.with_investment: ub = ub_relative * self.element.size.maximum_or_fixed_size - else: + elif self.element.size is not None: ub = ub_relative * self.element.size + else: + ub = np.inf # Unbounded when size is None return lb, ub diff --git a/flixopt/interface.py b/flixopt/interface.py index 0a9e9424c..b11576ef1 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -882,7 +882,7 @@ class InvestParameters(Interface): fixed_size: Creates binary decision at this exact size. None allows continuous sizing. minimum_size: Lower bound for continuous sizing. Default: CONFIG.Modeling.epsilon. Ignored if fixed_size is specified. - maximum_size: Upper bound for continuous sizing. Default: CONFIG.Modeling.big. + maximum_size: Upper bound for continuous sizing. Required if fixed_size is not set. Ignored if fixed_size is specified. mandatory: Controls whether investment is required. When True, forces investment to occur (useful for mandatory upgrades or replacement decisions). @@ -1065,7 +1065,7 @@ def __init__( ) self.piecewise_effects_of_investment = piecewise_effects_of_investment self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon - self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum + self.maximum_size = maximum_size self.linked_periods = linked_periods def link_to_flow_system(self, flow_system, prefix: str = '') -> None: @@ -1075,6 +1075,12 @@ def link_to_flow_system(self, flow_system, prefix: str = '') -> None: self.piecewise_effects_of_investment.link_to_flow_system(flow_system, self._sub_prefix('PiecewiseEffects')) def transform_data(self) -> None: + # Validate that either fixed_size or maximum_size is set + if self.fixed_size is None and self.maximum_size is None: + raise ValueError( + f'InvestParameters in "{self.prefix}" requires either fixed_size or maximum_size to be set. ' + f'An upper bound is needed to properly scale the optimization model.' + ) self.effects_of_investment = self._fit_effect_coords( prefix=self.prefix, effect_values=self.effects_of_investment, diff --git a/flixopt/io.py b/flixopt/io.py index 38c3e6286..f46cd8723 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -1031,7 +1031,7 @@ def build_repr_from_init( excluded_params: Set of parameter names to exclude (e.g., {'self', 'inputs', 'outputs'}) Default excludes 'self', 'label', and 'kwargs' label_as_positional: If True and 'label' param exists, show it as first positional arg - skip_default_size: If True, skip 'size' parameter when it equals CONFIG.Modeling.big + skip_default_size: Deprecated. Previously skipped size=CONFIG.Modeling.big, now size=None is default. Returns: Formatted repr string like: ClassName("label", param=value) diff --git a/tests/conftest.py b/tests/conftest.py index 49734911a..ee2c0f4e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -213,10 +213,10 @@ def piecewise(): """Piecewise converter from flow_system_piecewise_conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { @@ -233,10 +233,10 @@ def segments(timesteps_length): """Segments converter with time-varying piecewise conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { @@ -614,12 +614,12 @@ def flow_system_long(): ), fx.linear_converters.CHP( 'BHKW2', - thermal_efficiency=0.58, - electrical_efficiency=0.22, + thermal_efficiency=(eta_th := 0.58), + electrical_efficiency=(eta_el := 0.22), status_parameters=fx.StatusParameters(effects_per_startup=24000), - electrical_flow=fx.Flow('P_el', bus='Strom'), - thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), - fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), + fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=(fuel_size := 288), relative_minimum=87 / fuel_size), + electrical_flow=fx.Flow('P_el', bus='Strom', size=fuel_size * eta_el), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=fuel_size * eta_th), ), fx.Storage( 'Speicher', diff --git a/tests/deprecated/conftest.py b/tests/deprecated/conftest.py index b6ec5ac4b..65434f04c 100644 --- a/tests/deprecated/conftest.py +++ b/tests/deprecated/conftest.py @@ -216,10 +216,10 @@ def piecewise(): """Piecewise converter from flow_system_piecewise_conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { @@ -236,10 +236,10 @@ def segments(timesteps_length): """Segments converter with time-varying piecewise conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { @@ -613,12 +613,12 @@ def flow_system_long(): ), fx.linear_converters.CHP( 'BHKW2', - thermal_efficiency=0.58, - electrical_efficiency=0.22, + thermal_efficiency=(eta_th := 0.58), + electrical_efficiency=(eta_el := 0.22), status_parameters=fx.StatusParameters(effects_per_startup=24000), - electrical_flow=fx.Flow('P_el', bus='Strom'), - thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), - fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), + fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=(fuel_size := 288), relative_minimum=87 / fuel_size), + electrical_flow=fx.Flow('P_el', bus='Strom', size=fuel_size * eta_el), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=fuel_size * eta_th), ), fx.Storage( 'Speicher', diff --git a/tests/deprecated/test_component.py b/tests/deprecated/test_component.py index 8cde784c9..497a5c3aa 100644 --- a/tests/deprecated/test_component.py +++ b/tests/deprecated/test_component.py @@ -31,12 +31,12 @@ def test_component(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config inputs = [ - fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), - fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), + fx.Flow('In1', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), + fx.Flow('In2', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), ] outputs = [ - fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.01), - fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out1', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out2', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), ] comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) flow_system.add_elements(comp) @@ -464,7 +464,9 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -498,7 +500,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): size=fx.InvestParameters(effects_of_investment_per_size=5, maximum_size=1000), ), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters()), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters(maximum_size=1000)), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=True, ) @@ -541,7 +543,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -578,7 +582,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): in2=fx.Flow( 'Rohr2a', 'Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=100, minimum_size=10, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=100, minimum_size=10, maximum_size=1000, mandatory=True + ), ), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=False, diff --git a/tests/deprecated/test_effect.py b/tests/deprecated/test_effect.py index 10ae59bcc..b3bb278f0 100644 --- a/tests/deprecated/test_effect.py +++ b/tests/deprecated/test_effect.py @@ -253,7 +253,9 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=10, minimum_size=20, maximum_size=200, mandatory=True + ), ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/deprecated/test_flow.py b/tests/deprecated/test_flow.py index 0a1a03341..594bc1fbb 100644 --- a/tests/deprecated/test_flow.py +++ b/tests/deprecated/test_flow.py @@ -593,6 +593,7 @@ def test_effects_per_active_hour(self, basic_flow_system_linopy_coords, coords_c flow = fx.Flow( 'Wärme', bus='Fernwärme', + size=100, status_parameters=fx.StatusParameters( effects_per_active_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), diff --git a/tests/deprecated/test_functional.py b/tests/deprecated/test_functional.py index c72754003..409e20a5f 100644 --- a/tests/deprecated/test_functional.py +++ b/tests/deprecated/test_functional.py @@ -186,7 +186,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1, maximum_size=100), ), ) ) @@ -227,7 +227,9 @@ def test_size_bounds(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters( + minimum_size=40, maximum_size=100, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ) ) @@ -269,7 +271,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=40, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), @@ -281,7 +287,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=50, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), diff --git a/tests/deprecated/test_scenarios.py b/tests/deprecated/test_scenarios.py index 366429831..b4a1cd161 100644 --- a/tests/deprecated/test_scenarios.py +++ b/tests/deprecated/test_scenarios.py @@ -212,10 +212,10 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> flow_system.add_elements( fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { diff --git a/tests/deprecated/test_storage.py b/tests/deprecated/test_storage.py index a5d2c7a19..15170a321 100644 --- a/tests/deprecated/test_storage.py +++ b/tests/deprecated/test_storage.py @@ -451,6 +451,7 @@ def test_investment_parameters( 'effects_of_investment': 100, 'effects_of_investment_per_size': 10, 'mandatory': mandatory, + 'maximum_size': 100, } if minimum_size is not None: invest_params['minimum_size'] = minimum_size diff --git a/tests/test_component.py b/tests/test_component.py index 741f6390e..66d09aaee 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -31,12 +31,12 @@ def test_component(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config inputs = [ - fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), - fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), + fx.Flow('In1', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), + fx.Flow('In2', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), ] outputs = [ - fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.01), - fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out1', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out2', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), ] comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) flow_system.add_elements(comp) @@ -464,7 +464,9 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -498,7 +500,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): size=fx.InvestParameters(effects_of_investment_per_size=5, maximum_size=1000), ), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters()), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters(maximum_size=1000)), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=True, ) @@ -537,7 +539,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -574,7 +578,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): in2=fx.Flow( 'Rohr2a', 'Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=100, minimum_size=10, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=100, minimum_size=10, maximum_size=1000, mandatory=True + ), ), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=False, diff --git a/tests/test_effect.py b/tests/test_effect.py index 7dcac9e1c..015e054eb 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -251,7 +251,9 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config, highs_solv thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=10, minimum_size=20, maximum_size=200, mandatory=True + ), ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/test_flow.py b/tests/test_flow.py index 0a1a03341..594bc1fbb 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -593,6 +593,7 @@ def test_effects_per_active_hour(self, basic_flow_system_linopy_coords, coords_c flow = fx.Flow( 'Wärme', bus='Fernwärme', + size=100, status_parameters=fx.StatusParameters( effects_per_active_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), diff --git a/tests/test_functional.py b/tests/test_functional.py index 8cf67cff9..6d0f8a8fc 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -185,7 +185,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1, maximum_size=100), ), ) ) @@ -224,7 +224,9 @@ def test_size_bounds(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters( + minimum_size=40, maximum_size=100, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ) ) @@ -264,7 +266,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=40, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), @@ -276,7 +282,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=50, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 366429831..b4a1cd161 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -212,10 +212,10 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> flow_system.add_elements( fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { diff --git a/tests/test_storage.py b/tests/test_storage.py index a5d2c7a19..15170a321 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -451,6 +451,7 @@ def test_investment_parameters( 'effects_of_investment': 100, 'effects_of_investment_per_size': 10, 'mandatory': mandatory, + 'maximum_size': 100, } if minimum_size is not None: invest_params['minimum_size'] = minimum_size