Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

tellegen reactive power flow visualization

Introduction

tellegen is a reactive visualization interface for power flow cases. The name refers to Tellegen’s theorem and the adjoint sensitivity calculations.

The app uses a gradient preview, exact commit interaction model. Perturbations update the display from KKT sensitivity columns. Exact solves (DC OPF, AC power flow, and the SOCWR relaxation) run in the browser in WebAssembly. Case parsing uses powerio.

Demo

The public demo at tellegen.dev serves three TAMU ACTIVSg synthetic grids and the CATS California Test System at their staged geographic coordinates. These are synthetic grids on geographic footprints, not surveyed infrastructure:

caseterritorybusesbranches
ACTIVSg200central Illinois200245
ACTIVSg500South Carolina500597
ACTIVSg7000Texas67179140
CATSCalifornia887010823

Each case solves as DC OPF by default; a selector switches to the SOCWR relaxation, solved in the browser in WebAssembly. Bus color shows locational marginal price. Selecting a bus shows the dLMP/dd column for a demand perturbation at that bus. Moving the demand slider applies the local sensitivity immediately; releasing it computes the exact solution in WebAssembly.

Local Files

Dropped .m, .raw, and .aux files are parsed in the browser by the WebAssembly build of powerio. Files with coordinates render on the map. Files without coordinates can be placed by clicking the map, or paired with local geographic files in .csv, .json, or .geojson form. A dropped PowerWorld .pwd file is decoded as display data and rendered as approximate substation positions. Parsed local case files solve in the browser and are not uploaded.

API

  • GET /api/health
  • GET /api/cases
  • GET /api/cases/{id}/case
  • GET /api/cases/{id}/network
  • GET /api/cases/{id}/solution
  • GET /api/cases/{id}/sensitivity/lmp/d/{bus}
  • GET /api/cases/{id}/solve

The sensitivity and solve endpoints accept ?d=bus:mw,bus:mw, where each value is a MW delta from the base case. The solve stream emits status, solution, optional sensitivity, and done events.

Direction

Where the project is going and why. Architecture describes the boundary as shipped; this page describes the intent behind it. The ecosystem claims date from a June 2026 review; sources at the end.

Landscape

The sources below show no web native, developer facing framework for reactive power systems (public sources; a private implementation could exist). The closest tools:

  • Electrisim runs pandapower and OpenDSS from the browser; the compute is server side, and it reports no locational marginal prices.
  • RTE’s GridSuite (PowSyBl) is the most mature browser grid application in the field: closed operator software without a reactive price map.
  • GridStatus.io has a polished nodal price map of ISO published prices the user cannot recompute.
  • PowerPlots.jl is hover to inspect, driven from Julia.

None of them let a user load a case, click a bus, drag demand, and watch prices and flows re-solve live against exact KKT sensitivities. tellegen ships that interaction. PowerDiff.jl worked out the sensitivity columns behind it; tellegen exposes them as a framework, and the demo is one application built on it.

The boundary

Sensitivity columns give an immediate matrix vector preview during a drag; the exact solve fires on release and reconciles the preview. Around that loop:

  • powerio (Rust): parsing, encoding, the formats, the network data model, and the canonical display format (display-data.md).
  • tellegen (browser): interaction, the fast math, rendering. Its Rust is built in this repository against powerio and compiled to WebAssembly.
  • tellegen backend (Rust): the same numerical core compiled native. It hosts the bundled cases; its compute endpoints can serve browsers without a working WebAssembly path and ship disabled behind TELLEGEN_SERVER_COMPUTE, a single switch. Per endpoint control and authentication come before AC OPF ships as a server product.

PowerDiff.jl remains the reference harness for parity checks.

Browser solver status

Julia has no production WebAssembly path: the runtime port has been dormant since 2021, and the function level compilers cannot reach BLAS or JuMP. The numerics are therefore written in Rust and compiled to wasm.

  • DC OPF (sparse LP/QP): shipped. Clarabel.rs, a pure Rust interior point solver with no BLAS dependency, solves 200 to 7000 bus cases in the browser as the exact commit.
  • DC sensitivities (the dLMP/dd columns): shipped. One linear solve against the KKT factorization at the active set, reimplemented in Rust on faer.
  • AC power flow (Newton with sparse LU): feasible. faer provides sparse LU under wasm. There is no shipped precedent, and faer’s wasm sparse path has an open crash report, so it needs validation on real case matrices first.
  • AC OPF (nonconvex): the holdout. There is no Ipopt in wasm. The candidates are a second order cone relaxation through Clarabel (shipped as SOCWR) and a Rust nonlinear solver (thin ground). Until one matures, AC OPF is the reason the backend exists.

The browser owns the whole DC pipeline: parse, solve, differentiate, render (issue #2).

Decisions

  • wasm-pack and wasm-bindgen stay. Both shipped releases in 2026 (wasm-bindgen 0.2.125 in June). The WebAssembly Component Model targets server and edge with no browser DOM host, so it does not apply here.
  • deck.gl and MapLibre on WebGL2 stay. deck.gl holds 60 fps to about a million elements; grids run at ten thousand to a hundred thousand. WebGPU in deck.gl lacks picking and basemap interleave today and can be adopted later without touching the rendering code.
  • $state.raw on large payloads. Shared reactive state is scoped through context, so the packages stay SSR safe.

Roadmap

Near term: harden the framework package boundary as outside apps consume @tellegen/engine and @tellegen/svelte. The hosted demo is the reference consumer.

Mid term: prebake the bundled cases as static assets and make the no backend deployment the default. The DC pipeline in the browser has landed, including the Safari sensitivity gap (issue #8).

Long term: AC power flow in the browser once faer’s wasm sparse path is validated. AC OPF moves to the browser when a wasm nonlinear or cone path is solid, and stays in the backend until then. WebGPU when deck.gl’s backend gains picking and basemap support. Synthetic grid generation pairs with in browser compute.

Sources

Framework Quickstart

There are two integration references:

Install

For a Svelte app:

npm install @tellegen/svelte

For a custom UI:

npm install @tellegen/engine

For local development in this repository:

npm ci
npm run wasm
npm run build:engine
npm run build:svelte
npm --workspace @tellegen/example-svelte-minimal run dev

The engine package resolves wasm assets relative to its packaged modules with new URL(..., import.meta.url). Vite and SvelteKit handle that path for the Svelte package and for custom engine consumers.

Svelte Viewer

<script lang="ts">
  import { TellegenViewer } from "@tellegen/svelte";
  import "@tellegen/svelte/styles.css";
</script>

<TellegenViewer />

For local files only:

<script lang="ts">
  import { TellegenViewer } from "@tellegen/svelte";
  import "@tellegen/svelte/styles.css";
</script>

<TellegenViewer loadDefaultCases={false} showFooter={false} />

Run the local example:

npm --workspace @tellegen/example-svelte-minimal run dev

Engine Flow

Run the engine example:

npm --workspace @tellegen/example-browser-minimal run dev
import { createStudy, formatOf, ingestCase } from "@tellegen/engine";

const format = formatOf("case14.m");
if (!format) throw new Error("unsupported case format");

const parsed = await ingestCase(caseText, format);
const study = await createStudy(parsed.network_json, "dcopf");

try {
  const preview = study.preview({ 3: 25 });
  const committed = study.commit(parsed.name, { 3: 25 }, {}, { bus: 3 });
  console.log(preview.objectiveDelta, committed.sensitivity);
} finally {
  study.free();
}

The call sequence is:

  1. detect the case format;
  2. parse the case in browser WebAssembly;
  3. create a Study;
  4. preview a demand edit without a solve;
  5. commit the edit with a sensitivity request; and
  6. free the Study.

Privacy Boundary

Dropped files stay in the browser when the app uses the browser wasm path. The parser, solve, preview, commit, and sensitivity paths all run in WebAssembly in the page. A host app only sends data to a server if it chooses an HTTP path or writes its own upload path.

Framework Packages

The reusable browser packages live under packages/. The first framework release publishes both @tellegen/engine and @tellegen/svelte.

Use @tellegen/svelte when a Svelte app wants the map, panels, local file flow, and solve card as components. Use @tellegen/engine when an app wants case parsing, browser WebAssembly solves, studies, previews, and sensitivities without the tellegen UI.

apps/web is the hosted demo. It consumes @tellegen/svelte and keeps only route level concerns such as SEO pages, /credits, and /privacy.

Svelte UI Package

Install:

npm install @tellegen/svelte

Render the full viewer:

<script lang="ts">
  import { TellegenViewer } from "@tellegen/svelte";
  import "@tellegen/svelte/styles.css";
</script>

<TellegenViewer />

The default viewer loads bundled cases from /api, parses dropped local case files in the browser, and runs supported local solves in WebAssembly.

TellegenViewer accepts:

  • apiBase, default /api
  • loadDefaultCases, default true
  • docsHref
  • orgHref
  • orgLabel
  • showFooter, default true

Use a different backend base path like this:

<TellegenViewer apiBase="/tellegen/api" />

Use local files only by disabling bundled case loading:

<script lang="ts">
  import { TellegenViewer } from "@tellegen/svelte";
  import "@tellegen/svelte/styles.css";
</script>

<TellegenViewer loadDefaultCases={false} showFooter={false} />

For apps where state should survive route changes, mount the provider in a persistent layout and render the shell on the page:

<script lang="ts">
  import { TellegenProvider } from "@tellegen/svelte";
  import "@tellegen/svelte/styles.css";

  let { children } = $props();
</script>

<TellegenProvider>
  {@render children()}
</TellegenProvider>
<script lang="ts">
  import { TellegenShell } from "@tellegen/svelte";
</script>

<TellegenShell />

@tellegen/svelte also exports lower level pieces for custom shells:

  • TellegenMap
  • AppState, CaseState, LocalCase, and createAppState
  • Controller and createController
  • createApiClient
  • panels and controls from @tellegen/svelte/components
  • colors, display helpers, formatting helpers, and public types

Engine Package

Install:

npm install @tellegen/engine

Use the engine package when you want to build your own UI:

import { createStudy, formatOf, ingestCase, solveJson } from "@tellegen/engine";

The engine package resolves its wasm files relative to the package module. Apps must serve package asset files from node_modules; Vite and SvelteKit handle that path.

Examples

  • examples/svelte-minimal imports @tellegen/svelte and runs with loadDefaultCases={false} for local files only.
  • examples/browser-minimal imports @tellegen/engine directly and has no map stack.

Downstream apps should not import from apps/web/src/lib or from generated wasm folders.

Engine API Reference

@tellegen/engine is the public browser engine package. It is independent of SvelteKit and the hosted demo.

Constants

  • CONTRACT_VERSION: the public TypeScript contract version. It matches the package version.
  • CONTRACT_SOURCE_SHA256: the crates/tellegen/src/api.rs hash used to generate the TypeScript contracts.
  • FORMULATION_IDS and SOLVE_STATUSES: generated enum tags from the Rust API layer.
  • FORMULATIONS and DEFAULT_FORMULATION: browser UI formulation list and default formulation.

Browser Wasm Transport

  • browserWasmTransport: object facade for the browser wasm transport.
  • createBrowserWasmTransport(): returns the browser wasm transport facade.
  • preloadEngine(): initializes the wasm package.

The facade has the same methods as the direct exports:

  • ingestCase(text, format)
  • parseDisplay(bytes)
  • capabilities()
  • solveJson(networkJson, request)
  • createStudy(networkJson, formulation)

Case And Display Helpers

  • formatOf(name): returns m, raw, or aux for supported case names.
  • isDisplayFile(name): returns true for PowerWorld .pwd display files.
  • ingestCase(text, format): parses a case and returns a network JSON payload plus summary and topology.
  • parseDisplay(bytes): parses PowerWorld display data for diagram overlays.

Solves And Studies

  • capabilities(): returns available formulations, operands, and parameters.
  • solveJson(networkJson, request): stateless solve over the generalized Rust API.
  • createStudy(networkJson, formulation): builds a browser Study.
  • Study / BrowserStudy: browser handle with:
    • currentSolution()
    • preview(deltas, rates?)
    • commit(caseId, deltas, rates, target)
    • sensitivity(caseId, deltas, rates, target)
    • free()

deltas are demand deltas in MW keyed by bus; rates are thermal rating deltas in MW keyed by branch. A key is the original numeric id (bus id, 1-based branch position) or the powerio row uid string ("buses:1", "branches:2") stamped at ingest — ingestCase payloads carry the uid on every topology and view element, and solve responses echo it on bus and branch scalars. target is { bus } for the ∂LMP/∂d column, { branch } for the ∂LMP/∂rating column (nonzero only on binding lines), or null for no column.

Call free() when a host app discards a study.

Types

Generated public types include:

  • SolveRequest, SolveResponse, ProblemCaps
  • SensRequest, SensitivityMatrix, SensitivityColumn
  • Network, NetworkBus, NetworkBranch
  • Solution, SolveIteration, DemandDeltas
  • BrowserFormulation, FormulationId, SolveStatus

The generated file is committed at packages/engine/src/generated/contracts.ts and checked in CI.

Transports

The transport boundary is the line between a host app and the tellegen engine.

Browser Wasm

@tellegen/engine ships the browser wasm transport today. It wraps the wasm-pack output from crates/tellegen-wasm and exposes direct functions plus browserWasmTransport.

Use this transport when cases should stay local to the browser. Dropped case files are parsed in WebAssembly, and solves, Study.preview, Study.commit, and sensitivity requests run there as well. No case text or network JSON leaves the browser unless the host app sends it.

The wasm instance lives in a dedicated Web Worker, so a multi-second solve never blocks the page. When the browser cannot run a module worker the same requests execute on the calling thread instead; the API is identical either way.

The transport has one wasm package carrying parsing, all solves, Study, capabilities, and generalized sensitivity requests.

The loader is lazy. Host apps can call preloadEngine() to control when the browser downloads and initializes wasm.

HTTP

The hosted demo also uses HTTP for bundled case metadata and native server fallback paths. That server is a demo consumer, not a requirement for using @tellegen/engine.

An HTTP transport can implement the same shape as EngineTransport: parse or fetch a network, call a native solve_json endpoint, keep a server side study handle, and return the same generated TypeScript contract shapes.

That transport is optional for apps that need server sized cases, audit logs, or native deployment.

Tauri

The desktop and mobile path can use the same transport contract with a Tauri command layer. The UI keeps the Study workflow; only the call boundary changes from browser wasm to native commands.

The Rust contract stays the source of truth. New transports should return the generated SolveResponse, ProblemCaps, and sensitivity matrix shapes instead of defining parallel types.

Getting Started

Repository Layout

  • apps/web/: private SvelteKit hosted demo
  • crates/: the tellegen engine and its wasm, server, CLI, and benchmark adapters
  • packages/engine/: public @tellegen/engine browser package
  • packages/svelte/: public @tellegen/svelte component package
  • examples/browser-minimal/: minimal downstream Vite example
  • examples/svelte-minimal/: minimal Svelte example using the component package
  • scripts/: data staging and docs build helpers
  • deploy/: deployment compose files and proxy notes
  • docs/src/: mdBook documentation source

Prerequisites

  • Rust from rust-toolchain.toml, including rustfmt, clippy, and the wasm32-unknown-unknown target
  • Node.js 22 or newer
  • wasm-pack 0.15.x for browser WebAssembly builds
  • mdBook 0.5.x for local documentation builds

tellegen backend

cargo run -p tellegen-server

Set TELLEGEN_ALLOW_FALLBACK=1 to run without staged demo data:

TELLEGEN_ALLOW_FALLBACK=1 cargo run -p tellegen-server

WebAssembly Module

npm ci
npm run wasm
npm run build:engine
npm run build:svelte

tellegen frontend demo

npm ci
npm run wasm
npm run build:engine
npm run build:svelte
npm --workspace tellegen-frontend run dev

The Vite dev server proxies /api to http://localhost:8000. apps/web resolves @tellegen/svelte through its built dist/, so build:svelte must run before the dev server starts.

Framework Packages

Preview the package contents before publishing:

npm run pack:engine
npm run pack:svelte

Use @tellegen/svelte for the map, panels, local file flow, and solve card. Use @tellegen/engine for apps that want case parsing and browser solves without the tellegen UI. The hosted app is a private demo workspace that consumes @tellegen/svelte like another Svelte app would.

Data

The ACTIVSg and CATS distributions are downloaded by the operator and are not vendored. With the distributions under ~/Datasets:

scripts/stage-data.sh ~/Datasets

The script stages any complete case pairs it finds into data/. The backend serves the staged subset; if nothing is staged, it exits unless TELLEGEN_ALLOW_FALLBACK=1 is set. That fallback serves two pglib cases with synthetic coordinates for CI and local smoke checks.

Docs

Install mdBook, then build the public docs:

scripts/build-docs.sh

CI pins mdBook to v0.5.3. For local work, any recent mdBook 0.5.x release renders the book.

Local Case Files

Dropped case files stay in the browser. The current public demo does not upload local .m, .raw, .aux, .pwd, .csv, .json, or .geojson files.

MATPOWER .m, PSS/E .raw, and PowerWorld .aux files describe network topology. If a case file includes complete coordinates, tellegen draws it directly. If coordinates are missing, tellegen creates a local synthetic layout and asks the user to place it on the map. Dropped JSON is content sniffed: a .pio.json study package restores, a BMOPF or PowerModelsDistribution document opens the multiconductor viewer, and anything else is read as a geographic file.

After a parsed local case has coordinates, either from the file, a geographic file, or manual placement, tellegen solves the DC OPF in browser WebAssembly. Local case files do not call the tellegen backend solve endpoints.

Manual Placement

For case files with no coordinates, such as a plain case14.m, tellegen computes a deterministic topology layout from buses and in-service branches. The user then clicks the map to center that synthetic layout at the chosen location.

The placed local case can be moved later with the case panel move action. The first version uses explicit click placement rather than drag movement.

After placement, the local case enters the same bus selection and demand slider workflow as the bundled demo cases. The solve card reports the browser solve time and does not show backend iterations.

Geographic Files

Some case files contain network topology but keep map coordinates in separate GIS files. tellegen accepts those files as local geographic files: drop the case file with one or more .csv, .json, or .geojson files, or drop the geographic files after selecting a parsed local case.

All files stay in the browser. The tellegen backend does not receive dropped case files or geographic files.

Parsing is powerio’s GeoLayer tolerant reader, running in WebAssembly: it accepts headerless OpenDSS buscoords CSV, CSV and JSON records with the aliased field names below, and GeoJSON Point/LineString features, and it rejects input carrying no usable coordinates. Applied coordinates land on the network itself (Bus.location, Branch.route), so a saved study package or an exported case carries exactly the placement on screen. The case panel can also download the current layout as a .geo.json layer — canonical GeoJSON with provenance stamped (synthetic or manual for layouts) that powerio and tellegen read back.

Bus Coordinates

The geographic file must identify buses by the same ids used in the case file. CSV and JSON records can use these field names:

MeaningAccepted fields
Bus idbus_i, bus, bus_id, bus number, number, id
Latitudelat, latitude, y
Longitudelon, lng, longitude, x

Example:

bus_i,Lat,Lon
1,37.77243572,-122.2429162
2,37.77848161,-121.6259513

A geographic file does not need to cover every bus: matched buses place, and the panel reports the matched and unmatched counts. Buses left without coordinates are omitted from the map with a warning; a file that matches nothing is rejected and the case stays placeable. Records can also match by powerio row uid (buses:3) or by case insensitive bus name.

Branch Paths

Branch geometry is optional. Without branch paths, tellegen draws straight segments between placed buses.

CSV and JSON branch records can use:

MeaningAccepted fields
Branch idbranch, branch_id, branch number, cats_id, id
From busf_bus, from, from_bus
To bust_bus, to, to_bus
Endpoint coordinatesLat1, Lon1, Lat2, Lon2 and lowercase variants

GeoJSON LineString features are also accepted. The reader matches a route by uid or branch id when present, then by the unordered from/to bus pair. A LineString endpoint can also provide bus coordinates for its f_bus and t_bus properties. Applied routes land in Branch.route and render as polylines instead of straight segments.

Piecewise Costs

A dropped case with MATPOWER model 1 piecewise linear generator costs solves against a least squares quadratic fit of its breakpoints (formulations), so its objective and prices differ from a solver that carries the piecewise curve exactly.

Display Files

PowerWorld .pwd files are display overlays. Dropped alone, they show substation symbols at approximate projected positions. Dropped alongside a case file that has no coordinates, the substation points join onto buses through the SubNum field on the bus rows and fill the case’s positions; a .pwd whose numbers match nothing stays a separate overlay entry.

Data Provenance

The demo cases are synthetic grids from TAMU ACTIVSg and CATS. They are fictional networks built on geographic footprints. ACTIVSg200 and ACTIVSg500 coordinates come from PowerWorld aux exports. ACTIVSg7000 and CATS coordinates come from GIS bus CSVs. These positions are not surveyed infrastructure locations.

caseterritorybusesbranchesfiles
ACTIVSg200central Illinois200245case_ACTIVSg200.m + ACTIVSg200.aux
ACTIVSg500South Carolina500597case_ACTIVSg500.m + ACTIVSg500.aux
ACTIVSg7000Texas67179140Texas7k_20210804.m + Texas7k_lat_long.csv
CATSCalifornia887010823CaliforniaTestSystem.m + CATS_buses.csv + CATS_lines.json

The MATPOWER file feeds the DC OPF. For ACTIVSg200 and ACTIVSg500, the aux file supplies the coordinates. For ACTIVSg7000, the bus coordinate CSV supplies latitude and longitude. Operators download the ACTIVSg distributions from electricgrids.engr.tamu.edu and stage them with scripts/stage-data.sh; the repository does not vendor them.

CATS comes from the WISPO POP CATS repository. The server reads CaliforniaTestSystem.m for the network, CATS_buses.csv for bus latitude and longitude, and CATS_lines.json for branch paths. The staging script also copies CATS_gens.csv when present; the current map does not render a separate generator geometry layer.

Aux coordinate forms

PowerWorld aux exports have used two coordinate layouts:

  • ACTIVSg complete case exports repeat substation latitude and longitude on each bus row in Latitude:1 and Longitude:1.
  • Later exports can leave the bus latitude and longitude columns empty and reference the Substation table through SubNumber.

powerio promotes the bus row form (Latitude:1/Longitude:1) into the typed Bus.location at parse; tellegen keeps the two fallbacks upstream does not cover — bare Latitude/Longitude bus columns and the Substation table join — so dropped files of either form resolve when the data is present.

Buses Sharing Coordinates

Multiple buses can share one substation coordinate. tellegen spreads each group on a deterministic ring of about 400 m around the substation point, ordered by bus id. The group remains visually associated with the substation at network zoom, and individual buses remain hoverable at street zoom.

Explicit Fallback

The embedded fallback (deployment) serves two PGLib cases whose coordinates are synthetic and labeled as synthetic.

Display Data

Case files and display files are separate inputs. MATPOWER .m, PSS/E .raw, and PowerWorld .aux files describe the network. JSON files are accepted as geographic files in this release. PowerWorld .pwd files describe a one-line diagram. tellegen reads coordinates from case files when they exist and reads diagram positions from display files when they are dropped.

powerio 0.2.2 added a display API separate from network parsing. parse_display_bytes and parse_display_file return DisplayData::PowerWorld(PwdDisplay) for .pwd inputs. parse_file does not accept .pwd.

Reading .pwd

crates/tellegen-wasm/src/lib.rs exports parse_display(bytes, format) over powerio::parse_display_bytes. The frontend reads .pwd files with arrayBuffer() and passes a Uint8Array to the WebAssembly module.

A dropped .pwd creates a local display entry with substation points only. It does not create buses, branches, or a solvable case.

Projecting .pwd coordinates

PowerWorld .pwd coordinates are diagram coordinates, not latitude and longitude. TAMU-generated diagrams use Web Mercator scaled by one constant, K = 535.81608, with both axes expressed in degrees:

x = K * lon
y = K * mercdeg(lat)
mercdeg(lat) = (180 / pi) * ln(tan(pi / 4 + lat * pi / 360))

The inverse transform is powerio’s pwd_mercator_to_lonlat; the wasm parse_display export returns each substation with its projected lon/lat alongside the raw diagram coordinates, so the frontend never reimplements the constant. This places checked ACTIVSg diagrams within about 0.02 degrees of their corresponding named cities. Hand-edited diagrams can differ, so tellegen labels these positions as approximate.

The canonical geographic document

powerio 0.7.1 ships the standalone geographic document (GeoLayer): element points and branch routes in one coordinate space, keyed by uid, external id, name, or the branch endpoint pair, written as a GeoJSON FeatureCollection with the powerio_geo foreign member (.geo.json). tellegen consumes it rather than defining a separate format: dropped geographic sidecars parse through its tolerant reader, applied coordinates land on the network (Bus.location, Branch.route), and the case panel exports the current layout as a .geo.json layer with provenance stamped.

On top of it, a dropped .pwd sibling fills missing case coordinates through the SubNum join (geo_layer_from_pwd + apply_substation_points, projected by pwd_mercator_to_lonlat), and layouts computed by tellegen export with synthetic/manual provenance.

Synthetic Layout

Most public OPF test cases do not include geographic coordinates. tellegen uses synthetic topology layouts in two places: the explicit pglib dev fallback, and local files parsed in the browser that the user places on the map. The tellegen backend API marks fallback coordinates with synthetic_coords: true; local files are labeled in the panel as synthetic layouts.

Tree Short-Circuit for Radial Networks

Before the force pass, the browser layout counts independent cycles on the deduplicated graph (open switches are already excluded). When the count is at most max(2, ceil(0.02 * n)), the graph is a tree or nearly one — the shape of a distribution feeder — and it is drawn as a tidy tree instead: depth grows along one axis from the root, and each subtree occupies its own contiguous band of leaf slots on the other, so the drawing is planar and the longest chain renders as a straight trunk. The root is a source bus when the case identifies one (multiconductor cases pass their sources as hints), else a BFS diameter endpoint. Chords of a near-tree draw as plain segments between their placed endpoints. Meshed networks fall through to the force pass below.

Deterministic Seed

The layout starts from a golden angle spiral on the unit square. A deterministic jitter of about 1e-4, computed from bus index sines, breaks exact ties. No random number generator is used.

Force Refinement

tellegen refines the seed with a Fruchterman–Reingold force pass on the unit square:

  • repulsion k^2 / d between each bus pair, with k = sqrt(1 / n);
  • attraction d^2 / k along branches;
  • capped displacement with a linearly cooled temperature.

The force pass is O(iterations * n^2). In the tellegen backend it runs once at boot and the resulting network payload is cached. In the browser, local dropped files use the same deterministic force idea with a lighter seed and iteration count, then scale into a small footprint around the user’s chosen map point. The fit into that footprint uses one uniform scale for both axes, so a long feeder keeps its aspect ratio instead of stretching to a square. Once placed, the local network solves in browser WebAssembly, and the layout is stamped into the network payload (Bus.location, provenance synthetic), so saved study packages and exports carry the placement and the panel can download it as a .geo.json layer.

Determinism

The pipeline uses source order, fixed jitter, and fixed iteration counts. Identical input files produce identical coordinates across boots.

Tests

Rust tests check that:

  • every coordinate lies inside the bounding box;
  • no two buses remain stacked in the test fixture.

References

  • T. M. J. Fruchterman and E. M. Reingold, “Graph drawing by force-directed placement,” Software: Practice and Experience, 1991.

Architecture

tellegen is a differentiable power flow and optimal power flow engine, written in Rust and compiled to both native targets and WebAssembly. The public browser framework packages are @tellegen/engine and @tellegen/svelte; the SvelteKit hosted demo is one private consumer of them.

Repository layout

A Cargo workspace and a web app, side by side.

  • crates/tellegen: the engine. It parses a case (through powerio) and solves any formulation, returning a formulation-agnostic result and analytical sensitivities.
  • crates/tellegen-wasm: the wasm-bindgen adapter that exposes the engine to the browser, built with wasm-pack.
  • crates/tellegen-server: a native HTTP server that serves the bundled cases and the static app.
  • crates/tellegen-cli: a command-line front end over the engine’s JSON API.
  • crates/benchmarks: a non-shipping harness that runs the PGLib-OPF corpus for validation and timing.
  • packages/engine: the public browser engine package, generated TypeScript contracts, and browser wasm transport.
  • packages/svelte: the public Svelte component package for maps, panels, local case files, and browser solves.
  • apps/web: the private SvelteKit hosted demo that consumes @tellegen/svelte.
  • examples/browser-minimal: a minimal downstream app that imports @tellegen/engine directly.
  • examples/svelte-minimal: a minimal downstream app that imports @tellegen/svelte.

powerio owns parsing and the network and display formats; the engine and the app depend on it.

The engine

crates/tellegen solves four formulations through one interface:

  • DC power flow and DC OPF: a B–θ linear/quadratic program;
  • AC power flow: a polar Newton solve; and
  • SOCWR: the Jabr second-order cone relaxation of AC OPF, in W-space.

Every formulation returns the same result shape (locational marginal prices, voltages, branch flows, and dispatch) and exposes analytical sensitivities of any output (an Operand) with respect to any input (a Parameter) through one implicit differentiation contract, Differentiable. Each solved formulation builds its KKT or Newton system; the sensitivity driver solves that system, forward or adjoint, for the requested columns. Adding a formulation, operand, or parameter means implementing the contract, not special-casing the callers.

The whole engine is pure Rust and compiles to WebAssembly, so the same code runs natively and in the browser. The convex solves use Clarabel; the sensitivities use faer. The full nonlinear AC OPF (an interior-point program) is on the desktop and mobile roadmap: it parallelizes across threads, which the browser does not have.

The two API faces

One numerical core, two faces that share a driver and a result type:

  • Stateless: solve_json(network, request) and capabilities_json(). Each call parses, solves, and returns. This is the face for one-shot callers: the HTTP server, the CLI, fixtures, and the initial case load.
  • Stateful: the Study. It builds the model once. commit applies a set of NetworkEdits and re-solves exactly, optionally returning the requested sensitivity columns in the same solve; preview returns a first-order update at the committed point with no re-solve. This is the reactive hot path: a demand drag previews and commits without re-parsing the network every frame.

Browser framework packages

packages/engine is the reusable package surface. It exports generated contracts, case and display parsing helpers, stateless solve calls, capabilities, the browser Study, and the browser wasm transport. It has no SvelteKit dependency.

packages/svelte consumes @tellegen/engine and exports the map, panels, local file flow, solve card, state provider, and full viewer as Svelte components.

apps/web consumes the Svelte package and keeps demo concerns: routes, SEO, credits, privacy, deployment, and bundled case pages.

In the browser

@tellegen/engine ships one wasm package built from crates/tellegen-wasm (the conic feature): DC power flow, DC OPF, AC power flow, SOCWR, the Study, and the sensitivity columns. A browser that cannot load it does not solve; the hosted demo shows a notice, and the server’s compute endpoints exist as an opt-in fallback (TELLEGEN_SERVER_COMPUTE).

The Svelte package and the hosted app use the same Study loop: a drag calls preview (a first-order LMP and objective update, in WebAssembly, no server round trip) and release calls commit (an exact re-solve that also returns the displayed sensitivity column). Every formulation solves in the browser; dropped-in case files solve there too and are never uploaded.

Sources

  • Rust to WebAssembly: wasm-bindgen, wasm-pack
  • Solvers and linear algebra: Clarabel.rs, faer
  • Convex relaxation: R. A. Jabr, “Radial distribution load flow using conic programming,” IEEE Transactions on Power Systems, 21(3), 2006.
  • Svelte: $state

Release Architecture

The public framework surfaces are @tellegen/engine and @tellegen/svelte.

Stable release surfaces:

  • crates/tellegen Rust API layer, including the serde request and response shapes in src/api.rs;
  • crates/tellegen-wasm wasm adapter;
  • packages/engine TypeScript package, generated TypeScript types, and browser wasm entry points;
  • packages/svelte Svelte component package; and
  • examples under examples/.

The hosted demo under apps/web is one consumer of the packages. It keeps routes, SEO, credits, privacy, and deployment specific behavior.

JavaScript Workspace

The repository uses npm workspaces with one root package-lock.json for:

  • packages/engine;
  • packages/svelte;
  • apps/web;
  • examples/browser-minimal; and
  • examples/svelte-minimal.

Install JavaScript dependencies from the repository root:

npm ci

Root scripts define the package order:

  • npm run wasm builds both wasm packages into packages/engine;
  • npm run build:engine builds @tellegen/engine;
  • npm run build:svelte builds @tellegen/svelte;
  • npm run build:example builds both downstream examples;
  • npm run check:web, npm run build:web, and npm run smoke:web gate the hosted demo;
  • npm run pack:engine previews the engine npm package contents; and
  • npm run pack:svelte previews the Svelte npm package contents.

Versioning

@tellegen/engine and @tellegen/svelte start at 0.1.0.

Before 1.0, releases can refine public APIs while preserving the examples and hosted demo behavior. After 1.0, breaking public TypeScript, Svelte prop, or Rust API changes require a semver major version.

Examples of breaking changes after 1.0:

  • removing or renaming public exports;
  • removing or renaming request or response fields;
  • changing enum tags, formulation ids, solve status tags, operand tags, or parameter tags;
  • changing field units or meanings;
  • tightening optional fields to required fields; and
  • changing serialized request or response shapes.

Nonbreaking changes can ship in a minor version:

  • adding optional fields;
  • adding formulations, operands, parameters, statuses, or helper exports while preserving existing meanings; and
  • adding component props with defaults.

Patch versions are for bug fixes and docs that do not change public APIs.

Package Release

The package publish workflow is .github/workflows/npm-publish.yml. Manual runs build wasm, build packages, run downstream import checks, run packed Svelte consumer smoke tests, run npm pack --dry-run, and upload packed .tgz artifacts for inspection.

Publishing is gated by tags named engine-vX.Y.Z for @tellegen/engine and svelte-vX.Y.Z for @tellegen/svelte. The workflow checks that each tag version matches its package metadata, then publishes with npm provenance.

Publishing requires either npm trusted publishing for this repository or an NPM_TOKEN secret with publish access.

CI Gates

CI installs JavaScript dependencies once from the root lockfile, builds packages/engine before packages/svelte, builds the hosted demo, builds both examples, installs the packed Svelte tarball into a temporary downstream consumer, and runs a browser test against the hosted demo shell. The root ci:js script covers JS checks, builds, the Svelte package dry run, hosted demo smoke, and downstream smoke tests.

Formulations

tellegen solves the DC power flow and DC OPF, the AC power flow, and the Jabr SOCWR relaxation through one interface, each driven from the engine’s public API. Every formulation returns the same result shape (locational marginal prices, voltages, branch flows, and generator dispatch) and exposes analytical sensitivities through the contract described in the sensitivity contract.

DC power flow and DC OPF (B–θ)

The linearized power flow couples bus angles $\theta$ to injections through the susceptance-weighted graph Laplacian

$$ B = A\operatorname{diag}(b)A^\top, \qquad B\theta = p, $$

where $A$ is the branch–bus incidence and $b$ the branch susceptances. The OPF minimizes generation cost subject to the network balance and the thermal and generation limits; it is a convex quadratic program solved with Clarabel. Entry point: solve_network (or solve_prebuilt over a prebuilt DcNetwork).

Generator costs are quadratic. MATPOWER model 2 polynomial rows are read directly; model 1 piecewise linear rows are projected onto a least squares quadratic (a least squares line when the quadratic fit is rejected), so objectives and prices for such cases are for the fitted curve, not the piecewise original.

AC power flow (polar)

The nodal power balance in polar coordinates,

$$ S_i = V_i \sum_j \overline{Y_{ij}} \overline{V_j}, $$

is solved by Newton–Raphson on the reduced system $\partial(P, Q)/\partial(\theta, V_m)$. Buses are typed slack / PV / PQ (PV and slack buses hold the generator voltage setpoint; PQ buses solve for both angle and magnitude), and the solve takes damped steps with a backtracking line search from the setpoint start plus a few perturbations, keeping the lowest-residual result. Entry point: ac_pf.

Conic SOCWR (Jabr)

The Jabr second-order cone relaxation lifts the voltage product to W-space variables $w_i = |V_i|^2$, $w^r_{ij} = \Re(V_i \overline{V_j})$, $w^i_{ij} = \Im(V_i \overline{V_j})$, with the rotated cone coupling

$$ (w^r_{ij})^2 + (w^i_{ij})^2 \le w_i w_j. $$

The relaxation is a convex lower bound on AC OPF, solved with Clarabel’s second-order cone support. Entry point: socwr_opf.

Every formulation is pure Rust and compiles to WebAssembly, so the same code runs on a server and in the browser. The full nonlinear AC OPF (an interior-point program) is on the roadmap; it runs natively, where it can use threads.

The sensitivity contract

Every sensitivity comes from a converged solution by the implicit function theorem on a residual $K(z, p) = 0$:

$$ \frac{dz}{dp} = -\left(\frac{\partial K}{\partial z}\right)^{-1} \frac{\partial K}{\partial p}. $$

Operand / Parameter vocabulary

A sensitivity is named for the physical quantities it relates, not a formulation’s internal variables. The operand (what the derivative is of) and parameter (what it is taken with respect to) are the orthogonal sub-axes (active/reactive, from/to, voltage representation):

  • Operand: Price, Dispatch, Flow { power, end }, Voltage(kind).
  • Parameter: Demand, Cost, LineLimit, SeriesAdmittance, ShuntAdmittance, VoltageBound, GenBound, Transformer, Switching.

Each formulation maps a request to its own KKT rows or reports the combination as unsupported.

The object-safe Differentiable trait

One trait exposes the pieces the shared driver needs: the system Jacobian $K$, the parameter column $\partial K/\partial p$, the operand selector $S$, and the per-formulation regularization. It is object safe by construction (&dyn Differentiable), so the DC KKT, the AC Newton system (AcNewton), and the conic KKT (ConicKkt) all plug into one driver.

Forward and adjoint

The single driver runs whichever direction is cheaper:

  • forward solves $K X = \partial K/\partial p$ once per parameter, reads the operand rows;
  • adjoint solves $K^\top Y = S^\top$ once per operand, contracts with $\partial K/\partial p$.

The two are algebraically identical; Mode::Auto picks the smaller dimension.

Per-cell parity classes

Finite differences validate the analytic columns per cell:

  • clean: cells routed through active power, relative error $< 10^{-3}$.
  • Jabr-coupled / soft: squared-voltage or reactive cells, looser (the cone’s degenerate directions).
  • norm-floor skip: columns below the regularization floor carry no resolvable derivative and are not compared.

See Validation for how these classes are checked, and Methodology for how the figures are produced.

Methodology

The benchmarks crate is a non-shipping workspace member that drives tellegen’s public API over the PGLib-OPF corpus for validation and timing. It never vendors the corpus and reads it from $PGLIB_OPF_PATH, skipping when the path is absent.

Corpus

PGLib-OPF v23.07 at $PGLIB_OPF_PATH: 66 base, 66 congested (api/), and 66 small-angle (sad/) MATPOWER files, spanning a range of bus counts at baseMVA 100. PGLib data is CC BY 4.0 (see References).

What is driven

Per (case, variant): parse (through powerio), build DcNetwork / AcNetwork, then

  • DC OPF: solve_prebuilt (objective, dispatch, LMPs);
  • conic SOCWR: socwr_opf (objective, gap, W-space primals);
  • AC power flow: ac_pf (convergence, residual);
  • sensitivities: the typed engines (AcNewton / ConicKkt with sensitivity) and the solve_json front door, which carries the DC sensitivities.

Timing

The engine exposes iteration traces and residuals rather than wall time, so the harness times the public calls itself, per stage (parse / build / solve / sensitivity). Solves run single-threaded for clean timing.

Metrics

  1. OPF correctness vs the published reference: the DC objective, the SOCWR relaxation lower bound, and the SOC gap, rolled up into a per-case reproduction verdict (see Validation).
  2. Sensitivity parity: adjoint equals forward, and central finite differences against the analytic columns, classified per parity class (see the sensitivity contract).
  3. Feasibility / convergence: per-case status, interior-point iterations, residuals.
  4. Performance: wall time per stage, and a scaling curve against bus count.
  5. Coverage: a per-case status table, including the size caps applied by the harness flags.

Baselines

Correctness is checked against two independent baselines:

  • the published PGLib reference solves (PowerModels.jl with IPOPT), tabulated per case and variant in $PGLIB_OPF_PATH/BASELINE.md; and
  • finite differences, which validate the analytic sensitivity columns by perturbing the public network fields.

Reproducibility

The solves are deterministic, so the figures reproduce on a fixed toolchain. The harness writes results.json (one record per (case, variant)) and results.csv. With its book flag it also writes the markdown snapshot to docs/src/benchmark-results.md. That file is generated by running the harness; it is not checked in, and the published figures are whatever the current run produces. To regenerate it:

cargo run -p benchmarks --release -- [flags]

Validation

tellegen is checked against the published PGLib reference solves. For each case and variant, $PGLIB_OPF_PATH/BASELINE.md tabulates the PowerModels.jl with IPOPT reference: DC ($/h), AC ($/h), QC Gap (%), and SOC Gap (%). The Methodology chapter describes how the harness produces the comparison; this chapter records what each comparison asserts. The measured figures are in benchmark results, a committed snapshot of a full harness run with its provenance.

DC objective

tellegen’s DC objective (constant cost term included) is compared against the published DC ($/h). The per-unit cost scaling cancels exactly, so the comparison is in dollars per hour directly.

Relaxation lower bound

The SOCWR objective (socwr_opf objective) must be a lower bound on the published AC optimum (AC ($/h)):

$$ \text{socwr} \le \text{AC} + \text{tol}. $$

A bound violation is a correctness failure.

SOC gap

The gap is

$$ \text{gap} = \frac{\text{AC} - \text{socwr}}{\text{AC}} \cdot 100, $$

compared against the baseline SOC Gap (%). tellegen’s SOCWR is the Jabr SOC relaxation, the same family as the baseline SOC column, so a near-zero difference in gap is the expected result. The published SOC bound is recovered as $\text{AC} \cdot (1 - \text{SOC gap}/100)$.

All three variants are exercised: typical, congested (API), and small-angle (SAD). tellegen’s AcNetwork carries the angle difference limits and the SOCWR enforces them in W-space, so the SAD relaxation tracks the published SAD SOC. Where a case shows a large SOC gap, that measures relaxation quality, not a failure.

Benchmark results

Generated by the benchmarks harness over PGLib-OPF v23.07. Regenerate with cargo run -p benchmarks --release -- --out <dir> and copy the snapshot here. The solves are deterministic; the numbers reproduce on the recorded toolchain.

Provenance

tellegenrustcclarabelfaerpowerioos/arch
178da991.96.0 (ac68faa20 2026-05-25)0.11.10.24.40.7.1macos/aarch64

PGLib v23.07 (arXiv:1908.02788, CC BY 4.0). Command: ./target/release/pglib-bench --out target/pglib-bench-20260720 --book.

Coverage summary

198 (case, variant) rows.

statuscount
caveat50
failed2
skipped3
solved143

Reproduction of PGLib

Corpus roll-up of the per-case marks in the table below. “Reproduced” is an objective match (DC within 1%) or, for the SOCWR relaxation, a valid lower bound whose gap matches the published SOC gap. “Consistent” is an acceptable non-exact agreement: a DC the baseline also reports infeasible, or a SOCWR lower bound with a looser gap. “Mismatch” is a differing objective or a lower-bound violation.

formulationreproducedconsistentmismatchwith baseline
DC OPF148450193
SOCWR (lower bound)18360189

OPF correctness vs PGLib BASELINE

tellegen’s objective against the published value, per formulation: the DC OPF ($/h, constant cost included) vs the published DC, and the SOCWR relaxation (a lower bound on AC) vs the published AC. Δgap is tellegen’s SOC gap minus the published SOC gap; near zero is the steelman result (same Jabr family). The reproduces column is DC·SOC: ✓ matches the published objective (DC within 1%), inf✓ infeasible consistent with the published inf., ✓lb a valid lower bound whose gap matches the published SOC, lb a valid bound with a looser gap, ✗ a mismatch (a converged but differing objective), — no baseline.

casevarbusesDC $/hPGLib DCSOCWR $/hPGLib ACΔgapreproduces
case3_lmbdapi310444.410444.010194.911242.0-0.01✓·✓lb
case3_lmbdsad35856.05856.05736.25959.3-0.01✓·✓lb
case3_lmbdtyp35695.95695.95736.25812.6-0.01✓·✓lb
case5_pjmapi578025.278025.077571.478950.0-0.00✓·✓lb
case5_pjmsad525164.926109.0-0.00inf✓·✓lb
case5_pjmtyp517479.917480.014999.717552.0-0.01✓·✓lb
case14_ieeeapi144797.64797.65691.85999.4-0.00✓·✓lb
case14_ieeesad142179.22776.8-0.01inf✓·✓lb
case14_ieeetyp142051.52051.52175.72178.1-0.00✓·✓lb
case24_ieee_rtsapi24148845.5148850.0149168.3161220.0-0.00✓·✓lb
case24_ieee_rtssad2478122.578122.069578.976918.0-0.01✓·✓lb
case24_ieee_rtstyp2461001.261001.063344.663352.0-0.01✓·✓lb
case30_asapi303092.13092.12767.84996.2-0.01✓·✓lb
case30_assad30825.9897.40.08inf✓·✓lb
case30_astyp30767.6767.6802.7803.1-0.00✓·✓lb
case30_ieeeapi3016145.116145.017057.818037.0-0.00✓·✓lb
case30_ieeesad307412.68208.5-0.00inf✓·✓lb
case30_ieeetyp307472.87472.86662.28208.5-0.00✓·✓lb
case39_epriapi39252754.6252750.0253130.4256770.0-0.00✓·✓lb
case39_eprisad39150669.9150670.0147360.5148340.0-0.01✓·✓lb
case39_eprityp39136889.7136890.0137654.1138420.0-0.01✓·✓lb
case57_ieeeapi5734081.134081.033271.636242.0-0.00✓·✓lb
case57_ieeesad5738391.438663.0-0.01inf✓·✓lb
case57_ieeetyp5734773.034773.037529.737589.0-0.00✓·✓lb
case60_capi60176378.6176380.0181189.2185000.0-0.01✓·✓lb
case60_csad60108485.3113500.00.05inf✓·✓lb
case60_ctyp6090700.190700.092637.092694.0-0.01✓·✓lb
case73_ieee_rtsapi73472183.2472180.0488422.7509850.0-0.01✓·✓lb
case73_ieee_rtssad73232679.1232680.0212280.3227600.00.00✓·✓lb
case73_ieee_rtstyp73183003.7183000.0189706.1189760.0-0.01✓·✓lb
case89_pegaseapi89118452.5118630.0113367.9129570.0-0.01✓·✓lb
case89_pegasesad89106491.2107290.00.01inf✓·✓lb
case89_pegasetyp89104919.0105040.0106480.8107290.00.00✓·✓lb
case118_ieeeapi118231291.9231290.0184303.2249610.0-0.01✓·✓lb
case118_ieeesad11896535.5105160.00.03inf✓·✓lb
case118_ieeetyp11893100.793101.096334.797214.0-0.01✓·✓lb
case162_ieee_dtcapi162111570.9111570.0115655.9120880.0-0.01✓·✓lb
case162_ieee_dtcsad162106287.8106290.0101655.6108690.0-0.01✓·✓lb
case162_ieee_dtctyp162101462.3101460.0101654.9108080.0-0.01✓·✓lb
case179_gocapi1791813209.81813200.01727618.51883400.00.01✓·✓lb
case179_gocsad179754005.8762530.0-0.00inf✓·✓lb
case179_goctyp179751881.6751880.0753089.1754270.0-0.00✓·✓lb
case197_snemapi19715689.715690.016203.016363.0-0.00✓·✓lb
case197_snemsad1971.51.50.03inf✓·✓lb
case197_snemtyp1971.51.51.51.50.02✓·✓lb
case200_activapi20040129.840130.040692.940700.0-0.00✓·✓lb
case200_activsad20027556.627558.0-0.01inf✓·✓lb
case200_activtyp20027479.627480.027556.627558.0-0.01✓·✓lb
case240_psercapi2404624856.04624800.04636792.94692200.00.00✓·✓lb
case240_psercsad2403237213.73405400.00.01inf✓·✓lb
case240_pserctyp2403271442.23271400.03237193.63329700.0-0.00✓·✓lb
case300_ieeeapi300659786.2659840.0679566.9686040.0-0.01✓·✓lb
case300_ieeesad300527243.9527290.0550597.4565700.00.06✓·✓lb
case300_ieeetyp300517802.2517850.0550393.8565220.0-0.01✓·✓lb
case500_gocapi500646871.6646870.0663185.9688290.0-0.00✓·✓lb
case500_gocsad500454907.9487400.0-0.00inf✓·✓lb
case500_goctyp500440548.6440550.0453838.5454950.0-0.01✓·✓lb
case588_sdetapi588392953.8392950.0387619.1398760.0-0.01✓·✓lb
case588_sdetsad588307081.1329360.00.09inf✓·✓lb
case588_sdettyp588310125.6310130.0306447.9313140.0-0.00✓·✓lb
case793_gocapi793372180.2372180.0321148.8379800.0-0.01✓·✓lb
case793_gocsad793263030.4285800.0-0.00inf✓·✓lb
case793_goctyp793258308.0258310.0256757.9260200.0-0.01✓·✓lb
case1354_pegaseapi13541558525.91558500.01578360.11608200.00.01✓·✓lb
case1354_pegasesad13541236022.11258800.00.24inf✓·✓lb
case1354_pegasetyp13541218182.91218200.01235968.21258800.00.24✓·✓lb
case1803_snemapi180362064.261723.046102.380240.01.70✓·lb
case1803_snemsad180391290.5106340.00.16inf✓·✓lb
case1803_snemtyp180387706.887696.090240.498335.00.20✓·✓lb
case1888_rteapi18881961569.91961600.02013300.12019700.0-0.00✓·✓lb
case1888_rtesad18881353179.31353200.01373847.51413900.00.01✓·✓lb
case1888_rtetyp18881352872.61352900.01373847.41402500.0-0.01✓·✓lb
case1951_rteapi19512411505.22411500.02478404.22490300.0-0.00✓·✓lb
case1951_rtesad19512082719.52092400.00.00inf✓·✓lb
case1951_rtetyp19512031629.32031600.02082711.62085600.0-0.00✓·✓lb
case2000_gocapi20001409993.71410000.01441231.61483900.00.01✓·✓lb
case2000_gocsad2000977751.3992880.00.00inf✓·✓lb
case2000_goctyp2000943042.6943040.0970446.9973430.0-0.00✓·✓lb
case2312_gocapi2312615216.3615220.0546946.8663440.00.03✓·✓lb
case2312_gocsad2312444178.0462350.00.00inf✓·✓lb
case2312_goctyp2312440328.5440330.0432953.7441330.0-0.00✓·✓lb
case2383wp_kapi2383279125.8279130.0279125.8279130.0-0.01✓·✓lb
case2383wp_ksad23831856608.11911200.0-0.00inf✓·✓lb
case2383wp_ktyp23831804090.41804100.01848908.71868200.0-0.01✓·✓lb
case2736sp_kapi2736977821.5977820.0939178.21017800.0-0.01✓·✓lb
case2736sp_ksad27361305707.81326600.0-0.01inf✓·✓lb
case2736sp_ktyp27361276033.71276000.01303994.51308000.0-0.00✓·✓lb
case2737sop_kapi2737755318.3755320.0733986.1788310.0-0.01✓·✓lb
case2737sop_ksad2737776126.6790950.0-0.01inf✓·✓lb
case2737sop_ktyp2737764009.8764010.0775689.9777730.0-0.01✓·✓lb
case2742_gocapi2742505399.5505400.0471741.7609960.0-0.01✓·✓lb
case2742_gocsad2742259696.6259700.0271971.8275710.0-0.00✓·✓lb
case2742_goctyp2742259696.6259700.0271999.6275710.0-0.00✓·✓lb
case2746wop_kapi2746531726.6531550.0511658.6550480.0-0.01✓·✓lb
case2746wop_ksad27461205170.11233700.0-0.01inf✓·✓lb
case2746wop_ktyp27461178363.81178200.01203861.31208300.0-0.00✓·✓lb
case2746wp_kapi2746581827.9581830.0581825.1581830.0-0.01✓·✓lb
case2746wp_ksad27461630588.41666900.0-0.01inf✓·✓lb
case2746wp_ktyp27461581425.11581400.01626493.51631700.0-0.01✓·✓lb
case2848_rteapi28481488985.31489000.01527378.91531100.0-0.01✓·✓lb
case2848_rtesad28481285682.11289000.00.01inf✓·✓lb
case2848_rtetyp28481267732.31267700.01285034.31286600.0-0.01✓·✓lb
case2853_sdetapi28532456065.62456100.02420923.42484300.00.01✓·✓lb
case2853_sdetsad28532033761.72069200.00.01inf✓·✓lb
case2853_sdettyp28532036958.32037000.02033582.32052400.00.01✓·✓lb
case2868_rteapi28682277460.82277500.02334871.02343900.00.01✓·✓lb
case2868_rtesad28682009425.42021300.0-0.00inf✓·✓lb
case2868_rtetyp28681966685.01966700.02007570.82009600.00.00✓·✓lb
case2869_pegaseapi28692966298.32966600.03026118.53063000.00.02✓·✓lb
case2869_pegasesad28692438499.12468700.00.10inf✓·✓lb
case2869_pegasetyp28692386115.82386400.02435400.72462800.00.10✓·✓lb
case3012wp_kapi3012851750.5851750.0728874.0914590.0-0.00✓·✓lb
case3012wp_ksad30122578747.02619500.0-0.00inf✓·✓lb
case3012wp_ktyp30122509001.52509000.02574297.72600800.0-0.01✓·✓lb
case3022_gocapi3022666194.7666190.0594789.6687360.00.02✓·✓lb
case3022_gocsad3022599221.5599220.0584763.7601430.00.00✓·✓lb
case3022_goctyp3022599221.5599220.0584742.8601380.0-0.00✓·✓lb
case3120sp_kapi31201326454.41326500.01178608.61403500.0-0.01✓·✓lb
case3120sp_ksad31202141758.02174900.00.00inf✓·✓lb
case3120sp_ktyp31202087974.92088000.02135975.32148000.0-0.00✓·✓lb
case3375wp_kapi33746272121.16272100.05865209.86364100.0-0.00✓·✓lb
case3375wp_ksad33747319637.37319600.07397595.47438200.0-0.00✓·✓lb
case3375wp_ktyp33747317011.97317000.07397405.57438200.0-0.00✓·✓lb
case3970_gocapi39701227776.41227800.01070893.91749400.0-0.00✓·✓lb
case3970_gocsad3970958294.5965550.0-0.01inf✓·✓lb
case3970_goctyp3970934219.4934220.0957739.2960990.0-0.00✓·✓lb
case4020_gocapi40201082422.11082400.01062324.81281700.0-0.00✓·✓lb
case4020_gocsad4020812671.5889690.0-0.00inf✓·✓lb
case4020_goctyp4020795061.9795060.0812181.5822250.0-0.01✓·✓lb
case4601_gocapi4601796409.8796410.0687104.5871240.0-0.01✓·✓lb
case4601_gocsad46011195553.71195500.0822282.0878180.0-0.00✓·✓lb
case4601_goctyp4601793813.9793810.0821853.5826240.0-0.01✓·✓lb
case4619_gocapi46191010784.91010800.0971704.91068800.0-0.01✓·✓lb
case4619_gocsad4619474687.4484350.0-0.01inf✓·✓lb
case4619_goctyp4619457436.6457440.0472392.3476700.0-0.01✓·✓lb
case4661_sdetapi46612670623.12670600.02623054.32731500.00.00✓·✓lb
case4661_sdetsad46612216647.82261000.00.00inf✓·✓lb
case4661_sdettyp46612216303.92216300.02206725.82251300.0-0.01✓·✓lb
case4837_gocapi48371209606.31209600.01200021.11291900.0-0.01✓·✓lb
case4837_gocsad4837869666.7877120.0-0.00inf✓·✓lb
case4837_goctyp4837850397.1850400.0868168.2872260.0-0.00✓·✓lb
case4917_gocapi49171703464.01703500.01434729.51717400.00.08✓·✓lb
case4917_gocsad49171384325.41384300.01354002.91389000.0-0.00✓·✓lb
case4917_goctyp49171383656.11383700.01353066.31387800.00.00✓·✓lb
case5658_epigridsapi56581306345.01306300.01318822.61326800.00.00✓·✓lb
case5658_epigridssad56581206615.61235800.00.00inf✓·✓lb
case5658_epigridstyp56581195466.51195500.01205418.31207300.0-0.00✓·✓lb
case6468_rteapi64682343290.42343300.02436575.42452700.00.01✓·✓lb
case6468_rtesad64681982819.81982800.02046409.52069700.00.01✓·✓lb
case6468_rtetyp64681982819.81982800.02046150.92069700.00.01✓·✓lb
case6470_rteapi64702604137.72604100.02685118.42720900.00.01✓·✓lb
case6470_rtesad64702139078.92139100.02198690.42241600.00.00✓·✓lb
case6470_rtetyp64702136096.92136100.02198106.42237600.00.01✓·✓lb
case6495_rteapi64952924495.62924500.03015652.03130400.00.01✓·✓lb
case6495_rtesad64952561805.32561800.02604047.93067800.00.01✓·✓lb
case6495_rtetyp64952561788.82561800.02604047.93067800.00.01✓·✓lb
case6515_rteapi65152959744.82959700.03057632.13129200.00.01✓·✓lb
case6515_rtesad65152559524.42559500.02644411.02869800.00.00✓·✓lb
case6515_rtetyp65152559330.72559300.02644404.02825500.00.01✓·✓lb
case7336_epigridsapi73361980097.51980100.01998086.32035500.0-0.00✓·✓lb
case7336_epigridssad73361878684.11888400.0-0.01inf✓·✓lb
case7336_epigridstyp73361855900.11855900.01878683.51882400.0-0.00✓·✓lb
case8387_pegaseapi83874974002.44975900.04637519.25242800.01.42✓·lb
case8387_pegasesad83872595938.72597700.0977279.02803900.05.43✓·lb
case8387_pegasetyp83872501101.02502800.0822867.92771400.06.20✓·lb
case9241_pegaseapi92416814526.06816300.06840605.97067900.00.27✓·✓lb
case9241_pegasesad92416084311.76318500.01.24inf✓·lb
case9241_pegasetyp92416027173.86028700.06075800.76243100.00.14✓·✓lb
case9591_gocapi95911470245.01470200.01346495.01570300.0-0.01✓·✓lb
case9591_gocsad95911055855.41167400.0-0.01inf✓·✓lb
case9591_goctyp95911030938.81030900.01055119.51061700.0-0.00✓·✓lb
case10000_gocapi100002499133.42499100.0✓·—
case10000_gocsad10000inf✓·—
case10000_goctyp100001346114.31346100.0✓·—
case10192_epigridsapi101891856478.61856500.01849684.21977700.0-0.01✓·✓lb
case10192_epigridssad101891673015.61720200.0-0.01inf✓·✓lb
case10192_epigridstyp101891665551.11665600.01672591.51686900.0-0.00✓·✓lb
case10480_gocapi104802712412.52712400.02709085.22863500.0-0.01✓·✓lb
case10480_gocsad104802286465.72314700.0-0.01inf✓·✓lb
case10480_goctyp104802215826.22215800.02286386.92314600.0-0.01✓·✓lb
case13659_pegaseapi136599117036.99126300.09198494.69385800.00.15✓·✓lb
case13659_pegasesad136598829708.19042200.00.67inf✓·lb
case13659_pegasetyp136598763111.48769900.08817544.38948000.00.07✓·✓lb
case19402_gocapi194022458064.02458100.02449028.32583700.0-0.01✓·✓lb
case19402_gocsad194021910224.61910200.01954400.71983800.0-0.01✓·✓lb
case19402_goctyp194021897794.81897800.01954308.11977800.0-0.00✓·✓lb
case20758_epigridsapi207583034846.93034800.03042940.73126500.0-0.01✓·✓lb
case20758_epigridssad207582612726.12638200.0-0.00inf✓·✓lb
case20758_epigridstyp207582572328.12572300.02608669.62618600.0-0.01✓·✓lb
case24464_gocapi244642560710.22684000.0-0.01—·✓lb
case24464_gocsad244642605377.12654000.0-0.01—·✓lb
case24464_goctyp244642512811.82512800.02604171.32629500.0-0.01✓·✓lb
case30000_gocapi300001700874.21700900.0✓·—
case30000_gocsad300001297528.41297500.0✓·—
case30000_goctyp300001092116.11092100.0✓·—

Sensitivity parity (finite differences)

Per size band. adj−fwd (worst) is the adjoint/forward solve-consistency bound — the analytic guarantee, near machine precision. FdClean and coupled are the central-difference relative errors per parity class (coupled = Jabr-cone soft cells), shown as median/worst. The median is the validation signal: the typical agreement in the smooth interior. The worst is an outlier where the two-sided difference straddles a non-smooth point — an LMP at a congestion boundary, where the sensitivity is genuinely one-sided and a central difference is not a valid check, or a soft Jabr-cone direction. These are finite-difference limitations, not analytic errors; the analytic columns are pinned by adjoint == forward and by the median. Columns below the regularization floor are skipped.

bandformulationcasescellsFD colsworst adj−fwdFdClean med/worstcoupled med/worst
100–1kac30512947.4898e-148.4965e-9 / 8.7898e-7
100–1kdc3022162.6468e-134.4498e-2 / 9.2257e-1
100–1ksocwr3030011271.6558e-72.6745e-3 / 5.1572e58.4129e-2 / 4.1153e2
1k–10kac36361.6549e-157.1479e-9 / 9.2028e1
1k–10kdc3218.6689e-218.0256e-5 / 8.0256e-5
1k–10ksocwr3301161.3965e-101.9303e-1 / 1.3502e01.7219e0 / 6.1760e0
<100ac33783291.1102e-151.4183e-8 / 2.6527e-6
<100dc3326481.6409e-104.0039e-6 / 1.1623e1
<100socwr333309999.8460e-74.9246e-5 / 3.4204e13.9313e-3 / 2.8670e2

Performance (wall time per stage)

Median milliseconds per stage, by size band. Solves are single-threaded.

bandcasesparsebuildDCSOCWRAC PFsens
<100330.080.040.744.730.22385.35
100–1k300.280.187.6465.459.606125.20
1k–10k1082.812.13165.681444.30507.970.00
>10k2417.7821.601297.5610187.043892.620.00

Limits and skips

Every capped, failed, or caveated case, with its reason (no silent truncation).

casevarbusesstatusnotes
case5_pjmsad5caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case14_ieeesad14caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case30_assad30caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case30_ieeesad30caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case57_ieeesad57caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case60_csad60caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case89_pegasesad89caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case118_ieeesad118caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case179_gocsad179caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 4.857e1 over 5 starts
case197_snemsad197caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case200_activsad200caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case240_psercsad240caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 7.232e1 over 5 starts
case500_gocsad500caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case588_sdetsad588caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case793_gocsad793caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case1354_pegasesad1354caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.
case1803_snemsad1803caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 1.519e0 over 5 starts; sensitivity skipped: buses 1803 exceed –max-sens-bus 1500
case1951_rtesad1951caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 5.113e1 over 5 starts; sensitivity skipped: buses 1951 exceed –max-sens-bus 1500
case2000_gocsad2000caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 1.828e0 over 5 starts; sensitivity skipped: buses 2000 exceed –max-sens-bus 1500
case2312_gocsad2312caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2312 exceed –max-sens-bus 1500
case2383wp_ksad2383caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2383 exceed –max-sens-bus 1500
case2736sp_ksad2736caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2736 exceed –max-sens-bus 1500
case2737sop_ksad2737caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2737 exceed –max-sens-bus 1500
case2746wop_ksad2746caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2746 exceed –max-sens-bus 1500
case2746wp_ksad2746caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2746 exceed –max-sens-bus 1500
case2848_rtesad2848caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 5.923e-1 over 5 starts; sensitivity skipped: buses 2848 exceed –max-sens-bus 1500
case2853_sdetsad2853caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 1.030e1 over 5 starts; sensitivity skipped: buses 2853 exceed –max-sens-bus 1500
case2868_rtesad2868caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 3.169e1 over 5 starts; sensitivity skipped: buses 2868 exceed –max-sens-bus 1500
case2869_pegasesad2869caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2869 exceed –max-sens-bus 1500
case3012wp_ksad3012caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 1.335e0 over 5 starts; sensitivity skipped: buses 3012 exceed –max-sens-bus 1500
case3120sp_ksad3120caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 3120 exceed –max-sens-bus 1500
case3970_gocsad3970caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 3970 exceed –max-sens-bus 1500
case4020_gocsad4020caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 2.905e0 over 5 starts; sensitivity skipped: buses 4020 exceed –max-sens-bus 1500
case4619_gocsad4619caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 4619 exceed –max-sens-bus 1500
case4661_sdetsad4661caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 6.734e-1 over 5 starts; sensitivity skipped: buses 4661 exceed –max-sens-bus 1500
case4837_gocsad4837caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 8.525e0 over 5 starts; sensitivity skipped: buses 4837 exceed –max-sens-bus 1500
case5658_epigridssad5658caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 5658 exceed –max-sens-bus 1500
case7336_epigridssad7336caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 7336 exceed –max-sens-bus 1500
case9241_pegasesad9241caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 6.289e1 over 5 starts; sensitivity skipped: buses 9241 exceed –max-sens-bus 1500
case9591_gocsad9591caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 7.825e0 over 5 starts; sensitivity skipped: buses 9591 exceed –max-sens-bus 1500
case10000_gocapi10000caveatSOCWR solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 5.306e0 over 5 starts; sensitivity skipped: buses 10000 exceed –max-sens-bus 1500
case10000_gocsad10000caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; SOCWR solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 1.452e1 over 5 starts; sensitivity skipped: buses 10000 exceed –max-sens-bus 1500
case10000_goctyp10000caveatSOCWR solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 1.452e1 over 5 starts; sensitivity skipped: buses 10000 exceed –max-sens-bus 1500
case10192_epigridssad10189caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; bus count 10189 differs from BASELINE nodes 10192; AC power flow did not converge: best mismatch 1.441e1 over 5 starts; sensitivity skipped: buses 10192 exceed –max-sens-bus 1500
case10480_gocsad10480caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 5.689e0 over 5 starts; sensitivity skipped: buses 10480 exceed –max-sens-bus 1500
case13659_pegasesad13659caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 2.786e1 over 5 starts; sensitivity skipped: buses 13659 exceed –max-sens-bus 1500
case20758_epigridssad20758caveatDC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 2.751e-1 over 5 starts; sensitivity skipped: buses 20758 exceed –max-sens-bus 1500
case24464_gocapi24464failedDC OPF solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 1.699e1 over 5 starts; sensitivity skipped: buses 24464 exceed –max-sens-bus 1500
case24464_gocsad24464failedDC OPF solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 2.075e1 over 5 starts; sensitivity skipped: buses 24464 exceed –max-sens-bus 1500
case30000_gocapi30000caveatSOCWR solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 3.598e0 over 5 starts; sensitivity skipped: buses 30000 exceed –max-sens-bus 1500
case30000_gocsad30000caveatSOCWR solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 1.198e1 over 5 starts; sensitivity skipped: buses 30000 exceed –max-sens-bus 1500
case30000_goctyp30000caveatSOCWR solve failed: DC OPF solve did not converge: NumericalError; AC power flow did not converge: best mismatch 1.198e1 over 5 starts; sensitivity skipped: buses 30000 exceed –max-sens-bus 1500
case78484_epigridsapi78484skippedtimed out after 180s
case78484_epigridssad78484skippedtimed out after 180s
case78484_epigridstyp78484skippedtimed out after 180s

198 rows; 55 skipped/failed/caveat.

References

PGLib-OPF

The benchmark corpus and the reference solves.

  • IEEE PES Power Grid Library — Optimal Power Flow, v23.07. https://github.com/power-grid-lib/pglib-opf
  • Archive report: S. Babaeinejadsarookolaee et al., The Power Grid Library for Benchmarking AC Optimal Power Flow Algorithms, arXiv:1908.02788.
  • License: the PGLib data is CC BY 4.0; the software is MIT. The corpus is read from $PGLIB_OPF_PATH and never vendored. Per-file header attribution is preserved for any case quoted in these docs.
  • Reference solves: $PGLIB_OPF_PATH/BASELINE.md (PowerModels.jl with IPOPT).

Formulation references

  • Jabr SOCWR: R. A. Jabr, Radial distribution load flow using conic programming, IEEE Transactions on Power Systems, 21(3), 2006. The paper gives the W-space second-order cone relaxation, also implemented in PowerModels.jl (BSD-3-Clause); the conic implementation in tellegen is independent.

Solvers and linear algebra

  • Clarabel.rs: the convex conic and quadratic program solver.
  • faer: the dense linear algebra used by the sensitivity driver.

HTTP API

The demo server’s surface. Data endpoints are always served; the compute endpoints ship disabled and answer 403 unless TELLEGEN_SERVER_COMPUTE=1 (see Deployment).

Data

  • GET /api/health — liveness and the served case ids.
  • GET /api/compute{"enabled": bool}, whether the compute endpoints are on.
  • GET /api/cases — case summaries.
  • GET /api/cases/{id}/case — the raw powerio network JSON the browser engine consumes.
  • GET /api/cases/{id}/network — the map view (buses, branches, coordinates).
  • GET /api/cases/{id}/solution — the cached base DC OPF solution, computed once at startup.

Compute

  • GET /api/cases/{id}/sensitivity/lmp/d/{bus} — the ∂LMP/∂demand column at a bus.
  • GET /api/cases/{id}/sensitivity/lmp/fmax/{branch} — the ∂LMP/∂rating column at a branch.
  • GET /api/cases/{id}/solve — a DC OPF solve streamed over server-sent events: status, solution, optional sensitivity, and done.

The sensitivity and solve endpoints accept ?d=bus:mw,bus:mw, each value a MW delta from the base case.

Limits

Solve work is bounded by TELLEGEN_SOLVER_CONCURRENCY (default 2) and TELLEGEN_SOLVER_TIMEOUT_SECS (default 30). Compute routes are rate limited per client: 5 solve and 25 sensitivity requests per 10 seconds by default, tuned with TELLEGEN_RATE_LIMIT_WINDOW_SECS, TELLEGEN_SOLVE_RATE_LIMIT_EVENTS, and TELLEGEN_SENSITIVITY_RATE_LIMIT_EVENTS.

Deployment

tellegen deploys as one container: the tellegen backend with the built tellegen frontend copied into the image. Production can run behind an existing Caddy edge proxy that owns ports 80 and 443.

Requirements

  • Docker Engine with the Compose plugin
  • 4 GB RAM minimum; 8 GB recommended for the bundled cases
  • external Docker network edge, owned by the Caddy edge stack
  • staged demo data under the deploy path’s data/ directory
  • GHCR pull access on the host, or a public ghcr.io/eigenergy/tellegen package

Shared Edge Layout

On a host with an existing reverse proxy, tellegen joins external Docker network edge under container name tellegen. The proxy should route the public hostname to tellegen:8000. The tellegen workflow does not mutate the proxy config; it only deploys the app container and ensures that container joins edge.

The staged case data should live under the deploy path, for example:

$TELLEGEN_DEPLOY_PATH/data

For the full demo geometry, stage these files:

ACTIVSg200/case_ACTIVSg200.m
ACTIVSg200/ACTIVSg200.aux
ACTIVSg500/case_ACTIVSg500.m
ACTIVSg500/ACTIVSg500.aux
ACTIVSg7000/Texas7k_20210804.m
ACTIVSg7000/Texas7k_lat_long.csv
CATS/CaliforniaTestSystem.m
CATS/CATS_buses.csv
CATS/CATS_lines.json

The server serves the staged subset. If no complete case pair is staged, the container exits unless TELLEGEN_ALLOW_FALLBACK=1 is set for a CI or local smoke check. Production deploys should stage the intended public case set before enabling the workflow.

CATS/CATS_gens.csv is also staged when available. It is source metadata for generator locations; the current map does not draw a separate generator layer.

Treat every staged case as public. The browser fetches the full staged network JSON through /api/cases/{id}/case so it can build browser studies and exact solves locally.

Server Compute

The compute endpoints (/api/cases/{id}/solve over SSE and the /api/cases/{id}/sensitivity/... routes) ship disabled and answer 403 server compute is disabled. Set TELLEGEN_SERVER_COMPUTE=1 to enable them as the fallback for browsers that cannot run the WebAssembly engine; the rate limits and solver concurrency caps then apply. /api/compute reports the gate ({"enabled": bool}) so the frontend picks honest fallback copy and skips requests that would 403. The data endpoints, including the cached base /solution, are always served.

Local Build Deploy

For a host that builds from source:

git clone <repo> /opt/tellegen
cd /opt/tellegen
scripts/stage-data.sh /path/to/datasets
docker compose -f docker-compose.yml -f deploy/docker-compose.edge.yml up -d --build

docker-compose.yml binds the service to 127.0.0.1:8000. The edge overlay also joins the app to Docker network edge under container name tellegen, so the shared proxy can route to it. Set TELLEGEN_ALLOW_FALLBACK=1 only for CI or local smoke checks that intentionally use the two pglib cases with synthetic coordinates.

Image Deploy

The production compose file consumes an image built by GitHub Actions:

TELLEGEN_IMAGE=ghcr.io/eigenergy/tellegen:<sha>
TELLEGEN_DATA_DIR=/opt/tellegen/data
docker compose -p tellegen --env-file .env \
  -f deploy/docker-compose.prod.yml \
  -f deploy/docker-compose.edge.yml up -d

The production compose file binds 127.0.0.1:8000, mounts staged data read only, runs with a read only root filesystem, drops Linux capabilities, blocks new privileges, caps process count, and sets the memory limit. The edge overlay adds the fixed container name and external edge network membership needed by the Caddy route. The fixed Compose project name matters because the shared Caddy edge stack is a separate project.

Use the host deploy script for normal deploys and rollbacks:

bash deploy/remote-deploy.sh ghcr.io/eigenergy/tellegen:<sha> "$TELLEGEN_DEPLOY_PATH/data"

The script validates Docker, Compose, the external edge network, that at least one case directory exists, and the compose config. It pulls the selected image before recreating the container, then waits for Docker health and /api/health. It does not use --remove-orphans; the shared edge proxy is owned by a separate stack.

GitHub Actions Deploy

.github/workflows/deploy.yml runs on push to main and workflow_dispatch, but every job is gated by repository variable:

TELLEGEN_DEPLOY_ENABLED=true

Leave that variable unset until the host has staged data, an edge network, and GHCR pull access. Once enabled, the workflow:

  1. runs the tellegen backend and tellegen frontend checks;
  2. builds the Docker image;
  3. starts the image in Actions and checks /api/health with fallback data;
  4. pushes ghcr.io/eigenergy/tellegen:<sha> and ghcr.io/eigenergy/tellegen:main;
  5. copies docker-compose.prod.yml, docker-compose.edge.yml, and remote-deploy.sh to the host;
  6. runs the host deploy script with the immutable SHA image;
  7. checks ${TELLEGEN_DEMO_URL}/api/health from Actions.

Required repository secrets:

  • TELLEGEN_DEPLOY_HOST
  • TELLEGEN_DEPLOY_USER
  • TELLEGEN_DEPLOY_SSH_KEY
  • TELLEGEN_DEPLOY_PATH, for example /opt/tellegen
  • TELLEGEN_DEMO_URL, for example https://tellegen.dev
  • TELLEGEN_DEPLOY_KNOWN_HOSTS: the pinned SSH host key for the deploy host, the verified output of ssh-keyscan -H <host> or an equivalent known hosts entry. The workflow refuses to deploy without this secret.

The workflow does not send the GitHub Actions token to the host. The host must already be able to pull the GHCR image, or the GHCR package must be public.

Preflight

Read only checks for a configured host:

ssh "$DEPLOY_HOST" docker network inspect edge
ssh "$DEPLOY_HOST" find "$TELLEGEN_DEPLOY_PATH/data" -maxdepth 2 -type f
ssh "$DEPLOY_HOST" curl -fsS http://127.0.0.1:8000/api/health

After a deploy:

ssh "$DEPLOY_HOST" docker inspect tellegen --format '{{if index .NetworkSettings.Networks "edge"}}edge{{end}}'
curl -fsS "${TELLEGEN_DEMO_URL%/}/api/health"

Both health checks should report status: "ok" and a nonempty cases array. For the full public demo, that array should contain case200, case500, case7000, and cats.

Reverse Proxy

deploy/Caddyfile is a sample route for a shared edge proxy. Tellegen CI does not mutate proxy config files.

The sample route sets HSTS, CSP, permissions policy, referrer policy, frame ancestor denial, and content type sniffing protection. The CSP allows the current SvelteKit bootstrap, WebAssembly compilation, MapLibre blob workers, and CARTO tile images.

A Caddy image with github.com/mholt/caddy-ratelimit is needed for the sample rate limits; stock Caddy does not include that module. The tellegen backend also enforces solve and sensitivity rate limits with the same defaults, so the container has a guard even when the proxy module is absent. Keep solve endpoints compatible with SSE: do not buffer responses, and do not use short write timeouts on /api/cases/*/solve.

Capacity

Staged cases are parsed at boot, and the base DC OPF solution is cached for each case. Browser WebAssembly handles the normal exact solve path; the tellegen backend recomputes fallback solves on demand. Read endpoints serve prebuilt case payloads.

Public Hardening

  • Keep the Caddy security headers and rate limits on solve and sensitivity endpoints.
  • Keep the backend rate limits enabled. Defaults are 5 solve requests and 25 sensitivity requests per client per 10 seconds. Set TELLEGEN_SOLVE_RATE_LIMIT_EVENTS=0 or TELLEGEN_SENSITIVITY_RATE_LIMIT_EVENTS=0 only for local debugging.
  • Keep the image running as the bundled unprivileged user and keep the compose read only filesystem, dropped capabilities, no new privileges, process cap, and memory cap.
  • Keep staged data mounted read only.
  • Stage only cases that are cleared for public distribution; the demo serves the full network JSON for each staged case.
  • Add request body limits before adding any tellegen backend upload endpoint.
  • Current file drop parsing runs in the browser and does not reach the tellegen backend.

Privacy

tellegen parses dropped case files in your browser. The current public demo does not upload those files to the tellegen backend.

Current Demo

  • Dropped .m, .raw, .aux, .pwd, .csv, .json, and .geojson files stay on your device.
  • The browser uses local file contents to draw the map and run DC solves.
  • The tellegen backend receives ordinary page and API requests for the bundled demo cases.
  • There is no analytics product wired to uploaded case contents.

Future Opt In Sharing

If tellegen adds a sharing feature, it will be explicit. The action will say what will be sent and will require a separate confirmation before upload.

Planned defaults for shared files:

  • Raw shared files will be retained for 180 days.
  • Derived aggregate statistics may be retained without a fixed end date.
  • Each upload will return a share id that can be used to request deletion.
  • Shared files will not be sold or used for third party advertising.

The public demo is intended to run on Hetzner infrastructure. Before accepting uploaded user files, the operator will conclude Hetzner’s Data Processing Agreement and publish controller contact details.

Third Party Notices

This page records attribution sources for the repository and public demo. It does not replace the license metadata in Cargo.lock, package-lock.json, or the source distributions for each dependency.

Project Code

The project, including the Rust crates and the npm packages @tellegen/engine and @tellegen/svelte, is MIT licensed. See LICENSE.

The engine uses the W space SOCWR formulation as implemented in PowerModels.jl as a formulation reference. PowerModels.jl is BSD 3-Clause licensed. The tellegen implementation is independent.

Direct Software Dependencies

Rust dependencies are resolved by Cargo and governed by deny.toml. Direct runtime dependencies include powerio, clarabel, faer, num-complex, serde, serde_json, wasm-bindgen, console_error_panic_hook, axum, tokio, tokio-stream, tower-http, tracing, and tracing-subscriber.

The web app dependencies are resolved by npm. Direct dependencies include SvelteKit, Svelte, Vite, TypeScript, deck.gl, MapLibre GL JS, Prettier, @fontsource-variable/bricolage-grotesque, and @fontsource/ibm-plex-mono.

The repository does not modify or vendor those dependency sources. License and notice files from dependencies are distributed through their normal package archives.

Demo Case Data

The demo data is staged by the operator and is not vendored in this repository.

ACTIVSg synthetic grids come from the Texas A&M Electric Grid Test Case Repository. The case file headers request this citation:

A. B. Birchfield, T. Xu, K. M. Gegner, K. S. Shetye, and T. J. Overbye, “Grid Structural Characteristics as Validation Criteria for Synthetic Networks,” IEEE Transactions on Power Systems, 2017.

CATS comes from the WISPO POP CATS California Test System repository and is BSD 3 Clause licensed in that repository. The source repository requests this citation:

S. Taylor, A. Rangarajan, N. Rhodes, J. Snodgrass, B. C. Lesieutre, and L. A. Roald, “California Test System (CATS): A Geographically Accurate Test System Based on the California Grid,” IEEE Transactions on Energy Markets, Policy and Regulation, vol. 2, no. 1, pp. 107-118, 2024. doi:10.1109/TEMPR.2023.3338568.

Embedded fallback cases are from PGLib OPF and are used only when TELLEGEN_ALLOW_FALLBACK=1 is set. PGLib OPF data is CC BY 4.0; the PGLib software is MIT licensed.

Maps

The web app uses CARTO map tiles with OpenStreetMap attribution. Keep the map attribution visible in any hosted demo or derivative deployment.

Desktop and mobile (planned)

None of this is built yet. This page records the plan for native desktop and mobile builds.

tellegen runs in the browser today. A native build adds three things the browser cannot:

  • Cross-platform reach. Tauri 2 builds desktop (macOS, Windows, Linux) and mobile (iOS, Android) from one Rust and web codebase. Phones and tablets get an installable app, not a tab.
  • The full nonlinear AC OPF. The browser solves DC OPF, AC power flow, and the SOCWR relaxation. The interior point AC OPF parallelizes across threads, which the browser does not have, so it runs natively, on the device, with no server.
  • Offline and local. Cases stay on the device and solve in process.

The pieces already fit. The SvelteKit app is a static single-page app and the engine is a Rust crate, so the native build reuses both:

  • apps/desktop: the Tauri shell. It loads the same apps/web build.
  • crates/tellegen-tauri: a workspace member that holds a native Study and exposes it through #[tauri::command]. Built natively, it adds the interior point AC OPF backend.

One UI, three transports: the in-browser Study, the HTTP server, and the Tauri bridge, chosen at runtime. The Study contract is the same across all three; only the transport differs.