Multi-Carrier¶
Hospital with CHP producing both electricity and heat.
This notebook introduces:
- Multiple energy carriers: Electricity, heat, and gas in one system
- CHP (Cogeneration): Equipment producing multiple outputs
- Electricity market: Buying and selling to the grid
- Carrier colors: Visual distinction between energy types
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 hospital energy system:
Grid Buy ──►
[Electricity] ──► Hospital Elec. Load
Grid Sell ◄── ▲
│
Gas Grid ──► [Gas] ──► CHP ──────┘
│ │
│ ▼
│ [Heat] ──► Hospital Heat Load
│ ▲
└──► Boiler
Equipment:
- CHP: 200 kW electrical, ~250 kW thermal (η_el=40%, η_th=50%)
- Gas Boiler: 400 kW thermal backup
- Grid: Buy electricity at variable prices, sell at lower prices
Define Time Horizon and Demand Profiles¶
In [2]:
Copied!
from data.tutorial_data import get_multicarrier_data
data = get_multicarrier_data()
timesteps = data['timesteps']
electricity_demand = data['electricity_demand']
heat_demand = data['heat_demand']
elec_buy_price = data['elec_buy_price']
elec_sell_price = data['elec_sell_price']
gas_price = data['gas_price']
from data.tutorial_data import get_multicarrier_data data = get_multicarrier_data() timesteps = data['timesteps'] electricity_demand = data['electricity_demand'] heat_demand = data['heat_demand'] elec_buy_price = data['elec_buy_price'] elec_sell_price = data['elec_sell_price'] gas_price = data['gas_price']
In [3]:
Copied!
# Visualize demands and prices with plotly
profiles = xr.Dataset(
{
'Electricity Demand [kW]': xr.DataArray(electricity_demand, dims=['time'], coords={'time': timesteps}),
'Heat Demand [kW]': xr.DataArray(heat_demand, dims=['time'], coords={'time': timesteps}),
'Elec. Buy Price [EUR/kWh]': xr.DataArray(elec_buy_price, dims=['time'], coords={'time': timesteps}),
}
)
profiles.plotly.line(x='time', title='Hospital Energy Profiles', height=300)
# Visualize demands and prices with plotly profiles = xr.Dataset( { 'Electricity Demand [kW]': xr.DataArray(electricity_demand, dims=['time'], coords={'time': timesteps}), 'Heat Demand [kW]': xr.DataArray(heat_demand, dims=['time'], coords={'time': timesteps}), 'Elec. Buy Price [EUR/kWh]': xr.DataArray(elec_buy_price, dims=['time'], coords={'time': timesteps}), } ) profiles.plotly.line(x='time', title='Hospital Energy Profiles', height=300)
Build the Multi-Carrier System¶
In [4]:
Copied!
flow_system = fx.FlowSystem(timesteps, name='With CHP')
flow_system.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
flow_system.add_elements(
# === Buses with carriers for visual distinction ===
fx.Bus('Electricity', carrier='electricity'),
fx.Bus('Heat', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
# === Effects ===
fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True),
fx.Effect('CO2', 'kg', 'CO2 Emissions'), # Track emissions too
# === Gas Supply ===
fx.Source(
'GasGrid',
outputs=[
fx.Flow(
'Gas',
bus='Gas',
size=1000,
effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2}, # Gas: 0.2 kg CO2/kWh
)
],
),
# === Electricity Grid (buy) ===
fx.Source(
'GridBuy',
outputs=[
fx.Flow(
'Electricity',
bus='Electricity',
size=500,
effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4}, # Grid: 0.4 kg CO2/kWh
)
],
),
# === Electricity Grid (sell) - negative cost = revenue ===
fx.Sink(
'GridSell',
inputs=[
fx.Flow(
'Electricity',
bus='Electricity',
size=200,
effects_per_flow_hour={'costs': -elec_sell_price}, # Negative = income
)
],
),
# === CHP Unit (Combined Heat and Power) ===
fx.linear_converters.CHP(
'CHP',
electrical_efficiency=0.40, # 40% to electricity
thermal_efficiency=0.50, # 50% to heat (total: 90%)
status_parameters=fx.StatusParameters(
effects_per_startup={'costs': 30},
min_uptime=3,
),
electrical_flow=fx.Flow('P_el', bus='Electricity', size=200),
thermal_flow=fx.Flow('Q_th', bus='Heat', size=250),
fuel_flow=fx.Flow(
'Q_fuel',
bus='Gas',
size=500,
relative_minimum=0.4, # Min 40% load
),
),
# === Gas Boiler (heat only) ===
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=0.92,
thermal_flow=fx.Flow('Q_th', bus='Heat', size=400),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
# === Hospital Loads ===
fx.Sink(
'HospitalElec',
inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)],
),
fx.Sink(
'HospitalHeat',
inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_demand)],
),
)
flow_system = fx.FlowSystem(timesteps, name='With CHP') flow_system.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) flow_system.add_elements( # === Buses with carriers for visual distinction === fx.Bus('Electricity', carrier='electricity'), fx.Bus('Heat', carrier='heat'), fx.Bus('Gas', carrier='gas'), # === Effects === fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True), fx.Effect('CO2', 'kg', 'CO2 Emissions'), # Track emissions too # === Gas Supply === fx.Source( 'GasGrid', outputs=[ fx.Flow( 'Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2}, # Gas: 0.2 kg CO2/kWh ) ], ), # === Electricity Grid (buy) === fx.Source( 'GridBuy', outputs=[ fx.Flow( 'Electricity', bus='Electricity', size=500, effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4}, # Grid: 0.4 kg CO2/kWh ) ], ), # === Electricity Grid (sell) - negative cost = revenue === fx.Sink( 'GridSell', inputs=[ fx.Flow( 'Electricity', bus='Electricity', size=200, effects_per_flow_hour={'costs': -elec_sell_price}, # Negative = income ) ], ), # === CHP Unit (Combined Heat and Power) === fx.linear_converters.CHP( 'CHP', electrical_efficiency=0.40, # 40% to electricity thermal_efficiency=0.50, # 50% to heat (total: 90%) status_parameters=fx.StatusParameters( effects_per_startup={'costs': 30}, min_uptime=3, ), electrical_flow=fx.Flow('P_el', bus='Electricity', size=200), thermal_flow=fx.Flow('Q_th', bus='Heat', size=250), fuel_flow=fx.Flow( 'Q_fuel', bus='Gas', size=500, relative_minimum=0.4, # Min 40% load ), ), # === Gas Boiler (heat only) === fx.linear_converters.Boiler( 'Boiler', thermal_efficiency=0.92, thermal_flow=fx.Flow('Q_th', bus='Heat', size=400), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), # === Hospital Loads === fx.Sink( 'HospitalElec', inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)], ), fx.Sink( 'HospitalHeat', inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_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!
flow_system.stats.plot.balance('Electricity')
flow_system.stats.plot.balance('Electricity')
Out[6]:
Heat Balance¶
In [7]:
Copied!
flow_system.stats.plot.balance('Heat')
flow_system.stats.plot.balance('Heat')
Out[7]:
Gas Balance¶
In [8]:
Copied!
flow_system.stats.plot.balance('Gas')
flow_system.stats.plot.balance('Gas')
Out[8]:
CHP Operation Pattern¶
In [9]:
Copied!
flow_system.stats.plot.heatmap('CHP(P_el)')
flow_system.stats.plot.heatmap('CHP(P_el)')
Out[9]:
Cost and Emissions Summary¶
In [10]:
Copied!
# Energy flows
flow_rates = flow_system.stats.flow_rates
grid_buy = flow_rates['GridBuy(Electricity)'].sum().item()
grid_sell = flow_rates['GridSell(Electricity)'].sum().item()
chp_elec = flow_rates['CHP(P_el)'].sum().item()
chp_heat = flow_rates['CHP(Q_th)'].sum().item()
boiler_heat = flow_rates['Boiler(Q_th)'].sum().item()
total_elec = electricity_demand.sum()
total_heat = heat_demand.sum()
pd.DataFrame(
{
'CHP Electricity [kWh]': chp_elec,
'CHP Electricity [%]': chp_elec / total_elec * 100,
'Grid Buy [kWh]': grid_buy,
'Grid Sell [kWh]': grid_sell,
'CHP Heat [kWh]': chp_heat,
'CHP Heat [%]': chp_heat / total_heat * 100,
'Boiler Heat [kWh]': boiler_heat,
'Total Costs [EUR]': flow_system.solution['costs'].item(),
'Total CO2 [kg]': flow_system.solution['CO2'].item(),
},
index=['Value'],
).T
# Energy flows flow_rates = flow_system.stats.flow_rates grid_buy = flow_rates['GridBuy(Electricity)'].sum().item() grid_sell = flow_rates['GridSell(Electricity)'].sum().item() chp_elec = flow_rates['CHP(P_el)'].sum().item() chp_heat = flow_rates['CHP(Q_th)'].sum().item() boiler_heat = flow_rates['Boiler(Q_th)'].sum().item() total_elec = electricity_demand.sum() total_heat = heat_demand.sum() pd.DataFrame( { 'CHP Electricity [kWh]': chp_elec, 'CHP Electricity [%]': chp_elec / total_elec * 100, 'Grid Buy [kWh]': grid_buy, 'Grid Sell [kWh]': grid_sell, 'CHP Heat [kWh]': chp_heat, 'CHP Heat [%]': chp_heat / total_heat * 100, 'Boiler Heat [kWh]': boiler_heat, 'Total Costs [EUR]': flow_system.solution['costs'].item(), 'Total CO2 [kg]': flow_system.solution['CO2'].item(), }, index=['Value'], ).T
Out[10]:
| Value | |
|---|---|
| CHP Electricity [kWh] | 31304.424640 |
| CHP Electricity [%] | 102.063863 |
| Grid Buy [kWh] | 2475.425725 |
| Grid Sell [kWh] | 3108.441560 |
| CHP Heat [kWh] | 39130.530800 |
| CHP Heat [%] | 88.580014 |
| Boiler Heat [kWh] | 5044.818728 |
| Total Costs [EUR] | 4644.296833 |
| Total CO2 [kg] | 17739.082333 |
Compare: What if No CHP?¶
How much does the CHP save compared to buying all electricity?
In [11]:
Copied!
# Build system without CHP
fs_no_chp = fx.FlowSystem(timesteps, name='No CHP')
fs_no_chp.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
fs_no_chp.add_elements(
fx.Bus('Electricity', carrier='electricity'),
fx.Bus('Heat', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True),
fx.Effect('CO2', 'kg', 'CO2 Emissions'),
fx.Source(
'GasGrid',
outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2})],
),
fx.Source(
'GridBuy',
outputs=[
fx.Flow(
'Electricity', bus='Electricity', size=500, effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4}
)
],
),
# Only boiler for heat
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=0.92,
thermal_flow=fx.Flow('Q_th', bus='Heat', size=500),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
fx.Sink(
'HospitalElec', inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)]
),
fx.Sink('HospitalHeat', inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_demand)]),
)
fs_no_chp.optimize(fx.solvers.HighsSolver())
total_costs = flow_system.solution['costs'].item()
total_co2 = flow_system.solution['CO2'].item()
no_chp_costs = fs_no_chp.solution['costs'].item()
no_chp_co2 = fs_no_chp.solution['CO2'].item()
pd.DataFrame(
{
'Without CHP': {'Cost [EUR]': no_chp_costs, 'CO2 [kg]': no_chp_co2},
'With CHP': {'Cost [EUR]': total_costs, 'CO2 [kg]': total_co2},
'Savings': {
'Cost [EUR]': no_chp_costs - total_costs,
'CO2 [kg]': no_chp_co2 - total_co2,
},
'Savings [%]': {
'Cost [EUR]': (no_chp_costs - total_costs) / no_chp_costs * 100,
'CO2 [kg]': (no_chp_co2 - total_co2) / no_chp_co2 * 100,
},
}
)
# Build system without CHP fs_no_chp = fx.FlowSystem(timesteps, name='No CHP') fs_no_chp.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) fs_no_chp.add_elements( fx.Bus('Electricity', carrier='electricity'), fx.Bus('Heat', carrier='heat'), fx.Bus('Gas', carrier='gas'), fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True), fx.Effect('CO2', 'kg', 'CO2 Emissions'), fx.Source( 'GasGrid', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2})], ), fx.Source( 'GridBuy', outputs=[ fx.Flow( 'Electricity', bus='Electricity', size=500, effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4} ) ], ), # Only boiler for heat fx.linear_converters.Boiler( 'Boiler', thermal_efficiency=0.92, thermal_flow=fx.Flow('Q_th', bus='Heat', size=500), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), fx.Sink( 'HospitalElec', inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)] ), fx.Sink('HospitalHeat', inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_demand)]), ) fs_no_chp.optimize(fx.solvers.HighsSolver()) total_costs = flow_system.solution['costs'].item() total_co2 = flow_system.solution['CO2'].item() no_chp_costs = fs_no_chp.solution['costs'].item() no_chp_co2 = fs_no_chp.solution['CO2'].item() pd.DataFrame( { 'Without CHP': {'Cost [EUR]': no_chp_costs, 'CO2 [kg]': no_chp_co2}, 'With CHP': {'Cost [EUR]': total_costs, 'CO2 [kg]': total_co2}, 'Savings': { 'Cost [EUR]': no_chp_costs - total_costs, 'CO2 [kg]': no_chp_co2 - total_co2, }, 'Savings [%]': { 'Cost [EUR]': (no_chp_costs - total_costs) / no_chp_costs * 100, 'CO2 [kg]': (no_chp_co2 - total_co2) / no_chp_co2 * 100, }, } )
Out[11]:
| Without CHP | With CHP | Savings | Savings [%] | |
|---|---|---|---|---|
| Cost [EUR] | 11703.962760 | 4644.296833 | 7059.665927 | 60.318595 |
| CO2 [kg] | 21871.900376 | 17739.082333 | 4132.818042 | 18.895560 |
Side-by-Side Comparison¶
Use the Comparison class to visualize both systems together:
In [12]:
Copied!
comp = fx.Comparison([fs_no_chp, flow_system])
comp.stats.plot.balance('Electricity')
comp = fx.Comparison([fs_no_chp, flow_system]) comp.stats.plot.balance('Electricity')
Out[12]:
In [13]:
Copied!
comp.stats.plot.balance('Heat')
comp.stats.plot.balance('Heat')
Out[13]:
Energy Flow Sankey¶
A Sankey diagram visualizes the total energy flows through the multi-carrier system:
In [14]:
Copied!
flow_system.stats.plot.sankey.flows()
flow_system.stats.plot.sankey.flows()
Out[14]:
Key Concepts¶
Multi-Carrier Systems¶
- Multiple buses for different energy carriers (electricity, heat, gas)
- Components can connect to multiple buses (CHP produces both electricity and heat)
- Carriers enable automatic coloring in visualizations
CHP Modeling¶
fx.linear_converters.CHP(
'CHP',
electrical_efficiency=0.40, # Fuel → Electricity
thermal_efficiency=0.50, # Fuel → Heat
# Total efficiency = 0.40 + 0.50 = 0.90 (90%)
electrical_flow=fx.Flow('P_el', bus='Electricity', size=200),
thermal_flow=fx.Flow('Q_th', bus='Heat', size=250),
fuel_flow=fx.Flow('Q_fuel', bus='Gas', size=500),
)
Electricity Markets¶
- Buy: Source with positive cost
- Sell: Sink with negative cost (= revenue)
- Different prices for buy vs. sell (spread)
Tracking Multiple Effects¶
fx.Effect('costs', '€', 'Total Costs', is_objective=True) # Minimize this
fx.Effect('CO2', 'kg', 'CO2 Emissions') # Just track, don't optimize
Summary¶
You learned how to:
- Model multiple energy carriers (electricity, heat, gas)
- Use CHP for combined heat and power production
- Model electricity markets with buy/sell prices
- Track multiple effects (costs and emissions)
- Analyze multi-carrier balances
Next Steps¶
- 06a-time-varying-parameters: Variable efficiency based on conditions
- 07-scenarios-and-periods: Plan under uncertainty