Heat System¶
District heating with thermal storage and time-varying prices.
This notebook introduces:
- Storage: Thermal buffer tanks with charging/discharging
- Time series data: Using real demand profiles
- Multiple components: Combining boiler, storage, and loads
- Result visualization: Heatmaps, balance plots, and charge states
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
Define Time Horizon and Demand¶
We model one week with hourly resolution. The office has typical weekday patterns:
In [2]:
Copied!
from data.tutorial_data import get_heat_system_data
data = get_heat_system_data()
timesteps = data['timesteps']
heat_demand = data['heat_demand']
gas_price = data['gas_price']
from data.tutorial_data import get_heat_system_data data = get_heat_system_data() timesteps = data['timesteps'] heat_demand = data['heat_demand'] gas_price = data['gas_price']
In [3]:
Copied!
# Visualize the demand pattern with plotly
demand_ds = xr.Dataset(
{
'Heat Demand': xr.DataArray(heat_demand, dims=['time'], coords={'time': timesteps}),
}
)
demand_ds.plotly.line(x='time', title='Office Heat Demand Profile')
# Visualize the demand pattern with plotly demand_ds = xr.Dataset( { 'Heat Demand': xr.DataArray(heat_demand, dims=['time'], coords={'time': timesteps}), } ) demand_ds.plotly.line(x='time', title='Office Heat Demand Profile')
Define Gas Prices¶
Gas prices vary with time-of-use tariffs:
In [4]:
Copied!
# Visualize time-of-use gas prices with plotly
price_ds = xr.Dataset(
{
'Gas Price': xr.DataArray(gas_price, dims=['time'], coords={'time': timesteps}),
}
)
price_ds.plotly.line(x='time', title='Gas Price [€/kWh]')
# Visualize time-of-use gas prices with plotly price_ds = xr.Dataset( { 'Gas Price': xr.DataArray(gas_price, dims=['time'], coords={'time': timesteps}), } ) price_ds.plotly.line(x='time', title='Gas Price [€/kWh]')
Build the Energy System¶
The system includes:
- Gas boiler (150 kW thermal capacity)
- Thermal storage tank (500 kWh capacity)
- Office building heat demand
Gas Grid ──► [Gas] ──► Boiler ──► [Heat] ◄──► Storage
│
▼
Office
In [5]:
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('Gas', carrier='gas'),
fx.Bus('Heat', carrier='heat'),
# === Effect ===
fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True),
# === Gas Supply with time-varying price ===
fx.Source(
'GasGrid',
outputs=[fx.Flow('Gas', bus='Gas', size=500, effects_per_flow_hour=gas_price)],
),
# === Gas Boiler: 150 kW, 92% efficiency ===
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=0.92,
thermal_flow=fx.Flow('Heat', bus='Heat', size=150),
fuel_flow=fx.Flow('Gas', bus='Gas'),
),
# === Thermal Storage: 500 kWh tank ===
fx.Storage(
'ThermalStorage',
capacity_in_flow_hours=500, # 500 kWh capacity
initial_charge_state=250, # Start half-full
minimal_final_charge_state=200, # End with at least 200 kWh
eta_charge=0.98, # 98% charging efficiency
eta_discharge=0.98, # 98% discharging efficiency
relative_loss_per_hour=0.005, # 0.5% heat loss per hour
charging=fx.Flow('Charge', bus='Heat', size=100), # Max 100 kW charging
discharging=fx.Flow('Discharge', bus='Heat', size=100), # Max 100 kW discharging
),
# === Office Heat Demand ===
fx.Sink(
'Office',
inputs=[fx.Flow('Heat', 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('heat', '#e74c3c', 'kW'), ) flow_system.add_elements( # === Buses === fx.Bus('Gas', carrier='gas'), fx.Bus('Heat', carrier='heat'), # === Effect === fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True), # === Gas Supply with time-varying price === fx.Source( 'GasGrid', outputs=[fx.Flow('Gas', bus='Gas', size=500, effects_per_flow_hour=gas_price)], ), # === Gas Boiler: 150 kW, 92% efficiency === fx.linear_converters.Boiler( 'Boiler', thermal_efficiency=0.92, thermal_flow=fx.Flow('Heat', bus='Heat', size=150), fuel_flow=fx.Flow('Gas', bus='Gas'), ), # === Thermal Storage: 500 kWh tank === fx.Storage( 'ThermalStorage', capacity_in_flow_hours=500, # 500 kWh capacity initial_charge_state=250, # Start half-full minimal_final_charge_state=200, # End with at least 200 kWh eta_charge=0.98, # 98% charging efficiency eta_discharge=0.98, # 98% discharging efficiency relative_loss_per_hour=0.005, # 0.5% heat loss per hour charging=fx.Flow('Charge', bus='Heat', size=100), # Max 100 kW charging discharging=fx.Flow('Discharge', bus='Heat', size=100), # Max 100 kW discharging ), # === Office Heat Demand === fx.Sink( 'Office', inputs=[fx.Flow('Heat', 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.stats.plot.balance('Heat')
flow_system.stats.plot.balance('Heat')
Out[7]:
Storage Charge State¶
Track how the storage level varies over time:
In [8]:
Copied!
flow_system.stats.plot.balance('ThermalStorage')
flow_system.stats.plot.balance('ThermalStorage')
Out[8]:
Heatmap Visualization¶
Heatmaps show patterns across hours and days:
In [9]:
Copied!
flow_system.stats.plot.heatmap('Boiler(Heat)')
flow_system.stats.plot.heatmap('Boiler(Heat)')
Out[9]:
In [10]:
Copied!
flow_system.stats.plot.heatmap('ThermalStorage')
flow_system.stats.plot.heatmap('ThermalStorage')
Out[10]:
Cost Analysis¶
In [11]:
Copied!
total_heat = heat_demand.sum()
pd.DataFrame(
{
'Total operating costs [EUR]': flow_system.solution['costs'].item(),
'Total heat delivered [kWh]': total_heat,
'Average cost [ct/kWh]': flow_system.solution['costs'].item() / total_heat * 100,
},
index=['Value'],
).T
total_heat = heat_demand.sum() pd.DataFrame( { 'Total operating costs [EUR]': flow_system.solution['costs'].item(), 'Total heat delivered [kWh]': total_heat, 'Average cost [ct/kWh]': flow_system.solution['costs'].item() / total_heat * 100, }, index=['Value'], ).T
Out[11]:
| Value | |
|---|---|
| Total operating costs [EUR] | 558.830517 |
| Total heat delivered [kWh] | 8007.338386 |
| Average cost [ct/kWh] | 6.978980 |
Flow Rates and Charge States¶
Visualize all flow rates and storage charge states:
In [12]:
Copied!
# Plot all flow rates
flow_system.stats.plot.flows()
# Plot all flow rates flow_system.stats.plot.flows()
Out[12]:
In [13]:
Copied!
# Plot storage charge states
flow_system.stats.plot.storage('ThermalStorage')
# Plot storage charge states flow_system.stats.plot.storage('ThermalStorage')
Out[13]:
Energy Flow Sankey¶
A Sankey diagram visualizes the total energy flows through the system:
In [14]:
Copied!
flow_system.stats.plot.sankey.flows()
flow_system.stats.plot.sankey.flows()
Out[14]:
Key Insights¶
The optimization reveals how storage enables load shifting:
- Charge during off-peak: When gas is cheap (night), the boiler runs at higher output to charge the storage
- Discharge during peak: During expensive periods, storage supplements the boiler
- Weekend patterns: Lower demand allows more storage cycling
Summary¶
You learned how to:
- Add Storage components with efficiency and losses
- Use time-varying prices in effects
- Visualize results with heatmaps and balance plots
- Access raw data via statistics.flow_rates and statistics.charge_states
Next Steps¶
- 03-investment-optimization: Optimize storage size
- 04-operational-constraints: Add startup costs and minimum run times