← Back to portfolio

Hydrogen · Heat · Offshore wind · Pyomo dispatch

PyNEXUS Multi-Vector Dispatch Model

A Pyomo/HiGHS model coupling electricity, hydrogen and heat — offshore wind, PEM electrolysis, hydrogen storage/pipeline, grid exchange and electrolyser waste-heat recovery — run on a real fetched year of ERA5 wind, with an interactive Streamlit GUI.

PyNEXUS Streamlit GUI showing a solved dispatch run with independent verification

What I built

  • Offshore wind, PEM electrolyser, hydrogen pipeline, hydrogen storage, grid import/export, and electrolyser waste-heat recovery (with heat storage and backup boiler) — storage/grid/heat are opt-in and off by default, regression-tested against a locked reference objective.
  • A real ERA5 ingestion path (Copernicus CDS API): monthly-chunked fetch, provenance records, fail-loud gap checking. Used it to fetch a genuine year of 2023 wind for the site and validate the model's capacity factor (39.7%) against a peer-reviewed source.
  • Weighted-least-squares data reconciliation with a chi-square gross-error test on the hydrogen mass balance — the same method used in my ThermoTwin repo, applied here to a different balance.
  • A Streamlit GUI wrapping the exact same optimizer/verification code the CLI uses: run a dispatch live, browse committed runs, inspect the real capacity factor, and an interactive reconciliation demo.
  • An independent verification pass on every run (mass/energy balances, storage recursion, cost/objective reconstruction) — a solver reporting "optimal" is not treated as sufficient on its own.

Method

Multi-vector dispatch model

  1. Core MILP: electrolyser power p[t] (binary online status), minimising cost or emissions subject to wind availability, min/max load, ramp limits, pipeline capacity, and hourly or cumulative hydrogen demand. Solved with Pyomo + HiGHS.
  2. Phase A (opt-in): hydrogen storage with a terminal condition (so the optimiser can't drain it for free on the last hour) and grid import/export against a single energy balance — both off by default and regression-tested to leave the base model's locked reference objective unchanged.
  3. Phase B (opt-in): electrolyser waste heat (same linearised efficiency the hydrogen output already assumes, so the energy balance stays internally consistent), heat storage, and a backup boiler. A real swept finding: at the current demand-shortfall penalty, heat value has zero measurable effect on electrolyser dispatch — the penalty dominates it by orders of magnitude, confirmed by deliberately weakening the penalty and watching the coupling switch on.
  4. Phase C (real data): ERA5 wind fetched via the Copernicus CDS API, hub-height shear-corrected, and run through the model for the full 2023 year. The wind-only and grid-only-cumulative annual variants are genuinely infeasible against real demand — not a bug, a real capacity-planning finding, reported rather than hidden. The recorded successful run (grid + storage, soft hourly demand) still leaves ~16% of demand unmet.
  5. Verification: every run is independently re-checked from the exported CSV — wind/energy balances, storage recursion, cost identity, objective reconstruction — separate code from the optimizer itself, so a correct-looking solve can't silently hide an inconsistency.

Outputs

What the model produces

  • run_manifest.json — config/input/dispatch hashes, environment, solver status and gap, model size, objective value
  • dispatch.csv — per-hour power, hydrogen, storage, grid and heat flows for the solved run
  • verification.json — the independent check results (mass balance, cost identity, objective reconstruction, pass/fail)
  • ERA5 provenance records — one per monthly fetch plus an annual summary: source, grid cell, checksum, calendar-completeness check
  • A Streamlit GUI (screenshot above) for running, verifying, and browsing all of the above without writing Python

Limitations

What the model does not include

  • The MILP's hydrogen output uses a single linearised electrolyser efficiency (fixed at the 80%-load point), not the full nonlinear load-dependent curve — a deliberate tractability choice, stated explicitly rather than left implicit.
  • ENTSO-E day-ahead prices are implemented and unit-tested (same fetch/provenance discipline as ERA5) but not yet run live — needs a separate API credential I haven't requested yet. Dispatch runs pair real wind with a synthetic price series until then.
  • The data reconciliation module deliberately does not try to name which sensor is at fault when it flags an inconsistency: with one balance equation across four sensors, that ranking is mathematically determined by the configured variances alone, independent of which sensor is actually biased — proved and tested, not assumed.
  • It's a screening model: the turbine power curve is a cubic approximation, loss factors are literature-sourced rather than site-measured, and the demand/capacity figures are illustrative, not a project-specific engineering design.

Relevance

Why this matters

This project extends the energy-systems portfolio into genuinely multi-vector infrastructure — electricity, hydrogen and heat coupled in one optimisation, not three separate models bolted together. It carries the same discipline as the rest of the portfolio into a harder setting: real fetched data (not just synthetic examples), independent verification on every run, and honest reporting of infeasible/incomplete results (the wind-only annual case, the 16% unmet demand) rather than only showing the runs that came out clean.