Sizing¶
So far every size and capacity has been a number we picked by hand. Sizing lets the optimizer pick them, trading the cost of building bigger against the cost of operating smaller.
You'll meet Sizing — a parameter object that turns a fixed size (or capacity) into a decision variable, optionally with per-MW investment cost.
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.io as pio
from fluxopt import Carrier, Converter, Effect, Flow, Port, Sizing, Storage, optimize
pio.renderers.default = 'notebook_connected'
1. From numbers to Sizing¶
Anywhere you wrote a fixed size=120 or capacity=300, you can write Sizing(min_size=..., max_size=..., effects_per_size=...) instead. The fields:
min_size/max_size— the bracket the optimizer searches.effects_per_size— investment cost per unit, charged once.{'cost': 0.27}means €0.27 per kW of installed capacity.mandatory=True(default) — must build something within[min, max]. SetFalseto let the optimizer choose not to build at all (introduces a binary).
We use daily-amortized rates here so that the 24-hour run is meaningful: €0.27/kW/day for the boiler (~€1000/kW lifetime over 10 years), €0.06/kWh/day for the tank (~€220/kWh over 10 years). T7 will show how multi-period models handle proper investment timing across years.
n = 24
timesteps = [datetime(2024, 1, 15) + timedelta(hours=h) for h in range(n)]
gas_price = [0.04 if h < 6 or h >= 22 else 0.12 for h in range(n)]
demand = np.array([10, 10, 8, 8, 8, 12, 25, 60, 70, 75, 75, 70, 65, 60, 55, 50, 45, 40, 30, 25, 20, 15, 12, 10])
peak = max(demand)
result = optimize(
timesteps=timesteps,
carriers=[Carrier('gas'), Carrier('heat')],
effects=[Effect('cost', unit='EUR')],
ports=[
Port(
'gas_grid',
imports=[
Flow('gas', size=1000, effects_per_flow_hour={'cost': gas_price}),
],
),
Port(
'workshop',
exports=[
Flow('heat', size=peak, fixed_relative_profile=[d / peak for d in demand]),
],
),
],
converters=[
Converter.boiler(
'boiler',
thermal_efficiency=0.9,
fuel_flow=Flow('gas', size=300),
thermal_flow=Flow(
'heat',
size=Sizing(min_size=20, max_size=200, effects_per_size={'cost': 0.27}),
),
),
],
storages=[
Storage(
'tank',
capacity=Sizing(min_size=0, max_size=1000, effects_per_size={'cost': 0.06}),
charging=Flow('heat', size=80),
discharging=Flow('heat', size=80),
eta_charge=0.98,
eta_discharge=0.98,
relative_loss_per_hour=0.005,
),
],
objective_effects='cost',
)
print(f'Total cost (operating + investment): {result.objective:.2f} EUR')
Running HiGHS 1.13.1 (git hash: 1d267d9): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-n376t85a has 439 rows; 223 cols; 756 nonzeros
Coefficient ranges:
Matrix [4e-02, 1e+00]
Cost [1e+00, 1e+00]
Bound [2e+02, 1e+03]
RHS [8e+00, 1e+03]
Presolving model
96 rows, 98 cols, 264 nonzeros 0s
Dependent equations search running on 48 equations with time limit of 1000.00s
Dependent equations search removed 0 rows and 0 nonzeros in 0.00s (limit = 1000.00s)
96 rows, 98 cols, 264 nonzeros 0s
Presolve reductions: rows 96(-343); columns 98(-125); nonzeros 264(-492)
Solving the presolved LP
Using dual simplex solver
Iteration Objective Infeasibilities num(sum)
0 5.4000035147e+00 Pr: 24(858) 0.0s
85 1.1340561828e+02 Pr: 0(0) 0.0s
Performed postsolve
Solving the original LP from the solution after postsolve
Model name : linopy-problem-n376t85a
Model status : Optimal
Simplex iterations: 85
Objective value : 1.1340561828e+02
P-D objective error : 3.7427973081e-16
HiGHS run time : 0.00
Total cost (operating + investment): 113.41 EUR
/home/docs/checkouts/readthedocs.org/user_builds/fluxopt/envs/v0.0.8/lib/python3.13/site-packages/linopy/common.py:492: UserWarning: Coordinates across variables not equal. Perform outer join. warn(
2. The chosen sizes¶
result.sizes is a flow-indexed array (NaN for fixed-size flows); result.storage_capacities is storage-indexed. Drop the NaNs to get just the optimized values.
result.sizes.dropna(dim='flow').to_dataframe(name='size [kW]')
| size [kW] | |
|---|---|
| flow | |
| boiler(heat) | 40.164288 |
result.storage_capacities.to_dataframe(name='capacity [kWh]')
| capacity [kWh] | |
|---|---|
| storage | |
| tank | 234.359578 |
flows = result.flow_rates.to_dataframe('value').reset_index().assign(panel='flow rate (kW)')
level = result.storage_level('tank').to_dataframe('value').reset_index().assign(panel='tank level (kWh)', flow='tank')
df = pd.concat([flows, level], ignore_index=True)
fig = px.line(df, x='time', y='value', color='flow', facet_row='panel', line_shape='hv', height=440)
fig.update_yaxes(matches=None, title='')
fig.for_each_annotation(lambda a: a.update(text=a.text.split('=')[-1]))
fig.update_layout(template='plotly_white', margin={'l': 50, 'r': 20, 't': 30, 'b': 20})
fig
The optimizer picks a boiler smaller than the demand peak (75 kW) — it's cheaper to build less boiler and arbitrage across the TOU spread with a generous tank. Push the tank's effects_per_size up and the answer flips: the boiler grows toward the peak and the tank shrinks.
Recap¶
Sizing turns design choices into LP variables. Pair it with effects_per_size to capture investment cost; pair mandatory=False with min_size > 0 to give the optimizer a yes/no build choice (introduces a binary).
Next: Multi-period & Investment — handle multiple periods (years) at once and decide when to build, not just whether.