Skip to content

fluxopt

Energy system models from components: fluxopt writes them into tables for its math, which mathspec states and specsolve solves — detailed dispatch, scaled to multi-period planning.

PyPI Downloads License: MIT Python 3.12+ Ruff

Get started Read the math


What it is for

  • Composable elements. Build models from Carrier, Flow, Port, Converter, Storage and Effect — clear separation of physics, costs, and topology. API →
  • Math as a file. The math is YAML that mathspec checks and prints, the numbers are polars tables, and specsolve solves the two together. How it works →
  • Sizing & status. Capacity optimization and on/off behavior as first-class concerns, not bolt-ons. Sizing →
  • HiGHS out of the box. Open-source MIP solver bundled. Swap in another solver with solver=. Quickstart →
  • Math, documented. Every feature has a page that explains its formulation, and a page that prints the exact math the solver reads. Notation →
  • Companion ecosystem. Lean core, optional companions for plotting, YAML loading, and (planned) interactive marimo apps. Roadmap →

Quick start

# A gas boiler covers a heat demand, minimizing fuel cost
from datetime import datetime
from fluxopt import Carrier, Converter, Effect, Flow, Port, optimize

result = optimize(
    timesteps=[datetime(2024, 1, 1, h) for h in range(4)],
    carriers=[Carrier(id='gas'), Carrier(id='heat')],
    effects=[Effect(id='cost')],
    ports=[
        Port(id='grid', imports=[Flow(carrier='gas', size=500, effects_per_flow_hour={'cost': 0.04})]),
        Port(id='demand', exports=[Flow(carrier='heat', size=100, fixed_relative_profile=[0.4, 0.7, 0.5, 0.6])]),
    ],
    converters=[
        Converter.boiler(
            'boiler',
            thermal_efficiency=0.9,
            fuel_flow=Flow(carrier='gas', size=300),
            thermal_flow=Flow(carrier='heat', size=200),
        )
    ],
    objective='cost',
)

print(f'Total cost: {result.objective:.2f}')
print(result.primal('rate'))

One API, three levels of control

Every level answers with specsolve's Result; each one only adds control — pick the lowest rung that does the job.

1. One-shot — optimize(...) as above. Elements in, a solved answer out, with fail-fast validation of ids and references.

2. Declarative — gather the same arguments into a reusable, serializable system. Time series can stay out of the structure as ProfileRefs and be supplied at solve time via profiles, one polars table per ProfileRef.table:

system = fx.FlowSystem.from_yaml('system.yaml')  # or FlowSystem(...) in Python
load = pl.read_csv('load.csv', try_parse_dates=True)  # columns: time, demand
result = system.optimize(profiles={'load': load}, archive='run.zip')
system.to_yaml('system.yaml')  # round-trips

3. Spec and sources — the system is a mathspec spec and the tables bound to it. Read, typeset or extend the spec, edit any table, and solve with specsolve:

import mathspec, specsolve

spec = mathspec.override(system.spec(), ['my_cap.yaml'])
sources = system.sources(profiles={'load': load}) | {'grid_cap': caps}
result = specsolve.solve(spec, sources)

The spec's assumptions: check whatever tables arrive.

Read an answer as a tidy polars table: result.primal("rate") for a variable, or result.evaluate("flow_hours") for a reported quantity: flow hours, carrier balance, capacity factor, storage mean level, and each contribution with its cross-effects charged (priced_*). archive= writes the spec, its sources and the answer; specsolve.load_archive reads them back.