Scenarios¶
Multi-year planning with uncertain demand scenarios.
This notebook introduces:
- Periods: Multiple planning years with different conditions
- Scenarios: Uncertain futures (mild vs. harsh winter)
- Scenario weights: Probability-weighted optimization
- Multi-dimensional data: Parameters that vary by time, period, and scenario
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
The Planning Problem¶
We're designing a heating system with:
- 3 periods (years): 2024, 2025, 2026 - gas prices expected to rise
- 2 scenarios: "Mild Winter" (60% probability) and "Harsh Winter" (40% probability)
- Investment decision: Size of CHP unit (made once, works across all futures)
The optimizer finds the investment that minimizes expected cost across all scenarios.
Define Dimensions¶
In [2]:
Copied!
from data.tutorial_data import get_scenarios_data
data = get_scenarios_data()
timesteps = data['timesteps']
periods = data['periods']
scenarios = data['scenarios']
scenario_weights = data['scenario_weights']
heat_demand = data['heat_demand']
gas_prices = data['gas_prices']
elec_prices = data['elec_prices']
from data.tutorial_data import get_scenarios_data data = get_scenarios_data() timesteps = data['timesteps'] periods = data['periods'] scenarios = data['scenarios'] scenario_weights = data['scenario_weights'] heat_demand = data['heat_demand'] gas_prices = data['gas_prices'] elec_prices = data['elec_prices']
Scenario-Dependent Demand Profiles¶
Heat demand differs significantly between mild and harsh winters:
In [3]:
Copied!
# Visualize demand scenarios with plotly
demand_ds = xr.Dataset(
{
scenario: xr.DataArray(
heat_demand[scenario].values,
dims=['time'],
coords={'time': timesteps},
)
for scenario in scenarios
}
)
demand_ds.plotly.line(x='time', title='Heat Demand by Scenario')
# Visualize demand scenarios with plotly demand_ds = xr.Dataset( { scenario: xr.DataArray( heat_demand[scenario].values, dims=['time'], coords={'time': timesteps}, ) for scenario in scenarios } ) demand_ds.plotly.line(x='time', title='Heat Demand by Scenario')
Build the Flow System¶
Initialize with all dimensions:
In [4]:
Copied!
flow_system = fx.FlowSystem(
timesteps=timesteps,
periods=periods,
scenarios=scenarios,
scenario_weights=scenario_weights,
name='Both Scenarios',
)
flow_system.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
flow_system
flow_system = fx.FlowSystem( timesteps=timesteps, periods=periods, scenarios=scenarios, scenario_weights=scenario_weights, name='Both Scenarios', ) flow_system.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) flow_system
Out[4]:
FlowSystem ========== Timesteps: 168 (Hour) [2024-01-15 to 2024-01-21] Periods: 3 (2024, 2025, 2026) Scenarios: 2 (Mild Winter, Harsh Winter) Status: ⚠
Add Components¶
In [5]:
Copied!
flow_system.add_elements(
# === Buses ===
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),
# === Gas Supply (price varies by period) ===
fx.Source(
'GasGrid',
outputs=[
fx.Flow(
'Gas',
bus='Gas',
size=1000,
effects_per_flow_hour=gas_prices, # Array = varies by period
)
],
),
# === CHP Unit (investment decision) ===
fx.linear_converters.CHP(
'CHP',
electrical_efficiency=0.35,
thermal_efficiency=0.50,
electrical_flow=fx.Flow(
'P_el',
bus='Electricity',
# Investment optimization: find optimal CHP size
size=fx.InvestParameters(
minimum_size=0,
maximum_size=100,
effects_of_investment_per_size={'costs': 15}, # 15 €/kW/week annualized
),
),
thermal_flow=fx.Flow('Q_th', bus='Heat'),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
# === Gas Boiler (existing backup) ===
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=0.90,
thermal_flow=fx.Flow('Q_th', bus='Heat', size=500),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
# === Electricity Sales (revenue varies by period) ===
fx.Sink(
'ElecSales',
inputs=[
fx.Flow(
'P_el',
bus='Electricity',
size=100,
effects_per_flow_hour=-elec_prices, # Negative = revenue
)
],
),
# === Heat Demand (varies by scenario) ===
fx.Sink(
'HeatDemand',
inputs=[
fx.Flow(
'Q_th',
bus='Heat',
size=1,
fixed_relative_profile=heat_demand, # DataFrame with scenario columns
)
],
),
)
flow_system.add_elements( # === Buses === 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), # === Gas Supply (price varies by period) === fx.Source( 'GasGrid', outputs=[ fx.Flow( 'Gas', bus='Gas', size=1000, effects_per_flow_hour=gas_prices, # Array = varies by period ) ], ), # === CHP Unit (investment decision) === fx.linear_converters.CHP( 'CHP', electrical_efficiency=0.35, thermal_efficiency=0.50, electrical_flow=fx.Flow( 'P_el', bus='Electricity', # Investment optimization: find optimal CHP size size=fx.InvestParameters( minimum_size=0, maximum_size=100, effects_of_investment_per_size={'costs': 15}, # 15 €/kW/week annualized ), ), thermal_flow=fx.Flow('Q_th', bus='Heat'), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), # === Gas Boiler (existing backup) === fx.linear_converters.Boiler( 'Boiler', thermal_efficiency=0.90, thermal_flow=fx.Flow('Q_th', bus='Heat', size=500), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), # === Electricity Sales (revenue varies by period) === fx.Sink( 'ElecSales', inputs=[ fx.Flow( 'P_el', bus='Electricity', size=100, effects_per_flow_hour=-elec_prices, # Negative = revenue ) ], ), # === Heat Demand (varies by scenario) === fx.Sink( 'HeatDemand', inputs=[ fx.Flow( 'Q_th', bus='Heat', size=1, fixed_relative_profile=heat_demand, # DataFrame with scenario columns ) ], ), )
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!
chp_size = flow_system.stats.sizes['CHP(P_el)']
pd.DataFrame(
{
'CHP Electrical [kW]': float(chp_size.max()),
'CHP Thermal [kW]': float(chp_size.max()) * 0.50 / 0.35,
'Expected Cost [EUR]': float(flow_system.solution['costs'].sum()),
},
index=['Optimal'],
).T
chp_size = flow_system.stats.sizes['CHP(P_el)'] pd.DataFrame( { 'CHP Electrical [kW]': float(chp_size.max()), 'CHP Thermal [kW]': float(chp_size.max()) * 0.50 / 0.35, 'Expected Cost [EUR]': float(flow_system.solution['costs'].sum()), }, index=['Optimal'], ).T
Out[7]:
| Optimal | |
|---|---|
| CHP Electrical [kW] | 99.671000 |
| CHP Thermal [kW] | 142.387143 |
| Expected Cost [EUR] | 77.793625 |
Heat Balance by Scenario¶
See how the system operates differently in each scenario:
In [8]:
Copied!
flow_system.stats.plot.balance('Heat')
flow_system.stats.plot.balance('Heat')
Out[8]:
CHP Operation Patterns¶
In [9]:
Copied!
flow_system.stats.plot.heatmap('CHP(Q_th)')
flow_system.stats.plot.heatmap('CHP(Q_th)')
Out[9]:
Multi-Dimensional Data Access¶
Results include all dimensions (time, period, scenario):
In [10]:
Copied!
flow_rates = flow_system.stats.flow_rates
# Plot flow rates
flow_system.stats.plot.flows()
flow_rates = flow_system.stats.flow_rates # Plot flow rates flow_system.stats.plot.flows()
Out[10]:
In [11]:
Copied!
# CHP operation summary by scenario
chp_heat = flow_rates['CHP(Q_th)']
pd.DataFrame(
{
scenario: {
'Avg [kW]': float(chp_heat.sel(scenario=scenario).mean()),
'Max [kW]': float(chp_heat.sel(scenario=scenario).max()),
}
for scenario in scenarios
}
)
# CHP operation summary by scenario chp_heat = flow_rates['CHP(Q_th)'] pd.DataFrame( { scenario: { 'Avg [kW]': float(chp_heat.sel(scenario=scenario).mean()), 'Max [kW]': float(chp_heat.sel(scenario=scenario).max()), } for scenario in scenarios } )
Out[11]:
| Mild Winter | Harsh Winter | |
|---|---|---|
| Avg [kW] | 101.749508 | 133.619672 |
| Max [kW] | 142.387143 | 142.387143 |
Sensitivity: What if Only Mild Winter?¶
Compare optimal CHP size if we only planned for mild winters:
In [12]:
Copied!
# Select only the mild winter scenario
fs_mild = flow_system.transform.sel(scenario='Mild Winter')
fs_mild.optimize(fx.solvers.HighsSolver(mip_gap=0.01))
chp_size_mild = float(fs_mild.stats.sizes['CHP(P_el)'].max())
chp_size_both = float(chp_size.max())
pd.DataFrame(
{
'Mild Only': {'CHP Size [kW]': chp_size_mild},
'Both Scenarios': {'CHP Size [kW]': chp_size_both},
'Uncertainty Buffer': {'CHP Size [kW]': chp_size_both - chp_size_mild},
}
)
# Select only the mild winter scenario fs_mild = flow_system.transform.sel(scenario='Mild Winter') fs_mild.optimize(fx.solvers.HighsSolver(mip_gap=0.01)) chp_size_mild = float(fs_mild.stats.sizes['CHP(P_el)'].max()) chp_size_both = float(chp_size.max()) pd.DataFrame( { 'Mild Only': {'CHP Size [kW]': chp_size_mild}, 'Both Scenarios': {'CHP Size [kW]': chp_size_both}, 'Uncertainty Buffer': {'CHP Size [kW]': chp_size_both - chp_size_mild}, } )
Out[12]:
| Mild Only | Both Scenarios | Uncertainty Buffer | |
|---|---|---|---|
| CHP Size [kW] | 84.561091 | 99.671 | 15.109909 |
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¶
Multi-Dimensional FlowSystem¶
flow_system = fx.FlowSystem(
timesteps=timesteps, # Time dimension
periods=periods, # Planning periods (years)
scenarios=scenarios, # Uncertain futures
scenario_weights=weights, # Probabilities
)
Dimension-Varying Parameters¶
| Data Shape | Meaning |
|---|---|
| Scalar | Same for all time/period/scenario |
| Array (n_periods,) | Varies by period |
| Array (n_scenarios,) | Varies by scenario |
| DataFrame with columns | Columns match scenario names |
| Full array (time, period, scenario) | Full specification |
Scenario Optimization¶
The optimizer minimizes expected cost: $$\min \sum_s w_s \cdot \text{Cost}_s$$
where $w_s$ is the scenario weight (probability).
Selection Methods¶
# Select specific scenario
fs_mild = flow_system.transform.sel(scenario='Mild Winter')
# Select specific period
fs_2025 = flow_system.transform.sel(period=2025)
# Select time range
fs_day1 = flow_system.transform.sel(time=slice('2024-01-15', '2024-01-16'))
Summary¶
You learned how to:
- Define multiple periods for multi-year planning
- Create scenarios for uncertain futures
- Use scenario weights for probability-weighted optimization
- Pass dimension-varying parameters (arrays and DataFrames)
- Select specific scenarios or periods for analysis
Next Steps¶
- 08a-Aggregation: Speed up large problems with resampling and clustering
- 08b-Rolling Horizon: Decompose large problems into sequential time segments