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 numpy as np
import pandas as pd
import plotly.express as px
import xarray as xr
import flixopt as fx
fx.CONFIG.notebook()
import numpy as np import pandas as pd import plotly.express as px 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!
# One week, hourly
timesteps = pd.date_range('2024-02-05', periods=168, freq='h')
hours = np.arange(168)
hour_of_day = hours % 24
# Hospital electricity demand (kW)
# Base load + daily pattern (higher during day for equipment, lighting)
elec_base = 150 # 24/7 critical systems
elec_daily = 100 * np.sin((hour_of_day - 6) * np.pi / 12) # Peak at noon
elec_daily = np.maximum(0, elec_daily)
electricity_demand = elec_base + elec_daily
# Hospital heat demand (kW)
# Higher in morning, drops during day, increases for hot water in evening
heat_pattern = np.select(
[
(hour_of_day >= 5) & (hour_of_day < 9), # Morning warmup
(hour_of_day >= 9) & (hour_of_day < 17), # Daytime
(hour_of_day >= 17) & (hour_of_day < 22), # Evening
],
[350, 250, 300],
default=200, # Night
)
heat_demand = heat_pattern.astype(float)
# Add random variation
np.random.seed(456)
electricity_demand += np.random.normal(0, 15, len(timesteps))
heat_demand += np.random.normal(0, 20, len(timesteps))
electricity_demand = np.clip(electricity_demand, 100, 300)
heat_demand = np.clip(heat_demand, 150, 400)
print(f'Electricity: {electricity_demand.min():.0f} - {electricity_demand.max():.0f} kW')
print(f'Heat: {heat_demand.min():.0f} - {heat_demand.max():.0f} kW')
# One week, hourly timesteps = pd.date_range('2024-02-05', periods=168, freq='h') hours = np.arange(168) hour_of_day = hours % 24 # Hospital electricity demand (kW) # Base load + daily pattern (higher during day for equipment, lighting) elec_base = 150 # 24/7 critical systems elec_daily = 100 * np.sin((hour_of_day - 6) * np.pi / 12) # Peak at noon elec_daily = np.maximum(0, elec_daily) electricity_demand = elec_base + elec_daily # Hospital heat demand (kW) # Higher in morning, drops during day, increases for hot water in evening heat_pattern = np.select( [ (hour_of_day >= 5) & (hour_of_day < 9), # Morning warmup (hour_of_day >= 9) & (hour_of_day < 17), # Daytime (hour_of_day >= 17) & (hour_of_day < 22), # Evening ], [350, 250, 300], default=200, # Night ) heat_demand = heat_pattern.astype(float) # Add random variation np.random.seed(456) electricity_demand += np.random.normal(0, 15, len(timesteps)) heat_demand += np.random.normal(0, 20, len(timesteps)) electricity_demand = np.clip(electricity_demand, 100, 300) heat_demand = np.clip(heat_demand, 150, 400) print(f'Electricity: {electricity_demand.min():.0f} - {electricity_demand.max():.0f} kW') print(f'Heat: {heat_demand.min():.0f} - {heat_demand.max():.0f} kW')
Electricity: 106 - 272 kW Heat: 150 - 397 kW
In [3]:
Copied!
# Electricity prices (€/kWh)
# Time-of-use: expensive during day, cheaper at night
elec_buy_price = np.where(
(hour_of_day >= 7) & (hour_of_day <= 21),
0.35, # Peak - high electricity prices make CHP attractive
0.20, # Off-peak
)
# Feed-in tariff (sell price) - allows selling excess CHP electricity
elec_sell_price = 0.12 # Fixed feed-in rate
# Gas price - relatively low, favoring gas-based generation
gas_price = 0.05 # €/kWh
# Electricity prices (€/kWh) # Time-of-use: expensive during day, cheaper at night elec_buy_price = np.where( (hour_of_day >= 7) & (hour_of_day <= 21), 0.35, # Peak - high electricity prices make CHP attractive 0.20, # Off-peak ) # Feed-in tariff (sell price) - allows selling excess CHP electricity elec_sell_price = 0.12 # Fixed feed-in rate # Gas price - relatively low, favoring gas-based generation gas_price = 0.05 # €/kWh
In [4]:
Copied!
# Visualize demands and prices with plotly - using xarray and faceting
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 [€/kWh]': xr.DataArray(elec_buy_price, dims=['time'], coords={'time': timesteps}),
}
)
df = profiles.to_dataframe().reset_index().melt(id_vars='time', var_name='variable', value_name='value')
fig = px.line(df, x='time', y='value', facet_col='variable', height=300)
fig.update_yaxes(matches=None, showticklabels=True)
fig.for_each_annotation(lambda a: a.update(text=a.text.split('=')[-1]))
fig
# Visualize demands and prices with plotly - using xarray and faceting 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 [€/kWh]': xr.DataArray(elec_buy_price, dims=['time'], coords={'time': timesteps}), } ) df = profiles.to_dataframe().reset_index().melt(id_vars='time', var_name='variable', value_name='value') fig = px.line(df, x='time', y='value', facet_col='variable', height=300) fig.update_yaxes(matches=None, showticklabels=True) fig.for_each_annotation(lambda a: a.update(text=a.text.split('=')[-1])) fig
Build the Multi-Carrier System¶
In [5]:
Copied!
flow_system = fx.FlowSystem(timesteps)
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) 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 [6]:
Copied!
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
In [7]:
Copied!
flow_system.statistics.plot.balance('Electricity')
flow_system.statistics.plot.balance('Electricity')
Out[7]:
Heat Balance¶
In [8]:
Copied!
flow_system.statistics.plot.balance('Heat')
flow_system.statistics.plot.balance('Heat')
Out[8]:
Gas Balance¶
In [9]:
Copied!
flow_system.statistics.plot.balance('Gas')
flow_system.statistics.plot.balance('Gas')
Out[9]:
CHP Operation Pattern¶
In [10]:
Copied!
flow_system.statistics.plot.heatmap('CHP(P_el)')
flow_system.statistics.plot.heatmap('CHP(P_el)')
Out[10]:
Cost and Emissions Summary¶
In [11]:
Copied!
total_costs = flow_system.solution['costs'].item()
total_co2 = flow_system.solution['CO2'].item()
# Energy flows
flow_rates = flow_system.statistics.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()
# Display as compact summary
print(
f'Electricity: {chp_elec:.0f} kWh CHP ({chp_elec / total_elec * 100:.0f}%) + {grid_buy:.0f} kWh grid, {grid_sell:.0f} kWh sold'
)
print(f'Heat: {chp_heat:.0f} kWh CHP ({chp_heat / total_heat * 100:.0f}%) + {boiler_heat:.0f} kWh boiler')
print(f'Costs: {total_costs:.2f} € | CO2: {total_co2:.0f} kg')
total_costs = flow_system.solution['costs'].item() total_co2 = flow_system.solution['CO2'].item() # Energy flows flow_rates = flow_system.statistics.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() # Display as compact summary print( f'Electricity: {chp_elec:.0f} kWh CHP ({chp_elec / total_elec * 100:.0f}%) + {grid_buy:.0f} kWh grid, {grid_sell:.0f} kWh sold' ) print(f'Heat: {chp_heat:.0f} kWh CHP ({chp_heat / total_heat * 100:.0f}%) + {boiler_heat:.0f} kWh boiler') print(f'Costs: {total_costs:.2f} € | CO2: {total_co2:.0f} kg')
Electricity: 31304 kWh CHP (102%) + 2475 kWh grid, 3108 kWh sold Heat: 39131 kWh CHP (89%) + 5045 kWh boiler Costs: 4674.30 € | CO2: 17739 kg
Compare: What if No CHP?¶
How much does the CHP save compared to buying all electricity?
In [12]:
Copied!
# Build system without CHP
fs_no_chp = fx.FlowSystem(timesteps)
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())
no_chp_costs = fs_no_chp.solution['costs'].item()
no_chp_co2 = fs_no_chp.solution['CO2'].item()
cost_saving = (no_chp_costs - total_costs) / no_chp_costs * 100
co2_saving = (no_chp_co2 - total_co2) / no_chp_co2 * 100
print(
f'CHP saves {cost_saving:.1f}% costs ({no_chp_costs:.0f}→{total_costs:.0f} €) and {co2_saving:.1f}% CO2 ({no_chp_co2:.0f}→{total_co2:.0f} kg)'
)
# Build system without CHP fs_no_chp = fx.FlowSystem(timesteps) 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()) no_chp_costs = fs_no_chp.solution['costs'].item() no_chp_co2 = fs_no_chp.solution['CO2'].item() cost_saving = (no_chp_costs - total_costs) / no_chp_costs * 100 co2_saving = (no_chp_co2 - total_co2) / no_chp_co2 * 100 print( f'CHP saves {cost_saving:.1f}% costs ({no_chp_costs:.0f}→{total_costs:.0f} €) and {co2_saving:.1f}% CO2 ({no_chp_co2:.0f}→{total_co2:.0f} kg)' )
CHP saves 60.1% costs (11704→4674 €) and 18.9% CO2 (21872→17739 kg)
Energy Flow Sankey¶
A Sankey diagram visualizes the total energy flows through the multi-carrier system:
In [13]:
Copied!
flow_system.statistics.plot.sankey.flows()
flow_system.statistics.plot.sankey.flows()
Out[13]:
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