From 7a822c3120320bacbb77b41e0dd62ab84407a470 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:17:07 +0100 Subject: [PATCH 01/25] New lower case parameter names for flows in linear_converters.py --- flixopt/linear_converters.py | 347 ++++++++++++++++++++--------------- 1 file changed, 200 insertions(+), 147 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 046fcbd51..76aa25a47 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -34,11 +34,13 @@ class Boiler(LinearConverter): label: The label of the Element. Used to identify it in the FlowSystem. eta: Thermal efficiency factor (0-1 range). Defines the ratio of thermal output to fuel input energy content. - Q_fu: Fuel input-flow representing fuel consumption. - Q_th: Thermal output-flow representing heat generation. + fuel_flow: Fuel input-flow representing fuel consumption. + thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + Q_fu: *Deprecated*. Use `fuel_flow` instead. + Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: Natural gas boiler: @@ -47,8 +49,8 @@ class Boiler(LinearConverter): gas_boiler = Boiler( label='natural_gas_boiler', eta=0.85, # 85% thermal efficiency - Q_fu=natural_gas_flow, - Q_th=hot_water_flow, + fuel_flow=natural_gas_flow, + thermal_flow=hot_water_flow, ) ``` @@ -58,8 +60,8 @@ class Boiler(LinearConverter): biomass_boiler = Boiler( label='wood_chip_boiler', eta=seasonal_efficiency_profile, # Time-varying efficiency - Q_fu=biomass_flow, - Q_th=district_heat_flow, + fuel_flow=biomass_flow, + thermal_flow=district_heat_flow, on_off_parameters=OnOffParameters( consecutive_on_hours_min=4, # Minimum 4-hour operation effects_per_switch_on={'startup_fuel': 50}, # Startup fuel penalty @@ -68,7 +70,7 @@ class Boiler(LinearConverter): ``` Note: - The conversion relationship is: Q_th = Q_fu × eta + The conversion relationship is: thermal_flow = fuel_flow × eta Efficiency should be between 0 and 1, where 1 represents perfect conversion (100% of fuel energy converted to useful thermal output). @@ -78,30 +80,36 @@ def __init__( self, label: str, eta: Numeric_TPS, - Q_fu: Flow, - Q_th: Flow, + fuel_flow: Flow | None = None, + thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, + **kwargs, ): + # Handle deprecated parameters + fuel_flow = self._handle_deprecated_kwarg(kwargs, 'Q_fu', 'fuel_flow', fuel_flow) + thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + self._validate_kwargs(kwargs) + super().__init__( label, - inputs=[Q_fu], - outputs=[Q_th], - conversion_factors=[{Q_fu.label: eta, Q_th.label: 1}], + inputs=[fuel_flow], + outputs=[thermal_flow], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.Q_fu = Q_fu - self.Q_th = Q_th + self.fuel_flow = fuel_flow + self.thermal_flow = thermal_flow + self.eta = eta # Uses setter @property def eta(self): - return self.conversion_factors[0][self.Q_fu.label] + return self.conversion_factors[0][self.fuel_flow.label] @eta.setter def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_fu.label] = value + self.conversion_factors = [{self.fuel_flow.label: value, self.thermal_flow.label: 1}] @register_class_for_io @@ -119,11 +127,13 @@ class Power2Heat(LinearConverter): eta: Thermal efficiency factor (0-1 range). For resistance heating this is typically close to 1.0 (nearly 100% efficiency), but may be lower for electrode boilers or systems with distribution losses. - P_el: Electrical input-flow representing electricity consumption. - Q_th: Thermal output-flow representing heat generation. + power_flow: Electrical input-flow representing electricity consumption. + thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + P_el: *Deprecated*. Use `power_flow` instead. + Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: Electric resistance heater: @@ -132,8 +142,8 @@ class Power2Heat(LinearConverter): electric_heater = Power2Heat( label='resistance_heater', eta=0.98, # 98% efficiency (small losses) - P_el=electricity_flow, - Q_th=space_heating_flow, + power_flow=electricity_flow, + thermal_flow=space_heating_flow, ) ``` @@ -143,8 +153,8 @@ class Power2Heat(LinearConverter): electrode_boiler = Power2Heat( label='electrode_steam_boiler', eta=0.95, # 95% efficiency including boiler losses - P_el=industrial_electricity, - Q_th=process_steam_flow, + power_flow=industrial_electricity, + thermal_flow=process_steam_flow, on_off_parameters=OnOffParameters( consecutive_on_hours_min=1, # Minimum 1-hour operation effects_per_switch_on={'startup_cost': 100}, @@ -153,7 +163,7 @@ class Power2Heat(LinearConverter): ``` Note: - The conversion relationship is: Q_th = P_el × eta + The conversion relationship is: thermal_flow = power_flow × eta Unlike heat pumps, Power2Heat systems cannot exceed 100% efficiency (eta ≤ 1.0) as they only convert electrical energy without extracting additional energy @@ -165,31 +175,37 @@ def __init__( self, label: str, eta: Numeric_TPS, - P_el: Flow, - Q_th: Flow, + power_flow: Flow | None = None, + thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, + **kwargs, ): + # Handle deprecated parameters + power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + self._validate_kwargs(kwargs) + super().__init__( label, - inputs=[P_el], - outputs=[Q_th], - conversion_factors=[{P_el.label: eta, Q_th.label: 1}], + inputs=[power_flow], + outputs=[thermal_flow], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.P_el = P_el - self.Q_th = Q_th + self.power_flow = power_flow + self.thermal_flow = thermal_flow + self.eta = eta # Uses setter @property def eta(self): - return self.conversion_factors[0][self.P_el.label] + return self.conversion_factors[0][self.power_flow.label] @eta.setter def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) - self.conversion_factors[0][self.P_el.label] = value + self.conversion_factors = [{self.power_flow.label: value, self.thermal_flow.label: 1}] @register_class_for_io @@ -204,14 +220,17 @@ class HeatPump(LinearConverter): Args: label: The label of the Element. Used to identify it in the FlowSystem. - COP: Coefficient of Performance (typically 1-20 range). Defines the ratio of + cop: Coefficient of Performance (typically 1-20 range). Defines the ratio of thermal output to electrical input. COP > 1 indicates the heat pump extracts additional energy from the environment. - P_el: Electrical input-flow representing electricity consumption. - Q_th: Thermal output-flow representing heat generation. + power_flow: Electrical input-flow representing electricity consumption. + thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + COP: *Deprecated*. Use `cop` instead. + P_el: *Deprecated*. Use `power_flow` instead. + Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: Air-source heat pump with constant COP: @@ -219,9 +238,9 @@ class HeatPump(LinearConverter): ```python air_hp = HeatPump( label='air_source_heat_pump', - COP=3.5, # COP of 3.5 (350% efficiency) - P_el=electricity_flow, - Q_th=heating_flow, + cop=3.5, # COP of 3.5 (350% efficiency) + power_flow=electricity_flow, + thermal_flow=heating_flow, ) ``` @@ -230,9 +249,9 @@ class HeatPump(LinearConverter): ```python ground_hp = HeatPump( label='geothermal_heat_pump', - COP=temperature_dependent_cop, # Time-varying COP based on ground temp - P_el=electricity_flow, - Q_th=radiant_heating_flow, + cop=temperature_dependent_cop, # Time-varying COP based on ground temp + power_flow=electricity_flow, + thermal_flow=radiant_heating_flow, on_off_parameters=OnOffParameters( consecutive_on_hours_min=2, # Avoid frequent cycling effects_per_running_hour={'maintenance': 0.5}, @@ -241,7 +260,7 @@ class HeatPump(LinearConverter): ``` Note: - The conversion relationship is: Q_th = P_el × COP + The conversion relationship is: thermal_flow = power_flow × COP COP should be greater than 1 for realistic heat pump operation, with typical values ranging from 2-6 depending on technology and operating conditions. @@ -251,32 +270,39 @@ class HeatPump(LinearConverter): def __init__( self, label: str, - COP: Numeric_TPS, - P_el: Flow, - Q_th: Flow, + cop: Numeric_TPS, + power_flow: Flow | None = None, + thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, + **kwargs, ): + # Handle deprecated parameters + power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + cop = self._handle_deprecated_kwarg(kwargs, 'COP', 'cop', cop) + self._validate_kwargs(kwargs) + super().__init__( label, - inputs=[P_el], - outputs=[Q_th], - conversion_factors=[{P_el.label: COP, Q_th.label: 1}], + inputs=[power_flow], + outputs=[thermal_flow], + conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.P_el = P_el - self.Q_th = Q_th - self.COP = COP + self.power_flow = power_flow + self.thermal_flow = thermal_flow + self.cop = cop # Uses setter @property - def COP(self): # noqa: N802 - return self.conversion_factors[0][self.P_el.label] + def cop(self): + return self.conversion_factors[0][self.power_flow.label] - @COP.setter - def COP(self, value): # noqa: N802 - check_bounds(value, 'COP', self.label_full, 1, 20) - self.conversion_factors[0][self.P_el.label] = value + @cop.setter + def cop(self, value): + check_bounds(value, 'cop', self.label_full, 1, 20) + self.conversion_factors = [{self.power_flow.label: value, self.thermal_flow.label: 1}] @register_class_for_io @@ -294,11 +320,13 @@ class CoolingTower(LinearConverter): specific_electricity_demand: Auxiliary electricity demand per unit of cooling power (dimensionless, typically 0.01-0.05 range). Represents the fraction of thermal power that must be supplied as electricity for fans and pumps. - P_el: Electrical input-flow representing electricity consumption for fans/pumps. - Q_th: Thermal input-flow representing waste heat to be rejected to environment. + power_flow: Electrical input-flow representing electricity consumption for fans/pumps. + thermal_flow: Thermal input-flow representing waste heat to be rejected to environment. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + P_el: *Deprecated*. Use `power_flow` instead. + Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: Industrial cooling tower: @@ -307,8 +335,8 @@ class CoolingTower(LinearConverter): cooling_tower = CoolingTower( label='process_cooling_tower', specific_electricity_demand=0.025, # 2.5% auxiliary power - P_el=cooling_electricity, - Q_th=waste_heat_flow, + power_flow=cooling_electricity, + thermal_flow=waste_heat_flow, ) ``` @@ -318,8 +346,8 @@ class CoolingTower(LinearConverter): condenser_cooling = CoolingTower( label='power_plant_cooling', specific_electricity_demand=0.015, # 1.5% auxiliary power - P_el=auxiliary_electricity, - Q_th=condenser_waste_heat, + power_flow=auxiliary_electricity, + thermal_flow=condenser_waste_heat, on_off_parameters=OnOffParameters( consecutive_on_hours_min=4, # Minimum operation time effects_per_running_hour={'water_consumption': 2.5}, # m³/h @@ -328,7 +356,7 @@ class CoolingTower(LinearConverter): ``` Note: - The conversion relationship is: P_el = Q_th × specific_electricity_demand + The conversion relationship is: power_flow = thermal_flow × specific_electricity_demand The cooling tower consumes electrical power proportional to the thermal load. No thermal energy is produced - all thermal input is rejected to the environment. @@ -341,33 +369,37 @@ def __init__( self, label: str, specific_electricity_demand: Numeric_TPS, - P_el: Flow, - Q_th: Flow, + power_flow: Flow | None = None, + thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, + **kwargs, ): + # Handle deprecated parameters + power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + self._validate_kwargs(kwargs) + super().__init__( label, - inputs=[P_el, Q_th], + inputs=[power_flow, thermal_flow], outputs=[], - conversion_factors=[{P_el.label: -1, Q_th.label: specific_electricity_demand}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.P_el = P_el - self.Q_th = Q_th - - check_bounds(specific_electricity_demand, 'specific_electricity_demand', self.label_full, 0, 1) + self.power_flow = power_flow + self.thermal_flow = thermal_flow + self.specific_electricity_demand = specific_electricity_demand # Uses setter @property def specific_electricity_demand(self): - return self.conversion_factors[0][self.Q_th.label] + return self.conversion_factors[0][self.thermal_flow.label] @specific_electricity_demand.setter def specific_electricity_demand(self, value): check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_th.label] = value + self.conversion_factors = [{self.power_flow.label: -1, self.thermal_flow.label: value}] @register_class_for_io @@ -386,12 +418,15 @@ class CHP(LinearConverter): energy converted to useful thermal output. eta_el: Electrical efficiency factor (0-1 range). Defines the fraction of fuel energy converted to electrical output. - Q_fu: Fuel input-flow representing fuel consumption. - P_el: Electrical output-flow representing electricity generation. - Q_th: Thermal output-flow representing heat generation. + fuel_flow: Fuel input-flow representing fuel consumption. + power_flow: Electrical output-flow representing electricity generation. + thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + Q_fu: *Deprecated*. Use `fuel_flow` instead. + P_el: *Deprecated*. Use `power_flow` instead. + Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: Natural gas CHP unit: @@ -401,9 +436,9 @@ class CHP(LinearConverter): label='natural_gas_chp', eta_th=0.45, # 45% thermal efficiency eta_el=0.35, # 35% electrical efficiency (80% total) - Q_fu=natural_gas_flow, - P_el=electricity_flow, - Q_th=district_heat_flow, + fuel_flow=natural_gas_flow, + power_flow=electricity_flow, + thermal_flow=district_heat_flow, ) ``` @@ -414,9 +449,9 @@ class CHP(LinearConverter): label='industrial_chp', eta_th=0.40, eta_el=0.38, - Q_fu=fuel_gas_flow, - P_el=plant_electricity, - Q_th=process_steam, + fuel_flow=fuel_gas_flow, + power_flow=plant_electricity, + thermal_flow=process_steam, on_off_parameters=OnOffParameters( consecutive_on_hours_min=8, # Minimum 8-hour operation effects_per_switch_on={'startup_cost': 5000}, @@ -427,8 +462,8 @@ class CHP(LinearConverter): Note: The conversion relationships are: - - Q_th = Q_fu × eta_th (thermal output) - - P_el = Q_fu × eta_el (electrical output) + - thermal_flow = fuel_flow × eta_th (thermal output) + - power_flow = fuel_flow × eta_el (electrical output) Total efficiency (eta_th + eta_el) should be ≤ 1.0, with typical combined efficiencies of 80-90% for modern CHP units. This provides significant @@ -440,47 +475,61 @@ def __init__( label: str, eta_th: Numeric_TPS, eta_el: Numeric_TPS, - Q_fu: Flow, - P_el: Flow, - Q_th: Flow, + fuel_flow: Flow | None = None, + power_flow: Flow | None = None, + thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, + **kwargs, ): - heat = {Q_fu.label: eta_th, Q_th.label: 1} - electricity = {Q_fu.label: eta_el, P_el.label: 1} + # Handle deprecated parameters + fuel_flow = self._handle_deprecated_kwarg(kwargs, 'Q_fu', 'fuel_flow', fuel_flow) + power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + self._validate_kwargs(kwargs) super().__init__( label, - inputs=[Q_fu], - outputs=[Q_th, P_el], - conversion_factors=[heat, electricity], + inputs=[fuel_flow], + outputs=[thermal_flow, power_flow], + conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.Q_fu = Q_fu - self.P_el = P_el - self.Q_th = Q_th + self.fuel_flow = fuel_flow + self.power_flow = power_flow + self.thermal_flow = thermal_flow + self.eta_th = eta_th # Uses setter + self.eta_el = eta_el # Uses setter check_bounds(eta_el + eta_th, 'eta_th+eta_el', self.label_full, 0, 1) @property def eta_th(self): - return self.conversion_factors[0][self.Q_fu.label] + return self.conversion_factors[0][self.fuel_flow.label] @eta_th.setter def eta_th(self, value): check_bounds(value, 'eta_th', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_fu.label] = value + if len(self.conversion_factors) < 2: + # Initialize structure if not yet set + self.conversion_factors = [{self.fuel_flow.label: value, self.thermal_flow.label: 1}, {}] + else: + self.conversion_factors[0] = {self.fuel_flow.label: value, self.thermal_flow.label: 1} @property def eta_el(self): - return self.conversion_factors[1][self.Q_fu.label] + return self.conversion_factors[1][self.fuel_flow.label] @eta_el.setter def eta_el(self, value): check_bounds(value, 'eta_el', self.label_full, 0, 1) - self.conversion_factors[1][self.Q_fu.label] = value + if len(self.conversion_factors) < 2: + # Initialize structure if not yet set + self.conversion_factors = [{}, {self.fuel_flow.label: value, self.power_flow.label: 1}] + else: + self.conversion_factors[1] = {self.fuel_flow.label: value, self.power_flow.label: 1} @register_class_for_io @@ -495,16 +544,20 @@ class HeatPumpWithSource(LinearConverter): Args: label: The label of the Element. Used to identify it in the FlowSystem. - COP: Coefficient of Performance (typically 1-20 range). Defines the ratio of + cop: Coefficient of Performance (typically 1-20 range). Defines the ratio of thermal output to electrical input. The heat source extraction is automatically - calculated as Q_ab = Q_th × (COP-1)/COP. - P_el: Electrical input-flow representing electricity consumption for compressor. - Q_ab: Heat source input-flow representing thermal energy extracted from environment + calculated as heat_source_flow = thermal_flow × (COP-1)/COP. + power_flow: Electrical input-flow representing electricity consumption for compressor. + heat_source_flow: Heat source input-flow representing thermal energy extracted from environment (ground, air, water source). - Q_th: Thermal output-flow representing useful heat delivered to the application. + thermal_flow: Thermal output-flow representing useful heat delivered to the application. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + COP: *Deprecated*. Use `cop` instead. + P_el: *Deprecated*. Use `power_flow` instead. + Q_ab: *Deprecated*. Use `heat_source_flow` instead. + Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: Ground-source heat pump with explicit ground coupling: @@ -512,10 +565,10 @@ class HeatPumpWithSource(LinearConverter): ```python ground_source_hp = HeatPumpWithSource( label='geothermal_heat_pump', - COP=4.5, # High COP due to stable ground temperature - P_el=electricity_flow, - Q_ab=ground_heat_extraction, # Heat extracted from ground loop - Q_th=building_heating_flow, + cop=4.5, # High COP due to stable ground temperature + power_flow=electricity_flow, + heat_source_flow=ground_heat_extraction, # Heat extracted from ground loop + thermal_flow=building_heating_flow, ) ``` @@ -524,10 +577,10 @@ class HeatPumpWithSource(LinearConverter): ```python waste_heat_pump = HeatPumpWithSource( label='waste_heat_pump', - COP=temperature_dependent_cop, # Varies with temperature of heat source - P_el=electricity_consumption, - Q_ab=industrial_heat_extraction, # Heat extracted from a industrial process or waste water - Q_th=heat_supply, + cop=temperature_dependent_cop, # Varies with temperature of heat source + power_flow=electricity_consumption, + heat_source_flow=industrial_heat_extraction, # Heat extracted from a industrial process or waste water + thermal_flow=heat_supply, on_off_parameters=OnOffParameters( consecutive_on_hours_min=0.5, # 30-minute minimum runtime effects_per_switch_on={'costs': 1000}, @@ -537,9 +590,9 @@ class HeatPumpWithSource(LinearConverter): Note: The conversion relationships are: - - Q_th = P_el × COP (thermal output from electrical input) - - Q_ab = Q_th × (COP-1)/COP (heat source extraction) - - Energy balance: Q_th = P_el + Q_ab + - thermal_flow = power_flow × COP (thermal output from electrical input) + - heat_source_flow = thermal_flow × (COP-1)/COP (heat source extraction) + - Energy balance: thermal_flow = power_flow + heat_source_flow This formulation explicitly tracks the heat source, which is important for systems where the source capacity or temperature is limited, @@ -552,40 +605,46 @@ class HeatPumpWithSource(LinearConverter): def __init__( self, label: str, - COP: Numeric_TPS, - P_el: Flow, - Q_ab: Flow, - Q_th: Flow, + cop: Numeric_TPS, + power_flow: Flow | None = None, + heat_source_flow: Flow | None = None, + thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, + **kwargs, ): + # Handle deprecated parameters + power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + heat_source_flow = self._handle_deprecated_kwarg(kwargs, 'Q_ab', 'heat_source_flow', heat_source_flow) + thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + cop = self._handle_deprecated_kwarg(kwargs, 'COP', 'cop', cop) + self._validate_kwargs(kwargs) + super().__init__( label, - inputs=[P_el, Q_ab], - outputs=[Q_th], - conversion_factors=[{P_el.label: COP, Q_th.label: 1}, {Q_ab.label: COP / (COP - 1), Q_th.label: 1}], + inputs=[power_flow, heat_source_flow], + outputs=[thermal_flow], + conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.P_el = P_el - self.Q_ab = Q_ab - self.Q_th = Q_th - - if np.any(np.asarray(self.COP) <= 1): - raise ValueError(f'{self.label_full}.COP must be strictly > 1 for HeatPumpWithSource.') + self.power_flow = power_flow + self.heat_source_flow = heat_source_flow + self.thermal_flow = thermal_flow + self.cop = cop # Uses setter @property - def COP(self): # noqa: N802 - return self.conversion_factors[0][self.P_el.label] + def cop(self): # noqa: N802 + return self.conversion_factors[0][self.power_flow.label] - @COP.setter - def COP(self, value): # noqa: N802 - check_bounds(value, 'COP', self.label_full, 1, 20) + @cop.setter + def cop(self, value): # noqa: N802 + check_bounds(value, 'cop', self.label_full, 1, 20) if np.any(np.asarray(value) <= 1): - raise ValueError(f'{self.label_full}.COP must be strictly > 1 for HeatPumpWithSource.') + raise ValueError(f'{self.label_full}.cop must be strictly > 1 for HeatPumpWithSource.') self.conversion_factors = [ - {self.P_el.label: value, self.Q_th.label: 1}, - {self.Q_ab.label: value / (value - 1), self.Q_th.label: 1}, + {self.power_flow.label: value, self.thermal_flow.label: 1}, + {self.heat_source_flow.label: value / (value - 1), self.thermal_flow.label: 1}, ] @@ -612,19 +671,13 @@ def check_bounds( lower_bound = lower_bound.data if isinstance(upper_bound, TimeSeriesData): upper_bound = upper_bound.data - - # Convert to NumPy arrays to handle xr.DataArray, pd.Series, pd.DataFrame - value_arr = np.asarray(value) - lower_arr = np.asarray(lower_bound) - upper_arr = np.asarray(upper_bound) - - if not np.all(value_arr > lower_arr): + if not np.all(value > lower_bound): logger.warning( f"'{element_label}.{parameter_label}' is equal or below the common lower bound {lower_bound}." - f' {parameter_label}.min={np.min(value_arr)}; {parameter_label}={value}' + f' {parameter_label}.min={np.min(value)}; {parameter_label}={value}' ) - if not np.all(value_arr < upper_arr): + if not np.all(value < upper_bound): logger.warning( f"'{element_label}.{parameter_label}' exceeds or matches the common upper bound {upper_bound}." - f' {parameter_label}.max={np.max(value_arr)}; {parameter_label}={value}' + f' {parameter_label}.max={np.max(value)}; {parameter_label}={value}' ) From bcead8afcf0aff3f9ac23dc5b72862a1a71bdb93 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:22:14 +0100 Subject: [PATCH 02/25] Improve setting of conversion factors --- flixopt/linear_converters.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 76aa25a47..b5bdb5169 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -492,7 +492,7 @@ def __init__( label, inputs=[fuel_flow], outputs=[thermal_flow, power_flow], - conversion_factors=[], + conversion_factors=[{}, {}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) @@ -512,11 +512,7 @@ def eta_th(self): @eta_th.setter def eta_th(self, value): check_bounds(value, 'eta_th', self.label_full, 0, 1) - if len(self.conversion_factors) < 2: - # Initialize structure if not yet set - self.conversion_factors = [{self.fuel_flow.label: value, self.thermal_flow.label: 1}, {}] - else: - self.conversion_factors[0] = {self.fuel_flow.label: value, self.thermal_flow.label: 1} + self.conversion_factors[0] = {self.fuel_flow.label: value, self.thermal_flow.label: 1} @property def eta_el(self): @@ -525,11 +521,7 @@ def eta_el(self): @eta_el.setter def eta_el(self, value): check_bounds(value, 'eta_el', self.label_full, 0, 1) - if len(self.conversion_factors) < 2: - # Initialize structure if not yet set - self.conversion_factors = [{}, {self.fuel_flow.label: value, self.power_flow.label: 1}] - else: - self.conversion_factors[1] = {self.fuel_flow.label: value, self.power_flow.label: 1} + self.conversion_factors[1] = {self.fuel_flow.label: value, self.power_flow.label: 1} @register_class_for_io @@ -624,7 +616,6 @@ def __init__( label, inputs=[power_flow, heat_source_flow], outputs=[thermal_flow], - conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) @@ -634,14 +625,14 @@ def __init__( self.cop = cop # Uses setter @property - def cop(self): # noqa: N802 + def cop(self): return self.conversion_factors[0][self.power_flow.label] @cop.setter - def cop(self, value): # noqa: N802 + def cop(self, value): check_bounds(value, 'cop', self.label_full, 1, 20) - if np.any(np.asarray(value) <= 1): - raise ValueError(f'{self.label_full}.cop must be strictly > 1 for HeatPumpWithSource.') + if np.any(np.asarray(value) == 1): + raise ValueError(f'{self.label_full}.cop must be strictly !=1 for HeatPumpWithSource.') self.conversion_factors = [ {self.power_flow.label: value, self.thermal_flow.label: 1}, {self.heat_source_flow.label: value / (value - 1), self.thermal_flow.label: 1}, From b45618bf9f6f4b6ab8e6a7a537111722dc875eb1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:25:07 +0100 Subject: [PATCH 03/25] Add deprectaed parameter access --- flixopt/linear_converters.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index b5bdb5169..c1dfd66e3 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging +import warnings from typing import TYPE_CHECKING import numpy as np @@ -638,6 +639,15 @@ def cop(self, value): {self.heat_source_flow.label: value / (value - 1), self.thermal_flow.label: 1}, ] + @property + def Q_th(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_flow + def check_bounds( value: Numeric_TPS, From 46f8f6c9100f9ab7d0cc9961bd774d6727d6177e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:30:51 +0100 Subject: [PATCH 04/25] Add deprectaed parameter access --- flixopt/linear_converters.py | 135 +++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index c1dfd66e3..8b96f9185 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -112,6 +112,24 @@ def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) self.conversion_factors = [{self.fuel_flow.label: value, self.thermal_flow.label: 1}] + @property + def Q_fu(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_fu" property is deprecated. Use "fuel_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.fuel_flow + + @property + def Q_th(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_flow + @register_class_for_io class Power2Heat(LinearConverter): @@ -208,6 +226,24 @@ def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) self.conversion_factors = [{self.power_flow.label: value, self.thermal_flow.label: 1}] + @property + def P_el(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.power_flow + + @property + def Q_th(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_flow + @register_class_for_io class HeatPump(LinearConverter): @@ -305,6 +341,33 @@ def cop(self, value): check_bounds(value, 'cop', self.label_full, 1, 20) self.conversion_factors = [{self.power_flow.label: value, self.thermal_flow.label: 1}] + @property + def COP(self) -> Numeric_TPS: # noqa: N802 + warnings.warn( + 'The "COP" property is deprecated. Use "cop" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.cop + + @property + def P_el(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.power_flow + + @property + def Q_th(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_flow + @register_class_for_io class CoolingTower(LinearConverter): @@ -402,6 +465,24 @@ def specific_electricity_demand(self, value): check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1) self.conversion_factors = [{self.power_flow.label: -1, self.thermal_flow.label: value}] + @property + def P_el(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.power_flow + + @property + def Q_th(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_flow + @register_class_for_io class CHP(LinearConverter): @@ -524,6 +605,33 @@ def eta_el(self, value): check_bounds(value, 'eta_el', self.label_full, 0, 1) self.conversion_factors[1] = {self.fuel_flow.label: value, self.power_flow.label: 1} + @property + def Q_fu(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_fu" property is deprecated. Use "fuel_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.fuel_flow + + @property + def P_el(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.power_flow + + @property + def Q_th(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_flow + @register_class_for_io class HeatPumpWithSource(LinearConverter): @@ -639,6 +747,33 @@ def cop(self, value): {self.heat_source_flow.label: value / (value - 1), self.thermal_flow.label: 1}, ] + @property + def COP(self) -> Numeric_TPS: # noqa: N802 + warnings.warn( + 'The "COP" property is deprecated. Use "cop" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.cop + + @property + def P_el(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.power_flow + + @property + def Q_ab(self) -> Flow: # noqa: N802 + warnings.warn( + 'The "Q_ab" property is deprecated. Use "heat_source_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.heat_source_flow + @property def Q_th(self) -> Flow: # noqa: N802 warnings.warn( From b1f6f6c4ccf9930b6792a3a9a4ff4980216d04f8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:32:12 +0100 Subject: [PATCH 05/25] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd11fb442..40bf00825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,8 +70,17 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### ♻️ Changed - **Code structure**: Removed `commons.py` module and moved all imports directly to `__init__.py` for cleaner code organization (no public API changes) - **Type handling improvements**: Updated internal data handling to work seamlessly with the new type system +- **Parameter renaming in `linear_converters.py`**: Renamed parameters to use lowercase, descriptive names for better consistency: + - `Boiler`: `Q_fu` → `fuel_flow`, `Q_th` → `thermal_flow` + - `Power2Heat`: `P_el` → `power_flow`, `Q_th` → `thermal_flow` + - `HeatPump`: `COP` → `cop`, `P_el` → `power_flow`, `Q_th` → `thermal_flow` + - `CoolingTower`: `P_el` → `power_flow`, `Q_th` → `thermal_flow` + - `CHP`: `Q_fu` → `fuel_flow`, `P_el` → `power_flow`, `Q_th` → `thermal_flow` + - `HeatPumpWithSource`: `COP` → `cop`, `P_el` → `power_flow`, `Q_ab` → `heat_source_flow`, `Q_th` → `thermal_flow` ### 🗑️ Deprecated +- **Old parameter names in `linear_converters.py`**: The old uppercase parameter names are now deprecated and accessible as properties that emit `DeprecationWarning`. They will be removed in v4.0.0: + - `Q_fu`, `Q_th`, `P_el`, `COP`, `Q_ab` (use lowercase equivalents instead) ### 🔥 Removed From 59f82cc2be6bfc022a62e70d7573f04c8b64b239 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:11:36 +0100 Subject: [PATCH 06/25] Add setters --- flixopt/linear_converters.py | 170 +++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 5 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 8b96f9185..cb7595ccf 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -121,6 +121,15 @@ def Q_fu(self) -> Flow: # noqa: N802 ) return self.fuel_flow + @Q_fu.setter + def Q_fu(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_fu" property is deprecated. Use "fuel_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.fuel_flow = value + @property def Q_th(self) -> Flow: # noqa: N802 warnings.warn( @@ -130,6 +139,15 @@ def Q_th(self) -> Flow: # noqa: N802 ) return self.thermal_flow + @Q_th.setter + def Q_th(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_flow = value + @register_class_for_io class Power2Heat(LinearConverter): @@ -235,6 +253,15 @@ def P_el(self) -> Flow: # noqa: N802 ) return self.power_flow + @P_el.setter + def P_el(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.power_flow = value + @property def Q_th(self) -> Flow: # noqa: N802 warnings.warn( @@ -244,6 +271,15 @@ def Q_th(self) -> Flow: # noqa: N802 ) return self.thermal_flow + @Q_th.setter + def Q_th(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_flow = value + @register_class_for_io class HeatPump(LinearConverter): @@ -350,6 +386,15 @@ def COP(self) -> Numeric_TPS: # noqa: N802 ) return self.cop + @COP.setter + def COP(self, value: Numeric_TPS) -> None: # noqa: N802 + warnings.warn( + 'The "COP" property is deprecated. Use "cop" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.cop = value + @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( @@ -359,6 +404,15 @@ def P_el(self) -> Flow: # noqa: N802 ) return self.power_flow + @P_el.setter + def P_el(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.power_flow = value + @property def Q_th(self) -> Flow: # noqa: N802 warnings.warn( @@ -368,6 +422,15 @@ def Q_th(self) -> Flow: # noqa: N802 ) return self.thermal_flow + @Q_th.setter + def Q_th(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_flow = value + @register_class_for_io class CoolingTower(LinearConverter): @@ -474,6 +537,15 @@ def P_el(self) -> Flow: # noqa: N802 ) return self.power_flow + @P_el.setter + def P_el(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.power_flow = value + @property def Q_th(self) -> Flow: # noqa: N802 warnings.warn( @@ -483,6 +555,15 @@ def Q_th(self) -> Flow: # noqa: N802 ) return self.thermal_flow + @Q_th.setter + def Q_th(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_flow = value + @register_class_for_io class CHP(LinearConverter): @@ -614,6 +695,15 @@ def Q_fu(self) -> Flow: # noqa: N802 ) return self.fuel_flow + @Q_fu.setter + def Q_fu(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_fu" property is deprecated. Use "fuel_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.fuel_flow = value + @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( @@ -623,6 +713,15 @@ def P_el(self) -> Flow: # noqa: N802 ) return self.power_flow + @P_el.setter + def P_el(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.power_flow = value + @property def Q_th(self) -> Flow: # noqa: N802 warnings.warn( @@ -632,6 +731,15 @@ def Q_th(self) -> Flow: # noqa: N802 ) return self.thermal_flow + @Q_th.setter + def Q_th(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_flow = value + @register_class_for_io class HeatPumpWithSource(LinearConverter): @@ -740,7 +848,9 @@ def cop(self): @cop.setter def cop(self, value): check_bounds(value, 'cop', self.label_full, 1, 20) - if np.any(np.asarray(value) == 1): + # Unwrap TimeSeriesData before numpy comparison (consistent with check_bounds) + ts_value = value.data if isinstance(value, TimeSeriesData) else value + if np.any(np.asarray(ts_value) == 1): raise ValueError(f'{self.label_full}.cop must be strictly !=1 for HeatPumpWithSource.') self.conversion_factors = [ {self.power_flow.label: value, self.thermal_flow.label: 1}, @@ -756,6 +866,15 @@ def COP(self) -> Numeric_TPS: # noqa: N802 ) return self.cop + @COP.setter + def COP(self, value: Numeric_TPS) -> None: # noqa: N802 + warnings.warn( + 'The "COP" property is deprecated. Use "cop" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.cop = value + @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( @@ -765,6 +884,15 @@ def P_el(self) -> Flow: # noqa: N802 ) return self.power_flow + @P_el.setter + def P_el(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "P_el" property is deprecated. Use "power_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.power_flow = value + @property def Q_ab(self) -> Flow: # noqa: N802 warnings.warn( @@ -774,6 +902,15 @@ def Q_ab(self) -> Flow: # noqa: N802 ) return self.heat_source_flow + @Q_ab.setter + def Q_ab(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_ab" property is deprecated. Use "heat_source_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.heat_source_flow = value + @property def Q_th(self) -> Flow: # noqa: N802 warnings.warn( @@ -783,6 +920,15 @@ def Q_th(self) -> Flow: # noqa: N802 ) return self.thermal_flow + @Q_th.setter + def Q_th(self, value: Flow) -> None: # noqa: N802 + warnings.warn( + 'The "Q_th" property is deprecated. Use "thermal_flow" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_flow = value + def check_bounds( value: Numeric_TPS, @@ -807,13 +953,27 @@ def check_bounds( lower_bound = lower_bound.data if isinstance(upper_bound, TimeSeriesData): upper_bound = upper_bound.data + + # Convert to array for shape and statistics + value_arr = np.asarray(value) + if not np.all(value > lower_bound): + # Log shape and statistics instead of full array to avoid verbose output + if value_arr.size > 1: + value_info = f'shape={value_arr.shape}, min={np.min(value)}' + else: + value_info = f'{value}' logger.warning( - f"'{element_label}.{parameter_label}' is equal or below the common lower bound {lower_bound}." - f' {parameter_label}.min={np.min(value)}; {parameter_label}={value}' + f"'{element_label}.{parameter_label}' is equal or below the common lower bound {lower_bound}. " + f'{parameter_label}: {value_info}' ) if not np.all(value < upper_bound): + # Log shape and statistics instead of full array to avoid verbose output + if value_arr.size > 1: + value_info = f'shape={value_arr.shape}, max={np.max(value)}' + else: + value_info = f'{value}' logger.warning( - f"'{element_label}.{parameter_label}' exceeds or matches the common upper bound {upper_bound}." - f' {parameter_label}.max={np.max(value)}; {parameter_label}={value}' + f"'{element_label}.{parameter_label}' exceeds or matches the common upper bound {upper_bound}. " + f'{parameter_label}: {value_info}' ) From 8a46d03f465c7b5eba3adf4f946027d25a0a8f13 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:20:30 +0100 Subject: [PATCH 07/25] Merge from main --- CHANGELOG.md | 11 +- docs/getting-started.md | 18 + examples/02_Complex/complex_example.py | 3 +- .../two_stage_optimization.py | 4 +- flixopt/aggregation.py | 6 +- flixopt/calculation.py | 18 +- flixopt/color_processing.py | 5 +- flixopt/components.py | 4 +- flixopt/config.py | 473 ++++-------------- flixopt/core.py | 4 +- flixopt/effects.py | 4 +- flixopt/elements.py | 8 +- flixopt/features.py | 3 - flixopt/flow_system.py | 4 +- flixopt/interface.py | 5 +- flixopt/io.py | 8 +- flixopt/modeling.py | 5 +- flixopt/network_app.py | 5 +- flixopt/plotting.py | 4 +- flixopt/results.py | 14 +- flixopt/solvers.py | 5 +- flixopt/structure.py | 7 +- pyproject.toml | 2 +- tests/test_config.py | 223 +++++---- 24 files changed, 298 insertions(+), 545 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40bf00825..833ddd3c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? -**Summary**: Type system overhaul with comprehensive type hints for better IDE support and code clarity. +**Summary**: Type system overhaul and migration to loguru for logging If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). @@ -64,8 +64,13 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Added `Scalar` type for scalar-only numeric values - Added `NumericOrBool` utility type for internal use - Type system supports scalars, numpy arrays, pandas Series/DataFrames, and xarray DataArrays +- Lazy logging evaluation - expensive log operations only execute when log level is active +- `CONFIG.Logging.verbose_tracebacks` option for detailed debugging with variable values ### 💥 Breaking Changes +- **Logging framework**: Migrated to [loguru](https://loguru.readthedocs.io/) + - Removed `CONFIG.Logging` parameters: `rich`, `Colors`, `date_format`, `format`, `console_width`, `show_path`, `show_logger_name` + - For advanced formatting, use loguru's API directly after `CONFIG.apply()` ### ♻️ Changed - **Code structure**: Removed `commons.py` module and moved all imports directly to `__init__.py` for cleaner code organization (no public API changes) @@ -92,13 +97,15 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📦 Dependencies - Updated `mkdocs-material` to v9.6.23 +- Replaced `rich >= 13.0.0` with `loguru >= 0.7.0` for logging ### 📝 Docs - Enhanced documentation in `flixopt/types.py` with comprehensive examples and dimension explanation table - Clarified Effect type docstrings - Effect types are dicts, but single numeric values work through union types - Added clarifying comments in `effects.py` explaining parameter handling and transformation - Improved OnOffParameters attribute documentation - +- Updated getting-started guide with loguru examples +- Updated `config.py` docstrings for loguru integration ### 👷 Development - Added test for FlowSystem resampling diff --git a/docs/getting-started.md b/docs/getting-started.md index 044ffb872..5841de3a4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -22,6 +22,24 @@ For all features including interactive network visualizations and time series ag pip install "flixopt[full]" ``` +## Logging + +FlixOpt uses [loguru](https://loguru.readthedocs.io/) for logging. Logging is silent by default but can be easily configured. For beginners, use our internal convenience methods. Experts can use loguru directly. + +```python +from flixopt import CONFIG + +# Enable console logging +CONFIG.Logging.console = True +CONFIG.Logging.level = 'INFO' +CONFIG.apply() + +# Or use a preset configuration for exploring +CONFIG.exploring() +``` + +For more details on logging configuration, see the [`CONFIG.Logging`][flixopt.config.CONFIG.Logging] documentation. + ## Basic Workflow Working with FlixOpt follows a general pattern: diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index b8ef76a03..3ff5b251c 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -4,7 +4,6 @@ import numpy as np import pandas as pd -from rich.pretty import pprint # Used for pretty printing import flixopt as fx @@ -188,7 +187,7 @@ flow_system.add_elements(Costs, CO2, PE, Gaskessel, Waermelast, Gasbezug, Stromverkauf, speicher) flow_system.add_elements(bhkw_2) if use_chp_with_piecewise_conversion else flow_system.add_elements(bhkw) - pprint(flow_system) # Get a string representation of the FlowSystem + print(flow_system) # Get a string representation of the FlowSystem try: flow_system.start_network_app() # Start the network app except ImportError as e: diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 9647e803c..7354cb877 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -7,17 +7,15 @@ While the final optimum might differ from the global optimum, the solving will be much faster. """ -import logging import pathlib import timeit import pandas as pd import xarray as xr +from loguru import logger import flixopt as fx -logger = logging.getLogger('flixopt') - if __name__ == '__main__': fx.CONFIG.exploring() diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index cd0fdde3c..99b13bd45 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -6,12 +6,12 @@ from __future__ import annotations import copy -import logging import pathlib import timeit from typing import TYPE_CHECKING import numpy as np +from loguru import logger try: import tsam.timeseriesaggregation as tsam @@ -37,8 +37,6 @@ from .elements import Component from .flow_system import FlowSystem -logger = logging.getLogger('flixopt') - class Aggregation: """ @@ -106,7 +104,7 @@ def cluster(self) -> None: self.aggregated_data = self.tsam.predictOriginalData() self.clustering_duration_seconds = timeit.default_timer() - start_time # Zeit messen: - logger.info(self.describe_clusters()) + logger.opt(lazy=True).info('{result}', result=lambda: self.describe_clusters()) def describe_clusters(self) -> str: description = {} diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 1125da401..64c589e3a 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -10,7 +10,6 @@ from __future__ import annotations -import logging import math import pathlib import sys @@ -20,6 +19,7 @@ from typing import TYPE_CHECKING, Annotated, Any import numpy as np +from loguru import logger from tqdm import tqdm from . import io as fx_io @@ -39,8 +39,6 @@ from .solvers import _Solver from .structure import FlowSystemModel -logger = logging.getLogger('flixopt') - class Calculation: """ @@ -238,7 +236,7 @@ def solve( **solver.options, ) self.durations['solving'] = round(timeit.default_timer() - t_start, 2) - logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.') + logger.success(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.') logger.info(f'Model status after solve: {self.model.status}') if self.model.status == 'warning': @@ -255,12 +253,10 @@ def solve( # Log the formatted output should_log = log_main_results if log_main_results is not None else CONFIG.Solving.log_main_results if should_log: - logger.info( - f'{" Main Results ":#^80}\n' - + fx_io.format_yaml_string( - self.main_results, - compact_numeric_lists=True, - ) + logger.opt(lazy=True).info( + '{result}', + result=lambda: f'{" Main Results ":#^80}\n' + + fx_io.format_yaml_string(self.main_results, compact_numeric_lists=True), ) self.results = CalculationResults.from_calculation(self) @@ -673,7 +669,7 @@ def do_modeling_and_solve( for key, value in calc.durations.items(): self.durations[key] += value - logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.') + logger.success(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.') self.results = SegmentedCalculationResults.from_calculation(self) diff --git a/flixopt/color_processing.py b/flixopt/color_processing.py index 2959acc82..9d874e027 100644 --- a/flixopt/color_processing.py +++ b/flixopt/color_processing.py @@ -6,15 +6,12 @@ from __future__ import annotations -import logging - import matplotlib.colors as mcolors import matplotlib.pyplot as plt import plotly.express as px +from loguru import logger from plotly.exceptions import PlotlyError -logger = logging.getLogger('flixopt') - def _rgb_string_to_hex(color: str) -> str: """Convert Plotly RGB/RGBA string format to hex. diff --git a/flixopt/components.py b/flixopt/components.py index 6a5abfc4e..c51b4b7d2 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -4,12 +4,12 @@ from __future__ import annotations -import logging import warnings from typing import TYPE_CHECKING, Literal import numpy as np import xarray as xr +from loguru import logger from . import io as fx_io from .core import PlausibilityError @@ -25,8 +25,6 @@ from .flow_system import FlowSystem from .types import Numeric_PS, Numeric_TPS -logger = logging.getLogger('flixopt') - @register_class_for_io class LinearConverter(Component): diff --git a/flixopt/config.py b/flixopt/config.py index d7ea824d9..07d7e24a9 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -1,23 +1,16 @@ from __future__ import annotations -import logging import os import sys import warnings -from logging.handlers import RotatingFileHandler from pathlib import Path from types import MappingProxyType from typing import Literal -from rich.console import Console -from rich.logging import RichHandler -from rich.style import Style -from rich.theme import Theme +from loguru import logger __all__ = ['CONFIG', 'change_logging_level'] -logger = logging.getLogger('flixopt') - # SINGLE SOURCE OF TRUTH - immutable to prevent accidental modification _DEFAULTS = MappingProxyType( @@ -27,24 +20,10 @@ { 'level': 'INFO', 'file': None, - 'rich': False, 'console': False, 'max_file_size': 10_485_760, # 10MB 'backup_count': 5, - 'date_format': '%Y-%m-%d %H:%M:%S', - 'format': '%(message)s', - 'console_width': 120, - 'show_path': False, - 'show_logger_name': False, - 'colors': MappingProxyType( - { - 'DEBUG': '\033[90m', # Bright Black/Gray - 'INFO': '\033[0m', # Default/White - 'WARNING': '\033[33m', # Yellow - 'ERROR': '\033[31m', # Red - 'CRITICAL': '\033[1m\033[31m', # Bold Red - } - ), + 'verbose_tracebacks': False, } ), 'modeling': MappingProxyType( @@ -81,6 +60,9 @@ class CONFIG: Always call ``CONFIG.apply()`` after changes. + Note: + flixopt uses `loguru `_ for logging. + Attributes: Logging: Logging configuration. Modeling: Optimization modeling parameters. @@ -114,86 +96,48 @@ class Logging: Silent by default. Enable via ``console=True`` or ``file='path'``. Attributes: - level: Logging level. - file: Log file path for file logging. - console: Enable console output. - rich: Use Rich library for enhanced output. - max_file_size: Max file size before rotation. + level: Logging level (DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL). + file: Log file path for file logging (None to disable). + console: Enable console output (True/'stdout' or 'stderr'). + max_file_size: Max file size in bytes before rotation. backup_count: Number of backup files to keep. - date_format: Date/time format string. - format: Log message format string. - console_width: Console width for Rich handler. - show_path: Show file paths in messages. - show_logger_name: Show logger name in messages. - Colors: ANSI color codes for log levels. + verbose_tracebacks: Show detailed tracebacks with variable values. Examples: ```python + # Enable console logging + CONFIG.Logging.console = True + CONFIG.Logging.level = 'DEBUG' + CONFIG.apply() + # File logging with rotation CONFIG.Logging.file = 'app.log' CONFIG.Logging.max_file_size = 5_242_880 # 5MB CONFIG.apply() - # Rich handler with stdout - CONFIG.Logging.console = True # or 'stdout' - CONFIG.Logging.rich = True - CONFIG.apply() - - # Console output to stderr + # Console to stderr CONFIG.Logging.console = 'stderr' CONFIG.apply() ``` + + Note: + For advanced formatting or custom loguru configuration, + use loguru's API directly after calling CONFIG.apply(): + + ```python + from loguru import logger + + CONFIG.apply() # Basic setup + logger.add('custom.log', format='{time} {message}') + ``` """ - level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = _DEFAULTS['logging']['level'] + level: Literal['DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'] = _DEFAULTS['logging']['level'] file: str | None = _DEFAULTS['logging']['file'] - rich: bool = _DEFAULTS['logging']['rich'] console: bool | Literal['stdout', 'stderr'] = _DEFAULTS['logging']['console'] max_file_size: int = _DEFAULTS['logging']['max_file_size'] backup_count: int = _DEFAULTS['logging']['backup_count'] - date_format: str = _DEFAULTS['logging']['date_format'] - format: str = _DEFAULTS['logging']['format'] - console_width: int = _DEFAULTS['logging']['console_width'] - show_path: bool = _DEFAULTS['logging']['show_path'] - show_logger_name: bool = _DEFAULTS['logging']['show_logger_name'] - - class Colors: - """ANSI color codes for log levels. - - Attributes: - DEBUG: ANSI color for DEBUG level. - INFO: ANSI color for INFO level. - WARNING: ANSI color for WARNING level. - ERROR: ANSI color for ERROR level. - CRITICAL: ANSI color for CRITICAL level. - - Examples: - ```python - CONFIG.Logging.Colors.INFO = '\\033[32m' # Green - CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red - CONFIG.apply() - ``` - - Common ANSI codes: - - '\\033[30m' - Black - - '\\033[31m' - Red - - '\\033[32m' - Green - - '\\033[33m' - Yellow - - '\\033[34m' - Blue - - '\\033[35m' - Magenta - - '\\033[36m' - Cyan - - '\\033[37m' - White - - '\\033[90m' - Bright Black/Gray - - '\\033[0m' - Reset to default - - '\\033[1m\\033[3Xm' - Bold (replace X with color code 0-7) - - '\\033[2m\\033[3Xm' - Dim (replace X with color code 0-7) - """ - - DEBUG: str = _DEFAULTS['logging']['colors']['DEBUG'] - INFO: str = _DEFAULTS['logging']['colors']['INFO'] - WARNING: str = _DEFAULTS['logging']['colors']['WARNING'] - ERROR: str = _DEFAULTS['logging']['colors']['ERROR'] - CRITICAL: str = _DEFAULTS['logging']['colors']['CRITICAL'] + verbose_tracebacks: bool = _DEFAULTS['logging']['verbose_tracebacks'] class Modeling: """Optimization modeling parameters. @@ -274,12 +218,7 @@ class Plotting: def reset(cls): """Reset all configuration values to defaults.""" for key, value in _DEFAULTS['logging'].items(): - if key == 'colors': - # Reset nested Colors class - for color_key, color_value in value.items(): - setattr(cls.Logging.Colors, color_key, color_value) - else: - setattr(cls.Logging, key, value) + setattr(cls.Logging, key, value) for key, value in _DEFAULTS['modeling'].items(): setattr(cls.Modeling, key, value) @@ -296,15 +235,7 @@ def reset(cls): @classmethod def apply(cls): """Apply current configuration to logging system.""" - # Convert Colors class attributes to dict - colors_dict = { - 'DEBUG': cls.Logging.Colors.DEBUG, - 'INFO': cls.Logging.Colors.INFO, - 'WARNING': cls.Logging.Colors.WARNING, - 'ERROR': cls.Logging.Colors.ERROR, - 'CRITICAL': cls.Logging.Colors.CRITICAL, - } - valid_levels = list(colors_dict) + valid_levels = ['DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'] if cls.Logging.level.upper() not in valid_levels: raise ValueError(f"Invalid log level '{cls.Logging.level}'. Must be one of: {', '.join(valid_levels)}") @@ -320,16 +251,10 @@ def apply(cls): _setup_logging( default_level=cls.Logging.level, log_file=cls.Logging.file, - use_rich_handler=cls.Logging.rich, console=cls.Logging.console, max_file_size=cls.Logging.max_file_size, backup_count=cls.Logging.backup_count, - date_format=cls.Logging.date_format, - format=cls.Logging.format, - console_width=cls.Logging.console_width, - show_path=cls.Logging.show_path, - show_logger_name=cls.Logging.show_logger_name, - colors=colors_dict, + verbose_tracebacks=cls.Logging.verbose_tracebacks, ) @classmethod @@ -364,11 +289,7 @@ def _apply_config_dict(cls, config_dict: dict): for key, value in config_dict.items(): if key == 'logging' and isinstance(value, dict): for nested_key, nested_value in value.items(): - if nested_key == 'colors' and isinstance(nested_value, dict): - # Handle nested colors under logging - for color_key, color_value in nested_value.items(): - setattr(cls.Logging.Colors, color_key, color_value) - else: + if hasattr(cls.Logging, nested_key): setattr(cls.Logging, nested_key, nested_value) elif key == 'modeling' and isinstance(value, dict): for nested_key, nested_value in value.items(): @@ -394,22 +315,10 @@ def to_dict(cls) -> dict: 'logging': { 'level': cls.Logging.level, 'file': cls.Logging.file, - 'rich': cls.Logging.rich, 'console': cls.Logging.console, 'max_file_size': cls.Logging.max_file_size, 'backup_count': cls.Logging.backup_count, - 'date_format': cls.Logging.date_format, - 'format': cls.Logging.format, - 'console_width': cls.Logging.console_width, - 'show_path': cls.Logging.show_path, - 'show_logger_name': cls.Logging.show_logger_name, - 'colors': { - 'DEBUG': cls.Logging.Colors.DEBUG, - 'INFO': cls.Logging.Colors.INFO, - 'WARNING': cls.Logging.Colors.WARNING, - 'ERROR': cls.Logging.Colors.ERROR, - 'CRITICAL': cls.Logging.Colors.CRITICAL, - }, + 'verbose_tracebacks': cls.Logging.verbose_tracebacks, }, 'modeling': { 'big': cls.Modeling.big, @@ -451,11 +360,12 @@ def silent(cls) -> type[CONFIG]: def debug(cls) -> type[CONFIG]: """Configure for debug mode with verbose output. - Enables console logging at DEBUG level and all solver output for - troubleshooting. Automatically calls apply(). + Enables console logging at DEBUG level, verbose tracebacks, + and all solver output for troubleshooting. Automatically calls apply(). """ cls.Logging.console = True cls.Logging.level = 'DEBUG' + cls.Logging.verbose_tracebacks = True cls.Solving.log_to_console = True cls.Solving.log_main_results = True cls.apply() @@ -497,274 +407,106 @@ def browser_plotting(cls) -> type[CONFIG]: return cls -class MultilineFormatter(logging.Formatter): - """Formatter that handles multi-line messages with consistent prefixes. +def _format_multiline(record): + """Format multi-line messages with box-style borders for better readability. - Args: - fmt: Log message format string. - datefmt: Date/time format string. - show_logger_name: Show logger name in log messages. - """ - - def __init__(self, fmt: str = '%(message)s', datefmt: str | None = None, show_logger_name: bool = False): - super().__init__(fmt=fmt, datefmt=datefmt) - self.show_logger_name = show_logger_name - - def format(self, record) -> str: - record.message = record.getMessage() - message_lines = self._style.format(record).split('\n') - timestamp = self.formatTime(record, self.datefmt) - log_level = record.levelname.ljust(8) - - if self.show_logger_name: - # Truncate long logger names for readability - logger_name = record.name if len(record.name) <= 20 else f'...{record.name[-17:]}' - log_prefix = f'{timestamp} | {log_level} | {logger_name.ljust(20)} |' - else: - log_prefix = f'{timestamp} | {log_level} |' - - indent = ' ' * (len(log_prefix) + 1) # +1 for the space after prefix + Single-line messages use standard format. + Multi-line messages use boxed format with ┌─, │, └─ characters. - lines = [f'{log_prefix} {message_lines[0]}'] - if len(message_lines) > 1: - lines.extend([f'{indent}{line}' for line in message_lines[1:]]) - - return '\n'.join(lines) - - -class ColoredMultilineFormatter(MultilineFormatter): - """Formatter that adds ANSI colors to multi-line log messages. - - Args: - fmt: Log message format string. - datefmt: Date/time format string. - colors: Dictionary of ANSI color codes for each log level. - show_logger_name: Show logger name in log messages. + Note: Escapes curly braces in messages to prevent format string errors. """ + # Escape curly braces in message to prevent format string errors + message = record['message'].replace('{', '{{').replace('}', '}}') + lines = message.split('\n') - RESET = '\033[0m' - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - colors: dict[str, str] | None = None, - show_logger_name: bool = False, - ): - super().__init__(fmt=fmt, datefmt=datefmt, show_logger_name=show_logger_name) - self.COLORS = ( - colors - if colors is not None - else { - 'DEBUG': '\033[90m', - 'INFO': '\033[0m', - 'WARNING': '\033[33m', - 'ERROR': '\033[31m', - 'CRITICAL': '\033[1m\033[31m', - } - ) + # Format timestamp and level + time_str = record['time'].strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] # milliseconds + level_str = f'{record["level"].name: <8}' - def format(self, record): - lines = super().format(record).splitlines() - log_color = self.COLORS.get(record.levelname, self.RESET) - formatted_lines = [f'{log_color}{line}{self.RESET}' for line in lines] - return '\n'.join(formatted_lines) - - -def _create_console_handler( - use_rich: bool = False, - stream: Literal['stdout', 'stderr'] = 'stdout', - console_width: int = 120, - show_path: bool = False, - show_logger_name: bool = False, - date_format: str = '%Y-%m-%d %H:%M:%S', - format: str = '%(message)s', - colors: dict[str, str] | None = None, -) -> logging.Handler: - """Create a console logging handler. + # Single line messages - standard format + if len(lines) == 1: + result = f'{time_str} | {level_str} | {message}\n' + if record['exception']: + result += '{exception}' + return result - Args: - use_rich: If True, use RichHandler with color support. - stream: Output stream - console_width: Width of the console for Rich handler. - show_path: Show file paths in log messages (Rich only). - show_logger_name: Show logger name in log messages. - date_format: Date/time format string. - format: Log message format string. - colors: Dictionary of ANSI color codes for each log level. - - Returns: - Configured logging handler (RichHandler or StreamHandler). - """ - # Determine the stream object - stream_obj = sys.stdout if stream == 'stdout' else sys.stderr - - if use_rich: - # Convert ANSI codes to Rich theme - if colors: - theme_dict = {} - for level, ansi_code in colors.items(): - # Rich can parse ANSI codes directly! - try: - style = Style.from_ansi(ansi_code) - theme_dict[f'logging.level.{level.lower()}'] = style - except Exception: - # Fallback to default if parsing fails - pass - - theme = Theme(theme_dict) if theme_dict else None - else: - theme = None - - console = Console(width=console_width, theme=theme, file=stream_obj) - handler = RichHandler( - console=console, - rich_tracebacks=True, - omit_repeated_times=True, - show_path=show_path, - log_time_format=date_format, - ) - handler.setFormatter(logging.Formatter(format)) - else: - handler = logging.StreamHandler(stream=stream_obj) - handler.setFormatter( - ColoredMultilineFormatter( - fmt=format, - datefmt=date_format, - colors=colors, - show_logger_name=show_logger_name, - ) - ) - - return handler - - -def _create_file_handler( - log_file: str, - max_file_size: int = 10_485_760, - backup_count: int = 5, - show_logger_name: bool = False, - date_format: str = '%Y-%m-%d %H:%M:%S', - format: str = '%(message)s', -) -> RotatingFileHandler: - """Create a rotating file handler to prevent huge log files. + # Multi-line messages - boxed format + indent = ' ' * len(time_str) # Match timestamp length - Args: - log_file: Path to the log file. - max_file_size: Maximum size in bytes before rotation. - backup_count: Number of backup files to keep. - show_logger_name: Show logger name in log messages. - date_format: Date/time format string. - format: Log message format string. - - Returns: - Configured RotatingFileHandler (without colors). - """ + # Build the boxed output + result = f'{time_str} | {level_str} | ┌─ {lines[0]}\n' + for line in lines[1:-1]: + result += f'{indent} | {" " * 8} | │ {line}\n' + result += f'{indent} | {" " * 8} | └─ {lines[-1]}\n' - # Ensure parent directory exists - log_path = Path(log_file) - try: - log_path.parent.mkdir(parents=True, exist_ok=True) - except PermissionError as e: - raise PermissionError(f"Cannot create log directory '{log_path.parent}': Permission denied") from e + # Add exception info if present + if record['exception']: + result += '\n{exception}' - try: - handler = RotatingFileHandler( - log_file, - maxBytes=max_file_size, - backupCount=backup_count, - encoding='utf-8', - ) - except PermissionError as e: - raise PermissionError( - f"Cannot write to log file '{log_file}': Permission denied. " - f'Choose a different location or check file permissions.' - ) from e - - handler.setFormatter( - MultilineFormatter( - fmt=format, - datefmt=date_format, - show_logger_name=show_logger_name, - ) - ) - return handler + return result def _setup_logging( - default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO', + default_level: Literal['DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO', log_file: str | None = None, - use_rich_handler: bool = False, console: bool | Literal['stdout', 'stderr'] = False, max_file_size: int = 10_485_760, backup_count: int = 5, - date_format: str = '%Y-%m-%d %H:%M:%S', - format: str = '%(message)s', - console_width: int = 120, - show_path: bool = False, - show_logger_name: bool = False, - colors: dict[str, str] | None = None, + verbose_tracebacks: bool = False, ) -> None: """Internal function to setup logging - use CONFIG.apply() instead. - Configures the flixopt logger with console and/or file handlers. - If no handlers are configured, adds NullHandler (library best practice). + Configures loguru logger with console and/or file handlers. + Multi-line messages are automatically formatted with box-style borders. Args: default_level: Logging level for the logger. log_file: Path to log file (None to disable file logging). - use_rich_handler: Use Rich for enhanced console output. - console: Enable console logging. - max_file_size: Maximum log file size before rotation. + console: Enable console logging (True/'stdout' or 'stderr'). + max_file_size: Maximum log file size in bytes before rotation. backup_count: Number of backup log files to keep. - date_format: Date/time format for log messages. - format: Log message format string. - console_width: Console width for Rich handler. - show_path: Show file paths in log messages (Rich only). - show_logger_name: Show logger name in log messages. - colors: ANSI color codes for each log level. + verbose_tracebacks: If True, show detailed tracebacks with variable values. """ - logger = logging.getLogger('flixopt') - logger.setLevel(getattr(logging, default_level.upper())) - logger.propagate = False # Prevent duplicate logs - logger.handlers.clear() + # Remove all existing handlers + logger.remove() - # Handle console parameter: False = disabled, True = stdout, 'stdout' = stdout, 'stderr' = stderr + # Console handler with multi-line formatting if console: - # Convert True to 'stdout', keep 'stdout'/'stderr' as-is - stream = 'stdout' if console is True else console - logger.addHandler( - _create_console_handler( - use_rich=use_rich_handler, - stream=stream, - console_width=console_width, - show_path=show_path, - show_logger_name=show_logger_name, - date_format=date_format, - format=format, - colors=colors, - ) + stream = sys.stdout if console is True or console == 'stdout' else sys.stderr + logger.add( + stream, + format=_format_multiline, + level=default_level.upper(), + colorize=True, + backtrace=verbose_tracebacks, + diagnose=verbose_tracebacks, + enqueue=False, ) + # File handler with rotation (plain format for files) if log_file: - logger.addHandler( - _create_file_handler( - log_file=log_file, - max_file_size=max_file_size, - backup_count=backup_count, - show_logger_name=show_logger_name, - date_format=date_format, - format=format, - ) - ) + log_path = Path(log_file) + try: + log_path.parent.mkdir(parents=True, exist_ok=True) + except PermissionError as e: + raise PermissionError(f"Cannot create log directory '{log_path.parent}': Permission denied") from e - # Library best practice: NullHandler if no handlers configured - if not logger.handlers: - logger.addHandler(logging.NullHandler()) + logger.add( + log_file, + format='{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}', + level=default_level.upper(), + colorize=False, + rotation=max_file_size, + retention=backup_count, + encoding='utf-8', + backtrace=verbose_tracebacks, + diagnose=verbose_tracebacks, + enqueue=False, + ) -def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']): - """Change the logging level for the flixopt logger and all its handlers. +def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL']): + """Change the logging level for the flixopt logger. .. deprecated:: 2.1.11 Use ``CONFIG.Logging.level = level_name`` and ``CONFIG.apply()`` instead. @@ -785,11 +527,8 @@ def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR' DeprecationWarning, stacklevel=2, ) - logger = logging.getLogger('flixopt') - logging_level = getattr(logging, level_name.upper()) - logger.setLevel(logging_level) - for handler in logger.handlers: - handler.setLevel(logging_level) + CONFIG.Logging.level = level_name.upper() + CONFIG.apply() # Initialize default config diff --git a/flixopt/core.py b/flixopt/core.py index 0d70e255b..7f4d2a20f 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -3,7 +3,6 @@ It provides Datatypes, logging functionality, and some functions to transform data structures. """ -import logging import warnings from itertools import permutations from typing import Any, Literal, Union @@ -11,11 +10,10 @@ import numpy as np import pandas as pd import xarray as xr +from loguru import logger from .types import NumericOrBool -logger = logging.getLogger('flixopt') - FlowSystemDimensions = Literal['time', 'period', 'scenario'] """Possible dimensions of a FlowSystem.""" diff --git a/flixopt/effects.py b/flixopt/effects.py index 02c850050..ebfc2c906 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -7,7 +7,6 @@ from __future__ import annotations -import logging import warnings from collections import deque from typing import TYPE_CHECKING, Literal @@ -15,6 +14,7 @@ import linopy import numpy as np import xarray as xr +from loguru import logger from .features import ShareAllocationModel from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io @@ -25,8 +25,6 @@ from .flow_system import FlowSystem from .types import Effect_PS, Effect_TPS, Numeric_PS, Numeric_TPS, Scalar -logger = logging.getLogger('flixopt') - @register_class_for_io class Effect(Element): diff --git a/flixopt/elements.py b/flixopt/elements.py index 224cc0f9c..f47002b3a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -4,12 +4,12 @@ from __future__ import annotations -import logging import warnings from typing import TYPE_CHECKING import numpy as np import xarray as xr +from loguru import logger from . import io as fx_io from .config import CONFIG @@ -36,8 +36,6 @@ Scalar, ) -logger = logging.getLogger('flixopt') - @register_class_for_io class Component(Element): @@ -532,8 +530,8 @@ def _plausibility_checks(self) -> None: if np.any(self.relative_minimum > 0) and self.on_off_parameters is None: logger.warning( f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. ' - f'This prevents the flow_rate from switching off (flow_rate = 0). ' - f'Consider using on_off_parameters to allow the flow to be switched on and off.' + f'This prevents the Flow from switching off (flow_rate = 0). ' + f'Consider using on_off_parameters to allow the Flow to be switched on and off.' ) if self.previous_flow_rate is not None: diff --git a/flixopt/features.py b/flixopt/features.py index 519693885..fd9796ba1 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -5,7 +5,6 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING import linopy @@ -19,8 +18,6 @@ from .interface import InvestParameters, OnOffParameters, Piecewise from .types import Numeric_PS, Numeric_TPS -logger = logging.getLogger('flixopt') - class InvestmentModel(Submodel): """ diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 081359076..cf112a608 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -4,7 +4,6 @@ from __future__ import annotations -import logging import warnings from collections import defaultdict from itertools import chain @@ -13,6 +12,7 @@ import numpy as np import pandas as pd import xarray as xr +from loguru import logger from . import io as fx_io from .config import CONFIG @@ -34,8 +34,6 @@ from .types import Bool_TPS, Effect_TPS, Numeric_PS, Numeric_TPS, NumericOrBool -logger = logging.getLogger('flixopt') - class FlowSystem(Interface, CompositeContainerMixin[Element]): """ diff --git a/flixopt/interface.py b/flixopt/interface.py index e22ceebd5..f67f501ba 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -5,13 +5,13 @@ from __future__ import annotations -import logging import warnings from typing import TYPE_CHECKING, Any import numpy as np import pandas as pd import xarray as xr +from loguru import logger from .config import CONFIG from .structure import Interface, register_class_for_io @@ -23,9 +23,6 @@ from .types import Effect_PS, Effect_TPS, Numeric_PS, Numeric_TPS -logger = logging.getLogger('flixopt') - - @register_class_for_io class Piece(Interface): """Define a single linear segment with specified domain boundaries. diff --git a/flixopt/io.py b/flixopt/io.py index e83738d89..ffeb2474e 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -2,7 +2,6 @@ import inspect import json -import logging import os import pathlib import re @@ -15,14 +14,13 @@ import pandas as pd import xarray as xr import yaml +from loguru import logger if TYPE_CHECKING: import linopy from .types import Numeric_TPS -logger = logging.getLogger('flixopt') - def remove_none_and_empty(obj): """Recursively removes None and empty dicts and lists values from a dictionary or list.""" @@ -500,7 +498,7 @@ def document_linopy_model(model: linopy.Model, path: pathlib.Path | None = None) } if model.status == 'warning': - logger.critical(f'The model has a warning status {model.status=}. Trying to extract infeasibilities') + logger.warning(f'The model has a warning status {model.status=}. Trying to extract infeasibilities') try: import io from contextlib import redirect_stdout @@ -513,7 +511,7 @@ def document_linopy_model(model: linopy.Model, path: pathlib.Path | None = None) documentation['infeasible_constraints'] = f.getvalue() except NotImplementedError: - logger.critical( + logger.warning( 'Infeasible constraints could not get retrieved. This functionality is only availlable with gurobi' ) documentation['infeasible_constraints'] = 'Not possible to retrieve infeasible constraints' diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 13b4c0e3e..ebe739a85 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -1,14 +1,11 @@ -import logging - import linopy import numpy as np import xarray as xr +from loguru import logger from .config import CONFIG from .structure import Submodel -logger = logging.getLogger('flixopt') - class ModelingUtilitiesAbstract: """Utility functions for modeling calculations - leveraging xarray for temporal data""" diff --git a/flixopt/network_app.py b/flixopt/network_app.py index 2cc80e7b0..446a2e7ce 100644 --- a/flixopt/network_app.py +++ b/flixopt/network_app.py @@ -1,10 +1,11 @@ from __future__ import annotations -import logging import socket import threading from typing import TYPE_CHECKING, Any +from loguru import logger + try: import dash_cytoscape as cyto import dash_daq as daq @@ -24,8 +25,6 @@ if TYPE_CHECKING: from .flow_system import FlowSystem -logger = logging.getLogger('flixopt') - # Configuration class for better organization class VisualizationConfig: diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 045cf7e99..27dbaf78c 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -26,7 +26,6 @@ from __future__ import annotations import itertools -import logging import os import pathlib from typing import TYPE_CHECKING, Any, Literal @@ -40,6 +39,7 @@ import plotly.graph_objects as go import plotly.offline import xarray as xr +from loguru import logger from .color_processing import process_colors from .config import CONFIG @@ -47,8 +47,6 @@ if TYPE_CHECKING: import pyvis -logger = logging.getLogger('flixopt') - # Define the colors for the 'portland' colorscale in matplotlib _portland_colors = [ [12 / 255, 51 / 255, 131 / 255], # Dark blue diff --git a/flixopt/results.py b/flixopt/results.py index 3d9aedf62..eaff79fe4 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -2,7 +2,6 @@ import copy import datetime -import logging import pathlib import warnings from typing import TYPE_CHECKING, Any, Literal @@ -11,6 +10,7 @@ import numpy as np import pandas as pd import xarray as xr +from loguru import logger from . import io as fx_io from . import plotting @@ -28,9 +28,6 @@ from .core import FlowSystemDimensions -logger = logging.getLogger('flixopt') - - def load_mapping_from_file(path: pathlib.Path) -> dict[str, str | list[str]]: """Load color mapping from JSON or YAML file. @@ -344,18 +341,19 @@ def flow_system(self) -> FlowSystem: """The restored flow_system that was used to create the calculation. Contains all input parameters.""" if self._flow_system is None: - old_level = logger.level - logger.level = logging.CRITICAL + # Temporarily disable all logging to suppress messages during restoration + logger.disable('flixopt') try: self._flow_system = FlowSystem.from_dataset(self.flow_system_data) self._flow_system._connect_network() except Exception as e: + logger.enable('flixopt') # Re-enable before logging critical message logger.critical( f'Not able to restore FlowSystem from dataset. Some functionality is not availlable. {e}' ) raise _FlowSystemRestorationError(f'Not able to restore FlowSystem from dataset. {e}') from e finally: - logger.level = old_level + logger.enable('flixopt') return self._flow_system def setup_colors( @@ -1092,7 +1090,7 @@ def to_file( else: fx_io.document_linopy_model(self.model, path=paths.model_documentation) - logger.info(f'Saved calculation results "{name}" to {paths.model_documentation.parent}') + logger.success(f'Saved calculation results "{name}" to {paths.model_documentation.parent}') class _ElementResults: diff --git a/flixopt/solvers.py b/flixopt/solvers.py index e5db61192..a9a3afb46 100644 --- a/flixopt/solvers.py +++ b/flixopt/solvers.py @@ -4,13 +4,12 @@ from __future__ import annotations -import logging from dataclasses import dataclass, field from typing import Any, ClassVar -from flixopt.config import CONFIG +from loguru import logger -logger = logging.getLogger('flixopt') +from flixopt.config import CONFIG @dataclass diff --git a/flixopt/structure.py b/flixopt/structure.py index 2bce6aa52..9ddf46d31 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -6,11 +6,9 @@ from __future__ import annotations import inspect -import logging import re from dataclasses import dataclass from difflib import get_close_matches -from io import StringIO from typing import ( TYPE_CHECKING, Any, @@ -23,8 +21,7 @@ import numpy as np import pandas as pd import xarray as xr -from rich.console import Console -from rich.pretty import Pretty +from loguru import logger from . import io as fx_io from .core import TimeSeriesData, get_dataarray_stats @@ -36,8 +33,6 @@ from .effects import EffectCollectionModel from .flow_system import FlowSystem -logger = logging.getLogger('flixopt') - CLASS_REGISTRY = {} diff --git a/pyproject.toml b/pyproject.toml index eb1fea0f8..4a60d7754 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "netcdf4 >= 1.6.1, < 2", # Utilities "pyyaml >= 6.0.0, < 7", - "rich >= 13.0.0, < 15", + "loguru >= 0.7.0, < 1", "tqdm >= 4.66.0, < 5", "tomli >= 2.0.1, < 3; python_version < '3.11'", # Only needed with python 3.10 or earlier # Default solver diff --git a/tests/test_config.py b/tests/test_config.py index a78330eb4..7de58e8aa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,10 +1,10 @@ """Tests for the config module.""" -import logging import sys from pathlib import Path import pytest +from loguru import logger from flixopt.config import _DEFAULTS, CONFIG, _setup_logging @@ -26,7 +26,6 @@ def test_config_defaults(self): """Test that CONFIG has correct default values.""" assert CONFIG.Logging.level == 'INFO' assert CONFIG.Logging.file is None - assert CONFIG.Logging.rich is False assert CONFIG.Logging.console is False assert CONFIG.Modeling.big == 10_000_000 assert CONFIG.Modeling.epsilon == 1e-5 @@ -37,28 +36,27 @@ def test_config_defaults(self): assert CONFIG.Solving.log_main_results is True assert CONFIG.config_name == 'flixopt' - def test_module_initialization(self): + def test_module_initialization(self, capfd): """Test that logging is initialized on module import.""" # Apply config to ensure handlers are initialized CONFIG.apply() - logger = logging.getLogger('flixopt') - # Should have at least one handler (file handler by default) - assert len(logger.handlers) == 1 - # Should have a file handler with default settings - assert isinstance(logger.handlers[0], logging.NullHandler) + # With default config (console=False, file=None), logs should not appear + logger.info('test message') + captured = capfd.readouterr() + assert 'test message' not in captured.out + assert 'test message' not in captured.err - def test_config_apply_console(self): + def test_config_apply_console(self, capfd): """Test applying config with console logging enabled.""" CONFIG.Logging.console = True CONFIG.Logging.level = 'DEBUG' CONFIG.apply() - logger = logging.getLogger('flixopt') - assert logger.level == logging.DEBUG - # Should have a StreamHandler for console output - assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) - # Should not have NullHandler when console is enabled - assert not any(isinstance(h, logging.NullHandler) for h in logger.handlers) + # Test that DEBUG level logs appear in console output + test_message = 'test debug message 12345' + logger.debug(test_message) + captured = capfd.readouterr() + assert test_message in captured.out or test_message in captured.err def test_config_apply_file(self, tmp_path): """Test applying config with file logging enabled.""" @@ -67,34 +65,42 @@ def test_config_apply_file(self, tmp_path): CONFIG.Logging.level = 'WARNING' CONFIG.apply() - logger = logging.getLogger('flixopt') - assert logger.level == logging.WARNING - # Should have a RotatingFileHandler for file output - from logging.handlers import RotatingFileHandler + # Test that WARNING level logs appear in the file + test_message = 'test warning message 67890' + logger.warning(test_message) + # Loguru may buffer, so we need to ensure the log is written + import time - assert any(isinstance(h, RotatingFileHandler) for h in logger.handlers) + time.sleep(0.1) # Small delay to ensure write + assert log_file.exists() + log_content = log_file.read_text() + assert test_message in log_content - def test_config_apply_rich(self): - """Test applying config with rich logging enabled.""" - CONFIG.Logging.console = True - CONFIG.Logging.rich = True + def test_config_apply_console_stderr(self, capfd): + """Test applying config with console logging to stderr.""" + CONFIG.Logging.console = 'stderr' + CONFIG.Logging.level = 'INFO' CONFIG.apply() - logger = logging.getLogger('flixopt') - # Should have a RichHandler - from rich.logging import RichHandler + # Test that INFO logs appear in stderr + test_message = 'test info to stderr 11111' + logger.info(test_message) + captured = capfd.readouterr() + assert test_message in captured.err - assert any(isinstance(h, RichHandler) for h in logger.handlers) - - def test_config_apply_multiple_changes(self): + def test_config_apply_multiple_changes(self, capfd): """Test applying multiple config changes at once.""" CONFIG.Logging.console = True CONFIG.Logging.level = 'ERROR' CONFIG.apply() - logger = logging.getLogger('flixopt') - assert logger.level == logging.ERROR - assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) + # Test that ERROR level logs appear but lower levels don't + logger.warning('warning should not appear') + logger.error('error should appear 22222') + captured = capfd.readouterr() + output = captured.out + captured.err + assert 'warning should not appear' not in output + assert 'error should appear 22222' in output def test_config_to_dict(self): """Test converting CONFIG to dictionary.""" @@ -107,7 +113,6 @@ def test_config_to_dict(self): assert config_dict['logging']['level'] == 'DEBUG' assert config_dict['logging']['console'] is True assert config_dict['logging']['file'] is None - assert config_dict['logging']['rich'] is False assert 'modeling' in config_dict assert config_dict['modeling']['big'] == 10_000_000 assert 'solving' in config_dict @@ -172,36 +177,41 @@ def test_config_load_from_file_partial(self, tmp_path): # Verify console setting is preserved (not in YAML) assert CONFIG.Logging.console is True - def test_setup_logging_silent_default(self): + def test_setup_logging_silent_default(self, capfd): """Test that _setup_logging creates silent logger by default.""" _setup_logging() - logger = logging.getLogger('flixopt') - # Should have NullHandler when console=False and log_file=None - assert any(isinstance(h, logging.NullHandler) for h in logger.handlers) - assert not logger.propagate + # With default settings, logs should not appear + logger.info('should not appear') + captured = capfd.readouterr() + assert 'should not appear' not in captured.out + assert 'should not appear' not in captured.err - def test_setup_logging_with_console(self): + def test_setup_logging_with_console(self, capfd): """Test _setup_logging with console output.""" _setup_logging(console=True, default_level='DEBUG') - logger = logging.getLogger('flixopt') - assert logger.level == logging.DEBUG - assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) + # Test that DEBUG logs appear in console + test_message = 'debug console test 33333' + logger.debug(test_message) + captured = capfd.readouterr() + assert test_message in captured.out or test_message in captured.err - def test_setup_logging_clears_handlers(self): + def test_setup_logging_clears_handlers(self, capfd): """Test that _setup_logging clears existing handlers.""" - logger = logging.getLogger('flixopt') - - # Add a dummy handler - dummy_handler = logging.NullHandler() - logger.addHandler(dummy_handler) - _ = len(logger.handlers) - + # Setup a handler first _setup_logging(console=True) - # Should have cleared old handlers and added new one - assert dummy_handler not in logger.handlers + # Call setup again with different settings - should clear and re-add + _setup_logging(console=True, default_level='ERROR') + + # Verify new settings work: ERROR logs appear but INFO doesn't + logger.info('info should not appear') + logger.error('error should appear 44444') + captured = capfd.readouterr() + output = captured.out + captured.err + assert 'info should not appear' not in output + assert 'error should appear 44444' in output def test_change_logging_level_removed(self): """Test that change_logging_level function is deprecated but still exists.""" @@ -231,40 +241,43 @@ def test_public_api(self): # merge_configs should not exist (was removed) assert not hasattr(config, 'merge_configs') - def test_logging_levels(self): + def test_logging_levels(self, capfd): """Test all valid logging levels.""" - levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + levels = ['DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'] for level in levels: CONFIG.Logging.level = level CONFIG.Logging.console = True CONFIG.apply() - logger = logging.getLogger('flixopt') - assert logger.level == getattr(logging, level) - - def test_logger_propagate_disabled(self): - """Test that logger propagation is disabled.""" - CONFIG.apply() - logger = logging.getLogger('flixopt') - assert not logger.propagate + # Test that logs at the configured level appear + test_message = f'test message at {level} 55555' + getattr(logger, level.lower())(test_message) + captured = capfd.readouterr() + output = captured.out + captured.err + assert test_message in output, f'Expected {level} message to appear' def test_file_handler_rotation(self, tmp_path): - """Test that file handler uses rotation.""" + """Test that file handler rotation configuration is accepted.""" log_file = tmp_path / 'rotating.log' CONFIG.Logging.file = str(log_file) + CONFIG.Logging.max_file_size = 1024 + CONFIG.Logging.backup_count = 2 CONFIG.apply() - logger = logging.getLogger('flixopt') - from logging.handlers import RotatingFileHandler + # Write some logs + for i in range(10): + logger.info(f'Log message {i}') + + # Verify file logging works + import time - file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)] - assert len(file_handlers) == 1 + time.sleep(0.1) + assert log_file.exists(), 'Log file should be created' - handler = file_handlers[0] - # Check rotation settings - assert handler.maxBytes == 10_485_760 # 10MB - assert handler.backupCount == 5 + # Verify configuration values are preserved + assert CONFIG.Logging.max_file_size == 1024 + assert CONFIG.Logging.backup_count == 2 def test_custom_config_yaml_complete(self, tmp_path): """Test loading a complete custom configuration.""" @@ -274,7 +287,6 @@ def test_custom_config_yaml_complete(self, tmp_path): logging: level: CRITICAL console: true - rich: true file: /tmp/custom.log modeling: big: 50000000 @@ -293,7 +305,6 @@ def test_custom_config_yaml_complete(self, tmp_path): assert CONFIG.config_name == 'my_custom_config' assert CONFIG.Logging.level == 'CRITICAL' assert CONFIG.Logging.console is True - assert CONFIG.Logging.rich is True assert CONFIG.Logging.file == '/tmp/custom.log' assert CONFIG.Modeling.big == 50000000 assert float(CONFIG.Modeling.epsilon) == 1e-4 @@ -302,9 +313,22 @@ def test_custom_config_yaml_complete(self, tmp_path): assert CONFIG.Solving.time_limit_seconds == 900 assert CONFIG.Solving.log_main_results is False - # Verify logging was applied - logger = logging.getLogger('flixopt') - assert logger.level == logging.CRITICAL + # Verify logging was applied to both console and file + import time + + test_message = 'critical test message 66666' + logger.critical(test_message) + time.sleep(0.1) # Small delay to ensure write + # Check file exists and contains message + log_file_path = tmp_path / 'custom.log' + if not log_file_path.exists(): + # File might be at /tmp/custom.log as specified in config + import os + + log_file_path = os.path.expanduser('/tmp/custom.log') + # We can't reliably test the file at /tmp/custom.log in tests + # So just verify critical level messages would appear at this level + assert CONFIG.Logging.level == 'CRITICAL' def test_config_file_with_console_and_file(self, tmp_path): """Test configuration with both console and file logging enabled.""" @@ -314,21 +338,22 @@ def test_config_file_with_console_and_file(self, tmp_path): logging: level: INFO console: true - rich: false file: {log_file} """ config_file.write_text(config_content) CONFIG.load_from_file(config_file) - logger = logging.getLogger('flixopt') - # Should have both StreamHandler and RotatingFileHandler - from logging.handlers import RotatingFileHandler + # Verify logging to both console and file works + import time - assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) - assert any(isinstance(h, RotatingFileHandler) for h in logger.handlers) - # Should NOT have NullHandler when console/file are enabled - assert not any(isinstance(h, logging.NullHandler) for h in logger.handlers) + test_message = 'info test both outputs 77777' + logger.info(test_message) + time.sleep(0.1) # Small delay to ensure write + # Verify file logging works + assert log_file.exists() + log_content = log_file.read_text() + assert test_message in log_content def test_config_to_dict_roundtrip(self, tmp_path): """Test that config can be saved to dict, modified, and restored.""" @@ -416,7 +441,6 @@ def test_logger_actually_logs(self, tmp_path): CONFIG.Logging.level = 'DEBUG' CONFIG.apply() - logger = logging.getLogger('flixopt') test_message = 'Test log message from config test' logger.debug(test_message) @@ -443,8 +467,7 @@ def test_config_reset(self): """Test that CONFIG.reset() restores all defaults.""" # Modify all config values CONFIG.Logging.level = 'DEBUG' - CONFIG.Logging.console = False - CONFIG.Logging.rich = True + CONFIG.Logging.console = True CONFIG.Logging.file = '/tmp/test.log' CONFIG.Modeling.big = 99999999 CONFIG.Modeling.epsilon = 1e-8 @@ -461,7 +484,6 @@ def test_config_reset(self): # Verify all values are back to defaults assert CONFIG.Logging.level == 'INFO' assert CONFIG.Logging.console is False - assert CONFIG.Logging.rich is False assert CONFIG.Logging.file is None assert CONFIG.Modeling.big == 10_000_000 assert CONFIG.Modeling.epsilon == 1e-5 @@ -472,10 +494,23 @@ def test_config_reset(self): assert CONFIG.Solving.log_main_results is True assert CONFIG.config_name == 'flixopt' - # Verify logging was also reset - logger = logging.getLogger('flixopt') - assert logger.level == logging.INFO - assert isinstance(logger.handlers[0], logging.NullHandler) + # Verify logging was also reset (default is no logging to console/file) + # Test that logs don't appear with default config + from io import StringIO + + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = StringIO() + sys.stderr = StringIO() + try: + logger.info('should not appear after reset') + stdout_content = sys.stdout.getvalue() + stderr_content = sys.stderr.getvalue() + assert 'should not appear after reset' not in stdout_content + assert 'should not appear after reset' not in stderr_content + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr def test_reset_matches_class_defaults(self): """Test that reset() values match the _DEFAULTS constants. @@ -486,7 +521,6 @@ def test_reset_matches_class_defaults(self): # Modify all values to something different CONFIG.Logging.level = 'CRITICAL' CONFIG.Logging.file = '/tmp/test.log' - CONFIG.Logging.rich = True CONFIG.Logging.console = True CONFIG.Modeling.big = 999999 CONFIG.Modeling.epsilon = 1e-10 @@ -509,7 +543,6 @@ def test_reset_matches_class_defaults(self): # Verify reset() restored exactly the _DEFAULTS values assert CONFIG.Logging.level == _DEFAULTS['logging']['level'] assert CONFIG.Logging.file == _DEFAULTS['logging']['file'] - assert CONFIG.Logging.rich == _DEFAULTS['logging']['rich'] assert CONFIG.Logging.console == _DEFAULTS['logging']['console'] assert CONFIG.Modeling.big == _DEFAULTS['modeling']['big'] assert CONFIG.Modeling.epsilon == _DEFAULTS['modeling']['epsilon'] From 10efff33dbb2b6f75868c25c94b86d0e3efb842b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:31:52 +0100 Subject: [PATCH 08/25] Ensure flows are present --- flixopt/linear_converters.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 7b1b5cdbe..7fa914062 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -90,6 +90,12 @@ def __init__( thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) self._validate_kwargs(kwargs) + # Validate required parameters + if fuel_flow is None: + raise ValueError(f"'{label}': fuel_flow is required and cannot be None") + if thermal_flow is None: + raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + super().__init__( label, inputs=[fuel_flow], @@ -221,6 +227,12 @@ def __init__( thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) self._validate_kwargs(kwargs) + # Validate required parameters + if power_flow is None: + raise ValueError(f"'{label}': power_flow is required and cannot be None") + if thermal_flow is None: + raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + super().__init__( label, inputs=[power_flow], @@ -354,6 +366,12 @@ def __init__( cop = self._handle_deprecated_kwarg(kwargs, 'COP', 'cop', cop) self._validate_kwargs(kwargs) + # Validate required parameters + if power_flow is None: + raise ValueError(f"'{label}': power_flow is required and cannot be None") + if thermal_flow is None: + raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + super().__init__( label, inputs=[power_flow], @@ -505,6 +523,12 @@ def __init__( thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) self._validate_kwargs(kwargs) + # Validate required parameters + if power_flow is None: + raise ValueError(f"'{label}': power_flow is required and cannot be None") + if thermal_flow is None: + raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + super().__init__( label, inputs=[power_flow, thermal_flow], @@ -649,6 +673,14 @@ def __init__( thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) self._validate_kwargs(kwargs) + # Validate required parameters + if fuel_flow is None: + raise ValueError(f"'{label}': fuel_flow is required and cannot be None") + if power_flow is None: + raise ValueError(f"'{label}': power_flow is required and cannot be None") + if thermal_flow is None: + raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + super().__init__( label, inputs=[fuel_flow], @@ -827,6 +859,14 @@ def __init__( cop = self._handle_deprecated_kwarg(kwargs, 'COP', 'cop', cop) self._validate_kwargs(kwargs) + # Validate required parameters + if power_flow is None: + raise ValueError(f"'{label}': power_flow is required and cannot be None") + if heat_source_flow is None: + raise ValueError(f"'{label}': heat_source_flow is required and cannot be None") + if thermal_flow is None: + raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + super().__init__( label, inputs=[power_flow, heat_source_flow], From c7ad0e968c011ade0745575adbd3c15beb218f60 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:32:02 +0100 Subject: [PATCH 09/25] Improve logging --- flixopt/linear_converters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 7fa914062..343a5d277 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -997,7 +997,7 @@ def check_bounds( if not np.all(value > lower_bound): logger.warning( - "'{}.{}' <= lower bound {}. {}.min={} shape={}", + "'{}.{}' <= lower bound {}. {}.min={}, shape={}", element_label, parameter_label, lower_bound, @@ -1007,7 +1007,7 @@ def check_bounds( ) if not np.all(value < upper_bound): logger.warning( - "'{}.{}' >= upper bound {}. {}.max={} shape={}", + "'{}.{}' >= upper bound {}. {}.max={}, shape={}", element_label, parameter_label, upper_bound, From c3bb04798200cbe7bbffd0f2aedf9b6dea69fe0f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:40:43 +0100 Subject: [PATCH 10/25] Remove special handling for TimeSeriesData --- flixopt/linear_converters.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 343a5d277..f93304973 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -886,9 +886,7 @@ def cop(self): @cop.setter def cop(self, value): check_bounds(value, 'cop', self.label_full, 1, 20) - # Unwrap TimeSeriesData before numpy comparison (consistent with check_bounds) - ts_value = value.data if isinstance(value, TimeSeriesData) else value - if np.any(np.asarray(ts_value) == 1): + if np.any(np.asarray(value) == 1): raise ValueError(f'{self.label_full}.cop must be strictly !=1 for HeatPumpWithSource.') self.conversion_factors = [ {self.power_flow.label: value, self.thermal_flow.label: 1}, @@ -985,13 +983,6 @@ def check_bounds( lower_bound: The lower bound. upper_bound: The upper bound. """ - if isinstance(value, TimeSeriesData): - value = value.data - if isinstance(lower_bound, TimeSeriesData): - lower_bound = lower_bound.data - if isinstance(upper_bound, TimeSeriesData): - upper_bound = upper_bound.data - # Convert to array for shape and statistics value_arr = np.asarray(value) From 633e977c8de1ccde6f0ee6a0a6609368a0e0b49b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:07:50 +0100 Subject: [PATCH 11/25] Backward Compatibility for HeatPump and HeatPumpWithSource Robust Type Handling in check_bounds --- flixopt/linear_converters.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index f93304973..4a3160480 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -353,7 +353,7 @@ class HeatPump(LinearConverter): def __init__( self, label: str, - cop: Numeric_TPS, + cop: Numeric_TPS | None = None, power_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, @@ -371,6 +371,8 @@ def __init__( raise ValueError(f"'{label}': power_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + if cop is None: + raise ValueError(f"'{label}': cop is required and cannot be None") super().__init__( label, @@ -844,7 +846,7 @@ class HeatPumpWithSource(LinearConverter): def __init__( self, label: str, - cop: Numeric_TPS, + cop: Numeric_TPS | None = None, power_flow: Flow | None = None, heat_source_flow: Flow | None = None, thermal_flow: Flow | None = None, @@ -866,6 +868,8 @@ def __init__( raise ValueError(f"'{label}': heat_source_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + if cop is None: + raise ValueError(f"'{label}': cop is required and cannot be None") super().__init__( label, @@ -986,7 +990,7 @@ def check_bounds( # Convert to array for shape and statistics value_arr = np.asarray(value) - if not np.all(value > lower_bound): + if not np.all(value_arr > lower_bound): logger.warning( "'{}.{}' <= lower bound {}. {}.min={}, shape={}", element_label, @@ -996,7 +1000,7 @@ def check_bounds( float(np.min(value_arr)), np.shape(value_arr), ) - if not np.all(value < upper_bound): + if not np.all(value_arr < upper_bound): logger.warning( "'{}.{}' >= upper bound {}. {}.max={}, shape={}", element_label, From a048d1bb6a14a261dd546be779d851892b903b3a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 10:27:16 +0100 Subject: [PATCH 12/25] =?UTF-8?q?Renamed=20eta=20=E2=86=92=20thermal=5Feff?= =?UTF-8?q?iciency=20=20Renamed=20eta=5Fth=20=E2=86=92=20thermal=5Fefficie?= =?UTF-8?q?ncy=20=20=20Renamed=20eta=5Fel=20=E2=86=92=20electrical=5Feffic?= =?UTF-8?q?iency=20=20=20=20-=20Fixed=20to=20use=20value=5Farr=20for=20com?= =?UTF-8?q?parisons=20instead=20of=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flixopt/linear_converters.py | 184 ++++++++++++++++++++++++++--------- 1 file changed, 140 insertions(+), 44 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 4a3160480..830b7e588 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -31,13 +31,14 @@ class Boiler(LinearConverter): Args: label: The label of the Element. Used to identify it in the FlowSystem. - eta: Thermal efficiency factor (0-1 range). Defines the ratio of thermal + thermal_efficiency: Thermal efficiency factor (0-1 range). Defines the ratio of thermal output to fuel input energy content. fuel_flow: Fuel input-flow representing fuel consumption. thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + eta: *Deprecated*. Use `thermal_efficiency` instead. Q_fu: *Deprecated*. Use `fuel_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. @@ -47,7 +48,7 @@ class Boiler(LinearConverter): ```python gas_boiler = Boiler( label='natural_gas_boiler', - eta=0.85, # 85% thermal efficiency + thermal_efficiency=0.85, # 85% thermal efficiency fuel_flow=natural_gas_flow, thermal_flow=hot_water_flow, ) @@ -58,7 +59,7 @@ class Boiler(LinearConverter): ```python biomass_boiler = Boiler( label='wood_chip_boiler', - eta=seasonal_efficiency_profile, # Time-varying efficiency + thermal_efficiency=seasonal_efficiency_profile, # Time-varying efficiency fuel_flow=biomass_flow, thermal_flow=district_heat_flow, on_off_parameters=OnOffParameters( @@ -69,7 +70,7 @@ class Boiler(LinearConverter): ``` Note: - The conversion relationship is: thermal_flow = fuel_flow × eta + The conversion relationship is: thermal_flow = fuel_flow × thermal_efficiency Efficiency should be between 0 and 1, where 1 represents perfect conversion (100% of fuel energy converted to useful thermal output). @@ -78,7 +79,7 @@ class Boiler(LinearConverter): def __init__( self, label: str, - eta: Numeric_TPS, + thermal_efficiency: Numeric_TPS | None = None, fuel_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, @@ -88,6 +89,7 @@ def __init__( # Handle deprecated parameters fuel_flow = self._handle_deprecated_kwarg(kwargs, 'Q_fu', 'fuel_flow', fuel_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + thermal_efficiency = self._handle_deprecated_kwarg(kwargs, 'eta', 'thermal_efficiency', thermal_efficiency) self._validate_kwargs(kwargs) # Validate required parameters @@ -95,6 +97,8 @@ def __init__( raise ValueError(f"'{label}': fuel_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + if thermal_efficiency is None: + raise ValueError(f"'{label}': thermal_efficiency is required and cannot be None") super().__init__( label, @@ -105,17 +109,35 @@ def __init__( ) self.fuel_flow = fuel_flow self.thermal_flow = thermal_flow - self.eta = eta # Uses setter + self.thermal_efficiency = thermal_efficiency # Uses setter @property - def eta(self): + def thermal_efficiency(self): return self.conversion_factors[0][self.fuel_flow.label] - @eta.setter - def eta(self, value): - check_bounds(value, 'eta', self.label_full, 0, 1) + @thermal_efficiency.setter + def thermal_efficiency(self, value): + check_bounds(value, 'thermal_efficiency', self.label_full, 0, 1) self.conversion_factors = [{self.fuel_flow.label: value, self.thermal_flow.label: 1}] + @property + def eta(self) -> Numeric_TPS: + warnings.warn( + 'The "eta" property is deprecated. Use "thermal_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_efficiency + + @eta.setter + def eta(self, value: Numeric_TPS) -> None: + warnings.warn( + 'The "eta" property is deprecated. Use "thermal_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_efficiency = value + @property def Q_fu(self) -> Flow: # noqa: N802 warnings.warn( @@ -165,7 +187,7 @@ class Power2Heat(LinearConverter): Args: label: The label of the Element. Used to identify it in the FlowSystem. - eta: Thermal efficiency factor (0-1 range). For resistance heating this is + thermal_efficiency: Thermal efficiency factor (0-1 range). For resistance heating this is typically close to 1.0 (nearly 100% efficiency), but may be lower for electrode boilers or systems with distribution losses. power_flow: Electrical input-flow representing electricity consumption. @@ -173,6 +195,7 @@ class Power2Heat(LinearConverter): on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + eta: *Deprecated*. Use `thermal_efficiency` instead. P_el: *Deprecated*. Use `power_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. @@ -182,7 +205,7 @@ class Power2Heat(LinearConverter): ```python electric_heater = Power2Heat( label='resistance_heater', - eta=0.98, # 98% efficiency (small losses) + thermal_efficiency=0.98, # 98% efficiency (small losses) power_flow=electricity_flow, thermal_flow=space_heating_flow, ) @@ -193,7 +216,7 @@ class Power2Heat(LinearConverter): ```python electrode_boiler = Power2Heat( label='electrode_steam_boiler', - eta=0.95, # 95% efficiency including boiler losses + thermal_efficiency=0.95, # 95% efficiency including boiler losses power_flow=industrial_electricity, thermal_flow=process_steam_flow, on_off_parameters=OnOffParameters( @@ -204,9 +227,9 @@ class Power2Heat(LinearConverter): ``` Note: - The conversion relationship is: thermal_flow = power_flow × eta + The conversion relationship is: thermal_flow = power_flow × thermal_efficiency - Unlike heat pumps, Power2Heat systems cannot exceed 100% efficiency (eta ≤ 1.0) + Unlike heat pumps, Power2Heat systems cannot exceed 100% efficiency (thermal_efficiency ≤ 1.0) as they only convert electrical energy without extracting additional energy from the environment. However, they provide fast response times and precise temperature control. @@ -215,7 +238,7 @@ class Power2Heat(LinearConverter): def __init__( self, label: str, - eta: Numeric_TPS, + thermal_efficiency: Numeric_TPS | None = None, power_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, @@ -225,6 +248,7 @@ def __init__( # Handle deprecated parameters power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + thermal_efficiency = self._handle_deprecated_kwarg(kwargs, 'eta', 'thermal_efficiency', thermal_efficiency) self._validate_kwargs(kwargs) # Validate required parameters @@ -232,6 +256,8 @@ def __init__( raise ValueError(f"'{label}': power_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + if thermal_efficiency is None: + raise ValueError(f"'{label}': thermal_efficiency is required and cannot be None") super().__init__( label, @@ -243,17 +269,35 @@ def __init__( self.power_flow = power_flow self.thermal_flow = thermal_flow - self.eta = eta # Uses setter + self.thermal_efficiency = thermal_efficiency # Uses setter @property - def eta(self): + def thermal_efficiency(self): return self.conversion_factors[0][self.power_flow.label] - @eta.setter - def eta(self, value): - check_bounds(value, 'eta', self.label_full, 0, 1) + @thermal_efficiency.setter + def thermal_efficiency(self, value): + check_bounds(value, 'thermal_efficiency', self.label_full, 0, 1) self.conversion_factors = [{self.power_flow.label: value, self.thermal_flow.label: 1}] + @property + def eta(self) -> Numeric_TPS: + warnings.warn( + 'The "eta" property is deprecated. Use "thermal_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_efficiency + + @eta.setter + def eta(self, value: Numeric_TPS) -> None: + warnings.warn( + 'The "eta" property is deprecated. Use "thermal_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_efficiency = value + @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( @@ -601,9 +645,9 @@ class CHP(LinearConverter): Args: label: The label of the Element. Used to identify it in the FlowSystem. - eta_th: Thermal efficiency factor (0-1 range). Defines the fraction of fuel + thermal_efficiency: Thermal efficiency factor (0-1 range). Defines the fraction of fuel energy converted to useful thermal output. - eta_el: Electrical efficiency factor (0-1 range). Defines the fraction of fuel + electrical_efficiency: Electrical efficiency factor (0-1 range). Defines the fraction of fuel energy converted to electrical output. fuel_flow: Fuel input-flow representing fuel consumption. power_flow: Electrical output-flow representing electricity generation. @@ -611,6 +655,8 @@ class CHP(LinearConverter): on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + eta_th: *Deprecated*. Use `thermal_efficiency` instead. + eta_el: *Deprecated*. Use `electrical_efficiency` instead. Q_fu: *Deprecated*. Use `fuel_flow` instead. P_el: *Deprecated*. Use `power_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. @@ -621,8 +667,8 @@ class CHP(LinearConverter): ```python gas_chp = CHP( label='natural_gas_chp', - eta_th=0.45, # 45% thermal efficiency - eta_el=0.35, # 35% electrical efficiency (80% total) + thermal_efficiency=0.45, # 45% thermal efficiency + electrical_efficiency=0.35, # 35% electrical efficiency (80% total) fuel_flow=natural_gas_flow, power_flow=electricity_flow, thermal_flow=district_heat_flow, @@ -634,8 +680,8 @@ class CHP(LinearConverter): ```python industrial_chp = CHP( label='industrial_chp', - eta_th=0.40, - eta_el=0.38, + thermal_efficiency=0.40, + electrical_efficiency=0.38, fuel_flow=fuel_gas_flow, power_flow=plant_electricity, thermal_flow=process_steam, @@ -649,10 +695,10 @@ class CHP(LinearConverter): Note: The conversion relationships are: - - thermal_flow = fuel_flow × eta_th (thermal output) - - power_flow = fuel_flow × eta_el (electrical output) + - thermal_flow = fuel_flow × thermal_efficiency (thermal output) + - power_flow = fuel_flow × electrical_efficiency (electrical output) - Total efficiency (eta_th + eta_el) should be ≤ 1.0, with typical combined + Total efficiency (thermal_efficiency + electrical_efficiency) should be ≤ 1.0, with typical combined efficiencies of 80-90% for modern CHP units. This provides significant efficiency gains compared to separate heat and power generation. """ @@ -660,8 +706,8 @@ class CHP(LinearConverter): def __init__( self, label: str, - eta_th: Numeric_TPS, - eta_el: Numeric_TPS, + thermal_efficiency: Numeric_TPS | None = None, + electrical_efficiency: Numeric_TPS | None = None, fuel_flow: Flow | None = None, power_flow: Flow | None = None, thermal_flow: Flow | None = None, @@ -673,6 +719,10 @@ def __init__( fuel_flow = self._handle_deprecated_kwarg(kwargs, 'Q_fu', 'fuel_flow', fuel_flow) power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) + thermal_efficiency = self._handle_deprecated_kwarg(kwargs, 'eta_th', 'thermal_efficiency', thermal_efficiency) + electrical_efficiency = self._handle_deprecated_kwarg( + kwargs, 'eta_el', 'electrical_efficiency', electrical_efficiency + ) self._validate_kwargs(kwargs) # Validate required parameters @@ -682,6 +732,10 @@ def __init__( raise ValueError(f"'{label}': power_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") + if thermal_efficiency is None: + raise ValueError(f"'{label}': thermal_efficiency is required and cannot be None") + if electrical_efficiency is None: + raise ValueError(f"'{label}': electrical_efficiency is required and cannot be None") super().__init__( label, @@ -695,29 +749,71 @@ def __init__( self.fuel_flow = fuel_flow self.power_flow = power_flow self.thermal_flow = thermal_flow - self.eta_th = eta_th # Uses setter - self.eta_el = eta_el # Uses setter - - check_bounds(eta_el + eta_th, 'eta_th+eta_el', self.label_full, 0, 1) + self.thermal_efficiency = thermal_efficiency # Uses setter + self.electrical_efficiency = electrical_efficiency # Uses setter + + check_bounds( + electrical_efficiency + thermal_efficiency, + 'thermal_efficiency+electrical_efficiency', + self.label_full, + 0, + 1, + ) @property - def eta_th(self): + def thermal_efficiency(self): return self.conversion_factors[0][self.fuel_flow.label] - @eta_th.setter - def eta_th(self, value): - check_bounds(value, 'eta_th', self.label_full, 0, 1) + @thermal_efficiency.setter + def thermal_efficiency(self, value): + check_bounds(value, 'thermal_efficiency', self.label_full, 0, 1) self.conversion_factors[0] = {self.fuel_flow.label: value, self.thermal_flow.label: 1} @property - def eta_el(self): + def electrical_efficiency(self): return self.conversion_factors[1][self.fuel_flow.label] - @eta_el.setter - def eta_el(self, value): - check_bounds(value, 'eta_el', self.label_full, 0, 1) + @electrical_efficiency.setter + def electrical_efficiency(self, value): + check_bounds(value, 'electrical_efficiency', self.label_full, 0, 1) self.conversion_factors[1] = {self.fuel_flow.label: value, self.power_flow.label: 1} + @property + def eta_th(self) -> Numeric_TPS: + warnings.warn( + 'The "eta_th" property is deprecated. Use "thermal_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.thermal_efficiency + + @eta_th.setter + def eta_th(self, value: Numeric_TPS) -> None: + warnings.warn( + 'The "eta_th" property is deprecated. Use "thermal_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.thermal_efficiency = value + + @property + def eta_el(self) -> Numeric_TPS: + warnings.warn( + 'The "eta_el" property is deprecated. Use "electrical_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.electrical_efficiency + + @eta_el.setter + def eta_el(self, value: Numeric_TPS) -> None: + warnings.warn( + 'The "eta_el" property is deprecated. Use "electrical_efficiency" instead.', + DeprecationWarning, + stacklevel=2, + ) + self.electrical_efficiency = value + @property def Q_fu(self) -> Flow: # noqa: N802 warnings.warn( From 1c57f8356f78b07956c5460c9996d4999bef09d0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 10:28:21 +0100 Subject: [PATCH 13/25] Rename power_flow to electrical_flow --- CHANGELOG.md | 10 +-- flixopt/linear_converters.py | 168 +++++++++++++++++------------------ 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ddd3c0..7683da796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,11 +77,11 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - **Type handling improvements**: Updated internal data handling to work seamlessly with the new type system - **Parameter renaming in `linear_converters.py`**: Renamed parameters to use lowercase, descriptive names for better consistency: - `Boiler`: `Q_fu` → `fuel_flow`, `Q_th` → `thermal_flow` - - `Power2Heat`: `P_el` → `power_flow`, `Q_th` → `thermal_flow` - - `HeatPump`: `COP` → `cop`, `P_el` → `power_flow`, `Q_th` → `thermal_flow` - - `CoolingTower`: `P_el` → `power_flow`, `Q_th` → `thermal_flow` - - `CHP`: `Q_fu` → `fuel_flow`, `P_el` → `power_flow`, `Q_th` → `thermal_flow` - - `HeatPumpWithSource`: `COP` → `cop`, `P_el` → `power_flow`, `Q_ab` → `heat_source_flow`, `Q_th` → `thermal_flow` + - `Power2Heat`: `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `HeatPump`: `COP` → `cop`, `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `CoolingTower`: `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `CHP`: `Q_fu` → `fuel_flow`, `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `HeatPumpWithSource`: `COP` → `cop`, `P_el` → `electrical_flow`, `Q_ab` → `heat_source_flow`, `Q_th` → `thermal_flow` ### 🗑️ Deprecated - **Old parameter names in `linear_converters.py`**: The old uppercase parameter names are now deprecated and accessible as properties that emit `DeprecationWarning`. They will be removed in v4.0.0: diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 830b7e588..055425eae 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -190,13 +190,13 @@ class Power2Heat(LinearConverter): thermal_efficiency: Thermal efficiency factor (0-1 range). For resistance heating this is typically close to 1.0 (nearly 100% efficiency), but may be lower for electrode boilers or systems with distribution losses. - power_flow: Electrical input-flow representing electricity consumption. + electrical_flow: Electrical input-flow representing electricity consumption. thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. eta: *Deprecated*. Use `thermal_efficiency` instead. - P_el: *Deprecated*. Use `power_flow` instead. + P_el: *Deprecated*. Use `electrical_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: @@ -206,7 +206,7 @@ class Power2Heat(LinearConverter): electric_heater = Power2Heat( label='resistance_heater', thermal_efficiency=0.98, # 98% efficiency (small losses) - power_flow=electricity_flow, + electrical_flow=electricity_flow, thermal_flow=space_heating_flow, ) ``` @@ -217,7 +217,7 @@ class Power2Heat(LinearConverter): electrode_boiler = Power2Heat( label='electrode_steam_boiler', thermal_efficiency=0.95, # 95% efficiency including boiler losses - power_flow=industrial_electricity, + electrical_flow=industrial_electricity, thermal_flow=process_steam_flow, on_off_parameters=OnOffParameters( consecutive_on_hours_min=1, # Minimum 1-hour operation @@ -227,7 +227,7 @@ class Power2Heat(LinearConverter): ``` Note: - The conversion relationship is: thermal_flow = power_flow × thermal_efficiency + The conversion relationship is: thermal_flow = electrical_flow × thermal_efficiency Unlike heat pumps, Power2Heat systems cannot exceed 100% efficiency (thermal_efficiency ≤ 1.0) as they only convert electrical energy without extracting additional energy @@ -239,21 +239,21 @@ def __init__( self, label: str, thermal_efficiency: Numeric_TPS | None = None, - power_flow: Flow | None = None, + electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, **kwargs, ): # Handle deprecated parameters - power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + electrical_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'electrical_flow', electrical_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) thermal_efficiency = self._handle_deprecated_kwarg(kwargs, 'eta', 'thermal_efficiency', thermal_efficiency) self._validate_kwargs(kwargs) # Validate required parameters - if power_flow is None: - raise ValueError(f"'{label}': power_flow is required and cannot be None") + if electrical_flow is None: + raise ValueError(f"'{label}': electrical_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") if thermal_efficiency is None: @@ -261,24 +261,24 @@ def __init__( super().__init__( label, - inputs=[power_flow], + inputs=[electrical_flow], outputs=[thermal_flow], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.power_flow = power_flow + self.electrical_flow = electrical_flow self.thermal_flow = thermal_flow self.thermal_efficiency = thermal_efficiency # Uses setter @property def thermal_efficiency(self): - return self.conversion_factors[0][self.power_flow.label] + return self.conversion_factors[0][self.electrical_flow.label] @thermal_efficiency.setter def thermal_efficiency(self, value): check_bounds(value, 'thermal_efficiency', self.label_full, 0, 1) - self.conversion_factors = [{self.power_flow.label: value, self.thermal_flow.label: 1}] + self.conversion_factors = [{self.electrical_flow.label: value, self.thermal_flow.label: 1}] @property def eta(self) -> Numeric_TPS: @@ -301,20 +301,20 @@ def eta(self, value: Numeric_TPS) -> None: @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - return self.power_flow + return self.electrical_flow @P_el.setter def P_el(self, value: Flow) -> None: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - self.power_flow = value + self.electrical_flow = value @property def Q_th(self) -> Flow: # noqa: N802 @@ -350,13 +350,13 @@ class HeatPump(LinearConverter): cop: Coefficient of Performance (typically 1-20 range). Defines the ratio of thermal output to electrical input. COP > 1 indicates the heat pump extracts additional energy from the environment. - power_flow: Electrical input-flow representing electricity consumption. + electrical_flow: Electrical input-flow representing electricity consumption. thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. COP: *Deprecated*. Use `cop` instead. - P_el: *Deprecated*. Use `power_flow` instead. + P_el: *Deprecated*. Use `electrical_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: @@ -366,7 +366,7 @@ class HeatPump(LinearConverter): air_hp = HeatPump( label='air_source_heat_pump', cop=3.5, # COP of 3.5 (350% efficiency) - power_flow=electricity_flow, + electrical_flow=electricity_flow, thermal_flow=heating_flow, ) ``` @@ -377,7 +377,7 @@ class HeatPump(LinearConverter): ground_hp = HeatPump( label='geothermal_heat_pump', cop=temperature_dependent_cop, # Time-varying COP based on ground temp - power_flow=electricity_flow, + electrical_flow=electricity_flow, thermal_flow=radiant_heating_flow, on_off_parameters=OnOffParameters( consecutive_on_hours_min=2, # Avoid frequent cycling @@ -387,7 +387,7 @@ class HeatPump(LinearConverter): ``` Note: - The conversion relationship is: thermal_flow = power_flow × COP + The conversion relationship is: thermal_flow = electrical_flow × COP COP should be greater than 1 for realistic heat pump operation, with typical values ranging from 2-6 depending on technology and operating conditions. @@ -398,21 +398,21 @@ def __init__( self, label: str, cop: Numeric_TPS | None = None, - power_flow: Flow | None = None, + electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, **kwargs, ): # Handle deprecated parameters - power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + electrical_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'electrical_flow', electrical_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) cop = self._handle_deprecated_kwarg(kwargs, 'COP', 'cop', cop) self._validate_kwargs(kwargs) # Validate required parameters - if power_flow is None: - raise ValueError(f"'{label}': power_flow is required and cannot be None") + if electrical_flow is None: + raise ValueError(f"'{label}': electrical_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") if cop is None: @@ -420,24 +420,24 @@ def __init__( super().__init__( label, - inputs=[power_flow], + inputs=[electrical_flow], outputs=[thermal_flow], conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.power_flow = power_flow + self.electrical_flow = electrical_flow self.thermal_flow = thermal_flow self.cop = cop # Uses setter @property def cop(self): - return self.conversion_factors[0][self.power_flow.label] + return self.conversion_factors[0][self.electrical_flow.label] @cop.setter def cop(self, value): check_bounds(value, 'cop', self.label_full, 1, 20) - self.conversion_factors = [{self.power_flow.label: value, self.thermal_flow.label: 1}] + self.conversion_factors = [{self.electrical_flow.label: value, self.thermal_flow.label: 1}] @property def COP(self) -> Numeric_TPS: # noqa: N802 @@ -460,20 +460,20 @@ def COP(self, value: Numeric_TPS) -> None: # noqa: N802 @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - return self.power_flow + return self.electrical_flow @P_el.setter def P_el(self, value: Flow) -> None: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - self.power_flow = value + self.electrical_flow = value @property def Q_th(self) -> Flow: # noqa: N802 @@ -509,12 +509,12 @@ class CoolingTower(LinearConverter): specific_electricity_demand: Auxiliary electricity demand per unit of cooling power (dimensionless, typically 0.01-0.05 range). Represents the fraction of thermal power that must be supplied as electricity for fans and pumps. - power_flow: Electrical input-flow representing electricity consumption for fans/pumps. + electrical_flow: Electrical input-flow representing electricity consumption for fans/pumps. thermal_flow: Thermal input-flow representing waste heat to be rejected to environment. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. - P_el: *Deprecated*. Use `power_flow` instead. + P_el: *Deprecated*. Use `electrical_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: @@ -524,7 +524,7 @@ class CoolingTower(LinearConverter): cooling_tower = CoolingTower( label='process_cooling_tower', specific_electricity_demand=0.025, # 2.5% auxiliary power - power_flow=cooling_electricity, + electrical_flow=cooling_electricity, thermal_flow=waste_heat_flow, ) ``` @@ -535,7 +535,7 @@ class CoolingTower(LinearConverter): condenser_cooling = CoolingTower( label='power_plant_cooling', specific_electricity_demand=0.015, # 1.5% auxiliary power - power_flow=auxiliary_electricity, + electrical_flow=auxiliary_electricity, thermal_flow=condenser_waste_heat, on_off_parameters=OnOffParameters( consecutive_on_hours_min=4, # Minimum operation time @@ -545,7 +545,7 @@ class CoolingTower(LinearConverter): ``` Note: - The conversion relationship is: power_flow = thermal_flow × specific_electricity_demand + The conversion relationship is: electrical_flow = thermal_flow × specific_electricity_demand The cooling tower consumes electrical power proportional to the thermal load. No thermal energy is produced - all thermal input is rejected to the environment. @@ -558,32 +558,32 @@ def __init__( self, label: str, specific_electricity_demand: Numeric_TPS, - power_flow: Flow | None = None, + electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, **kwargs, ): # Handle deprecated parameters - power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + electrical_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'electrical_flow', electrical_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) self._validate_kwargs(kwargs) # Validate required parameters - if power_flow is None: - raise ValueError(f"'{label}': power_flow is required and cannot be None") + if electrical_flow is None: + raise ValueError(f"'{label}': electrical_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") super().__init__( label, - inputs=[power_flow, thermal_flow], + inputs=[electrical_flow, thermal_flow], outputs=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.power_flow = power_flow + self.electrical_flow = electrical_flow self.thermal_flow = thermal_flow self.specific_electricity_demand = specific_electricity_demand # Uses setter @@ -594,25 +594,25 @@ def specific_electricity_demand(self): @specific_electricity_demand.setter def specific_electricity_demand(self, value): check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1) - self.conversion_factors = [{self.power_flow.label: -1, self.thermal_flow.label: value}] + self.conversion_factors = [{self.electrical_flow.label: -1, self.thermal_flow.label: value}] @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - return self.power_flow + return self.electrical_flow @P_el.setter def P_el(self, value: Flow) -> None: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - self.power_flow = value + self.electrical_flow = value @property def Q_th(self) -> Flow: # noqa: N802 @@ -650,7 +650,7 @@ class CHP(LinearConverter): electrical_efficiency: Electrical efficiency factor (0-1 range). Defines the fraction of fuel energy converted to electrical output. fuel_flow: Fuel input-flow representing fuel consumption. - power_flow: Electrical output-flow representing electricity generation. + electrical_flow: Electrical output-flow representing electricity generation. thermal_flow: Thermal output-flow representing heat generation. on_off_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but @@ -658,7 +658,7 @@ class CHP(LinearConverter): eta_th: *Deprecated*. Use `thermal_efficiency` instead. eta_el: *Deprecated*. Use `electrical_efficiency` instead. Q_fu: *Deprecated*. Use `fuel_flow` instead. - P_el: *Deprecated*. Use `power_flow` instead. + P_el: *Deprecated*. Use `electrical_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. Examples: @@ -670,7 +670,7 @@ class CHP(LinearConverter): thermal_efficiency=0.45, # 45% thermal efficiency electrical_efficiency=0.35, # 35% electrical efficiency (80% total) fuel_flow=natural_gas_flow, - power_flow=electricity_flow, + electrical_flow=electricity_flow, thermal_flow=district_heat_flow, ) ``` @@ -683,7 +683,7 @@ class CHP(LinearConverter): thermal_efficiency=0.40, electrical_efficiency=0.38, fuel_flow=fuel_gas_flow, - power_flow=plant_electricity, + electrical_flow=plant_electricity, thermal_flow=process_steam, on_off_parameters=OnOffParameters( consecutive_on_hours_min=8, # Minimum 8-hour operation @@ -696,7 +696,7 @@ class CHP(LinearConverter): Note: The conversion relationships are: - thermal_flow = fuel_flow × thermal_efficiency (thermal output) - - power_flow = fuel_flow × electrical_efficiency (electrical output) + - electrical_flow = fuel_flow × electrical_efficiency (electrical output) Total efficiency (thermal_efficiency + electrical_efficiency) should be ≤ 1.0, with typical combined efficiencies of 80-90% for modern CHP units. This provides significant @@ -709,7 +709,7 @@ def __init__( thermal_efficiency: Numeric_TPS | None = None, electrical_efficiency: Numeric_TPS | None = None, fuel_flow: Flow | None = None, - power_flow: Flow | None = None, + electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, @@ -717,7 +717,7 @@ def __init__( ): # Handle deprecated parameters fuel_flow = self._handle_deprecated_kwarg(kwargs, 'Q_fu', 'fuel_flow', fuel_flow) - power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + electrical_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'electrical_flow', electrical_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) thermal_efficiency = self._handle_deprecated_kwarg(kwargs, 'eta_th', 'thermal_efficiency', thermal_efficiency) electrical_efficiency = self._handle_deprecated_kwarg( @@ -728,8 +728,8 @@ def __init__( # Validate required parameters if fuel_flow is None: raise ValueError(f"'{label}': fuel_flow is required and cannot be None") - if power_flow is None: - raise ValueError(f"'{label}': power_flow is required and cannot be None") + if electrical_flow is None: + raise ValueError(f"'{label}': electrical_flow is required and cannot be None") if thermal_flow is None: raise ValueError(f"'{label}': thermal_flow is required and cannot be None") if thermal_efficiency is None: @@ -740,14 +740,14 @@ def __init__( super().__init__( label, inputs=[fuel_flow], - outputs=[thermal_flow, power_flow], + outputs=[thermal_flow, electrical_flow], conversion_factors=[{}, {}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.fuel_flow = fuel_flow - self.power_flow = power_flow + self.electrical_flow = electrical_flow self.thermal_flow = thermal_flow self.thermal_efficiency = thermal_efficiency # Uses setter self.electrical_efficiency = electrical_efficiency # Uses setter @@ -776,7 +776,7 @@ def electrical_efficiency(self): @electrical_efficiency.setter def electrical_efficiency(self, value): check_bounds(value, 'electrical_efficiency', self.label_full, 0, 1) - self.conversion_factors[1] = {self.fuel_flow.label: value, self.power_flow.label: 1} + self.conversion_factors[1] = {self.fuel_flow.label: value, self.electrical_flow.label: 1} @property def eta_th(self) -> Numeric_TPS: @@ -835,20 +835,20 @@ def Q_fu(self, value: Flow) -> None: # noqa: N802 @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - return self.power_flow + return self.electrical_flow @P_el.setter def P_el(self, value: Flow) -> None: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - self.power_flow = value + self.electrical_flow = value @property def Q_th(self) -> Flow: # noqa: N802 @@ -884,7 +884,7 @@ class HeatPumpWithSource(LinearConverter): cop: Coefficient of Performance (typically 1-20 range). Defines the ratio of thermal output to electrical input. The heat source extraction is automatically calculated as heat_source_flow = thermal_flow × (COP-1)/COP. - power_flow: Electrical input-flow representing electricity consumption for compressor. + electrical_flow: Electrical input-flow representing electricity consumption for compressor. heat_source_flow: Heat source input-flow representing thermal energy extracted from environment (ground, air, water source). thermal_flow: Thermal output-flow representing useful heat delivered to the application. @@ -892,7 +892,7 @@ class HeatPumpWithSource(LinearConverter): meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. COP: *Deprecated*. Use `cop` instead. - P_el: *Deprecated*. Use `power_flow` instead. + P_el: *Deprecated*. Use `electrical_flow` instead. Q_ab: *Deprecated*. Use `heat_source_flow` instead. Q_th: *Deprecated*. Use `thermal_flow` instead. @@ -903,7 +903,7 @@ class HeatPumpWithSource(LinearConverter): ground_source_hp = HeatPumpWithSource( label='geothermal_heat_pump', cop=4.5, # High COP due to stable ground temperature - power_flow=electricity_flow, + electrical_flow=electricity_flow, heat_source_flow=ground_heat_extraction, # Heat extracted from ground loop thermal_flow=building_heating_flow, ) @@ -915,7 +915,7 @@ class HeatPumpWithSource(LinearConverter): waste_heat_pump = HeatPumpWithSource( label='waste_heat_pump', cop=temperature_dependent_cop, # Varies with temperature of heat source - power_flow=electricity_consumption, + electrical_flow=electricity_consumption, heat_source_flow=industrial_heat_extraction, # Heat extracted from a industrial process or waste water thermal_flow=heat_supply, on_off_parameters=OnOffParameters( @@ -927,9 +927,9 @@ class HeatPumpWithSource(LinearConverter): Note: The conversion relationships are: - - thermal_flow = power_flow × COP (thermal output from electrical input) + - thermal_flow = electrical_flow × COP (thermal output from electrical input) - heat_source_flow = thermal_flow × (COP-1)/COP (heat source extraction) - - Energy balance: thermal_flow = power_flow + heat_source_flow + - Energy balance: thermal_flow = electrical_flow + heat_source_flow This formulation explicitly tracks the heat source, which is important for systems where the source capacity or temperature is limited, @@ -943,7 +943,7 @@ def __init__( self, label: str, cop: Numeric_TPS | None = None, - power_flow: Flow | None = None, + electrical_flow: Flow | None = None, heat_source_flow: Flow | None = None, thermal_flow: Flow | None = None, on_off_parameters: OnOffParameters | None = None, @@ -951,15 +951,15 @@ def __init__( **kwargs, ): # Handle deprecated parameters - power_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'power_flow', power_flow) + electrical_flow = self._handle_deprecated_kwarg(kwargs, 'P_el', 'electrical_flow', electrical_flow) heat_source_flow = self._handle_deprecated_kwarg(kwargs, 'Q_ab', 'heat_source_flow', heat_source_flow) thermal_flow = self._handle_deprecated_kwarg(kwargs, 'Q_th', 'thermal_flow', thermal_flow) cop = self._handle_deprecated_kwarg(kwargs, 'COP', 'cop', cop) self._validate_kwargs(kwargs) # Validate required parameters - if power_flow is None: - raise ValueError(f"'{label}': power_flow is required and cannot be None") + if electrical_flow is None: + raise ValueError(f"'{label}': electrical_flow is required and cannot be None") if heat_source_flow is None: raise ValueError(f"'{label}': heat_source_flow is required and cannot be None") if thermal_flow is None: @@ -969,19 +969,19 @@ def __init__( super().__init__( label, - inputs=[power_flow, heat_source_flow], + inputs=[electrical_flow, heat_source_flow], outputs=[thermal_flow], on_off_parameters=on_off_parameters, meta_data=meta_data, ) - self.power_flow = power_flow + self.electrical_flow = electrical_flow self.heat_source_flow = heat_source_flow self.thermal_flow = thermal_flow self.cop = cop # Uses setter @property def cop(self): - return self.conversion_factors[0][self.power_flow.label] + return self.conversion_factors[0][self.electrical_flow.label] @cop.setter def cop(self, value): @@ -989,7 +989,7 @@ def cop(self, value): if np.any(np.asarray(value) == 1): raise ValueError(f'{self.label_full}.cop must be strictly !=1 for HeatPumpWithSource.') self.conversion_factors = [ - {self.power_flow.label: value, self.thermal_flow.label: 1}, + {self.electrical_flow.label: value, self.thermal_flow.label: 1}, {self.heat_source_flow.label: value / (value - 1), self.thermal_flow.label: 1}, ] @@ -1014,20 +1014,20 @@ def COP(self, value: Numeric_TPS) -> None: # noqa: N802 @property def P_el(self) -> Flow: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - return self.power_flow + return self.electrical_flow @P_el.setter def P_el(self, value: Flow) -> None: # noqa: N802 warnings.warn( - 'The "P_el" property is deprecated. Use "power_flow" instead.', + 'The "P_el" property is deprecated. Use "electrical_flow" instead.', DeprecationWarning, stacklevel=2, ) - self.power_flow = value + self.electrical_flow = value @property def Q_ab(self) -> Flow: # noqa: N802 From f5d6ebee545ceb737c979e8270cf960bf061b947 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 10:39:59 +0100 Subject: [PATCH 14/25] Update CHANGELOG.md --- CHANGELOG.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7683da796..93df0ac28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,23 +75,34 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### ♻️ Changed - **Code structure**: Removed `commons.py` module and moved all imports directly to `__init__.py` for cleaner code organization (no public API changes) - **Type handling improvements**: Updated internal data handling to work seamlessly with the new type system -- **Parameter renaming in `linear_converters.py`**: Renamed parameters to use lowercase, descriptive names for better consistency: - - `Boiler`: `Q_fu` → `fuel_flow`, `Q_th` → `thermal_flow` - - `Power2Heat`: `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` - - `HeatPump`: `COP` → `cop`, `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` - - `CoolingTower`: `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` - - `CHP`: `Q_fu` → `fuel_flow`, `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` - - `HeatPumpWithSource`: `COP` → `cop`, `P_el` → `electrical_flow`, `Q_ab` → `heat_source_flow`, `Q_th` → `thermal_flow` +- **Parameter renaming in `linear_converters.py`**: Renamed parameters to use lowercase, descriptive names for better consistency and clarity: + - **Flow parameters** (deprecated uppercase abbreviations → descriptive names): + - `Boiler`: `Q_fu` → `fuel_flow`, `Q_th` → `thermal_flow` + - `Power2Heat`: `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` + - `HeatPump`: `COP` → `cop`, `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` + - `CoolingTower`: `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` + - `CHP`: `Q_fu` → `fuel_flow`, `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` + - `HeatPumpWithSource`: `COP` → `cop`, `P_el` → `power_flow` → `electrical_flow`, `Q_ab` → `heat_source_flow`, `Q_th` → `thermal_flow` + - **Efficiency parameters** (abbreviated → descriptive names): + - `Boiler`: `eta` → `thermal_efficiency` + - `Power2Heat`: `eta` → `thermal_efficiency` + - `CHP`: `eta_th` → `thermal_efficiency`, `eta_el` → `electrical_efficiency` + - `HetaPump`: `COP` → `cop` + - `HetaPumpWithSource`: `COP` → `cop` ### 🗑️ Deprecated -- **Old parameter names in `linear_converters.py`**: The old uppercase parameter names are now deprecated and accessible as properties that emit `DeprecationWarning`. They will be removed in v4.0.0: - - `Q_fu`, `Q_th`, `P_el`, `COP`, `Q_ab` (use lowercase equivalents instead) +- **Old parameter names in `linear_converters.py`**: The following parameter names are now deprecated and accessible as properties/kwargs that emit `DeprecationWarning`. They will be removed in v4.0.0: + - **Flow parameters**: `Q_fu`, `Q_th`, `P_el`, `Q_ab`, `power_flow` (use `fuel_flow`, `thermal_flow`, `electrical_flow`, `heat_source_flow` instead) + - **Efficiency parameters**: `eta`, `eta_th`, `eta_el` (use `thermal_efficiency`, `electrical_efficiency` instead) + - **COP parameter**: `COP` (use lowercase `cop` instead) ### 🔥 Removed ### 🐛 Fixed - Fixed `ShareAllocationModel` inconsistency where None/inf conversion happened in `__init__` instead of during modeling, which could cause issues with parameter validation - Fixed numerous type hint inconsistencies across the codebase +- Fixed backward compatibility issue in `HeatPump` and `HeatPumpWithSource` constructors where deprecated `COP` kwarg would fail before being handled +- Fixed `check_bounds` function in `linear_converters.py` to use normalized array for comparisons, improving robustness with array-like inputs ### 🔒 Security From 60b2bf6152f4b727c770019fa7d71f3ea51f7f8c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 10:58:44 +0100 Subject: [PATCH 15/25] Update all tests and examples --- examples/00_Minmal/minimal_example.py | 6 +- examples/01_Simple/simple_example.py | 16 ++--- examples/02_Complex/complex_example.py | 16 ++--- .../example_calculation_types.py | 16 ++--- examples/04_Scenarios/scenario_example.py | 18 ++--- .../two_stage_optimization.py | 16 ++--- tests/conftest.py | 48 ++++++------- tests/test_component.py | 27 +++++--- tests/test_effect.py | 6 +- tests/test_flow_system_resample.py | 4 +- tests/test_functional.py | 68 +++++++++---------- tests/test_scenarios.py | 6 +- 12 files changed, 129 insertions(+), 118 deletions(-) diff --git a/examples/00_Minmal/minimal_example.py b/examples/00_Minmal/minimal_example.py index 92e6801b2..9756396b3 100644 --- a/examples/00_Minmal/minimal_example.py +++ b/examples/00_Minmal/minimal_example.py @@ -18,9 +18,9 @@ fx.Effect('Costs', '€', 'Cost', is_standard=True, is_objective=True), fx.linear_converters.Boiler( 'Boiler', - eta=0.5, - Q_th=fx.Flow(label='Heat', bus='Heat', size=50), - Q_fu=fx.Flow(label='Gas', bus='Gas'), + thermal_efficiency=0.5, + thermal_flow=fx.Flow(label='Heat', bus='Heat', size=50), + fuel_flow=fx.Flow(label='Gas', bus='Gas'), ), fx.Sink( 'Sink', diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index fd5a3d9b7..d9737cf7b 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -46,19 +46,19 @@ # Boiler: Converts fuel (gas) into thermal energy (heat) boiler = fx.linear_converters.Boiler( label='Boiler', - eta=0.5, - Q_th=fx.Flow(label='Q_th', bus='Fernwärme', size=50, relative_minimum=0.1, relative_maximum=1), - Q_fu=fx.Flow(label='Q_fu', bus='Gas'), + thermal_efficiency=0.5, + thermal_flow=fx.Flow(label='Q_th', bus='Fernwärme', size=50, relative_minimum=0.1, relative_maximum=1), + fuel_flow=fx.Flow(label='Q_fu', bus='Gas'), ) # Combined Heat and Power (CHP): Generates both electricity and heat from fuel chp = fx.linear_converters.CHP( label='CHP', - eta_th=0.5, - eta_el=0.4, - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Gas'), + thermal_efficiency=0.5, + electrical_efficiency=0.4, + electrical_flow=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) # Storage: Energy storage system with charging and discharging capabilities diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 3ff5b251c..0f7ca0b82 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -50,11 +50,11 @@ # A gas boiler that converts fuel into thermal output, with investment and on-off parameters Gaskessel = fx.linear_converters.Boiler( 'Kessel', - eta=0.5, # Efficiency ratio + thermal_efficiency=0.5, # Efficiency ratio on_off_parameters=fx.OnOffParameters( effects_per_running_hour={Costs.label: 0, CO2.label: 1000} ), # CO2 emissions per hour - Q_th=fx.Flow( + thermal_flow=fx.Flow( label='Q_th', # Thermal output bus='Fernwärme', # Linked bus size=fx.InvestParameters( @@ -79,19 +79,19 @@ switch_on_total_max=1000, # Max number of starts ), ), - Q_fu=fx.Flow(label='Q_fu', bus='Gas', size=200), + fuel_flow=fx.Flow(label='Q_fu', bus='Gas', size=200), ) # 2. Define CHP Unit # Combined Heat and Power unit that generates both electricity and heat from fuel bhkw = fx.linear_converters.CHP( 'BHKW2', - eta_th=0.5, - eta_el=0.4, + thermal_efficiency=0.5, + electrical_efficiency=0.4, on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3, previous_flow_rate=20), # The CHP was ON previously + electrical_flow=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=1e3), + fuel_flow=fx.Flow('Q_fu', bus='Gas', size=1e3, previous_flow_rate=20), # The CHP was ON previously ) # 3. Define CHP with Piecewise Conversion diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index e339c1c24..fa57e6f9a 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -71,9 +71,9 @@ # 1. Boiler a_gaskessel = fx.linear_converters.Boiler( 'Kessel', - eta=0.85, - Q_th=fx.Flow(label='Q_th', bus='Fernwärme'), - Q_fu=fx.Flow( + thermal_efficiency=0.85, + thermal_flow=fx.Flow(label='Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow( label='Q_fu', bus='Gas', size=95, @@ -86,12 +86,12 @@ # 2. CHP a_kwk = fx.linear_converters.CHP( 'BHKW2', - eta_th=0.58, - eta_el=0.22, + thermal_efficiency=0.58, + electrical_efficiency=0.22, on_off_parameters=fx.OnOffParameters(effects_per_switch_on=24000), - P_el=fx.Flow('P_el', bus='Strom', size=200), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=200), - Q_fu=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288, previous_flow_rate=100), + electrical_flow=fx.Flow('P_el', bus='Strom', size=200), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=200), + fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288, previous_flow_rate=100), ) # 3. Storage diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index bf4f24617..ca50876c7 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -114,8 +114,8 @@ # Modern condensing gas boiler with realistic efficiency boiler = fx.linear_converters.Boiler( label='Boiler', - eta=0.92, # Realistic efficiency for modern condensing gas boiler (92%) - Q_th=fx.Flow( + thermal_efficiency=0.92, # Realistic efficiency for modern condensing gas boiler (92%) + thermal_flow=fx.Flow( label='Q_th', bus='Fernwärme', size=50, @@ -123,18 +123,20 @@ relative_maximum=1, on_off_parameters=fx.OnOffParameters(), ), - Q_fu=fx.Flow(label='Q_fu', bus='Gas'), + fuel_flow=fx.Flow(label='Q_fu', bus='Gas'), ) # Combined Heat and Power (CHP): Generates both electricity and heat from fuel # Modern CHP unit with realistic efficiencies (total efficiency ~88%) chp = fx.linear_converters.CHP( label='CHP', - eta_th=0.48, # Realistic thermal efficiency (48%) - eta_el=0.40, # Realistic electrical efficiency (40%) - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Gas'), + thermal_efficiency=0.48, # Realistic thermal efficiency (48%) + electrical_efficiency=0.40, # Realistic electrical efficiency (40%) + electrical_flow=fx.Flow( + 'P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters() + ), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) # Storage: Thermal energy storage system with charging and discharging capabilities diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 7354cb877..39a3aea0b 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -45,9 +45,9 @@ fx.Effect('PE', 'kWh_PE', 'Primärenergie'), fx.linear_converters.Boiler( 'Kessel', - eta=0.85, - Q_th=fx.Flow(label='Q_th', bus='Fernwärme'), - Q_fu=fx.Flow( + thermal_efficiency=0.85, + thermal_flow=fx.Flow(label='Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow( label='Q_fu', bus='Gas', size=fx.InvestParameters( @@ -60,14 +60,14 @@ ), fx.linear_converters.CHP( 'BHKW2', - eta_th=0.58, - eta_el=0.22, + thermal_efficiency=0.58, + electrical_efficiency=0.22, on_off_parameters=fx.OnOffParameters( effects_per_switch_on=1_000, consecutive_on_hours_min=10, consecutive_off_hours_min=10 ), - P_el=fx.Flow('P_el', bus='Strom'), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow( + 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=fx.InvestParameters( diff --git a/tests/conftest.py b/tests/conftest.py index bd940b843..1873bab0e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,8 +131,8 @@ def simple(): """Simple boiler from simple_flow_system""" return fx.linear_converters.Boiler( 'Boiler', - eta=0.5, - Q_th=fx.Flow( + thermal_efficiency=0.5, + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=50, @@ -140,7 +140,7 @@ def simple(): relative_maximum=1, on_off_parameters=fx.OnOffParameters(), ), - Q_fu=fx.Flow('Q_fu', bus='Gas'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @staticmethod @@ -148,9 +148,9 @@ def complex(): """Complex boiler with investment parameters from flow_system_complex""" return fx.linear_converters.Boiler( 'Kessel', - eta=0.5, + thermal_efficiency=0.5, on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), - Q_th=fx.Flow( + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', load_factor_max=1.0, @@ -175,7 +175,7 @@ def complex(): ), flow_hours_total_max=1e6, ), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), + fuel_flow=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), ) class CHPs: @@ -184,13 +184,13 @@ def simple(): """Simple CHP from simple_flow_system""" return fx.linear_converters.CHP( 'CHP_unit', - eta_th=0.5, - eta_el=0.4, - P_el=fx.Flow( + thermal_efficiency=0.5, + electrical_efficiency=0.4, + electrical_flow=fx.Flow( 'P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters() ), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @staticmethod @@ -198,12 +198,12 @@ def base(): """CHP from flow_system_base""" return fx.linear_converters.CHP( 'KWK', - eta_th=0.5, - eta_el=0.4, + thermal_efficiency=0.5, + electrical_efficiency=0.4, on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3), + electrical_flow=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=1e3), + fuel_flow=fx.Flow('Q_fu', bus='Gas', size=1e3), ) class LinearConverters: @@ -596,9 +596,9 @@ def flow_system_long(): flow_system.add_elements( fx.linear_converters.Boiler( 'Kessel', - eta=0.85, - Q_th=fx.Flow(label='Q_th', bus='Fernwärme'), - Q_fu=fx.Flow( + thermal_efficiency=0.85, + thermal_flow=fx.Flow(label='Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow( label='Q_fu', bus='Gas', size=95, @@ -609,12 +609,12 @@ def flow_system_long(): ), fx.linear_converters.CHP( 'BHKW2', - eta_th=0.58, - eta_el=0.22, + thermal_efficiency=0.58, + electrical_efficiency=0.22, on_off_parameters=fx.OnOffParameters(effects_per_switch_on=24000), - P_el=fx.Flow('P_el', bus='Strom'), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), + 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), ), fx.Storage( 'Speicher', diff --git a/tests/test_component.py b/tests/test_component.py index be1eecf3b..dbbd85c8f 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -416,7 +416,10 @@ def test_transmission_basic(self, basic_flow_system, highs_solver): flow_system.add_elements(fx.Bus('Wärme lokal')) boiler = fx.linear_converters.Boiler( - 'Boiler', eta=0.5, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + 'Boiler', + thermal_efficiency=0.5, + thermal_flow=fx.Flow('Q_th', bus='Wärme lokal'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) transmission = fx.Transmission( @@ -453,13 +456,16 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', - eta=0.9, - Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), - Q_fu=fx.Flow('Q_fu', bus='Gas'), + 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])), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) boiler2 = fx.linear_converters.Boiler( - 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + 'Boiler_backup', + thermal_efficiency=0.4, + thermal_flow=fx.Flow('Q_th', bus='Wärme lokal'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) last2 = fx.Sink( @@ -527,13 +533,16 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', - eta=0.9, - Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), - Q_fu=fx.Flow('Q_fu', bus='Gas'), + 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])), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) boiler2 = fx.linear_converters.Boiler( - 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + 'Boiler_backup', + thermal_efficiency=0.4, + thermal_flow=fx.Flow('Q_th', bus='Wärme lokal'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) last2 = fx.Sink( diff --git a/tests/test_effect.py b/tests/test_effect.py index cd3edc537..8293ec62f 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -247,13 +247,13 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): effect3, fx.linear_converters.Boiler( 'Boiler', - eta=0.5, - Q_th=fx.Flow( + thermal_efficiency=0.5, + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, mandatory=True), ), - Q_fu=fx.Flow('Q_fu', bus='Gas'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), ), ) diff --git a/tests/test_flow_system_resample.py b/tests/test_flow_system_resample.py index d28872a0f..8946dd02f 100644 --- a/tests/test_flow_system_resample.py +++ b/tests/test_flow_system_resample.py @@ -52,9 +52,9 @@ def complex_fs(): # Piecewise converter converter = fx.linear_converters.Boiler( - 'boiler', eta=0.9, Q_fu=fx.Flow('gas', bus='elec'), Q_th=fx.Flow('heat', bus='heat') + 'boiler', thermal_efficiency=0.9, fuel_flow=fx.Flow('gas', bus='elec'), thermal_flow=fx.Flow('heat', bus='heat') ) - converter.Q_th.size = 100 + converter.thermal_flow.size = 100 fs.add_elements(converter) # Component with investment diff --git a/tests/test_functional.py b/tests/test_functional.py index a83bf112f..6fe272a1a 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -86,8 +86,8 @@ def flow_system_minimal(timesteps) -> fx.FlowSystem: fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), ) ) return flow_system @@ -142,8 +142,8 @@ def test_fixed_size(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=fx.InvestParameters(fixed_size=1000, effects_of_investment=10, effects_of_investment_per_size=1), @@ -183,8 +183,8 @@ def test_optimize_size(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1), @@ -224,8 +224,8 @@ def test_size_bounds(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + 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), @@ -265,8 +265,8 @@ def test_optional_invest(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=fx.InvestParameters( @@ -277,8 +277,8 @@ def test_optional_invest(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler_optional', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=fx.InvestParameters( @@ -337,8 +337,8 @@ def test_on(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100, on_off_parameters=fx.OnOffParameters()), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100, on_off_parameters=fx.OnOffParameters()), ) ) @@ -376,8 +376,8 @@ def test_off(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=100, @@ -427,8 +427,8 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=100, @@ -485,8 +485,8 @@ def test_on_total_max(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=100, @@ -496,8 +496,8 @@ def test_on_total_max(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler_backup', 0.2, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100), ), ) @@ -535,8 +535,8 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=100, @@ -546,8 +546,8 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler_backup', 0.2, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=100, @@ -609,8 +609,8 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=100, @@ -620,8 +620,8 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler_backup', 0.2, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100), ), ) flow_system['Wärmelast'].inputs[0].fixed_relative_profile = np.array([5, 10, 20, 18, 12]) @@ -670,14 +670,14 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): fx.linear_converters.Boiler( 'Boiler', 0.5, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), ), fx.linear_converters.Boiler( 'Boiler_backup', 0.2, - Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow( + fuel_flow=fx.Flow('Q_fu', bus='Gas'), + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', size=100, diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 928eb88c6..c25750259 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -139,9 +139,9 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: boiler = fx.linear_converters.Boiler( 'Kessel', - eta=0.5, + thermal_efficiency=0.5, on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), - Q_th=fx.Flow( + thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', load_factor_max=1.0, @@ -166,7 +166,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: ), flow_hours_total_max=1e6, ), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), + fuel_flow=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), ) invest_speicher = fx.InvestParameters( From 56664703fa2e1b2237db4efdc8aa86eeb83e2df9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:34:41 +0100 Subject: [PATCH 16/25] Update remaining tests --- tests/test_functional.py | 66 +++++++++++++++++++-------------------- tests/test_integration.py | 6 ++-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/test_functional.py b/tests/test_functional.py index 6fe272a1a..36274d494 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -162,14 +162,14 @@ def test_fixed_size(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel._investment.size.solution.item(), 1000, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -203,14 +203,14 @@ def test_optimize_size(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel._investment.size.solution.item(), 20, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -244,14 +244,14 @@ def test_size_bounds(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel._investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -300,14 +300,14 @@ def test_optional_invest(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel._investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -315,14 +315,14 @@ def test_optional_invest(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_optional.Q_th.submodel._investment.size.solution.item(), + boiler_optional.thermal_flow.submodel._investment.size.solution.item(), 0, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler_optional.Q_th.submodel._investment.invested.solution.item(), + boiler_optional.thermal_flow.submodel._investment.invested.solution.item(), 0, rtol=1e-5, atol=1e-10, @@ -354,14 +354,14 @@ def test_on(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.flow_rate.solution.values, + boiler.thermal_flow.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -398,21 +398,21 @@ def test_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.on_off.off.solution.values, - 1 - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.on_off.off.solution.values, + 1 - boiler.thermal_flow.submodel.on_off.on.solution.values, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__off" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.flow_rate.solution.values, + boiler.thermal_flow.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -449,28 +449,28 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.on_off.switch_on.solution.values, + boiler.thermal_flow.submodel.on_off.switch_on.solution.values, [0, 1, 0, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__switch_on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.on_off.switch_off.solution.values, + boiler.thermal_flow.submodel.on_off.switch_off.solution.values, [0, 0, 0, 1, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__switch_on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.flow_rate.solution.values, + boiler.thermal_flow.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -513,14 +513,14 @@ def test_on_total_max(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.on_off.on.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.flow_rate.solution.values, + boiler.thermal_flow.submodel.flow_rate.solution.values, [0, 0, 20, 0, 0], rtol=1e-5, atol=1e-10, @@ -572,14 +572,14 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.on_off.on.solution.values, [0, 0, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.flow_rate.solution.values, + boiler.thermal_flow.submodel.flow_rate.solution.values, [0, 0, 20, 0, 12 - 1e-5], rtol=1e-5, atol=1e-10, @@ -587,14 +587,14 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - sum(boiler_backup.Q_th.submodel.on_off.on.solution.values), + sum(boiler_backup.thermal_flow.submodel.on_off.on.solution.values), 3, rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__on" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.submodel.flow_rate.solution.values, + boiler_backup.thermal_flow.submodel.flow_rate.solution.values, [0, 10, 1.0e-05, 0, 1.0e-05], rtol=1e-5, atol=1e-10, @@ -640,14 +640,14 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.on_off.on.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.flow_rate.solution.values, + boiler.thermal_flow.submodel.flow_rate.solution.values, [5, 10, 0, 18, 12], rtol=1e-5, atol=1e-10, @@ -655,7 +655,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.Q_th.submodel.flow_rate.solution.values, + boiler_backup.thermal_flow.submodel.flow_rate.solution.values, [0, 0, 20, 0, 0], rtol=1e-5, atol=1e-10, @@ -703,21 +703,21 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.Q_th.submodel.on_off.on.solution.values, + boiler_backup.thermal_flow.submodel.on_off.on.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__on" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.submodel.on_off.off.solution.values, + boiler_backup.thermal_flow.submodel.on_off.off.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__off" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.submodel.flow_rate.solution.values, + boiler_backup.thermal_flow.submodel.flow_rate.solution.values, [0, 0, 1e-5, 0, 0], rtol=1e-5, atol=1e-10, @@ -725,7 +725,7 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.flow_rate.solution.values, + boiler.thermal_flow.submodel.flow_rate.solution.values, [5, 0, 20 - 1e-5, 18, 12], rtol=1e-5, atol=1e-10, diff --git a/tests/test_integration.py b/tests/test_integration.py index 6e5da63d6..88e4a21af 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -37,14 +37,14 @@ def test_model_components(self, simple_flow_system, highs_solver): # Boiler assertions assert_almost_equal_numeric( - comps['Boiler'].Q_th.submodel.flow_rate.solution.values, + comps['Boiler'].thermal_flow.submodel.flow_rate.solution.values, [0, 0, 0, 28.4864, 35, 0, 0, 0, 0], 'Q_th doesnt match expected value', ) # CHP unit assertions assert_almost_equal_numeric( - comps['CHP_unit'].Q_th.submodel.flow_rate.solution.values, + comps['CHP_unit'].thermal_flow.submodel.flow_rate.solution.values, [30.0, 26.66666667, 75.0, 75.0, 75.0, 20.0, 20.0, 20.0, 20.0], 'Q_th doesnt match expected value', ) @@ -220,7 +220,7 @@ def test_piecewise_conversion(self, flow_system_piecewise_conversion, highs_solv effects['CO2'].submodel.total.solution.item(), 1278.7939026086956, 'CO2 doesnt match expected value' ) assert_almost_equal_numeric( - comps['Kessel'].Q_th.submodel.flow_rate.solution.values, + comps['Kessel'].thermal_flow.submodel.flow_rate.solution.values, [0, 0, 0, 45, 0, 0, 0, 0, 0], 'Kessel doesnt match expected value', ) From 8b35f0ceff618abf41fa85d7c4f7079dc08b99bf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:59:33 +0100 Subject: [PATCH 17/25] use the explicit thermal_efficiency keyword argument --- tests/test_functional.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_functional.py b/tests/test_functional.py index 36274d494..3d76c8c81 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -85,7 +85,7 @@ def flow_system_minimal(timesteps) -> fx.FlowSystem: flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), ) @@ -141,7 +141,7 @@ def test_fixed_size(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -182,7 +182,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -223,7 +223,7 @@ def test_size_bounds(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -264,7 +264,7 @@ def test_optional_invest(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -276,7 +276,7 @@ def test_optional_invest(solver_fixture, time_steps_fixture): ), fx.linear_converters.Boiler( 'Boiler_optional', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -336,7 +336,7 @@ def test_on(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100, on_off_parameters=fx.OnOffParameters()), ) @@ -375,7 +375,7 @@ def test_off(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -426,7 +426,7 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -484,7 +484,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -495,7 +495,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): ), fx.linear_converters.Boiler( 'Boiler_backup', - 0.2, + thermal_efficiency=0.2, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100), ), @@ -534,7 +534,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -545,7 +545,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ), fx.linear_converters.Boiler( 'Boiler_backup', - 0.2, + thermal_efficiency=0.2, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -608,7 +608,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', @@ -619,7 +619,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ), fx.linear_converters.Boiler( 'Boiler_backup', - 0.2, + thermal_efficiency=0.2, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100), ), @@ -669,13 +669,13 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): flow_system.add_elements( fx.linear_converters.Boiler( 'Boiler', - 0.5, + thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), ), fx.linear_converters.Boiler( 'Boiler_backup', - 0.2, + thermal_efficiency=0.2, fuel_flow=fx.Flow('Q_fu', bus='Gas'), thermal_flow=fx.Flow( 'Q_th', From ed051db00be4a855678b4c32b8267ff510e52638 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 16 Nov 2025 22:24:59 +0100 Subject: [PATCH 18/25] Update test to new porperty --- tests/test_functional.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_functional.py b/tests/test_functional.py index 3d76c8c81..9b3c3e6d4 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -162,14 +162,14 @@ def test_fixed_size(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel.investment.size.solution.item(), 1000, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel.investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -203,14 +203,14 @@ def test_optimize_size(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel.investment.size.solution.item(), 20, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel.investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -244,14 +244,14 @@ def test_size_bounds(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel.investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel.investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -300,14 +300,14 @@ def test_optional_invest(solver_fixture, time_steps_fixture): err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.size.solution.item(), + boiler.thermal_flow.submodel.investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.thermal_flow.submodel._investment.invested.solution.item(), + boiler.thermal_flow.submodel.investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -315,14 +315,14 @@ def test_optional_invest(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_optional.thermal_flow.submodel._investment.size.solution.item(), + boiler_optional.thermal_flow.submodel.investment.size.solution.item(), 0, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler_optional.thermal_flow.submodel._investment.invested.solution.item(), + boiler_optional.thermal_flow.submodel.investment.invested.solution.item(), 0, rtol=1e-5, atol=1e-10, From 3ebfeb6a4a0273f57bccc09ca2e97eac557d7a29 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:51:49 +0100 Subject: [PATCH 19/25] Rename lastValueOfSim to equals_last --- CHANGELOG.md | 4 +++ .../two_stage_optimization.py | 2 +- flixopt/components.py | 26 ++++++++++++------- tests/test_scenarios.py | 2 +- tests/test_storage.py | 2 +- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ccfc968..3a6af2e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,9 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - `CHP`: `eta_th` → `thermal_efficiency`, `eta_el` → `electrical_efficiency` - `HetaPump`: `COP` → `cop` - `HetaPumpWithSource`: `COP` → `cop` + - **Storage Parameters**: + - `initial_charge_state="lastValueOfSim"` -> `initial_charge_state="equals_last"` + ### 🗑️ Deprecated @@ -81,6 +84,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - **Flow parameters**: `Q_fu`, `Q_th`, `P_el`, `Q_ab`, `power_flow` (use `fuel_flow`, `thermal_flow`, `electrical_flow`, `heat_source_flow` instead) - **Efficiency parameters**: `eta`, `eta_th`, `eta_el` (use `thermal_efficiency`, `electrical_efficiency` instead) - **COP parameter**: `COP` (use lowercase `cop` instead) + - **Storage Parameter**: 'initial_charge_state'="lastValueOfSim" (use ="equals_last") ### 🔥 Removed diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 39a3aea0b..6c7b20276 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -82,7 +82,7 @@ capacity_in_flow_hours=fx.InvestParameters( minimum_size=10, maximum_size=1000, effects_of_investment_per_size={'costs': 60} ), - initial_charge_state='lastValueOfSim', + initial_charge_state='equals_final', eta_charge=1, eta_discharge=1, relative_loss_per_hour=0.001, diff --git a/flixopt/components.py b/flixopt/components.py index c51b4b7d2..83b47d09c 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -275,7 +275,7 @@ class Storage(Component): Scalar for fixed size or InvestParameters for optimization. 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 'lastValueOfSim'. Default: 0. + initial_charge_state: Charge at start. Numeric or 'equals_final'. Default: 0. minimal_final_charge_state: Minimum absolute charge required at end (optional). maximal_final_charge_state: Maximum absolute charge allowed at end (optional). relative_minimum_final_charge_state: Minimum relative charge at end. @@ -339,7 +339,7 @@ class Storage(Component): ), eta_charge=0.85, # Pumping efficiency eta_discharge=0.90, # Turbine efficiency - initial_charge_state='lastValueOfSim', # Ensuring no deficit compared to start + initial_charge_state='equals_final', # Ensuring no deficit compared to start relative_loss_per_hour=0.0001, # Minimal evaporation ) ``` @@ -388,7 +388,7 @@ def __init__( capacity_in_flow_hours: Numeric_PS | InvestParameters, relative_minimum_charge_state: Numeric_TPS = 0, relative_maximum_charge_state: Numeric_TPS = 1, - initial_charge_state: Numeric_PS | Literal['lastValueOfSim'] = 0, + initial_charge_state: Numeric_PS | Literal['equals_final'] = 0, minimal_final_charge_state: Numeric_PS | None = None, maximal_final_charge_state: Numeric_PS | None = None, relative_minimum_final_charge_state: Numeric_PS | None = None, @@ -408,6 +408,13 @@ def __init__( prevent_simultaneous_flows=[charging, discharging] if prevent_simultaneous_charge_and_discharge else None, meta_data=meta_data, ) + if isinstance(initial_charge_state, str) and initial_charge_state == 'lastValusOfSim': + warnings.warn( + f'{initial_charge_state=} is deprecated. Use "equals_final" instead.', + DeprecationWarning, + stacklevel=2, + ) + initial_charge_state = 'equals_final' self.charging = charging self.discharging = discharging @@ -483,12 +490,11 @@ def _plausibility_checks(self) -> None: super()._plausibility_checks() # Validate string values and set flag - initial_is_last = False + initial_equals_final = False if isinstance(self.initial_charge_state, str): - if self.initial_charge_state == 'lastValueOfSim': - initial_is_last = True - else: - raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') + if not self.initial_charge_state == 'equals_final': + raise PlausibilityError(f'equals_finalcharge_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): @@ -502,8 +508,8 @@ def _plausibility_checks(self) -> None: 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 'lastValueOfSim' - if not initial_is_last: + # Only perform numeric comparisons if not using 'equals_final' + if not initial_equals_final: if (self.initial_charge_state > maximum_initial_capacity).any(): raise PlausibilityError( f'{self.label_full}: {self.initial_charge_state=} ' diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index c25750259..f63405641 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -85,7 +85,7 @@ def test_system(): ), eta_charge=0.95, eta_discharge=0.95, - initial_charge_state='lastValueOfSim', + initial_charge_state='equals_final', ) # Create effects and objective diff --git a/tests/test_storage.py b/tests/test_storage.py index 8d0c495c2..6220ee08a 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -362,7 +362,7 @@ def test_storage_cyclic_initialization(self, basic_flow_system_linopy_coords, co charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), capacity_in_flow_hours=30, - initial_charge_state='lastValueOfSim', # Cyclic initialization + initial_charge_state='equals_final', # Cyclic initialization eta_charge=0.9, eta_discharge=0.9, relative_loss_per_hour=0.05, From 33e227816576e17d6c2fa48c4929e7299cb94acd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:53:17 +0100 Subject: [PATCH 20/25] Remove white space --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6af2e8f..b33cdfd3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,8 +77,6 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - **Storage Parameters**: - `initial_charge_state="lastValueOfSim"` -> `initial_charge_state="equals_last"` - - ### 🗑️ Deprecated - **Old parameter names in `linear_converters.py`**: The following parameter names are now deprecated and accessible as properties/kwargs that emit `DeprecationWarning`. They will be removed in v4.0.0: - **Flow parameters**: `Q_fu`, `Q_th`, `P_el`, `Q_ab`, `power_flow` (use `fuel_flow`, `thermal_flow`, `electrical_flow`, `heat_source_flow` instead) From 34543fc84c6b10e39c3850a3584bfb54e9494d7b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:01:24 +0100 Subject: [PATCH 21/25] Fix CHANGELOG.md --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ccfc968..5bb134d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,11 +63,11 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - **Parameter renaming in `linear_converters.py`**: Renamed parameters to use lowercase, descriptive names for better consistency and clarity: - **Flow parameters** (deprecated uppercase abbreviations → descriptive names): - `Boiler`: `Q_fu` → `fuel_flow`, `Q_th` → `thermal_flow` - - `Power2Heat`: `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` - - `HeatPump`: `COP` → `cop`, `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` - - `CoolingTower`: `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` - - `CHP`: `Q_fu` → `fuel_flow`, `P_el` → `power_flow` → `electrical_flow`, `Q_th` → `thermal_flow` - - `HeatPumpWithSource`: `COP` → `cop`, `P_el` → `power_flow` → `electrical_flow`, `Q_ab` → `heat_source_flow`, `Q_th` → `thermal_flow` + - `Power2Heat`: `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `HeatPump`: `COP` → `cop`, `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `CoolingTower`: `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `CHP`: `Q_fu` → `fuel_flow`, `P_el` → `electrical_flow`, `Q_th` → `thermal_flow` + - `HeatPumpWithSource`: `COP` → `cop`, `P_el` → `electrical_flow`, `Q_ab` → `heat_source_flow`, `Q_th` → `thermal_flow` - **Efficiency parameters** (abbreviated → descriptive names): - `Boiler`: `eta` → `thermal_efficiency` - `Power2Heat`: `eta` → `thermal_efficiency` @@ -78,7 +78,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 🗑️ Deprecated - **Old parameter names in `linear_converters.py`**: The following parameter names are now deprecated and accessible as properties/kwargs that emit `DeprecationWarning`. They will be removed in v4.0.0: - - **Flow parameters**: `Q_fu`, `Q_th`, `P_el`, `Q_ab`, `power_flow` (use `fuel_flow`, `thermal_flow`, `electrical_flow`, `heat_source_flow` instead) + - **Flow parameters**: `Q_fu`, `Q_th`, `P_el`, `Q_ab` (use `fuel_flow`, `thermal_flow`, `electrical_flow`, `heat_source_flow` instead) - **Efficiency parameters**: `eta`, `eta_th`, `eta_el` (use `thermal_efficiency`, `electrical_efficiency` instead) - **COP parameter**: `COP` (use lowercase `cop` instead) From 71c2c7e79eb9c29cee74b960d74516854a9b3f82 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:48:49 +0100 Subject: [PATCH 22/25] Typo --- flixopt/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/components.py b/flixopt/components.py index 83b47d09c..c5b3b496d 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -493,7 +493,7 @@ def _plausibility_checks(self) -> None: initial_equals_final = False if isinstance(self.initial_charge_state, str): if not self.initial_charge_state == 'equals_final': - raise PlausibilityError(f'equals_finalcharge_state has undefined value: {self.initial_charge_state}') + 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 From 809f20bf52d9243965072eff08a4ba39920380fc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:26:43 +0100 Subject: [PATCH 23/25] Typo --- flixopt/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/components.py b/flixopt/components.py index c5b3b496d..0de9cf982 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -408,7 +408,7 @@ def __init__( prevent_simultaneous_flows=[charging, discharging] if prevent_simultaneous_charge_and_discharge else None, meta_data=meta_data, ) - if isinstance(initial_charge_state, str) and initial_charge_state == 'lastValusOfSim': + if isinstance(initial_charge_state, str) and initial_charge_state == 'lastValueOfSim': warnings.warn( f'{initial_charge_state=} is deprecated. Use "equals_final" instead.', DeprecationWarning, From 7cf22f228c544c91f7c4ce06fadf27590bfcf431 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:28:39 +0100 Subject: [PATCH 24/25] Improve CHANGELOG.md --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba53e1a09..fd85e9249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,19 +75,19 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - `HetaPump`: `COP` → `cop` - `HetaPumpWithSource`: `COP` → `cop` - **Storage Parameters**: - - `initial_charge_state="lastValueOfSim"` -> `initial_charge_state="equals_last"` + - `Storage`: `initial_charge_state="lastValueOfSim"` → `initial_charge_state="equals_last"` ### 🗑️ Deprecated - **Old parameter names in `linear_converters.py`**: The following parameter names are now deprecated and accessible as properties/kwargs that emit `DeprecationWarning`. They will be removed in v4.0.0: - **Flow parameters**: `Q_fu`, `Q_th`, `P_el`, `Q_ab` (use `fuel_flow`, `thermal_flow`, `electrical_flow`, `heat_source_flow` instead) - **Efficiency parameters**: `eta`, `eta_th`, `eta_el` (use `thermal_efficiency`, `electrical_efficiency` instead) - **COP parameter**: `COP` (use lowercase `cop` instead) - - **Storage Parameter**: 'initial_charge_state'="lastValueOfSim" (use ="equals_last") + - **Storage Parameter**: `Storage`: `initial_charge_state="lastValueOfSim"` (use `initial_charge_state="equals_last"`) ### 🔥 Removed ### 🐛 Fixed -- Fixed `check_bounds` function in `linear_converters.py` to use normalized array for comparisons, improving robustness with array-like inputs +- Fixed `check_bounds` function in `linear_converters.py` to normalize array inputs before comparisons, ensuring correct boundary checks with DataFrames, Series, and other array-like types ### 🔒 Security From 2373dea52d466ea0c40448eeb052d68699c58db2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 18 Nov 2025 19:17:58 +0100 Subject: [PATCH 25/25] Update Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b42834cf..82a98ea85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? -**Summary**: Renaming parameters in Linear Transformers for readability & Internal architecture improvements to simplify FlowSystem-Element coupling and eliminate circular dependencies. +**Summary**: Renaming parameters in Linear Transformers for readability & Internal architecture improvements to simplify FlowSystem-Element coupling and eliminate circular dependencies. Old parameters till work but emmit warnings. If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).