Sizing¶
Size a solar heating system - let the optimizer decide equipment sizes.
This notebook introduces:
- InvestParameters: Define investment decisions with size bounds and costs
- Investment costs: Fixed costs and size-dependent costs
- Optimal sizing: Let the optimizer find the best equipment sizes
- Trade-off analysis: Balance investment vs. operating costs
Setup¶
In [1]:
Copied!
import pandas as pd
import xarray as xr
import flixopt as fx
fx.CONFIG.notebook()
import pandas as pd import xarray as xr import flixopt as fx fx.CONFIG.notebook()
Out[1]:
flixopt.config.CONFIG
System Description¶
The swimming pool heating system:
- Solar collectors: Convert solar radiation to heat (size to be optimized)
- Gas boiler: Backup heating when solar is insufficient (existing, 200 kW)
- Buffer tank: Store excess solar heat (size to be optimized)
- Pool: Constant heat demand of 150 kW during operating hours
☀️ Solar ──► [Heat] ◄── Boiler ◄── [Gas]
│
▼
Buffer Tank
│
▼
Pool 🏊
Define Time Horizon and Profiles¶
We model one representative summer week:
In [2]:
Copied!
from data.tutorial_data import get_investment_data
data = get_investment_data()
timesteps = data['timesteps']
solar_profile = data['solar_profile']
pool_demand = data['pool_demand']
GAS_PRICE = data['gas_price']
SOLAR_COST_WEEKLY = data['solar_cost_per_kw_week']
TANK_COST_WEEKLY = data['tank_cost_per_kwh_week']
from data.tutorial_data import get_investment_data data = get_investment_data() timesteps = data['timesteps'] solar_profile = data['solar_profile'] pool_demand = data['pool_demand'] GAS_PRICE = data['gas_price'] SOLAR_COST_WEEKLY = data['solar_cost_per_kw_week'] TANK_COST_WEEKLY = data['tank_cost_per_kwh_week']
In [3]:
Copied!
# Visualize profiles with plotly
profiles = xr.Dataset(
{
'Solar Profile [kW/kW]': xr.DataArray(solar_profile, dims=['time'], coords={'time': timesteps}),
'Pool Demand [kW]': xr.DataArray(pool_demand, dims=['time'], coords={'time': timesteps}),
}
)
profiles.plotly.line(x='time', title='Solar and Pool Profiles', height=300)
# Visualize profiles with plotly profiles = xr.Dataset( { 'Solar Profile [kW/kW]': xr.DataArray(solar_profile, dims=['time'], coords={'time': timesteps}), 'Pool Demand [kW]': xr.DataArray(pool_demand, dims=['time'], coords={'time': timesteps}), } ) profiles.plotly.line(x='time', title='Solar and Pool Profiles', height=300)
Build the System with Investment Options¶
Use InvestParameters to define which sizes should be optimized:
In [4]:
Copied!
flow_system = fx.FlowSystem(timesteps)
flow_system.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
flow_system.add_elements(
# === Buses ===
fx.Bus('Heat', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
# === Effects ===
fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True),
# === Gas Supply ===
fx.Source(
'GasGrid',
outputs=[fx.Flow('Gas', bus='Gas', size=500, effects_per_flow_hour=GAS_PRICE)],
),
# === Gas Boiler (existing, fixed size) ===
fx.linear_converters.Boiler(
'GasBoiler',
thermal_efficiency=0.92,
thermal_flow=fx.Flow('Heat', bus='Heat', size=200), # 200 kW existing
fuel_flow=fx.Flow('Gas', bus='Gas'),
),
# === Solar Collectors (size to be optimized) ===
fx.Source(
'SolarCollectors',
outputs=[
fx.Flow(
'Heat',
bus='Heat',
# Investment optimization: find optimal size between 0-500 kW
size=fx.InvestParameters(
minimum_size=0,
maximum_size=500,
effects_of_investment_per_size={'costs': SOLAR_COST_WEEKLY},
),
# Solar output depends on radiation profile
fixed_relative_profile=solar_profile,
)
],
),
# === Buffer Tank (size to be optimized) ===
fx.Storage(
'BufferTank',
# Investment optimization: find optimal capacity between 0-2000 kWh
capacity_in_flow_hours=fx.InvestParameters(
minimum_size=0,
maximum_size=2000,
effects_of_investment_per_size={'costs': TANK_COST_WEEKLY},
),
initial_charge_state=0,
eta_charge=0.95,
eta_discharge=0.95,
relative_loss_per_hour=0.01, # 1% loss per hour
charging=fx.Flow('Charge', bus='Heat', size=200),
discharging=fx.Flow('Discharge', bus='Heat', size=200),
),
# === Pool Heat Demand ===
fx.Sink(
'Pool',
inputs=[fx.Flow('Heat', bus='Heat', size=1, fixed_relative_profile=pool_demand)],
),
)
flow_system = fx.FlowSystem(timesteps) flow_system.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) flow_system.add_elements( # === Buses === fx.Bus('Heat', carrier='heat'), fx.Bus('Gas', carrier='gas'), # === Effects === fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True), # === Gas Supply === fx.Source( 'GasGrid', outputs=[fx.Flow('Gas', bus='Gas', size=500, effects_per_flow_hour=GAS_PRICE)], ), # === Gas Boiler (existing, fixed size) === fx.linear_converters.Boiler( 'GasBoiler', thermal_efficiency=0.92, thermal_flow=fx.Flow('Heat', bus='Heat', size=200), # 200 kW existing fuel_flow=fx.Flow('Gas', bus='Gas'), ), # === Solar Collectors (size to be optimized) === fx.Source( 'SolarCollectors', outputs=[ fx.Flow( 'Heat', bus='Heat', # Investment optimization: find optimal size between 0-500 kW size=fx.InvestParameters( minimum_size=0, maximum_size=500, effects_of_investment_per_size={'costs': SOLAR_COST_WEEKLY}, ), # Solar output depends on radiation profile fixed_relative_profile=solar_profile, ) ], ), # === Buffer Tank (size to be optimized) === fx.Storage( 'BufferTank', # Investment optimization: find optimal capacity between 0-2000 kWh capacity_in_flow_hours=fx.InvestParameters( minimum_size=0, maximum_size=2000, effects_of_investment_per_size={'costs': TANK_COST_WEEKLY}, ), initial_charge_state=0, eta_charge=0.95, eta_discharge=0.95, relative_loss_per_hour=0.01, # 1% loss per hour charging=fx.Flow('Charge', bus='Heat', size=200), discharging=fx.Flow('Discharge', bus='Heat', size=200), ), # === Pool Heat Demand === fx.Sink( 'Pool', inputs=[fx.Flow('Heat', bus='Heat', size=1, fixed_relative_profile=pool_demand)], ), )
Run Optimization¶
In [5]:
Copied!
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
In [6]:
Copied!
solar_size = flow_system.stats.sizes['SolarCollectors(Heat)'].item()
tank_size = flow_system.stats.sizes['BufferTank'].item()
pd.DataFrame(
{
'Solar [kW]': solar_size,
'Tank [kWh]': tank_size,
'Ratio [kWh/kW]': tank_size / solar_size if solar_size > 0 else float('nan'),
},
index=['Optimal Size'],
).T
solar_size = flow_system.stats.sizes['SolarCollectors(Heat)'].item() tank_size = flow_system.stats.sizes['BufferTank'].item() pd.DataFrame( { 'Solar [kW]': solar_size, 'Tank [kWh]': tank_size, 'Ratio [kWh/kW]': tank_size / solar_size if solar_size > 0 else float('nan'), }, index=['Optimal Size'], ).T
Out[6]:
| Optimal Size | |
|---|---|
| Solar [kW] | 458.451276 |
| Tank [kWh] | 747.696332 |
| Ratio [kWh/kW] | 1.630918 |
Visualize Sizes¶
In [7]:
Copied!
flow_system.stats.plot.sizes()
flow_system.stats.plot.sizes()
Out[7]:
Cost Breakdown¶
In [8]:
Copied!
total_costs = flow_system.solution['costs'].item()
# Calculate cost components
solar_invest = solar_size * SOLAR_COST_WEEKLY
tank_invest = tank_size * TANK_COST_WEEKLY
gas_costs = total_costs - solar_invest - tank_invest
pd.DataFrame(
{
'Solar Investment': {'EUR': solar_invest, '%': solar_invest / total_costs * 100},
'Tank Investment': {'EUR': tank_invest, '%': tank_invest / total_costs * 100},
'Gas Costs': {'EUR': gas_costs, '%': gas_costs / total_costs * 100},
'Total': {'EUR': total_costs, '%': 100.0},
}
)
total_costs = flow_system.solution['costs'].item() # Calculate cost components solar_invest = solar_size * SOLAR_COST_WEEKLY tank_invest = tank_size * TANK_COST_WEEKLY gas_costs = total_costs - solar_invest - tank_invest pd.DataFrame( { 'Solar Investment': {'EUR': solar_invest, '%': solar_invest / total_costs * 100}, 'Tank Investment': {'EUR': tank_invest, '%': tank_invest / total_costs * 100}, 'Gas Costs': {'EUR': gas_costs, '%': gas_costs / total_costs * 100}, 'Total': {'EUR': total_costs, '%': 100.0}, } )
Out[8]:
| Solar Investment | Tank Investment | Gas Costs | Total | |
|---|---|---|---|---|
| EUR | 176.327414 | 21.568163 | 585.088163 | 782.98374 |
| % | 22.519933 | 2.754612 | 74.725455 | 100.00000 |
System Operation¶
In [9]:
Copied!
flow_system.stats.plot.balance('Heat')
flow_system.stats.plot.balance('Heat')
Out[9]:
In [10]:
Copied!
flow_system.stats.plot.heatmap('SolarCollectors(Heat)')
flow_system.stats.plot.heatmap('SolarCollectors(Heat)')
Out[10]:
In [11]:
Copied!
flow_system.stats.plot.balance('BufferTank')
flow_system.stats.plot.balance('BufferTank')
Out[11]:
Compare: What if No Solar?¶
Let's see how much the solar system saves:
In [12]:
Copied!
# Gas-only scenario for comparison
total_demand = pool_demand.sum()
gas_only_cost = total_demand / 0.92 * GAS_PRICE # All heat from gas boiler
savings = gas_only_cost - total_costs
pd.DataFrame(
{
'Gas-only [EUR/week]': gas_only_cost,
'With Solar [EUR/week]': total_costs,
'Savings [EUR/week]': savings,
'Savings [%]': savings / gas_only_cost * 100,
'Savings [EUR/year]': savings * 52,
},
index=['Value'],
).T
# Gas-only scenario for comparison total_demand = pool_demand.sum() gas_only_cost = total_demand / 0.92 * GAS_PRICE # All heat from gas boiler savings = gas_only_cost - total_costs pd.DataFrame( { 'Gas-only [EUR/week]': gas_only_cost, 'With Solar [EUR/week]': total_costs, 'Savings [EUR/week]': savings, 'Savings [%]': savings / gas_only_cost * 100, 'Savings [EUR/year]': savings * 52, }, index=['Value'], ).T
Out[12]:
| Value | |
|---|---|
| Gas-only [EUR/week] | 2465.217391 |
| With Solar [EUR/week] | 782.983740 |
| Savings [EUR/week] | 1682.233651 |
| Savings [%] | 68.238755 |
| Savings [EUR/year] | 87476.149872 |
Energy Flow Sankey¶
A Sankey diagram visualizes the total energy flows through the system:
In [13]:
Copied!
flow_system.stats.plot.sankey.flows()
flow_system.stats.plot.sankey.flows()
Out[13]:
Key Concepts¶
InvestParameters Options¶
fx.InvestParameters(
minimum_size=0, # Lower bound (can be 0 for optional)
maximum_size=500, # Upper bound
fixed_size=100, # Or: fixed size (binary decision)
mandatory=True, # Force investment to happen
effects_of_investment={'costs': 1000}, # Fixed cost if invested
effects_of_investment_per_size={'costs': 25}, # Cost per unit size
)
Where to Use InvestParameters¶
- Flow.size: Optimize converter/source/sink capacity
- Storage.capacity_in_flow_hours: Optimize storage capacity
Summary¶
You learned how to:
- Define investment decisions with
InvestParameters - Set size bounds (minimum/maximum)
- Add investment costs (per-size and fixed)
- Access optimal sizes via
statistics.sizes - Visualize sizes with
statistics.plot.sizes()
Next Steps¶
- 04-operational-constraints: Add startup costs and minimum run times
- 05-multi-carrier-system: Model combined heat and power