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.
Persistent Studies
A Study records a goal, the electrical states explored in pursuit of it, and the exact evidence behind a recommendation. Open Studies in the network view, state a goal, resolve the equipment and weights, and review the interpretation before creating the Study. The editable JSON is the numerical request.
The starting PowerIO calculation instance preserves the inner OPF objective and constraints. The Study objective is a separate scalar expression over its solved prices, dispatch, flows or voltages. Expressions compose weighted observables, sums, scaling, squared target deviations and direct intervention penalties. The engine evaluates the complete derivative with a combined adjoint solve, including the direct penalty terms. It does not build a dense matrix containing all observable/decision pairs.
Explore, compare and apply
Capacity decisions use MW for DC OPF and MVA for SOCWR. Active-demand decisions use MW. Each decision states a stable element identity, bounds and an increment. The shared feasible set adds a weighted absolute-change budget and a maximum number of changed elements. A goal retains its starting anchor: continuing from a candidate measures cumulative changes from that anchor and does not reset the budget. Placement allocates the declared total additional load; redistribution uses paired transfers that preserve total demand.
Find a proposal ranks feasible moves with the current gradient, solves a bounded beam exactly, keeps a verified improvement and recomputes the direction. Every attempted solve consumes the budget, including baseline reconstruction and failed trials. The returned recommendation is the best verified candidate found, not a certificate of global optimality. Active-set changes can invalidate a local derivative. Trial evidence records predictions, exact values, changed active constraints, failures and termination. Derivative evidence includes its regularization and refinement settings.
The Study keeps three separate state pointers:
| Pointer | Meaning |
|---|---|
| Inspecting | The saved state shown on the map and used for the next action |
| Recommended | The best verified candidate from the current proposal |
| Applied | The state explicitly accepted through the Apply action |
Selecting a state or branching from it changes the view. Applying a proposal is
an explicit action. Approval binds the proposal to its goal, base state and
revision; changing any of them expires the approval. A goal revision retains the
previous interpretation and its evidence. Comparisons can evaluate saved
candidates under a selected goal revision. Inspection and attached evidence do
not invent a new electrical state. A challenge can name the recommendation it
assesses with assessed_recommendation.
The saved-state map belongs to the Study. Return to live case returns to the interactive case loaded outside it. Importing or inspecting a Study does not execute imported text or silently overwrite that live case.
Demand edits and base-case reset
Solve demand edit adds a signed MW increment at a permitted bus in the selected saved state. Successive edits accumulate across buses and survive export and reload. Bounds, increments, the absolute-change budget and the number of changed buses apply to the cumulative changes from the goal anchor. A partial placement or redistribution remains a saved candidate until its total is satisfied; it cannot be applied as a recommendation yet. Edit evidence includes the sparse demand-change vector relative to the original network data.
Prepare base-case reset exactly solves the original network input and saves
a reset candidate. Applying that candidate restores the original demand and
ratings without deleting the goal revisions, earlier states or activity. The
original input is distinct from the Study’s starting point, which may already
contain live edits. Native CreateStudy accepts optional base_input for that
original PowerIO IR; without it, the supplied input is the base. Older imported
bundles without retained base data report that reset is unavailable.
The shared operations are edit_demand and restore_base. Both leave the
applied pointer unchanged until an explicit Apply action. Observations,
edits and solves, planning trials, and decisions remain distinct in Activity.
Storage and continuation
Electrical inputs, calculation instances and exact solutions use PowerIO IR generation 2. Study semantics live in the application document. A portable bundle contains a deduplicated SHA-256 artifact map, immutable state and goal records, experiments, evidence references and decisions. Browser storage uses IndexedDB; the native interface saves the same bundle on the filesystem. Completed operations save atomically before the controller publishes them.
Export a Study to continue in another browser or through the native CLI. Import checks document versions, identities, artifact hashes and references, and verifies that saved solution instances agree with their state inputs. Approval tokens never enter the bundle. Older experiment journals import as historical evidence; unavailable electrical states remain explicitly unavailable.
A storage failure reports the failed save and recovery choices. Free storage or export the current saved Study before retrying. Filesystem writers use a revision check and an exclusive lock; inspect a leftover lock’s process before removing it. A second writer cannot silently replace a newer revision.
Cancellation finishes the current exact trial, then saves the completed trials, best candidate and cancelled termination. An exact solve is indivisible, so cancellation latency depends on that trial. Closing or forcibly killing the process before the save can lose the current operation; the previous completed revision remains durable. PowerMCP defaults to a 300-second graceful termination window before forcing a stopped process to exit. Its runtime and cancellation limits are configurable.
Native and agent interfaces
cargo build -p tellegen-cli --features conic
tellegen contract
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 study import another-study.json < portable-study.json
tellegen contract returns generated schemas for CreateStudy, StudyRequest,
StudyBundle and the operation result. Rust is the contract authority; the
browser’s TypeScript types and runtime schemas are generated from those same
records. expected_revision accompanies each mutation.
Browser WebMCP exposes create_study, inspect_study, revise_study_goal,
branch_study, compare_study_states, propose_study and
record_study_evidence, edit_demand and restore_base_case. Inspection returns compact continuation context and
bounded pages of larger records. The browser controls call the same controller.
PowerMCP’s tellegen adapter invokes the native CLI directly and uses the same
request schemas. Its agent interface leaves application to an explicit user
action.
A sensitivity build supports DC OPF and AC power flow Studies. The conic
feature adds SOCWR. Nonlinear AC OPF and multiconductor solving are unavailable;
unsupported objective/formulation combinations fail before exploration.
The capacity WebMCP tools create persistent Studies and use the same bounded
search as propose_study. Their compact response includes study_id for
continuation. The creation solve and every planning solve count toward the
capacity call’s budget. A human capacity approval binds the Study’s goal, starting
state and recommended state. Goal changes expire it; recording inspection evidence
does not. Failed persistence leaves the approval available for a retry.
When a matching live case is open, case inspection, network queries and sensitivity
queries attach evidence to its captured Study state. They create no electrical
state. Queries about a different or edited live case remain independent until a
new Study captures that case. If a different case finishes loading while an
approved capacity change is being saved, the saved Study remains the authoritative
result and the tool reports case_updated: false; the new live case is untouched.
Reproducible large-case declarations and an installed-CLI runner live in
evidence/studies.
The original Texas7k cost curves include decreasing piecewise slopes, which the
convex DC OPF formulation rejects. A separately labelled example constructs and
records their lower convex envelope through PowerIO’s typed network API. It
changes the inner economic model and does not establish an optimum for the
original nonconvex case.
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 |
|---|---|
inspect_case | Read the active case, formulation, solve summary, edit counts, case ID, 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 OPF tools register for the page lifecycle. inspect_case reports
when no solvable case is active; the other general tools require one.
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 invokes the CLI directly.
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.
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 |
| To bus | t_bus, to, to_bus |
| 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 are display overlays. Dropped alone, they show
substation symbols at approximate projected positions. Dropped alongside a
case file that has no coordinates, the substation points join onto buses
through the SubNum field on the bus rows and fill the case’s positions; a
.pwd whose numbers match nothing stays a separate overlay entry.
Data Provenance
The demo cases are synthetic grids from TAMU ACTIVSg and CATS. They are fictional networks built on geographic footprints. ACTIVSg200 and ACTIVSg500 coordinates come from PowerWorld aux exports. ACTIVSg7000 and CATS coordinates come from GIS bus CSVs. These positions are not surveyed infrastructure locations.
| 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 coordinate forms
PowerWorld aux exports have used two coordinate layouts:
- ACTIVSg complete case exports repeat substation latitude and longitude on each
bus row in
Latitude:1andLongitude:1. - Later exports can leave the bus latitude and longitude columns empty and
reference the
Substationtable throughSubNumber.
powerio promotes the bus row form (Latitude:1/Longitude:1) into the typed
Bus.location at parse; tellegen keeps the two fallbacks upstream does not
cover — bare Latitude/Longitude bus columns and the Substation table join
— so dropped files of either form resolve when the data is present.
Buses Sharing Coordinates
Multiple buses can share one substation coordinate. tellegen spreads each group on a deterministic ring of about 400 m around the substation point, ordered by bus id. The group remains visually associated with the substation at network zoom, and individual buses remain hoverable at street zoom.
Explicit Fallback
The embedded fallback (deployment) serves two PGLib cases whose coordinates are synthetic and labeled as synthetic.
Display Data
Case files and display files are separate inputs. MATPOWER .m, PSS/E .raw,
and PowerWorld .aux files describe the network. JSON files are accepted as
geographic files in this release. PowerWorld .pwd files describe a one-line
diagram. tellegen reads coordinates from case files when they exist and reads
diagram positions from display files when they are dropped.
PowerIO 0.11 routes display data through its universal parser. A .pwd source
passed to powerio::parse returns a generation-2 PioModule whose value is a
PioValue::GeoLayer; the older parse_display, parse_display_file, and
DisplayData::PowerWorld(PwdDisplay) integration surface no longer exists.
Reading .pwd
crates/tellegen-wasm/src/lib.rs retains its browser-facing
parse_display(bytes, format) wrapper, but implements it with the universal
powerio::parse entry point and narrows the returned module to GeoLayer.
The frontend reads .pwd files with arrayBuffer() and passes a Uint8Array
to the WebAssembly module.
A dropped .pwd creates a local display entry with substation points only. It
does not create buses, branches, or a solvable case.
Projecting .pwd coordinates
PowerWorld .pwd coordinates are diagram coordinates, not latitude and
longitude. TAMU-generated diagrams use Web Mercator scaled by one constant,
K = 535.81608, with both axes expressed in degrees:
x = K * lon
y = K * mercdeg(lat)
mercdeg(lat) = (180 / pi) * ln(tan(pi / 4 + lat * pi / 360))
The inverse transform is PowerIO’s to_lonlat_from_pwd_mercator; the wasm
parse_display export returns each substation with its projected lon/lat
alongside the raw diagram coordinates, so the frontend never reimplements the
constant. This places checked ACTIVSg diagrams within about 0.02 degrees of
their corresponding named cities. Hand-edited diagrams can differ, so tellegen
labels these positions as approximate.
The canonical geographic document
PowerIO’s standalone geographic document (GeoLayer) carries element
points and branch routes in one coordinate space, keyed by uid, external id,
name, or the branch endpoint pair, written as a GeoJSON FeatureCollection
with the powerio_geo foreign member (.geo.json). tellegen consumes it
rather than defining a separate format: dropped geographic sidecars parse
through its tolerant reader, applied coordinates land on the network
(Bus.location, Branch.route), and the case panel exports the current
layout as a .geo.json layer with provenance stamped.
On top of it, a dropped .pwd sibling already parses to GeoLayer and fills
missing case coordinates through the SubNum join
(apply_substation_points, projected by to_lonlat_from_pwd_mercator), and
layouts computed by tellegen export with synthetic/manual provenance.
Synthetic Layout
Most public OPF test cases do not include geographic coordinates. tellegen uses
synthetic topology layouts in two places: the explicit pglib dev fallback, and
local files parsed in the browser that the user places on the map. The tellegen
backend API marks fallback coordinates with synthetic_coords: true; local
files are labeled in the panel as synthetic layouts.
Tree Short-Circuit for Radial Networks
Before the force pass, the browser layout counts independent cycles on the
deduplicated graph (open switches are already excluded). When the count is at
most max(2, ceil(0.02 * n)), the graph is a tree or nearly one — the shape of
a distribution feeder — and it is drawn as a tidy tree instead: depth grows
along one axis from the root, and each subtree occupies its own contiguous band
of leaf slots on the other, so the drawing is planar and the longest chain
renders as a straight trunk. The root is a source bus when the case identifies
one (multiconductor cases pass their sources as hints), else a BFS diameter
endpoint. Chords of a near-tree draw as plain segments between their placed
endpoints. Meshed networks fall through to the force pass below.
Deterministic Seed
The layout starts from a golden angle spiral on the unit square. A deterministic
jitter of about 1e-4, computed from bus index sines, breaks exact ties. No
random number generator is used.
Force Refinement
tellegen refines the seed with a Fruchterman–Reingold force pass on the unit square:
- repulsion
k^2 / 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.
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
PowerIO v0.11.0 is the baseline for this integration. The release preparation
merged in powerio#482, with
dependency maintenance in
powerio#485.
The final documentation and release checks are in
powerio#491.
Tellegen pins merge revision ca8cfcec8bdc35d083dfc91b0bb9025ac8bb7507
until the component crates are published. The earlier 1.0 candidate in
powerio#454 is no longer the
release target.
Every pull request that changes the temporary pin runs the PowerIO Candidate
workflow at the checked-in revision. Once the workflow is on main, it can also
be dispatched with a proposed full PowerIO commit SHA before a PowerIO release.
The isolated runner resolves all six PowerIO components together, then exercises
the Rust, WebAssembly, WebMCP package, and browser integration. Normal CI also
rejects a manifest and lockfile that name different PowerIO revisions.
After publication, the same check accepts unpatched crates.io dependencies
only when all six components have one version and registry checksums. A manual
candidate run can add temporary Git patches to that published baseline.
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.0 | dependency requirement 0.11 |
| Stored IR | "schema": "pio-ir", "version": 2 | sole durable browser and CLI JSON boundary |
| Producer | powerio 0.11.0 | 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 pin.
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.