Quickstart¶
Heat a workshop with a gas boiler — the simplest possible system.
Concepts: Carrier: energy type/medium (e.g. electricity, heat) · Node: optional balance/location where flows are summed · Flow (energy transfer) · Effect (cost tracking) · Port (source/sink) · Converter (efficiency coupling)
from datetime import datetime
import plotly.graph_objects as go
import plotly.io as pio
from plotly.subplots import make_subplots
from fluxopt import Carrier, Converter, Effect, Flow, Port, optimize
pio.renderers.default = 'notebook_connected'
System¶
Gas Grid ──► [gas] ──► Boiler η=90% ──► [heat] ──► Workshop
0.08 €/kWh 25–50 kW
timesteps = [datetime(2024, 1, 15, h) for h in range(8, 12)]
result = optimize(
timesteps=timesteps,
carriers=[Carrier('gas'), Carrier('heat')],
effects=[Effect('cost', unit='EUR', is_objective=True)],
ports=[
Port(
'gas_grid',
imports=[
Flow('gas', size=1000, effects_per_flow_hour={'cost': 0.08}),
],
),
Port(
'workshop',
exports=[
Flow('heat', size=50, fixed_relative_profile=[0.6, 1.0, 0.9, 0.5]),
],
),
],
converters=[
Converter.boiler(
'boiler',
thermal_efficiency=0.9,
fuel_flow=Flow('gas', size=200),
thermal_flow=Flow('heat', size=100),
),
],
)
Running HiGHS 1.13.1 (git hash: 1d267d9): Copyright (c) 2026 under MIT licence terms LP linopy-problem-2wlc0n4s has 54 rows; 30 cols; 82 nonzeros Coefficient ranges: Matrix [8e-02, 1e+00] Cost [1e+00, 1e+00] Bound [0e+00, 0e+00] RHS [2e+01, 1e+03] Presolving model 0 rows, 0 cols, 0 nonzeros 0s 0 rows, 0 cols, 0 nonzeros 0s Presolve reductions: rows 0(-54); columns 0(-30); nonzeros 0(-82) - Reduced to empty Performed postsolve Solving the original LP from the solution after postsolve Model name : linopy-problem-2wlc0n4s Model status : Optimal Objective value : 1.3333333333e+01 P-D objective error : 0.0000000000e+00 HiGHS run time : 0.00
Results¶
demand = result.flow_rate('workshop(heat)')
total_heat = float(demand.sum())
print(f'Total cost: {result.objective:.2f} EUR | Avg: {result.objective / total_heat * 100:.1f} ct/kWh')
Total cost: 13.33 EUR | Avg: 8.9 ct/kWh
result.effect_totals
<xarray.DataArray 'effect--total' (effect: 2)> Size: 16B array([13.33333333, -0. ]) Coordinates: * effect (effect) <U7 56B 'cost' 'penalty'
fig = make_subplots(
rows=2,
cols=1,
shared_xaxes=True,
vertical_spacing=0.15,
subplot_titles=('Flow Rates', 'Cost per Timestep'),
)
times = result.flow_rates.coords['time'].values
for fid in result.flow_rates.coords['flow'].values:
fig.add_trace(
go.Scatter(x=times, y=result.flow_rate(str(fid)).values, name=str(fid), line_shape='hv'),
row=1,
col=1,
)
cost = result.effects_temporal.sel(effect='cost')
fig.add_trace(go.Bar(x=times, y=cost.values, name='cost', showlegend=False), row=2, col=1)
fig.update_layout(height=450, margin={'l': 50, 'r': 20, 't': 30, 'b': 20}, template='plotly_white')
fig.update_yaxes(title_text='kW', row=1, col=1)
fig.update_yaxes(title_text='EUR', row=2, col=1)
fig
Dual Values (Marginal Costs)¶
For LP problems, result.duals contains shadow prices on constraints.
The most useful is carrier_balance — the marginal cost of supplying one
more kWh on each carrier at each timestep.
result.duals['carrier_balance'].to_pandas()
| time | 2024-01-15 08:00:00 | 2024-01-15 09:00:00 | 2024-01-15 10:00:00 | 2024-01-15 11:00:00 |
|---|---|---|---|---|
| carrier | ||||
| gas | 0.080000 | 0.080000 | 0.080000 | 0.080000 |
| heat | 0.088889 | 0.088889 | 0.088889 | 0.088889 |
Workflow: define flows → group into ports & converters → optimize() → inspect results
By default, flow ids are qualified as component(carrier) — e.g. boiler(gas), workshop(heat).
This applies when a Flow is created without an explicit short_id and without a node.
Setting Flow(short_id=...) uses that short_id instead of the carrier name, and providing
node='X' produces component(carrier:X).
Next: Heat System with Storage — time-varying prices and load shifting