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/).