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:
| case | territory | buses | branches |
|---|---|---|---|
| ACTIVSg200 | central Illinois | 200 | 245 |
| ACTIVSg500 | South Carolina | 500 | 597 |
| ACTIVSg7000 | Texas | 6717 | 9140 |
| CATS | California | 8870 | 10823 |
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/healthGET /api/casesGET /api/cases/{id}/caseGET /api/cases/{id}/networkGET /api/cases/{id}/solutionGET /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.rawon 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
- Rust to wasm toolchain: wasm-bindgen, wasm-pack (both active in 2026)
- wasm features and browser support: caniuse wasm-simd, wasm-threads, COOP/COEP
- In-browser solving: Clarabel.rs (wasm support, issue #133), faer (wasm sparse caveat)
- Rendering: deck.gl performance, deck.gl WebGPU status
- Svelte: $state, packaging
- Julia to wasm status: julia-wasm (dormant), WasmTarget.jl (early)
- Landscape: GridSuite, GridStatus, Electrisim, PowerPlots.jl
Framework Quickstart
There are two integration references:
examples/svelte-minimalimports@tellegen/svelteand renders the full viewer for local files only.examples/browser-minimalimports@tellegen/enginedirectly and builds its own simple UI.
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} />
The viewer accepts at most 32 files in one ingestion batch. No individual file and no batch total may exceed 128 MiB.
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 bytes = new TextEncoder().encode(caseText);
const parsed = await ingestCase(bytes, format);
const study = await createStudy(parsed.module_json, "dcopf");
try {
const preview = await study.preview({ 3: 25 });
const committed = await study.commit(parsed.name, { 3: 25 }, {}, { bus: 3 });
console.log(preview.objectiveDelta, committed.sensitivity);
} finally {
study.free();
}
The call sequence is:
- detect the case format;
- parse the case in browser WebAssembly;
- create a
Study; - preview a demand edit without a solve;
- commit the edit with a sensitivity request; and
- free the
Study.
JSON Drops
Use ingestJsonDrop when a .json file may be a PowerIO module, model JSON, or a
transmission or distribution document:
import { ingestJsonDrop } from "@tellegen/engine";
const bytes = new Uint8Array(await file.arrayBuffer());
const result = await ingestJsonDrop(bytes);
if (result.kind === "unknown" || result.kind === "ambiguous") {
console.log("JSON was not ingested", result.kind);
} else {
console.log(result.kind, result.format, result.payload);
}
The result is discriminated by kind; payload is null only for unknown
and ambiguous input.
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/apiloadDefaultCases, defaulttruedocsHreforgHreforgLabelshowFooter, defaulttrue
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:
TellegenMapAppState,CaseState,LocalCase, andcreateAppStateControllerandcreateControllercreateApiClient- 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, solveModule } from "@tellegen/engine";
ingestCase and solvable JSON ingestion return a retained PowerIO module in
module_json. Both createStudy and solveModule accept that generation-2
PowerIO IR module. Geographic transforms update and return the same module.
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-minimalimports@tellegen/svelteand runs withloadDefaultCases={false}for local files only.examples/browser-minimalimports@tellegen/enginedirectly and has no map stack.
Downstream apps should not import from apps/web/src/lib or from generated wasm
folders.
Saved Studies
A Study is a saved case with its changes and results. Open Studies, give the current case a name, and select Save study. A planning goal is optional. Saving keeps the current result when it matches the case; an unsolved case can also be saved. Saving does not move the network or reset the camera.
Case, History, and Plan
- Case shows the selected saved state. Demand rows show base demand, current demand, and the accumulated change in MW. Edits at different buses remain part of one cumulative set of changes.
- History holds saved states and activity. Select a state to inspect it or branch from it to explore another option. Observations, edits, solves, planning trials, and decisions have distinct labels. An observation adds evidence without inventing an electrical state.
- Plan is optional. Choose a goal, then explicitly choose the whole network, an area, or selected equipment. Searchable tables show bus names and IDs, line endpoints, weights, limits, and units. The displayed counts include every selected element; there are no hidden bus or line limits.
Reset to base case restores the original electrical inputs and retains previous states and activity. The base is the network data supplied to the Study, which can differ from its starting state when demand was already edited. Older imported Studies without a base input report that reset is unavailable.
The saved state currently being inspected, the recommended candidate, and the applied state remain distinct. Selecting a candidate changes the displayed network. An agent proposal requires an explicit Apply action. Live case returns to the case outside the Study.
Planning
The OPF objective describes operating cost inside the power-system calculation. A planning goal describes what to improve across candidate cases, such as a weighted LMP or voltage target. Custom expressions remain available through the structured API: weighted observables, sums, scaling, squared target deviations, and direct intervention penalties.
Line capacity uses MW for DC OPF and MVA for SOCWR. Demand uses MW. Limits and increments apply to the cumulative changes from the goal’s starting state. Demand placement allocates a stated additional total; redistribution preserves the total through paired transfers.
Find a proposal uses the objective gradient to choose candidate edits, solves each candidate, and retains verified improvements. Every attempted solve counts against Solve budget, including failed trials and any required starting-point solve. The result is the best verified candidate found within that budget. It does not establish a global optimum. Expand the evidence for prediction error, active-constraint changes, failed trials, and numerical settings.
The derivative uses a combined adjoint calculation, including direct penalties, without constructing a dense observable-by-decision matrix. Changing the goal creates a new revision and retains earlier results. Changing a proposal’s goal or starting state invalidates its approval.
Save, reopen, and continue
The browser stores completed operations atomically in IndexedDB. Export creates a portable bundle containing PowerIO generation-2 inputs and solutions, deduplicated SHA-256 artifacts, goals, states, and activity. Geographic layers, line paths, drawings, and the selected view travel with it. Import checks versions, hashes, identities, and references. It never restores approval tokens or executes imported text. Older journals remain historical evidence when their electrical states are unavailable.
A failed save reports the problem instead of discarding history. Free storage or export the saved Study before retrying. Cancellation retains completed trials and the best candidate after the running solve finishes.
Native and agent access
cargo build -p tellegen-cli --features conic
tellegen describe
tellegen study create study.json < create-request.json
tellegen study inspect study.json
tellegen study run study.json < operation-request.json
tellegen study export study.json > portable-study.json
tellegen describe lists commands and generated JSON schemas. Rust definitions
also generate the TypeScript types. Browser controls, WebMCP, and the CLI use
the same Study operations and revision checks.
PowerMCP’s Tellegen server (powermcp run tellegen) invokes the native CLI.
Its solve, solve_module, and plan tools handle calculations;
study_create, study_inspect, study_run, study_export, and study_import
handle saved cases. study_create also accepts a grid file for PowerIO to read.
The server provides read-only capability descriptions and leaves applying a
proposal to an explicit user action: study_run refuses apply.
WebMCP list_cases and select_case change the visible case without proposal
approval. Network queries identify the displayed case and saved state, so an
LMP question refers to the same result shown on screen. Creating a Study is
needed only when saving or planning is requested.
Supported calculations are DC OPF, AC power flow, and, with the conic build
option, SOCWR. Unsupported objective and solver combinations are rejected
before planning.
Model details
Texas7k uses an explicitly configured convex quadratic or linear fit to its piecewise generator costs. Model details records the method, original and prepared costs, affected generators, units, and measured breakpoint errors. Those errors describe an approximation; they are not a relaxation bound. The prepared costs persist through saved cases, exports, and reset.
The same preparation is available for any balanced case through
tellegen prepare-model. The command returns prepared PowerIO IR and its
approximation report. Other cases retain their declared costs by default.
WebMCP
Open Agent, then Connect, on the map for connection status, a copyable example request and the connection guide. A connected agent must discover the WebMCP tools exposed by this browser tab. A regular browser without WebMCP still supports manual interaction. The first-visit introduction links to the application changelog.
Tellegen exposes the solved case in the current browser tab through structured tools. They read and update the state shown in the interface and run the same WebAssembly solver.
Browsers without document.modelContext run the application without these
tools.
Tools
| Tool | Behavior |
|---|---|
list_cases | List configured and imported cases, availability, and stable case IDs. |
select_case | Switch the displayed case without deleting saved Studies or applying proposals. |
inspect_case | Read the displayed case or saved state, formulation, solve summary, IDs, units, and revision. |
query_network | Return a bounded set of buses or branches by stable identity or solved metric. |
analyze_sensitivity | Compute an LMP sensitivity column for the current DC OPF solution. |
focus_network | Select a bus or branch in the visible interface. |
preview_case_update | Predict demand or rating edits from cloned state. |
update_case | Solve and commit a bounded batch of demand or rating edits. |
reset_case | Solve and commit the base state with the current formulation. |
propose_capacity_plan | Use implicit derivatives to choose bounded capacity increase trials, verify them with exact solves, and stage an unapplied proposal. |
apply_capacity_plan | Apply an approved proposal after checking its session, case, and revision. |
The general tools register for the page lifecycle. list_cases and select_case
also cover local files and distribution cases. Distribution queries return connected
load and equipment data; they do not claim computed LMPs or flows. Inspection and
queries use the saved state when one is displayed. Live edit tools require
returning to the live case through select_case.
propose_capacity_plan registers when the active formulation supports it.
apply_capacity_plan registers only while a staged proposal matches the current
state.
Revisions and mutations
Every page load receives a random session identifier. Each case has a monotonic revision within that session. A committed edit, reset, formulation change, or visible network selection advances the revision and expires pending proposals and approvals.
Preview, standalone sensitivity analysis, and planning operations use cloned state. Electrical mutations enter one queue, check their expected revision inside the queue, solve a candidate state, and commit once after the solve succeeds. A failed solve leaves the case, formulation, revision, proposal, and approval unchanged.
Tool callback options are optional because WebMCP clients do not all provide
an AbortSignal. When a caller supplies one, reads and planning stop at safe
boundaries. The browser reports dynamic registration failures instead of
claiming that a missing tool group registered successfully.
Capacity planning
propose_capacity_plan accepts a weighted LMP objective, stable candidate
branch identities, a total MW budget, a maximum increase per branch, a fixed
increment, a global line count, and an exact solve budget. The solve budget
counts the baseline and every accepted or rejected trial.
The planner computes a vector product through the solved DC OPF KKT system, orders capacity increases by that local direction, and checks each trial with an exact solve. It recomputes the direction after every accepted trial. Each solved trial records its first order prediction and exact result; a failed trial records the failure reason without an exact delta.
The proposal changes no electrical state. A visible control grants one use approval for its proposal ID and base revision. Applying it repeats every identity and revision check inside the mutation queue. A failed or stale application consumes neither the proposal nor its approval.
Persistence
The activity panel records the last 100 completed tool calls in an experiment journal, including validated requests, bounded results, failures, and elapsed time. Export journal downloads that record with the retained capacity planning trials and their decisions. The export identifies its browser session and reports how many older records were discarded. Invalid requests are omitted from the recorded input.
When a preview and a successful update name the same case, revision, formulation, and edits, the panel shows the predicted objective change beside the exact change and absolute error. A failed update leaves the comparison empty. The relative error is undefined when the exact change is zero.
Journal exports contain data only; importing one never authorizes or executes
an edit. Proposals and approvals stay in memory. Saving a case writes
its materialized PowerIO module. Loading that module starts a fresh runtime
Study; its history records transformations applied to the stored value.
Saving an exact result writes a PowerIO solution module containing the
amended instance that was solved.
Package boundary
@tellegen/webmcp owns tool descriptions, JSON Schemas, runtime validation,
annotations, bounded result shaping, and registration lifecycle. It depends on
a TellegenWebMcpAdapter, not on Svelte or the Rust engine. The application
adapter connects that interface to the active controller and visible activity
panel.
Imported labels and identifiers are treated as untrusted content. Every mutation requires the active case ID and revision. The package rejects unknown fields, nonfinite values, duplicate edits, excessive arrays, and unsupported enum values at the execute boundary as well as in its schemas.
Testing
Run the package and application checks from the repository root:
npm run check:webmcp
npm run check:web
npm run build:web
npm run test:browser
The tests cover registration and teardown, calls with and without callback options, validation, output bounds, stale revisions, one use approval, transaction rollback, queued mutations, cancellation, and visible state. Playwright uses a test host with the WebMCP API shape. Before release, invoke the tools through native WebMCP in the in-app browser as a separate bridge check.
References
Tellegen product showcase
Tellegen is a power-system laboratory shared by a user and an agent. The network stays central while a persistent Study records the goal, alternatives explored, exact results and recommendation. OPF supplies the operating point; an outer Study objective describes what the user wants to improve.
For example:
Lower demand-weighted prices in this region. Add at most 20 MW across two lines, and show how prices elsewhere change.
The goal interpretation resolves the region to equipment identities and weights. The user can inspect and edit that interpretation before exploration. A combined implicit derivative ranks feasible interventions, and exact solves determine whether a trial improves the objective. The Study preserves accepted, rejected and failed trials, constraint changes and the reason exploration stopped. Prediction error belongs in the expandable evidence beside those trials.
One Study across interfaces
The browser controls and WebMCP call the same Study controller. Both can create a
Study, inspect its history, revise a goal, branch from a saved state, compare
candidates, propose interventions and attach evidence. Capacity-tool compatibility
adapters create the same persistent Studies. Native Rust and CLI operations use
the same generated contracts; PowerMCP’s tellegen server invokes the CLI
directly and exchanges PowerIO IR with it.
Three pointers keep navigation clear: the inspected state, the recommended state and the applied state. Viewing an alternative does not apply it. Human approval binds a particular proposal, starting state and goal. A revised goal or changed proposal expires that approval.
Electrical inputs, instances and solutions use PowerIO generation-2 IR. Study semantics live in a separate document with hashed, deduplicated artifacts. Browser IndexedDB and atomic filesystem storage preserve completed operations. A portable bundle can move from a browser to a headless agent and back. Import validates identities, hashes and references, and restores no approvals.
Showcase sequence
- Open a congested case and inspect dispatch, prices and limiting branches.
- State the regional objective, resolve its weights and review the intervention budget.
- Ask for a proposal. Inspect exact improvements and consequences elsewhere.
- Select a rejected candidate in the branching history to explain the explored alternative.
- Revise the goal to conserve total demand and redistribute it among selected buses.
- Compare candidates under the chosen goal revision. Expand a trial’s numerical evidence.
- Apply the reviewed recommendation through the explicit user control.
- Export, reload and resume the Study with another agent.
A small AC power-flow example instead minimizes squared voltage-target error through demand transfers. This uses the existing AC power-flow solver. Nonlinear AC OPF and multiconductor solving are outside Tellegen’s supported calculations.
Reproducible evidence
Studies describes the document and operations. The checked-in Study declarations and runner record executable and input hashes, exact trial outcomes, solve budgets and numerical tolerances. Browser tests run the same declared Study through native and WASM implementations and compare semantic results.
The Texas7k example explicitly records a lower-convex-cost scenario because the original input contains nonconvex piecewise cost curves. Its results describe that scenario, not an optimum for the original economic model.
The earlier capacity demonstration retains its own call records, revisions and hashes. Fresh native WebMCP evidence accompanies the persistent Study release. The showcase can be reproduced independently of a contest submission.
Verified examples
Fresh result records include the declared tolerances and all attempted trials. The values below are the outer objectives specified by each declaration; capacity and demand examples sum selected nodal prices, while the AC example measures squared voltage error.
| Study | Starting objective | Recommended objective | Planning solves |
|---|---|---|---|
| CATS capacity | 76.11900127 | 53.61631934 | 6 |
| CATS demand redistribution | 76.11900127 | 74.25936675 | 5 |
| Texas7k convex-cost scenario, capacity | 87.08117943 | 85.74773920 | 8 |
| Texas7k convex-cost scenario, redistribution | 87.08117943 | 87.08117943 | 3 |
| Three-bus AC voltage target | 0.00043750235 | 0.00040678296 | 6 |
Each Study also has one creation solve. The Texas7k redistribution search retains its starting state: the tested changes are below its recorded improvement tolerance. The planner reports that limit instead of presenting numerical noise as a gain. None of these searches applies its recommendation automatically.
The native WebMCP demonstration records all seven Study tools, reload, branching, goal revision and a rejected stale request. Application and stale approvals have separate browser tests. On its synthetic three-bus case, a 5 MW capacity increase lowers the target bus price but raises the price at another bus. A second goal explores demand transfers from the original starting state. The inspected capacity choice, demand recommendation and applied starting point remain distinct.

References
Challenge Evidence
The checked in harness under evidence/webmcp produces the large case records.
CATS and Texas7k each have one complete invocation spec. The runner parses the
named MATPOWER source through the PowerIO revision in Cargo.lock, invokes
tellegen plan, checks its solve accounting, and writes one machine readable
result.
Each result records:
- the SHA256 digests of the MATPOWER source, invocation spec, and lockfile;
- the exact Tellegen commit and tree and the reviewed PowerIO commit;
- the complete
CapacityPlanSpecrequest; - for success, the exact baseline and proposed summaries, every accepted and
rejected trial, plus the kind, termination, objective, and canonical JSON
digest of the final generation-2
pio-irsolution on the amended instance.
A spec can name one enumerated preparation failure. A matching clean run records a typed error, its preparation stage, and zero completed exact solves. Any other failure exits nonzero and writes nothing.
Run from a clean repository checkout:
node evidence/webmcp/run.mjs evidence/webmcp/specs/cats.json evidence/webmcp/results/cats.json
node evidence/webmcp/run.mjs evidence/webmcp/specs/texas7k.json evidence/webmcp/results/texas7k.json
The runner refuses tracked or untracked changes other than earlier generated
JSON files in results/, and it refuses to replace an existing result.
--allow-dirty is only a harness smoke test; its output always says
reproducible: false and is not submission evidence. A clean run can write
only the named result path for its case.
During review, the lock resolves every PowerIO component from one Git commit.
After publication, it records the common version and each registry checksum;
evidence/webmcp/powerio-releases.json then supplies the checked release tag to
commit mapping. Both paths retain powerio_revision in the result.
Publish a numerical case claim only when its result file comes from a clean run
and validates against evidence/webmcp/result.schema.json.
Native browser record
evidence/webmcp/native/in-app-browser.json records the native WebMCP calls,
transaction checks, browser session change, and artifact hashes. Its screenshots
show the pending and applied proposal states. video-run.json records the calls
shown in the native browser video, ending with a second proposal expired by a
committed edit. The manifests name the exact Tellegen tree and PowerIO commit.
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: thecrates/tellegen/src/api.rshash used to generate the TypeScript contracts.FORMULATION_IDSandSOLVE_STATUSES: generated enum tags from the Rust API layer.FORMULATIONSandDEFAULT_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:
classifyJson(bytes)ingestJsonDrop(bytes)ingestCase(bytes, format)parseDisplay(bytes)capabilities()solveModule(moduleJson, request)createStudy(moduleJson, formulation)
Case And Display Helpers
formatOf(name): returns the powerio format token for a supported case name.isDisplayFile(name): returns true for PowerWorld.pwddisplay files.classifyJson(bytes): asynchronously classifies JSON bytes and returns{ kind, format }.ingestJsonDrop(bytes): classifies and parses a JSON drop in one call.ingestCase(bytes, format): parses case bytes and returns a retained PowerIO module plus derived display data, summary, and topology.parseDisplay(bytes): parses PowerWorld display data for diagram overlays.
ingestJsonDrop returns a discriminated IngestedJsonDrop union:
kind | format | payload |
|---|---|---|
module | null | balanced or multiconductor payload |
transmission | string | IngestedCase |
distribution | string | IngestedDistCase |
ambiguous or unknown | null | null |
Use the kind discriminant before reading payload. format carries the
reader selected for transmission and distribution documents.
Solves And Studies
capabilities(): returns available formulations, operands, and parameters.solveModule(moduleJson, request): stateless solve from a PowerIO module.createStudy(moduleJson, formulation): builds a browserStudyfrom a PowerIO module and solves its declared problem instance.Study/BrowserStudy: browser handle with:currentSolution()preview(deltas, rates?)commit(caseId, deltas, rates, target)sensitivity(caseId, deltas, rates, target)saveModule()saveSolutionModule()for an exact DC OPF resultplan(spec, signal?)for a read only, bounded capacity planning run on a disposable Study cloneexport(format)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 a PowerIO row uid string when the source carries one.
ingestCase returns null for topology and view UIDs when the source has none;
solve responses omit absent UIDs.
Three-winding transformers remain typed in the retained module, while topology and
view payloads include their lowered star rows so the rendered graph matches the
solver. Those display-only rows have editable: false; persist edits only on
canonical rows. Closed transmission switches, in-service storage, and
in-service HVDC links are rejected until their solver models are implemented.
n_bus and n_branch count canonical typed rows;
n_analysis_bus and n_analysis_branch count the lowered topology rows. The
analysis counts are optional in TypeScript so clients remain compatible with
older engine builds.
target is { bus } for the LMP/demand 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.
CapacityPlanSpecJson accepts a weighted LMP objective, canonical
candidate branch identities, MW bounds and increments, a final line count,
and an exact solve budget. BrowserStudy.plan materializes the committed
PowerIO module onto a disposable host, so the returned
CapacityPlanOutcomeJson is an unapplied proposal and cancellation does not
invalidate the retained interactive Study.
Migrating To 0.2
ingestCasenow takesUint8Array, not a string. Encode an in-memory string withnew TextEncoder().encode(text); usenew Uint8Array(await file.arrayBuffer())for a browserFile.classifyJsonnow takes bytes, is asynchronous, and returns{ kind, format }:const { kind, format } = await classifyJson(bytes).@tellegen/svelteno longer exportsisStudyPackageText. For classification only, check(await classifyJson(bytes)).kind === "module". To identify the module value family and parse the drop, useawait ingestJsonDrop(bytes).JsonDropKindnow usestransmissionanddistributionwith a separateformat, plusambiguousandunknown. The formerbmopfandpmdkinds are distribution formats;not-jsonis nowunknown.
Types
Public types include:
SolveRequest,SolveResponse,ProblemCapsSensRequest,SensitivityMatrix,SensitivityColumnNetwork,NetworkBus,NetworkBranchSolution,SolveIteration,DemandDeltasImplicitObjectiveJson,CapacityPlanSpecJson,CapacityPlanOutcomeJsonIngestedJsonDrop,JsonDropClassification,JsonDropKindBrowserFormulation,FormulationId,SolveStatus
The generated file is committed at packages/engine/src/generated/contracts.ts and checked in CI.
Multiconductor AC power flow
Tellegen solves prescribed-load AC power flow on PowerIO’s MulticonductorNetwork. Frederik Geth contributed the fixed-point solver, winding models, and independent comparison fixtures in PR 109.
Import a BMOPF JSON case or load the 4-conductor example from Studies. Run AC power flow, then inspect terminal voltages, conductor currents, and source powers. Results use volts, degrees, amperes, watts, and var. A declared neutral also allows phase-to-neutral voltage comparisons. This calculation does not optimize generation or calculate LMPs.
Geographic GeoJSON can accompany the case or be attached to the selected case later. Bus points use equipment identities; line routes can use bus_from and bus_to. Geographic positions appear on the map. Drawing positions appear on a plain canvas. Attaching geometry retains the electrical input and any current result.
Supported calculations
The solver uses one complex sparse LU factorization and updates compensated load currents until both voltage changes and physical KCL residuals meet their tolerances. Constant-power, constant-current, constant-impedance, ZIP, and exponential loads are included, along with ideal voltage sources, lines, shunts, explicit neutral conductors, and supported two-winding equipment. A two-winding entry expressed using BMOPF’s n-winding representation is also supported.
Finite source impedance, active device controls, generator/IBR injections, unsupported load models, unsupported per-phase taps, ideal zero-leakage winding models, and more than two windings require additional numerical models. Such data can remain available for inspection, but the calculation reports unsupported physics instead of silently simplifying it. Draft BMOPF 0.2 data remains subject to Task Force review.
A current result belongs to the input that produced it. Cancelling or failing a new calculation retains the preceding result. Numeric columns exported to PowerIO are ordered by bus/terminal and source/terminal identities, with missing or duplicate identities rejected.
Save a result in Studies to retain its input, solver options, terminal values, and PowerIO solution. Import/export moves the saved result between browsers. Reopening checks that its input and results agree without solving again. A geographic attachment updates both saved modules and retains all electrical values. These versioned multiconductor snapshots do not offer the balanced-network planning objectives or sensitivities.
Raw OpenDSS files remain available for inspection. The multiconductor calculation requires supported BMOPF data or a typed PowerIO AC power flow input with explicit source and device settings.
Native and browser APIs
The native API accepts a typed McAcPfInstance:
let result = tellegen::solve_mc_ac_pf_instance(&instance, &tellegen::McPfOptions::default())?;
let portable = result.to_powerio_solution(&instance)?;
The command line accepts either PowerIO IR or BMOPF JSON:
tellegen solve-mc < case.pio.json
tellegen solve-mc-bmopf '{"max_iterations":200}' < case.bmopf.json
tellegen describe includes the options and result schemas. The JSON result carries complex terminal voltages, currents, device powers, source reactions, iteration counts, and KCL residuals.
The browser package provides solveMcModule(moduleJson, options, signal) and solveMcBmopf(text, options). The module operation runs in a separate worker so cancelling it leaves other calculations intact. Environments without workers check cancellation after synchronous execution and discard a cancelled result.
solveMcStudy, replayMcStudy, and applyMcStudyGeo create, reopen, and update saved multiconductor results. Native callers use McStudySnapshot for the same operations.
An agent can use inspect_case, solve_multiconductor_pf, and query_network. A solve requires the case ID and displayed revision. Querying voltage_v ranks buses by their largest terminal-to-ground voltage; terminal_values lists the individual terminals. price remains unavailable for AC power flow.
Evidence
The test suite includes analytic resistive feeders and comparisons against OpenDSS reference results for grounded, floating, and impedance-grounded neutrals, three-phase lines, delta loads, and multiple winding connections. The published tolerances distinguish voltage, current, and complex-power comparisons. These fixtures establish the supported calculations; they do not establish universal convergence or multiconductor AC OPF support.
Further tests exercise the real WASM worker, unsupported-data rejection through IR reload, geographic points and multi-point routes after attachment, and unchanged electrical results after geographic edits.
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 PowerIO module
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 PowerIO module, call a native module solve 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 democrates/: the tellegen engine and its wasm, server, CLI, and benchmark adapterspackages/engine/: public@tellegen/enginebrowser packagepackages/svelte/: public@tellegen/sveltecomponent packageexamples/browser-minimal/: minimal downstream Vite exampleexamples/svelte-minimal/: minimal Svelte example using the component packagescripts/: data staging and docs build helpersdeploy/: deployment compose files and proxy notesdocs/src/: mdBook documentation source
Prerequisites
- Rust from
rust-toolchain.toml, includingrustfmt,clippy, and thewasm32-unknown-unknowntarget - Node.js 22 or newer
wasm-pack0.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, .epc, .pwb, .dss, .pwd, .csv, .json,
or .geojson files.
MATPOWER .m, PSS/E .raw, PowerWorld .aux/.pwb, and PSLF .epc files
describe balanced network topology; OpenDSS .dss opens the multiconductor
viewer. If a balanced case includes complete coordinates, tellegen draws it
directly. Otherwise it creates a local synthetic layout and asks the user to
place it on the map. PowerIO classifies dropped JSON by content. A stored
generation-2 pio-ir document opens by its value type: a balanced network or
DC OPF instance becomes a case, while supported multiconductor networks,
instances, and solutions open in the viewer. AC PF and AC OPF instance modules
remain unopened until the browser exposes those formulation choices.
Transmission exchange JSON opens as a balanced case, and BMOPF or
PowerModelsDistribution documents open the multiconductor viewer. The retired
bare model-JSON shape is unrecognized; regenerate it from source data as
pio-ir. Tellegen does not define a separate saved case format. Other
unrecognized JSON falls through to the geographic reader.
After a parsed local case has coordinates, either from the file, a geographic file, or manual placement, tellegen solves the selected formulation 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 PowerIO case module or an
exported case carries the placement on screen. Loading that module starts a
fresh runtime Study at the saved point. Its history records transformations
applied to the stored value. The case panel can also download the current
layout as a .geo.json layer 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:
| Meaning | Accepted fields |
|---|---|
| Bus id | bus_i, bus, bus_id, bus number, number, id |
| Latitude | lat, latitude, y |
| Longitude | lon, 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:
| Meaning | Accepted fields |
|---|---|
| Branch id | branch, branch_id, branch number, cats_id, id |
| From bus | f_bus, from, from_bus, bus_from |
| To bus | t_bus, to, to_bus, bus_to |
| Endpoint coordinates | Lat1, 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 the declared convex curve exactly (formulations). Malformed rows and rows with decreasing segment slopes are rejected before a solve.
Display Files
PowerWorld .pwd files supply drawing positions and branch paths. Drop one
with its case or attach it to the explicitly selected case. Tellegen reports
matched buses, matched routes, and unmatched objects. A file with no supported
equipment positions returns an error.
Drawings use a plain canvas with pan, zoom, and fit controls. Geographic files use the map. When both are supplied, Map and Diagram select the view; neither replaces the other. A later attachment to a demo case creates a local copy and preserves its current edits and result.
Supported New England PWB files include geographic bus locations. Their RAW revision 33 companions lack those records and need a matching geographic sidecar. Arbitrary drawing positions are never treated as latitude/longitude.
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.
| case | territory | buses | branches | files |
|---|---|---|---|---|
| ACTIVSg200 | central Illinois | 200 | 245 | case_ACTIVSg200.m + ACTIVSg200.aux |
| ACTIVSg500 | South Carolina | 500 | 597 | case_ACTIVSg500.m + ACTIVSg500.aux |
| ACTIVSg7000 | Texas | 6717 | 9140 | Texas7k_20210804.m + Texas7k_lat_long.csv |
| CATS | California | 8870 | 10823 | CaliforniaTestSystem.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 coordinates
PowerWorld aux exports have used two coordinate layouts:
- ACTIVSg complete case exports repeat substation latitude and longitude on each
bus row in
Latitude:1andLongitude:1. - Later exports can leave the bus latitude and longitude columns empty and
reference the
Substationtable throughSubNumber.
PowerIO promotes the bus columns (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 using either layout 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.
Prepared generator costs
The Texas7k demo selects Tellegen’s reusable convex quadratic/linear fitting option. It fits each piecewise curve by least squares and retains a linear fit when the quadratic fit is nonconvex or singular. Other demo cases keep their declared costs. Model details retain the source and prepared cost rows and report RMS and maximum errors at the source breakpoints.
All four configured cases must load and solve for the deployment health check to pass. Unavailable cases remain in the catalogue with an explanation.
Geographic data and drawings
Electrical case files describe equipment and operating values. Geographic and
drawing files describe where to display that equipment. Tellegen uses PowerIO’s
GeoLayer for both, keeping the coordinate system explicit.
GeoJSON, CSV, and AUX files can supply geographic bus positions and line routes. PowerWorld PWB files can also contain geographic bus locations. PowerWorld PWD files supply drawing positions and polylines. A drawing appears on a plain canvas; geographic coordinates appear on the map.
Drop companion files together, or select a case before attaching another file. The attachment report lists matched equipment and unmatched objects. A later attachment to a demo creates a local copy, preserving current demand changes, line capacities, and the current result. No unrelated recent case is selected.
When a case has both geographic data and a drawing, use Map or Diagram to choose the view. Save a Study to retain both layers, line paths, and the current camera position. Drawing coordinates stay in their source units.
Shared GeoJSON
A GeoLayer uses a FeatureCollection with Point and LineString features. The
powerio_geo member records geographic, projected, drawing, or unknown
coordinates. Equipment matches by stable identity, source ID, name, or branch
endpoints. BMOPFTools bus_from and bus_to names are accepted.
PowerIO reads the proposed BMOPFTools embedded bus[id].geo and line[id].geo
values. Explicit BMOPF output retains geometry under extras.geojson, which
is permitted by the currently identified draft schema. This supports edited
coordinates without claiming that the Task Force has approved new schema fields.
Download geography exports geographic positions and routes. Download drawing exports the selected drawing as a GeoLayer. Both can be reopened by other PowerIO consumers.
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 / dbetween each bus pair, withk = sqrt(1 / n); - attraction
d^2 / kalong 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 retained PowerIO module (Bus.location, provenance
synthetic), so saved modules and case 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.
Technology choices
Keep Svelte for controls and MapLibre GL JS for geographic rendering. Rust remains the numerical engine, compiled for native execution and WASM. This recommendation follows the capabilities below, checked on September 7, 2026.
| Project | Use in Tellegen |
|---|---|
| MapLibre Rust | Its published feature list still lacks GeoJSON, text, and a TypeScript API. Replacing the current renderer would remove capabilities used by case import and equipment selection. |
| MapLibre Native | Appropriate for a future native map application. It does not replace browser controls, accessibility, or persistent Study management. |
| Martin | Consider for a large shared geographic catalogue or self-hosted basemaps. Local case files do not require a tile server. |
| MapLibre Tile | Useful for large tiled geographic collections. Keep editable equipment geometry in GeoJSON: tile encoders can reorder features, and equipment identity must remain explicit. |
| wasm-pack | Already builds the engine package. Keep reproducible versions and exercise the installed package in CI. |
| Walrus | Already participates through wasm-bindgen’s tooling. Add a custom module-processing step only for a measured requirement; it is not a replacement for the application. |
| wgpu | Supports WebGPU and optional WebGL on WASM. A GPU drawing experiment could help unusually large networks, but needs measurements against the current map before adoption. |
| Verus | Worth a small PowerIO experiment on byte-range arithmetic or identity-index invariants. Its supported Rust subset makes whole-parser adoption a separate research effort. Existing parser, binding, and cross-tool tests remain necessary. |
A useful next graphics measurement separates parsing, numerical solving, moving result arrays to JavaScript, layer updates, and map rendering. Record first-load bytes and time, memory growth, interaction latency, and frame time on Texas7k and a larger case. Typed arrays and selective layer updates should be evaluated before changing renderers. GPU support alone does not establish faster double-precision sparse power-system solves.
AC OPF in the browser
Pounce’s current browser implementation uses wasm32-wasip1 and a small JavaScript WASI implementation for clocks, output, and randomness. The worker receives an AMPL NL model and returns solution data. Tellegen’s engine uses wasm32-unknown-unknown; the two modules can coexist in separate workers.
A local feasibility check instantiated Pounce’s published WASM module under Node with its browser WASI implementation. The two-variable nonlinear test nonconvex_qcqp.nl returned SolveSucceeded, objective approximately -2, in 59 iterations. This verifies execution of the published module, not an AC OPF implementation or a build from that source revision. The evidence packet records the downloaded module’s hash and the input revision.
A Pounce AC OPF addition should first use the existing native model implementation with its analytic Jacobian and Hessian tests. Then compare native and browser results on small power-system cases, checking the original electrical equations and limits independently of solver-reported residuals. A dedicated worker allows cancellation by terminating the calculation. BMOPF multiconductor AC OPF additionally needs verified winding, neutral, device-control, objective, and per-terminal limit equations. The fixed-point AC power flow does not claim that support.
PowerIO and geographic data
PowerIO already represents a geographic document as PioModule<PioValue::GeoLayer>, and its module-aware apply_geo_layer supports both electrical network types. Keep GeoJSON as the exchange document and PioModule as the owner of source descriptions, diagnostics, and history. A second combined network/geography container would duplicate those responsibilities.
Tellegen retains the imported multiconductor module and its GeoLayer together, applies geographic files to the selected case, and reports matched buses, routes, and unmatched features. A geographic edit retains the calculation input and can be saved as generation-2 IR. Geographic coordinates and diagram positions keep their declared coordinate type. Tests use BMOPFTools’ bus_from and bus_to route identities and check unchanged electrical results after attachment and reload.
The remaining useful upstream simplification is consistent module-aware extraction and application for calculation inputs and solutions as well as bare networks. Any such addition must preserve explicit device settings and numerical results. It should build on the existing functions without changing published ABI 7 or replacing BMOPF’s geographic work.
Architecture
tellegen is a differentiable power flow and optimal power flow engine written
in Rust for native targets and WebAssembly. The public browser packages are
@tellegen/engine, @tellegen/svelte, and @tellegen/webmcp; the SvelteKit
hosted demo is one private consumer.
Repository layout
A Cargo workspace and a web app, side by side.
crates/tellegen: the engine. It parses a case through powerio and solves the requested supported formulation. One result envelope carries the fields and sensitivities that formulation defines.crates/tellegen-wasm: thewasm-bindgenadapter that exposes the engine to the browser, built withwasm-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 private 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.packages/webmcp: WebMCP tool definitions, validation, and registration lifecycle.apps/web: the private SvelteKit hosted demo that consumes@tellegen/svelte.examples/browser-minimal: a minimal downstream app that imports@tellegen/enginedirectly.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.
The formulations share one result envelope, but they do not claim the same
quantities. DC power flow returns angles and branch flows without prices,
optimized dispatch, or sensitivities. DC OPF adds dispatch, objective value,
LMPs, and KKT sensitivities. AC power flow returns voltages and nodal
injections with Newton sensitivities. SOCWR exposes the quantities and conic
KKT sensitivities defined by the relaxation. Formulations that implement the
Differentiable contract accept an output Operand and input Parameter;
the common driver solves the retained KKT or Newton system for the requested
forward or adjoint columns.
The engine is 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 is on the desktop and mobile roadmap. Its planned interior point solver uses threads; the current browser solver build is single threaded.
The two API faces
One numerical core, two faces that share a driver and a result type:
- Stateless:
solve_module(module, request)andcapabilities_json(). Each call reads a PowerIO module, solves its declared problem instance, and returns. - Stateful: the
Study. It starts from a PowerIO module and builds the private solver workspace once.commitapplies a set ofNetworkEdits and re-solves exactly, optionally returning the requested sensitivity columns in the same solve;previewreturns a first order update at the committed point with no re-solve.
Browser packages
packages/engine is the reusable package surface. It exports generated
contracts, case and display parsing helpers, module based solves, 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.
packages/webmcp exports tool definitions, runtime validation, and registration
through an adapter that does not depend on Svelte or the engine package.
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 Study for DC OPF, AC power flow,
and SOCWR. preview returns a first order update for the quantities the chosen
formulation defines; commit performs an exact re-solve and can return the
displayed sensitivity column. DC power flow uses the stateless solve path, and
full nonlinear AC OPF is unavailable. Supported solvable case files run in the
browser and are not 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 browser surfaces are @tellegen/engine, @tellegen/svelte, and
@tellegen/webmcp.
Stable release surfaces:
crates/tellegenRust API layer, including the serde request and response shapes insrc/api.rs;crates/tellegen-wasmwasm adapter;packages/engineTypeScript package, generated TypeScript types, and browser wasm entry points;packages/svelteSvelte component package;packages/webmcpWebMCP tool definitions and runtime validation without a UI framework dependency;- 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/webmcp;packages/svelte;apps/web;examples/browser-minimal; andexamples/svelte-minimal.
Install JavaScript dependencies from the repository root:
npm ci
Root scripts define the package order:
npm run wasmbuilds both wasm packages intopackages/engine;npm run build:enginebuilds@tellegen/engine;npm run build:webmcpbuilds@tellegen/webmcp;npm run build:sveltebuilds@tellegen/svelte;npm run build:examplebuilds both downstream examples;npm run check:web,npm run build:web, andnpm run smoke:webgate the hosted demo;npm run pack:enginepreviews the engine npm package contents;npm run pack:webmcppreviews the WebMCP npm package contents;npm run pack:sveltepreviews the Svelte npm package contents.
Versioning
@tellegen/engine, @tellegen/svelte, and @tellegen/webmcp use independent
package versions.
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.
Saved cases
Tellegen saves the materialized network as a stored PowerIO module. The module
keeps applicable records, severs source linkage invalidated by edits, and adds
descriptive edit history. Loading it starts a fresh runtime Study at the
saved point. Its history records transformations applied to the stored value.
Exact OPF results are stored as PowerIO solution modules containing the instance that was solved.
Releasing
Package versions and the crate version move independently, so two release bots drive the pipeline. A release that touches one surface publishes that surface only.
Nobody cuts a tag by hand. This repository stores no registry token. Both
registries authenticate with OIDC, and each exchange runs in a deployment
environment (crates-io, npm). Reviewing and merging the generated version
pull request is the final human publication gate. Publication after that merge
is unattended.
Each environment allows deployments from main only and has no required
reviewer. Set the same environment on the registry’s trusted publisher, along
with the repository and workflow filename. The workflow also rejects non-main
refs; the publisher binding makes the registry enforce that identity.
Enabling the pipeline
The release workflows do nothing until the TELLEGEN_RELEASE_ENABLED repository
variable is true. What they need lives outside the repository. Set the
variable after you make all of these:
- a
crates-ioenvironment and annpmenvironment, each with a deployment branch rule limited tomainand no required reviewers; - a crates.io trusted publisher for
tellegen, set to this repository,release-crate.yml, and thecrates-ioenvironment; - an npm trusted publisher for
@tellegen/engine,@tellegen/svelte, and@tellegen/webmcp, each set to this repository,release-npm.yml, and thenpmenvironment. The publisher binds to the workflow filename. If you rename the workflow, publishing stops until you change the publisher; - a GitHub App on this repository with
contents: writeandpull-requests: write. Put its id in theRELEASE_PLZ_APP_IDvariable and its private key in theRELEASE_PLZ_APP_PRIVATE_KEYsecret. Both release bots use it so their version pull requests start CI. Pull requests opened byGITHUB_TOKENdo not start workflows.
Protect main with a repository ruleset that requires pull requests, at least
one approval, approval of the most recent reviewable push (or dismissal of
stale approvals), an up-to-date branch, and the CI checks before merge. Block
force pushes and deletion. Do not give the Release App a ruleset bypass. Those
protections ensure an approval cannot survive a bot refresh with different
release contents and make the version pull-request merge the publication gate.
If a package or crate does not yet exist at its registry, publish its first version manually before configuring the trusted publisher. Later releases use this pipeline end to end.
Delete any NPM_TOKEN or CARGO_REGISTRY_TOKEN repository secrets. The
workflows do not read long-lived registry credentials.
Packages
@tellegen/engine, @tellegen/svelte, and @tellegen/webmcp release through
changesets.
A change to any of these packages needs a changeset. Run npm run changeset
and commit the file it writes. See .changeset/README.md.
On a push to main, .github/workflows/release-npm.yml keeps a “Release
packages” pull request open. That pull request applies every pending changeset:
version bumps, changelog entries, the engine CONTRACT_VERSION, and the
lockfile. Merge it to run the gates and publish. Tags take the form
@tellegen/<name>@X.Y.Z.
The workflow selects version or publish mode before it requests privileged
permissions. Versioning runs without the GitHub App, validates an exact output
allowlist, and passes a SHA-256-verified patch to a fresh runner. That runner
revalidates and commits the patch before minting the App token; only git and
gh run afterward. Publishing builds immutable tarballs in an unprivileged
job, then gives only the final npm job the npm environment and OIDC
permission. Merging the version pull request is therefore the only manual
release action.
@tellegen/svelte resolves @tellegen/engine from the registry, so a release
that moves both publishes the engine first.
Crate
tellegen is the only crate that publishes to crates.io. tellegen-wasm,
tellegen-server, tellegen-cli, and benchmarks carry publish = false, and
the release-plz workspace defaults to release = false. A new crate needs an
explicit package opt-in.
On a push to main, .github/workflows/release-crate.yml keeps a pull request
open that bumps the version. Merge it to run the gates, continue the existing
vX.Y.Z tag series, make the GitHub release, and publish. The configuration
sets release_always = false, limiting publication to a merged release-plz pull
request. The unprivileged gate runs cargo package --locked; the OIDC-enabled
release uses the ordinary verified Cargo publish path against that exact
commit. release-plz obtains its short-lived crates.io credential directly from
OIDC.
The crate update uses the same sealed-patch privilege boundary as package
versioning. Its fixed release-plz-main branch is intentional: with
release_always = false, release-plz recognizes a release commit by the
release-plz- head prefix. Merge this generated pull request with a normal
merge commit, not squash, so its release commit remains unambiguous if another
change reaches main nearby.
An emergency repair to a generated crate release must retain its
release-plz-* branch name; that is how release_always = false recognizes the
merged release commit.
Inspecting an artifact
.github/workflows/package-inspect.yml is a manual run. It builds the artifacts
a release would upload, cargo package and all three npm pack tarballs, and
attaches them to the run. It publishes nothing.
Changelogs
Each surface keeps its own generated changelog:
crates/tellegen/CHANGELOG.md, packages/engine/CHANGELOG.md, and
packages/svelte/CHANGELOG.md, and packages/webmcp/CHANGELOG.md. The root
CHANGELOG.md is the curated view across all four and the only record for
releases before the split.
CI Gates
.github/workflows/gates-rust.yml and .github/workflows/gates-js.yml define
what green means. CI calls both on every pull request. The crate release calls
the Rust gates, and the npm release calls both. Add a gate to those files, not
to a caller.
The Rust gates are cargo fmt --check, clippy with warnings denied on the
shipping crates, cargo-deny, the EPL guard, the workspace tests, the engine’s
conic path, tellegen-wasm built with conic, and cargo package --locked
for the published crate. The wasm test matters
because tellegen-wasm declares default = [], so the workspace run skips the
tests that assert an untrusted package or case rejects rather than panicking.
The JavaScript gates install once from the root lockfile, check and pack
packages/webmcp, build and check packages/engine before packages/svelte,
run the engine import smoke test, check the Svelte package through a packed
temporary consumer, build the hosted demo, and run its browser tests. just ci
runs everything locally.
Formulations
Tellegen solves DC power flow, DC OPF, AC power flow, and the Jabr SOCWR relaxation through the PowerIO module boundary. The shared response has optional fields for the quantities each formulation produces. Economic fields are present only when the declared objective gives them that interpretation. Supported implicit derivatives use 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^\top\operatorname{diag}(b)A, \qquad B\theta = p, $$
where $A$ is the branch by bus incidence and $b$ contains positive solver
weights derived from the public 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.
solve_module_json is the portable entry. Rust callers that already own a
DcOpfInstance can use solve_instance.
MATPOWER model 2 quadratic generator costs are read directly. Convex model 1 piecewise linear costs use one epigraph variable and one inequality per segment, so dispatch, objective values, prices, and supported implicit derivatives refer to the declared curve. Malformed and nonconvex model 1 rows are rejected before the program is assembled. At a breakpoint or an active set change, the marginal value or its derivative need not be unique; the sensitivity API reports the local KKT linearization and numerical checks identify stencils that cross a different active set.
Branch angle-difference bounds are enforced in radians after normalization.
MATPOWER’s unconstrained -360/360 spelling and an unset 0/0 pair become
exactly -60/+60 degrees. When a branch has no thermal rating, Tellegen
synthesizes its fallback rating from that same 60 degree window and the terminal
voltage bands. Explicit tighter source bounds are preserved.
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.
Select acpf in solve_module_json.
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. It uses the same exact quadratic and convex piecewise
linear generator cost representation as DC OPF. Select socwr in
solve_module_json.
These formulations compile to native Rust and WebAssembly.
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 private 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), the harness parses a PowerIO module and declares the
calculation as DcOpfInstance, AcOpfInstance, or AcPfInstance. It then
drives the same typed facade available to downstream consumers:
- DC OPF:
solve_instance(objective, dispatch, LMPs); - conic SOCWR:
solve_ac_instance(objective, gap, W-space primals); - AC power flow:
solve_ac_pf_instance(convergence, residual); - sensitivities: the same typed solve entries in forward and adjoint modes.
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 on one thread for clean timing.
Metrics
- 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).
- Sensitivity parity: adjoint equals forward, and central finite differences against the analytic columns, classified per parity class (see the sensitivity contract).
- Feasibility / convergence: per-case status, interior-point iterations, residuals.
- Performance: wall time per stage, and a scaling curve against bus count.
- 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 harness records the source revision, toolchain, dependency versions, host,
and invocation used for each run. It 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. The repository keeps that
generated snapshot, and a rerun can be compared with its recorded provenance:
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 declared DC objective, including its constant cost term, is compared against the published DC baseline. Both values come from the same PGLib cost data. Per-unit power scaling cancels; tellegen does not assign a currency to objectives from other sources.
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 provenance table records the toolchain used for this run.
Provenance
| tellegen | rustc | clarabel | faer | powerio | os/arch |
|---|---|---|---|---|---|
178da99 | 1.96.0 (ac68faa20 2026-05-25) | 0.11.1 | 0.24.4 | 0.7.1 | macos/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.
| status | count |
|---|---|
| caveat | 50 |
| failed | 2 |
| skipped | 3 |
| solved | 143 |
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.
| formulation | reproduced | consistent | mismatch | with baseline |
|---|---|---|---|---|
| DC OPF | 148 | 45 | 0 | 193 |
| SOCWR (lower bound) | 183 | 6 | 0 | 189 |
OPF correctness vs PGLib BASELINE
Tellegen’s objective against the published value, per formulation: the DC OPF (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. 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.
| case | var | buses | DC objective | PGLib DC | SOCWR objective | PGLib AC | Δgap | reproduces |
|---|---|---|---|---|---|---|---|---|
| case3_lmbd | api | 3 | 10444.4 | 10444.0 | 10194.9 | 11242.0 | -0.01 | ✓·✓lb |
| case3_lmbd | sad | 3 | 5856.0 | 5856.0 | 5736.2 | 5959.3 | -0.01 | ✓·✓lb |
| case3_lmbd | typ | 3 | 5695.9 | 5695.9 | 5736.2 | 5812.6 | -0.01 | ✓·✓lb |
| case5_pjm | api | 5 | 78025.2 | 78025.0 | 77571.4 | 78950.0 | -0.00 | ✓·✓lb |
| case5_pjm | sad | 5 | — | — | 25164.9 | 26109.0 | -0.00 | inf✓·✓lb |
| case5_pjm | typ | 5 | 17479.9 | 17480.0 | 14999.7 | 17552.0 | -0.01 | ✓·✓lb |
| case14_ieee | api | 14 | 4797.6 | 4797.6 | 5691.8 | 5999.4 | -0.00 | ✓·✓lb |
| case14_ieee | sad | 14 | — | — | 2179.2 | 2776.8 | -0.01 | inf✓·✓lb |
| case14_ieee | typ | 14 | 2051.5 | 2051.5 | 2175.7 | 2178.1 | -0.00 | ✓·✓lb |
| case24_ieee_rts | api | 24 | 148845.5 | 148850.0 | 149168.3 | 161220.0 | -0.00 | ✓·✓lb |
| case24_ieee_rts | sad | 24 | 78122.5 | 78122.0 | 69578.9 | 76918.0 | -0.01 | ✓·✓lb |
| case24_ieee_rts | typ | 24 | 61001.2 | 61001.0 | 63344.6 | 63352.0 | -0.01 | ✓·✓lb |
| case30_as | api | 30 | 3092.1 | 3092.1 | 2767.8 | 4996.2 | -0.01 | ✓·✓lb |
| case30_as | sad | 30 | — | — | 825.9 | 897.4 | 0.08 | inf✓·✓lb |
| case30_as | typ | 30 | 767.6 | 767.6 | 802.7 | 803.1 | -0.00 | ✓·✓lb |
| case30_ieee | api | 30 | 16145.1 | 16145.0 | 17057.8 | 18037.0 | -0.00 | ✓·✓lb |
| case30_ieee | sad | 30 | — | — | 7412.6 | 8208.5 | -0.00 | inf✓·✓lb |
| case30_ieee | typ | 30 | 7472.8 | 7472.8 | 6662.2 | 8208.5 | -0.00 | ✓·✓lb |
| case39_epri | api | 39 | 252754.6 | 252750.0 | 253130.4 | 256770.0 | -0.00 | ✓·✓lb |
| case39_epri | sad | 39 | 150669.9 | 150670.0 | 147360.5 | 148340.0 | -0.01 | ✓·✓lb |
| case39_epri | typ | 39 | 136889.7 | 136890.0 | 137654.1 | 138420.0 | -0.01 | ✓·✓lb |
| case57_ieee | api | 57 | 34081.1 | 34081.0 | 33271.6 | 36242.0 | -0.00 | ✓·✓lb |
| case57_ieee | sad | 57 | — | — | 38391.4 | 38663.0 | -0.01 | inf✓·✓lb |
| case57_ieee | typ | 57 | 34773.0 | 34773.0 | 37529.7 | 37589.0 | -0.00 | ✓·✓lb |
| case60_c | api | 60 | 176378.6 | 176380.0 | 181189.2 | 185000.0 | -0.01 | ✓·✓lb |
| case60_c | sad | 60 | — | — | 108485.3 | 113500.0 | 0.05 | inf✓·✓lb |
| case60_c | typ | 60 | 90700.1 | 90700.0 | 92637.0 | 92694.0 | -0.01 | ✓·✓lb |
| case73_ieee_rts | api | 73 | 472183.2 | 472180.0 | 488422.7 | 509850.0 | -0.01 | ✓·✓lb |
| case73_ieee_rts | sad | 73 | 232679.1 | 232680.0 | 212280.3 | 227600.0 | 0.00 | ✓·✓lb |
| case73_ieee_rts | typ | 73 | 183003.7 | 183000.0 | 189706.1 | 189760.0 | -0.01 | ✓·✓lb |
| case89_pegase | api | 89 | 118452.5 | 118630.0 | 113367.9 | 129570.0 | -0.01 | ✓·✓lb |
| case89_pegase | sad | 89 | — | — | 106491.2 | 107290.0 | 0.01 | inf✓·✓lb |
| case89_pegase | typ | 89 | 104919.0 | 105040.0 | 106480.8 | 107290.0 | 0.00 | ✓·✓lb |
| case118_ieee | api | 118 | 231291.9 | 231290.0 | 184303.2 | 249610.0 | -0.01 | ✓·✓lb |
| case118_ieee | sad | 118 | — | — | 96535.5 | 105160.0 | 0.03 | inf✓·✓lb |
| case118_ieee | typ | 118 | 93100.7 | 93101.0 | 96334.7 | 97214.0 | -0.01 | ✓·✓lb |
| case162_ieee_dtc | api | 162 | 111570.9 | 111570.0 | 115655.9 | 120880.0 | -0.01 | ✓·✓lb |
| case162_ieee_dtc | sad | 162 | 106287.8 | 106290.0 | 101655.6 | 108690.0 | -0.01 | ✓·✓lb |
| case162_ieee_dtc | typ | 162 | 101462.3 | 101460.0 | 101654.9 | 108080.0 | -0.01 | ✓·✓lb |
| case179_goc | api | 179 | 1813209.8 | 1813200.0 | 1727618.5 | 1883400.0 | 0.01 | ✓·✓lb |
| case179_goc | sad | 179 | — | — | 754005.8 | 762530.0 | -0.00 | inf✓·✓lb |
| case179_goc | typ | 179 | 751881.6 | 751880.0 | 753089.1 | 754270.0 | -0.00 | ✓·✓lb |
| case197_snem | api | 197 | 15689.7 | 15690.0 | 16203.0 | 16363.0 | -0.00 | ✓·✓lb |
| case197_snem | sad | 197 | — | — | 1.5 | 1.5 | 0.03 | inf✓·✓lb |
| case197_snem | typ | 197 | 1.5 | 1.5 | 1.5 | 1.5 | 0.02 | ✓·✓lb |
| case200_activ | api | 200 | 40129.8 | 40130.0 | 40692.9 | 40700.0 | -0.00 | ✓·✓lb |
| case200_activ | sad | 200 | — | — | 27556.6 | 27558.0 | -0.01 | inf✓·✓lb |
| case200_activ | typ | 200 | 27479.6 | 27480.0 | 27556.6 | 27558.0 | -0.01 | ✓·✓lb |
| case240_pserc | api | 240 | 4624856.0 | 4624800.0 | 4636792.9 | 4692200.0 | 0.00 | ✓·✓lb |
| case240_pserc | sad | 240 | — | — | 3237213.7 | 3405400.0 | 0.01 | inf✓·✓lb |
| case240_pserc | typ | 240 | 3271442.2 | 3271400.0 | 3237193.6 | 3329700.0 | -0.00 | ✓·✓lb |
| case300_ieee | api | 300 | 659786.2 | 659840.0 | 679566.9 | 686040.0 | -0.01 | ✓·✓lb |
| case300_ieee | sad | 300 | 527243.9 | 527290.0 | 550597.4 | 565700.0 | 0.06 | ✓·✓lb |
| case300_ieee | typ | 300 | 517802.2 | 517850.0 | 550393.8 | 565220.0 | -0.01 | ✓·✓lb |
| case500_goc | api | 500 | 646871.6 | 646870.0 | 663185.9 | 688290.0 | -0.00 | ✓·✓lb |
| case500_goc | sad | 500 | — | — | 454907.9 | 487400.0 | -0.00 | inf✓·✓lb |
| case500_goc | typ | 500 | 440548.6 | 440550.0 | 453838.5 | 454950.0 | -0.01 | ✓·✓lb |
| case588_sdet | api | 588 | 392953.8 | 392950.0 | 387619.1 | 398760.0 | -0.01 | ✓·✓lb |
| case588_sdet | sad | 588 | — | — | 307081.1 | 329360.0 | 0.09 | inf✓·✓lb |
| case588_sdet | typ | 588 | 310125.6 | 310130.0 | 306447.9 | 313140.0 | -0.00 | ✓·✓lb |
| case793_goc | api | 793 | 372180.2 | 372180.0 | 321148.8 | 379800.0 | -0.01 | ✓·✓lb |
| case793_goc | sad | 793 | — | — | 263030.4 | 285800.0 | -0.00 | inf✓·✓lb |
| case793_goc | typ | 793 | 258308.0 | 258310.0 | 256757.9 | 260200.0 | -0.01 | ✓·✓lb |
| case1354_pegase | api | 1354 | 1558525.9 | 1558500.0 | 1578360.1 | 1608200.0 | 0.01 | ✓·✓lb |
| case1354_pegase | sad | 1354 | — | — | 1236022.1 | 1258800.0 | 0.24 | inf✓·✓lb |
| case1354_pegase | typ | 1354 | 1218182.9 | 1218200.0 | 1235968.2 | 1258800.0 | 0.24 | ✓·✓lb |
| case1803_snem | api | 1803 | 62064.2 | 61723.0 | 46102.3 | 80240.0 | 1.70 | ✓·lb |
| case1803_snem | sad | 1803 | — | — | 91290.5 | 106340.0 | 0.16 | inf✓·✓lb |
| case1803_snem | typ | 1803 | 87706.8 | 87696.0 | 90240.4 | 98335.0 | 0.20 | ✓·✓lb |
| case1888_rte | api | 1888 | 1961569.9 | 1961600.0 | 2013300.1 | 2019700.0 | -0.00 | ✓·✓lb |
| case1888_rte | sad | 1888 | 1353179.3 | 1353200.0 | 1373847.5 | 1413900.0 | 0.01 | ✓·✓lb |
| case1888_rte | typ | 1888 | 1352872.6 | 1352900.0 | 1373847.4 | 1402500.0 | -0.01 | ✓·✓lb |
| case1951_rte | api | 1951 | 2411505.2 | 2411500.0 | 2478404.2 | 2490300.0 | -0.00 | ✓·✓lb |
| case1951_rte | sad | 1951 | — | — | 2082719.5 | 2092400.0 | 0.00 | inf✓·✓lb |
| case1951_rte | typ | 1951 | 2031629.3 | 2031600.0 | 2082711.6 | 2085600.0 | -0.00 | ✓·✓lb |
| case2000_goc | api | 2000 | 1409993.7 | 1410000.0 | 1441231.6 | 1483900.0 | 0.01 | ✓·✓lb |
| case2000_goc | sad | 2000 | — | — | 977751.3 | 992880.0 | 0.00 | inf✓·✓lb |
| case2000_goc | typ | 2000 | 943042.6 | 943040.0 | 970446.9 | 973430.0 | -0.00 | ✓·✓lb |
| case2312_goc | api | 2312 | 615216.3 | 615220.0 | 546946.8 | 663440.0 | 0.03 | ✓·✓lb |
| case2312_goc | sad | 2312 | — | — | 444178.0 | 462350.0 | 0.00 | inf✓·✓lb |
| case2312_goc | typ | 2312 | 440328.5 | 440330.0 | 432953.7 | 441330.0 | -0.00 | ✓·✓lb |
| case2383wp_k | api | 2383 | 279125.8 | 279130.0 | 279125.8 | 279130.0 | -0.01 | ✓·✓lb |
| case2383wp_k | sad | 2383 | — | — | 1856608.1 | 1911200.0 | -0.00 | inf✓·✓lb |
| case2383wp_k | typ | 2383 | 1804090.4 | 1804100.0 | 1848908.7 | 1868200.0 | -0.01 | ✓·✓lb |
| case2736sp_k | api | 2736 | 977821.5 | 977820.0 | 939178.2 | 1017800.0 | -0.01 | ✓·✓lb |
| case2736sp_k | sad | 2736 | — | — | 1305707.8 | 1326600.0 | -0.01 | inf✓·✓lb |
| case2736sp_k | typ | 2736 | 1276033.7 | 1276000.0 | 1303994.5 | 1308000.0 | -0.00 | ✓·✓lb |
| case2737sop_k | api | 2737 | 755318.3 | 755320.0 | 733986.1 | 788310.0 | -0.01 | ✓·✓lb |
| case2737sop_k | sad | 2737 | — | — | 776126.6 | 790950.0 | -0.01 | inf✓·✓lb |
| case2737sop_k | typ | 2737 | 764009.8 | 764010.0 | 775689.9 | 777730.0 | -0.01 | ✓·✓lb |
| case2742_goc | api | 2742 | 505399.5 | 505400.0 | 471741.7 | 609960.0 | -0.01 | ✓·✓lb |
| case2742_goc | sad | 2742 | 259696.6 | 259700.0 | 271971.8 | 275710.0 | -0.00 | ✓·✓lb |
| case2742_goc | typ | 2742 | 259696.6 | 259700.0 | 271999.6 | 275710.0 | -0.00 | ✓·✓lb |
| case2746wop_k | api | 2746 | 531726.6 | 531550.0 | 511658.6 | 550480.0 | -0.01 | ✓·✓lb |
| case2746wop_k | sad | 2746 | — | — | 1205170.1 | 1233700.0 | -0.01 | inf✓·✓lb |
| case2746wop_k | typ | 2746 | 1178363.8 | 1178200.0 | 1203861.3 | 1208300.0 | -0.00 | ✓·✓lb |
| case2746wp_k | api | 2746 | 581827.9 | 581830.0 | 581825.1 | 581830.0 | -0.01 | ✓·✓lb |
| case2746wp_k | sad | 2746 | — | — | 1630588.4 | 1666900.0 | -0.01 | inf✓·✓lb |
| case2746wp_k | typ | 2746 | 1581425.1 | 1581400.0 | 1626493.5 | 1631700.0 | -0.01 | ✓·✓lb |
| case2848_rte | api | 2848 | 1488985.3 | 1489000.0 | 1527378.9 | 1531100.0 | -0.01 | ✓·✓lb |
| case2848_rte | sad | 2848 | — | — | 1285682.1 | 1289000.0 | 0.01 | inf✓·✓lb |
| case2848_rte | typ | 2848 | 1267732.3 | 1267700.0 | 1285034.3 | 1286600.0 | -0.01 | ✓·✓lb |
| case2853_sdet | api | 2853 | 2456065.6 | 2456100.0 | 2420923.4 | 2484300.0 | 0.01 | ✓·✓lb |
| case2853_sdet | sad | 2853 | — | — | 2033761.7 | 2069200.0 | 0.01 | inf✓·✓lb |
| case2853_sdet | typ | 2853 | 2036958.3 | 2037000.0 | 2033582.3 | 2052400.0 | 0.01 | ✓·✓lb |
| case2868_rte | api | 2868 | 2277460.8 | 2277500.0 | 2334871.0 | 2343900.0 | 0.01 | ✓·✓lb |
| case2868_rte | sad | 2868 | — | — | 2009425.4 | 2021300.0 | -0.00 | inf✓·✓lb |
| case2868_rte | typ | 2868 | 1966685.0 | 1966700.0 | 2007570.8 | 2009600.0 | 0.00 | ✓·✓lb |
| case2869_pegase | api | 2869 | 2966298.3 | 2966600.0 | 3026118.5 | 3063000.0 | 0.02 | ✓·✓lb |
| case2869_pegase | sad | 2869 | — | — | 2438499.1 | 2468700.0 | 0.10 | inf✓·✓lb |
| case2869_pegase | typ | 2869 | 2386115.8 | 2386400.0 | 2435400.7 | 2462800.0 | 0.10 | ✓·✓lb |
| case3012wp_k | api | 3012 | 851750.5 | 851750.0 | 728874.0 | 914590.0 | -0.00 | ✓·✓lb |
| case3012wp_k | sad | 3012 | — | — | 2578747.0 | 2619500.0 | -0.00 | inf✓·✓lb |
| case3012wp_k | typ | 3012 | 2509001.5 | 2509000.0 | 2574297.7 | 2600800.0 | -0.01 | ✓·✓lb |
| case3022_goc | api | 3022 | 666194.7 | 666190.0 | 594789.6 | 687360.0 | 0.02 | ✓·✓lb |
| case3022_goc | sad | 3022 | 599221.5 | 599220.0 | 584763.7 | 601430.0 | 0.00 | ✓·✓lb |
| case3022_goc | typ | 3022 | 599221.5 | 599220.0 | 584742.8 | 601380.0 | -0.00 | ✓·✓lb |
| case3120sp_k | api | 3120 | 1326454.4 | 1326500.0 | 1178608.6 | 1403500.0 | -0.01 | ✓·✓lb |
| case3120sp_k | sad | 3120 | — | — | 2141758.0 | 2174900.0 | 0.00 | inf✓·✓lb |
| case3120sp_k | typ | 3120 | 2087974.9 | 2088000.0 | 2135975.3 | 2148000.0 | -0.00 | ✓·✓lb |
| case3375wp_k | api | 3374 | 6272121.1 | 6272100.0 | 5865209.8 | 6364100.0 | -0.00 | ✓·✓lb |
| case3375wp_k | sad | 3374 | 7319637.3 | 7319600.0 | 7397595.4 | 7438200.0 | -0.00 | ✓·✓lb |
| case3375wp_k | typ | 3374 | 7317011.9 | 7317000.0 | 7397405.5 | 7438200.0 | -0.00 | ✓·✓lb |
| case3970_goc | api | 3970 | 1227776.4 | 1227800.0 | 1070893.9 | 1749400.0 | -0.00 | ✓·✓lb |
| case3970_goc | sad | 3970 | — | — | 958294.5 | 965550.0 | -0.01 | inf✓·✓lb |
| case3970_goc | typ | 3970 | 934219.4 | 934220.0 | 957739.2 | 960990.0 | -0.00 | ✓·✓lb |
| case4020_goc | api | 4020 | 1082422.1 | 1082400.0 | 1062324.8 | 1281700.0 | -0.00 | ✓·✓lb |
| case4020_goc | sad | 4020 | — | — | 812671.5 | 889690.0 | -0.00 | inf✓·✓lb |
| case4020_goc | typ | 4020 | 795061.9 | 795060.0 | 812181.5 | 822250.0 | -0.01 | ✓·✓lb |
| case4601_goc | api | 4601 | 796409.8 | 796410.0 | 687104.5 | 871240.0 | -0.01 | ✓·✓lb |
| case4601_goc | sad | 4601 | 1195553.7 | 1195500.0 | 822282.0 | 878180.0 | -0.00 | ✓·✓lb |
| case4601_goc | typ | 4601 | 793813.9 | 793810.0 | 821853.5 | 826240.0 | -0.01 | ✓·✓lb |
| case4619_goc | api | 4619 | 1010784.9 | 1010800.0 | 971704.9 | 1068800.0 | -0.01 | ✓·✓lb |
| case4619_goc | sad | 4619 | — | — | 474687.4 | 484350.0 | -0.01 | inf✓·✓lb |
| case4619_goc | typ | 4619 | 457436.6 | 457440.0 | 472392.3 | 476700.0 | -0.01 | ✓·✓lb |
| case4661_sdet | api | 4661 | 2670623.1 | 2670600.0 | 2623054.3 | 2731500.0 | 0.00 | ✓·✓lb |
| case4661_sdet | sad | 4661 | — | — | 2216647.8 | 2261000.0 | 0.00 | inf✓·✓lb |
| case4661_sdet | typ | 4661 | 2216303.9 | 2216300.0 | 2206725.8 | 2251300.0 | -0.01 | ✓·✓lb |
| case4837_goc | api | 4837 | 1209606.3 | 1209600.0 | 1200021.1 | 1291900.0 | -0.01 | ✓·✓lb |
| case4837_goc | sad | 4837 | — | — | 869666.7 | 877120.0 | -0.00 | inf✓·✓lb |
| case4837_goc | typ | 4837 | 850397.1 | 850400.0 | 868168.2 | 872260.0 | -0.00 | ✓·✓lb |
| case4917_goc | api | 4917 | 1703464.0 | 1703500.0 | 1434729.5 | 1717400.0 | 0.08 | ✓·✓lb |
| case4917_goc | sad | 4917 | 1384325.4 | 1384300.0 | 1354002.9 | 1389000.0 | -0.00 | ✓·✓lb |
| case4917_goc | typ | 4917 | 1383656.1 | 1383700.0 | 1353066.3 | 1387800.0 | 0.00 | ✓·✓lb |
| case5658_epigrids | api | 5658 | 1306345.0 | 1306300.0 | 1318822.6 | 1326800.0 | 0.00 | ✓·✓lb |
| case5658_epigrids | sad | 5658 | — | — | 1206615.6 | 1235800.0 | 0.00 | inf✓·✓lb |
| case5658_epigrids | typ | 5658 | 1195466.5 | 1195500.0 | 1205418.3 | 1207300.0 | -0.00 | ✓·✓lb |
| case6468_rte | api | 6468 | 2343290.4 | 2343300.0 | 2436575.4 | 2452700.0 | 0.01 | ✓·✓lb |
| case6468_rte | sad | 6468 | 1982819.8 | 1982800.0 | 2046409.5 | 2069700.0 | 0.01 | ✓·✓lb |
| case6468_rte | typ | 6468 | 1982819.8 | 1982800.0 | 2046150.9 | 2069700.0 | 0.01 | ✓·✓lb |
| case6470_rte | api | 6470 | 2604137.7 | 2604100.0 | 2685118.4 | 2720900.0 | 0.01 | ✓·✓lb |
| case6470_rte | sad | 6470 | 2139078.9 | 2139100.0 | 2198690.4 | 2241600.0 | 0.00 | ✓·✓lb |
| case6470_rte | typ | 6470 | 2136096.9 | 2136100.0 | 2198106.4 | 2237600.0 | 0.01 | ✓·✓lb |
| case6495_rte | api | 6495 | 2924495.6 | 2924500.0 | 3015652.0 | 3130400.0 | 0.01 | ✓·✓lb |
| case6495_rte | sad | 6495 | 2561805.3 | 2561800.0 | 2604047.9 | 3067800.0 | 0.01 | ✓·✓lb |
| case6495_rte | typ | 6495 | 2561788.8 | 2561800.0 | 2604047.9 | 3067800.0 | 0.01 | ✓·✓lb |
| case6515_rte | api | 6515 | 2959744.8 | 2959700.0 | 3057632.1 | 3129200.0 | 0.01 | ✓·✓lb |
| case6515_rte | sad | 6515 | 2559524.4 | 2559500.0 | 2644411.0 | 2869800.0 | 0.00 | ✓·✓lb |
| case6515_rte | typ | 6515 | 2559330.7 | 2559300.0 | 2644404.0 | 2825500.0 | 0.01 | ✓·✓lb |
| case7336_epigrids | api | 7336 | 1980097.5 | 1980100.0 | 1998086.3 | 2035500.0 | -0.00 | ✓·✓lb |
| case7336_epigrids | sad | 7336 | — | — | 1878684.1 | 1888400.0 | -0.01 | inf✓·✓lb |
| case7336_epigrids | typ | 7336 | 1855900.1 | 1855900.0 | 1878683.5 | 1882400.0 | -0.00 | ✓·✓lb |
| case8387_pegase | api | 8387 | 4974002.4 | 4975900.0 | 4637519.2 | 5242800.0 | 1.42 | ✓·lb |
| case8387_pegase | sad | 8387 | 2595938.7 | 2597700.0 | 977279.0 | 2803900.0 | 5.43 | ✓·lb |
| case8387_pegase | typ | 8387 | 2501101.0 | 2502800.0 | 822867.9 | 2771400.0 | 6.20 | ✓·lb |
| case9241_pegase | api | 9241 | 6814526.0 | 6816300.0 | 6840605.9 | 7067900.0 | 0.27 | ✓·✓lb |
| case9241_pegase | sad | 9241 | — | — | 6084311.7 | 6318500.0 | 1.24 | inf✓·lb |
| case9241_pegase | typ | 9241 | 6027173.8 | 6028700.0 | 6075800.7 | 6243100.0 | 0.14 | ✓·✓lb |
| case9591_goc | api | 9591 | 1470245.0 | 1470200.0 | 1346495.0 | 1570300.0 | -0.01 | ✓·✓lb |
| case9591_goc | sad | 9591 | — | — | 1055855.4 | 1167400.0 | -0.01 | inf✓·✓lb |
| case9591_goc | typ | 9591 | 1030938.8 | 1030900.0 | 1055119.5 | 1061700.0 | -0.00 | ✓·✓lb |
| case10000_goc | api | 10000 | 2499133.4 | 2499100.0 | — | — | — | ✓·— |
| case10000_goc | sad | 10000 | — | — | — | — | — | inf✓·— |
| case10000_goc | typ | 10000 | 1346114.3 | 1346100.0 | — | — | — | ✓·— |
| case10192_epigrids | api | 10189 | 1856478.6 | 1856500.0 | 1849684.2 | 1977700.0 | -0.01 | ✓·✓lb |
| case10192_epigrids | sad | 10189 | — | — | 1673015.6 | 1720200.0 | -0.01 | inf✓·✓lb |
| case10192_epigrids | typ | 10189 | 1665551.1 | 1665600.0 | 1672591.5 | 1686900.0 | -0.00 | ✓·✓lb |
| case10480_goc | api | 10480 | 2712412.5 | 2712400.0 | 2709085.2 | 2863500.0 | -0.01 | ✓·✓lb |
| case10480_goc | sad | 10480 | — | — | 2286465.7 | 2314700.0 | -0.01 | inf✓·✓lb |
| case10480_goc | typ | 10480 | 2215826.2 | 2215800.0 | 2286386.9 | 2314600.0 | -0.01 | ✓·✓lb |
| case13659_pegase | api | 13659 | 9117036.9 | 9126300.0 | 9198494.6 | 9385800.0 | 0.15 | ✓·✓lb |
| case13659_pegase | sad | 13659 | — | — | 8829708.1 | 9042200.0 | 0.67 | inf✓·lb |
| case13659_pegase | typ | 13659 | 8763111.4 | 8769900.0 | 8817544.3 | 8948000.0 | 0.07 | ✓·✓lb |
| case19402_goc | api | 19402 | 2458064.0 | 2458100.0 | 2449028.3 | 2583700.0 | -0.01 | ✓·✓lb |
| case19402_goc | sad | 19402 | 1910224.6 | 1910200.0 | 1954400.7 | 1983800.0 | -0.01 | ✓·✓lb |
| case19402_goc | typ | 19402 | 1897794.8 | 1897800.0 | 1954308.1 | 1977800.0 | -0.00 | ✓·✓lb |
| case20758_epigrids | api | 20758 | 3034846.9 | 3034800.0 | 3042940.7 | 3126500.0 | -0.01 | ✓·✓lb |
| case20758_epigrids | sad | 20758 | — | — | 2612726.1 | 2638200.0 | -0.00 | inf✓·✓lb |
| case20758_epigrids | typ | 20758 | 2572328.1 | 2572300.0 | 2608669.6 | 2618600.0 | -0.01 | ✓·✓lb |
| case24464_goc | api | 24464 | — | — | 2560710.2 | 2684000.0 | -0.01 | —·✓lb |
| case24464_goc | sad | 24464 | — | — | 2605377.1 | 2654000.0 | -0.01 | —·✓lb |
| case24464_goc | typ | 24464 | 2512811.8 | 2512800.0 | 2604171.3 | 2629500.0 | -0.01 | ✓·✓lb |
| case30000_goc | api | 30000 | 1700874.2 | 1700900.0 | — | — | — | ✓·— |
| case30000_goc | sad | 30000 | 1297528.4 | 1297500.0 | — | — | — | ✓·— |
| case30000_goc | typ | 30000 | 1092116.1 | 1092100.0 | — | — | — | ✓·— |
Sensitivity parity (finite differences)
Per size band. adj−fwd is the largest observed difference between the two implicit solve directions. FdClean and coupled are central difference relative errors per parity class, shown as median/worst. Coupled cells contain reactive quantities, squared voltage, or conductance. A central difference that crosses an active set boundary measures two local regimes, so the table keeps both median and worst values. Columns below the stated numerical floor are skipped.
| band | formulation | cases | cells | FD cols | worst adj−fwd | FdClean med/worst | coupled med/worst |
|---|---|---|---|---|---|---|---|
| 100–1k | ac | 30 | 51 | 294 | 7.4898e-14 | 8.4965e-9 / 8.7898e-7 | — |
| 100–1k | dc | 30 | 22 | 16 | 2.6468e-13 | 4.4498e-2 / 9.2257e-1 | — |
| 100–1k | socwr | 30 | 300 | 1127 | 1.6558e-7 | 2.6745e-3 / 5.1572e5 | 8.4129e-2 / 4.1153e2 |
| 1k–10k | ac | 3 | 6 | 36 | 1.6549e-15 | 7.1479e-9 / 9.2028e1 | — |
| 1k–10k | dc | 3 | 2 | 1 | 8.6689e-21 | 8.0256e-5 / 8.0256e-5 | — |
| 1k–10k | socwr | 3 | 30 | 116 | 1.3965e-10 | 1.9303e-1 / 1.3502e0 | 1.7219e0 / 6.1760e0 |
| <100 | ac | 33 | 78 | 329 | 1.1102e-15 | 1.4183e-8 / 2.6527e-6 | — |
| <100 | dc | 33 | 26 | 48 | 1.6409e-10 | 4.0039e-6 / 1.1623e1 | — |
| <100 | socwr | 33 | 330 | 999 | 9.8460e-7 | 4.9246e-5 / 3.4204e1 | 3.9313e-3 / 2.8670e2 |
Performance (wall time per stage)
Median milliseconds per stage, by size band. Solves are single-threaded.
| band | cases | parse | build | DC | SOCWR | AC PF | sens |
|---|---|---|---|---|---|---|---|
| <100 | 33 | 0.08 | 0.04 | 0.74 | 4.73 | 0.22 | 385.35 |
| 100–1k | 30 | 0.28 | 0.18 | 7.64 | 65.45 | 9.60 | 6125.20 |
| 1k–10k | 108 | 2.81 | 2.13 | 165.68 | 1444.30 | 507.97 | 0.00 |
| >10k | 24 | 17.78 | 21.60 | 1297.56 | 10187.04 | 3892.62 | 0.00 |
Limits and skips
Every capped, failed, or caveated case, with its reason (no silent truncation).
| case | var | buses | status | notes |
|---|---|---|---|---|
| case5_pjm | sad | 5 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case14_ieee | sad | 14 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case30_as | sad | 30 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case30_ieee | sad | 30 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case57_ieee | sad | 57 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case60_c | sad | 60 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case89_pegase | sad | 89 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case118_ieee | sad | 118 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case179_goc | sad | 179 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 4.857e1 over 5 starts |
| case197_snem | sad | 197 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case200_activ | sad | 200 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case240_pserc | sad | 240 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; AC power flow did not converge: best mismatch 7.232e1 over 5 starts |
| case500_goc | sad | 500 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case588_sdet | sad | 588 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case793_goc | sad | 793 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case1354_pegase | sad | 1354 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf. |
| case1803_snem | sad | 1803 | caveat | DC 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_rte | sad | 1951 | caveat | DC 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_goc | sad | 2000 | caveat | DC 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_goc | sad | 2312 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2312 exceed –max-sens-bus 1500 |
| case2383wp_k | sad | 2383 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2383 exceed –max-sens-bus 1500 |
| case2736sp_k | sad | 2736 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2736 exceed –max-sens-bus 1500 |
| case2737sop_k | sad | 2737 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2737 exceed –max-sens-bus 1500 |
| case2746wop_k | sad | 2746 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2746 exceed –max-sens-bus 1500 |
| case2746wp_k | sad | 2746 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2746 exceed –max-sens-bus 1500 |
| case2848_rte | sad | 2848 | caveat | DC 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_sdet | sad | 2853 | caveat | DC 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_rte | sad | 2868 | caveat | DC 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_pegase | sad | 2869 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 2869 exceed –max-sens-bus 1500 |
| case3012wp_k | sad | 3012 | caveat | DC 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_k | sad | 3120 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 3120 exceed –max-sens-bus 1500 |
| case3970_goc | sad | 3970 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 3970 exceed –max-sens-bus 1500 |
| case4020_goc | sad | 4020 | caveat | DC 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_goc | sad | 4619 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 4619 exceed –max-sens-bus 1500 |
| case4661_sdet | sad | 4661 | caveat | DC 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_goc | sad | 4837 | caveat | DC 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_epigrids | sad | 5658 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 5658 exceed –max-sens-bus 1500 |
| case7336_epigrids | sad | 7336 | caveat | DC infeasible (DC OPF solve infeasible: PrimalInfeasible); consistent with BASELINE inf.; sensitivity skipped: buses 7336 exceed –max-sens-bus 1500 |
| case9241_pegase | sad | 9241 | caveat | DC 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_goc | sad | 9591 | caveat | DC 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_goc | api | 10000 | caveat | SOCWR 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_goc | sad | 10000 | caveat | DC 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_goc | typ | 10000 | caveat | 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 |
| case10192_epigrids | sad | 10189 | caveat | DC 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_goc | sad | 10480 | caveat | DC 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_pegase | sad | 13659 | caveat | DC 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_epigrids | sad | 20758 | caveat | DC 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_goc | api | 24464 | failed | DC 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_goc | sad | 24464 | failed | DC 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_goc | api | 30000 | caveat | SOCWR 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_goc | sad | 30000 | caveat | SOCWR 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_goc | typ | 30000 | caveat | SOCWR 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_epigrids | api | 78484 | skipped | timed out after 180s |
| case78484_epigrids | sad | 78484 | skipped | timed out after 180s |
| case78484_epigrids | typ | 78484 | skipped | timed 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_PATHand 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.
PowerIO 0.11 consumer review
Tellegen uses the published PowerIO 0.11.1 crates. All six PowerIO components
resolve from crates.io at 0.11.1 with registry checksums. The release tag
v0.11.1 identifies
commit 432f795a3cc10ad3ee2ed5041406e562e6eb929b.
evidence/webmcp/powerio-releases.json records published release revisions.
Pull requests that change the dependency manifests, lockfile, or pin scripts
run the PowerIO Candidate workflow against the committed dependency set.
Manual dispatch can test a full PowerIO commit SHA using temporary Git patches
and a disposable lockfile. Both paths verify one common version and source for
all six components before exercising Rust, WebAssembly, WebMCP, and browser
integration.
Tellegen consumes PowerIO modules at its public entry points. DcNetwork and
AcNetwork are private solver workspaces built from a PowerIO problem
instance. The browser and CLI save PowerIO case and solution modules; Tellegen
uses PowerIO IR for portable networks and solutions. Its optional experiment
journal records browser tool activity and never replaces the electrical module. PowerIO IR
text is written and read through tellegen::ir, which calls
powerio::serialize and powerio::deserialize.
Release, IR, and ABI versions
The PowerIO release, stored IR generation, and C ABI are independent:
| Concern | Current value | Tellegen integration |
|---|---|---|
| Rust crate release | 0.11.1 | dependency requirement 0.11.1 |
| Stored IR | "schema": "pio-ir", "version": 2 | sole durable browser and CLI JSON boundary |
| Producer | powerio 0.11.1 | records which release wrote an IR document |
| C ABI | 7 | unchanged and not used as an IR or crate version |
The historical powerio.module version 1 document and bare balanced-network
model JSON are not current input formats. Regenerate those documents from
their original case data. Checked-in evidence produced with the old candidate
is historical and must be rerun before it is cited for this release.
Current module API
Tellegen uses the v0.11 facade directly:
powerio::parse(input)for automatic routing andpowerio::parse_with_options(input, &ParseOptions)for an explicit format;powerio::serializeandpowerio::deserializefor.pio.jsondocuments;PioModule::value()anddiagnostics()for reads, withvalue_mut()for edits;PioModule::try_map_valuefor typed narrowing;PioValue::type_name()for canonical structural type names;powerio::emitfor grid exchange formats.
Calling value_mut() is material to correctness: PowerIO drops retained
source bytes and severs value source-map targets before an in-place edit.
Tellegen therefore applies geographic data and auxiliary substation locations
through the retained module, then serializes that updated module back to the
browser.
PowerIO JSON classification now has five families: module, transmission,
distribution, ambiguous, and unknown. The removed model-json family is
not recreated in Tellegen. A PowerWorld .pwd display also follows the
universal parse route and narrows to powerio.GeoLayer.
Browser boundary
Every solvable browser payload carries module_json. That generation-2 IR is
used for Study construction, geographic transforms, saved cases, and exact
solution modules. Counts, topology, and map views remain derived response data;
there is no separately serialized network_json integration point.
This keeps provenance and mutation behavior intact across the whole browser flow. It also prevents a stale generation-1 shape from being accepted by a display helper while the solver receives a different module.
Preparation and results
Tellegen continues to consume typed PowerIO problem instances and shared numerical preparation. Objective and constraint selections, persistent identities, analysis-to-source row mappings, three-winding lowering, and declared thermal limits come from PowerIO. DC and AC solution modules retain convention-neutral demand marginals and terminal multipliers.
When Tellegen commits edits, it retains valid module diagnostics, history,
extensions, and producer data; replaces the module value with the committed
network; and severs obsolete source targets. A saved exact solution contains
the amended OPF instance that was solved and is emitted as generation-2
pio-ir with value type powerio.DcOpfSolution.
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.
Case summaries report canonical PowerIO row counts as n_bus/n_branch and
rendered, three-winding-lowered counts as
n_analysis_bus/n_analysis_branch. Older servers may omit the analysis
counts.
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, optionalsensitivity, anddone.
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
flock(normally provided by util-linux)- 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 - For manual deployment, GHCR pull access on the host or a public
ghcr.io/eigenergy/tellegenpackage; the workflow authenticates its own pulls
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@sha256:<digest>
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@sha256:<digest> \
"$TELLEGEN_DEPLOY_PATH/data"
The script validates Docker, Compose, the external edge network, that at
least one case directory exists below the canonical deploy root, and the
compose config. A host flock serializes workflow and operator invocations. The
script pulls the selected digest 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.
Before changing the container, the script records a pending snapshot containing the digest, data path, and exact two Compose files. Promotion atomically makes that snapshot the last known good state. Rollback therefore restores the prior Compose configuration as well as its image, and a later invocation recovers an unpromoted candidate left by runner cancellation or SSH loss. The first run migrates a healthy existing deployment from Docker’s Compose file labels.
GitHub Actions Deploy
.github/workflows/deploy.yml runs after a successful CI workflow for a push
to main, or by workflow_dispatch from main. Every job is gated by the
repository variable:
TELLEGEN_DEPLOY_ENABLED=true
Leave that variable unset until the host has staged data and an edge network.
Once enabled, the workflow builds and smoke-tests the
exact commit whose CI passed, then checks that it is still the latest successful
CI push for main. Superseded runs stop without publishing; a newer failing or
in-progress commit does not suppress the last green deployment. The current run
pushes the already smoke-tested local image, resolves its immutable registry
digest, and repeats the latest-green check after any environment wait. It then
copies the deploy files into a unique per-run bundle, invokes the locked
host deploy with that bundle and digest, and checks
${TELLEGEN_DEMO_URL}/api/health. A manual run redeploys the latest green
commit. It checks latest-green state again immediately before promotion; a
candidate superseded during health checks is rolled back.
All demo runs share one non-canceling workflow mutex with the maximum pending queue. Dispatch and start order are not a publication guarantee, so the repeated latest-green checks remain the source of truth for which commit may publish or reach the host. Per-run bundles keep an interrupted remote process from observing files uploaded by its successor; the host lock serializes their use. After a healthy deploy, bundles older than seven days are removed.
Required repository secrets:
TELLEGEN_DEPLOY_HOSTTELLEGEN_DEPLOY_USERTELLEGEN_DEPLOY_SSH_KEYTELLEGEN_DEPLOY_PATH, for example/opt/tellegenTELLEGEN_DEMO_URL, for examplehttps://tellegen.devTELLEGEN_DEPLOY_KNOWN_HOSTS: the pinned SSH host key for the deploy host, the verified output ofssh-keyscan -H <host>or an equivalent known hosts entry. The workflow refuses to deploy without this secret.
The deployment job has read-only package access. It sends its temporary
GITHUB_TOKEN through SSH stdin for docker login, with a private temporary
Docker configuration used only by that deployment or rollback command.
The helper removes the configuration on success, failure, or a handled signal,
and the job token expires when the job finishes. Existing host credentials
remain untouched. No registry token needs to be stored as a repository secret
or kept on the host.
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=0orTELLEGEN_SENSITIVITY_RATE_LIMIT_EVENTS=0only 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,.epc,.pwb,.dss,.pwd,.csv,.json, and.geojsonfiles 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.
- The map basemap comes from CARTO (
*.basemaps.cartocdn.com). Your browser requests those tiles directly, so CARTO receives your IP address, your user agent, and the map area you view. Your case file is never sent, but the map area shows where a locally parsed case sits. Nothing else on the page loads from a third party.
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.
Hosting And Legal Notes
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 sameapps/webbuild.crates/tellegen-tauri: a workspace member that holds a nativeStudyand 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.