g2elin_api and the web UI

g2elin_api is a thin FastAPI layer wrapping the real g2elin_core functions (no mocking). web/ is a build-free, vanilla-JS single-page app that calls it (index.html + app.css + one script per page under web/js/). This page (the one you’re reading, if you got here through the Documentation page) is itself served as part of that same app — see Mount layout below.

Endpoints → pages

The UI loads a preset’s Network once (GET /api/presets/{id}/network) and from then on calls only the /api/network/* endpoints with that (possibly edited) network. The preset-id endpoints below are kept for scripts and tests; both sets share the same analysis.py functions.

        flowchart LR
    subgraph API["g2elin_api (FastAPI)"]
        EP1["GET /api/presets<br/>GET /api/presets/{id}/network"]
        EPT["POST /api/network/topology<br/>POST /api/network/validate"]
        EP2["POST .../powerflow<br/>(solver options)"]
        EP2B["POST .../powerflow/batch"]
        EP3["POST .../modal"]
        EP3X["POST .../modal/sensitivity, mode_shape,<br/>free_response, step_response"]
        EP5["POST .../states"]
        EP6["POST .../emt<br/>(t_pre, linear_overlay)"]
        EP6L["POST .../emt/live<br/>(streamed NDJSON)"]
    end
    subgraph UI["web/ pages (left navigation)"]
        T0["Home"]
        TN["Network"]
        T1["Power Flow"]
        T2["Modal Analysis<br/>(7 sub-pages)"]
        T4["Time-Domain Simulation"]
        T6["Documentation"]
    end
    EP1 --> T0
    EP1 --> TN
    EPT --> TN
    EP2 --> T1
    EP2B --> T1
    EP3 --> T2
    EP3X --> T2
    EP5 --> T4
    EP6 --> T4
    EP6L --> T4

    EPT -.-> TOPO["network.compute_topology_layout()<br/>network.validate_network()"]
    EP2 -.-> PF["powerflow.run_power_flow(**runpp options)"]
    EP2B -.-> TS["timeseries.apply_snapshot() per ramp step"]
    EP3 -.-> PIPE["pipeline.linearize_network()<br/>+ modal.analyze()"]
    EP3X -.-> TOOL["modal toolbox"]
    EP5 -.-> BNM["timedomain.build_nonlinear_network()"]
    EP6 -.-> EMT["timedomain.simulate()<br/>+ scipy.signal.lsim on (A, B, C, D) for the overlay"]
    EP6L -.-> EMTLIVE["timedomain.simulate_steps()"]
    

/api/network/* — arbitrary networks, not just presets

Every endpoint above also has a counterpart under /api/network/* (network_routes.py) taking a full Network JSON body instead of a preset_id path param — POST /api/network/powerflow, /modal, /modal/sensitivity, /modal/parameter_sensitivity, /modal/mode_shape, /modal/free_response, /modal/step_response, /timeseries, /emt, /emt/live, plus /topology and /states as POST (their preset-side counterparts are GET, but a Network body can’t ride a GET request — an intentional divergence).

Three endpoints have no preset-side counterpart because they are about a network’s model order (reduction — model order, and what a time-domain run means), which a preset does not fix: POST /api/network/model reports what the current settings add up to (the model class — EMT, RMS or Mixed — each unit’s resolved level, and the state count against full order), and POST /api/network/model/adequacy builds the same network at full order and says whether the reduction is safe for it. The catalogue they are read against is static, so it is a plain GET /api/model-levels: every element type’s named levels and the state groups behind them, which is what the web UI builds its model-order controls from rather than hard-coding either. A machine’s regulators and a converter’s power-control law arrive on the same response, under regulators — each option carrying the state group (or groups) it contributes, so the UI can redraw the per-group controls when one is swapped without knowing a single model name. There’s no preset-side equivalent of POST /api/network/validate (a preset is always already known-valid) — it runs network.validate_network() (see network) and returns every structural issue found, not just the first, without needing power flow to have run first; the web UI’s network editor calls it on a debounce as the user edits. Since Network (g2elin_core.network.schema) is already a pydantic model, it round-trips to/from JSON for free — a client-edited or from-scratch-built network is just another Network, validated by pydantic before any handler runs (bus references resolve, exactly one slack, etc.), same as any preset.

Both sets of endpoints delegate to the same functions in analysis.py (each takes a Network, returns the exact response model its endpoint returns) so the actual analysis logic exists in exactly one place — main.py’s preset handlers are just “resolve preset_id -> Network (404 on unknown id), then call into analysis.py,” and network_routes.py’s handlers skip straight to the same call with the POSTed Network. Nothing here is persisted server-side: the web UI’s network editor holds the edited Network client-side and resends it whole on every call (see “The network editor,” below) — this is a deliberate scope choice, not a missing feature; see the module docstring in g2elin_api/presets.py.

GET /api/presets/{id}/network returns a preset’s own Network definition verbatim — how the web UI clones a preset into an editable copy.

Response models (g2elin_api/schemas.py) are deliberately separate from g2elin_core’s own result types — PowerFlowResult wraps a live pandapower net, ModalAnalysisResult holds numpy arrays, neither is JSON-serializable — so the wire format is a considered choice, not whatever dict() happened to produce. PowerFlowResponse’s lines/transformers/loads/generators/ static_generators/external_grid fields are loosely typed (list[dict], not a row model per table) since each pandapower result table’s column set genuinely differs (13 columns for lines, 2 for loads) and this is an inspection/dropdown feature, not something else in this codebase consumes programmatically.

/states exists because the nonlinear model’s state/input/output names are entirely preset-dependent — which DER id is the slack, which unit types exist, whether a dw_r_* (synchronous-machine speed deviation) state exists at all — so the EMT tab’s pickers are populated from this endpoint at preset-selection time rather than hard-coded, the same problem find_state_index()’s substring matching solves (which the EMT tab’s perturb-state selection reuses server-side). /emt’s plot_states/plot_inputs/plot_outputs, by contrast, take exact names from that same list (not substrings) — real multi-select pickers over the full list, not a text filter — with an unknown name coming back as a 422 listing what is known, not a silent empty result.

/emt/live is the same request body, streamed instead of returned in one piece — newline-delimited JSON, one line per accepted solver step ({"t": ..., "states": {...}, "inputs": {...}, "outputs": {...}}), a final {"done": true, ...} line on success, or a single {"error": "..."} line if the coupled Newton solve fails partway through (a stream that’s already started can’t turn into an HTTP error status any more, unlike the one-shot endpoint’s clean 422 on the same failure). Backed by timedomain.simulate_steps() — the manually-stepped sibling of simulate(), driving scipy’s low-level OOP stepper class directly instead of the one-shot solve_ivp() wrapper, since only the low-level interface can yield control back between steps. All validation (analysis.prepare_emt_live()) runs synchronously in the route handler before StreamingResponse is constructed, so a bad request still 422s normally rather than producing a broken stream. The web UI’s own EMT tab gates this behind an opt-in “Trace live” checkbox next to the regular “Run EMT simulation” button (default off) rather than a second button — tracing step by step is a real speed trade-off, not just a display preference: a trajectory the one-shot endpoint answers in a couple of seconds can take several minutes traced live (same integration, same tolerance; every step now separately pays a JSON-serialize + HTTP-write + DOM update it wouldn’t otherwise pay), so it needs to be something a user opts into, not something that silently replaces the fast path.

Mount layout

        flowchart TD
    ROOT["FastAPI app"] --> API["/api/*<br/>JSON endpoints (defined in main.py)"]
    ROOT --> MANUAL["/manual<br/>StaticFiles: docs/sphinx/_build/html<br/>(this Sphinx site — build it first, see below)"]
    ROOT --> WEB["/ (catch-all, mounted last)<br/>StaticFiles: web/, html=True"]
    

Mount order matters: FastAPI/Starlette match mounts in registration order, so /api/* routes and the /manual static mount are registered before the catch-all / mount for web/ — otherwise the catch-all would shadow them. /manual (not /docs) was chosen specifically to not collide with FastAPI’s own built-in Swagger UI, which lives at /docs by default and is left alone.

The Sphinx site has to be built before /manual serves anything real — app.py checks whether docs/sphinx/_build/html exists and only mounts it if so, so a fresh checkout without a docs build doesn’t crash the app, it just doesn’t have a Documentation tab content yet. Build it with:

pip install -e ".[docs]"
python tools/build_docs.py

The web UI itself

A fixed left navigation panel with six pages, routed by URL hash (#/home, #/docs/<page>, #/network, #/powerflow, #/modal/<view>, #/emt). Every page works on the same client-held Network (state.network in web/js/core.js); editing it bumps a version counter that invalidates every cached result, and nothing is ever persisted server-side. Always light-themed.

  • Home — what the tool is, its features and workflow, the component models, and the preset list (click one to load it).

  • Documentation — this Sphinx site in an <iframe>. Its table of contents is read from the built index.html and listed as sub-items of Documentation in the app’s own navigation (with the active page’s sections underneath); Furo’s own sidebars are hidden inside the frame so the pages read as part of the app. In-frame navigation is mirrored into the app URL.

  • Network — preset picker (the network is plotted immediately), a drag-and-drop canvas (palette of Bus / Load / SM / GFM / GFL / IB; a unit dropped on or near a bus gets its own terminal bus and transformer; Draw line / Draw transformer modes), and an inspector that opens on the right when an element is clicked, listing every editable Network field for it (bus, its loads and its unit; line; transformer; network-level settings) plus the unit’s derived control parameters (read-only). Live validation (/validate), a bulk table view, and JSON import/export (network + diagram positions). My networks (web/js/saved_networks.js): the network being edited — a modified preset or one built from scratch — can be saved under a name and loaded again later, after the tab or the browser has been closed. A saved network keeps its diagram layout, remembers which preset it started from, and can be renamed, deleted, saved again in place (Save changes) or as a separate copy. Work that has not been saved is also kept, as a draft written a moment after every edit: the next visit offers to restore it (or discard it), so closing the tab mid-edit loses nothing. Everything is stored in the browser’s own storage, not on the server: it is per user, needs no account, and works the same on a laptop and on the hosted app (where a server folder would be shared by every visitor and wiped by every redeploy). The flip side is that it belongs to one browser profile at one address — localhost:8000, 127.0.0.1:8000 and 127.0.0.1:8001 are three different places for it — and that clearing the site’s data removes it. Export all (backup) writes every saved network to one JSON file, and Import a backup (or a single network file from Export JSON) puts them back, on this or any other computer. If the browser blocks storage (a private window does), the section says so and the JSON export remains the way to keep a network. Elements added for the networks above are edited the same way: a unit’s rating (sn_mva, the base its overrides are given on), a transformer’s tap ratio and phase shift (branch transformers only), and a bus’s shunt compensation — a button each for a capacitor bank and a reactor, a table of their own, and a symbol on the diagram (two plates for a bank, a bar for a reactor) carrying a breaker like any other element. Changing a unit’s type drops the parameter overrides the new type does not have and keeps those it does — a machine’s flux linkages mean nothing to a converter, while the two converter types share their filter and inner current loop — and names what it dropped, rather than leaving a network that fails validation. A unit’s control & electrical parameters are editable: changed values are stored as overrides in DerUnit.params (only what differs from the default) and applied on top of sm_params()/gfm_params()/gfl_params() by operating_point.compute_operating_point, so modal analysis and EMT both use them; an unknown parameter name is a validation error. Defaults come from POST /api/units/defaults (unit type, base values, and which regulator models or control law the unit runs — each brings its own parameter names, so the key set depends on them), so they’re available before the unit is connected. POST /api/units/retune re-expresses a converter’s outer loop: for another control law, for another tuning, or both. Every grid-forming law is written in terms of the same droop tuning, so the editor can offer \(m_p\), \(n_q\), \(w_f\) and the equivalent inertia \(H\) on any of them — including the laws that carry no parameter by those names — and can swap a law without throwing the tuning away. The formulas stay in operating_point rather than being mirrored in the frontend. For GFM/GFL units a loop tuner converts response time t_r and damping ζ to Kp/Ki and back with the same pole-placement formulas as the defaults (ωn = 3/(ζ·t_r)). Changing a plant value a loop was tuned against (Rf, Lf, Cf, Cdc, Gdc) does not retune its gains; the tuner flags the affected loops and can re-apply the default t_r/ζ to the new plant. Every per-unit value is shown next to its SI equivalent, and either can be edited (per unit stays the stored value; SI is derived from it with the current base values, so changing a bus voltage or the base power changes the SI figures, not the per-unit ones). Lines show R/X in Ω and B in µS, both for the whole line and per km; transformers in Ω referred to the HV side on their rating; a unit’s R/L/C in Ω/mH/µF on its own base (DC link in mF and µS on the DC base, pu_base.py). A line’s R, X and B are whole-line values, so its length has no effect on the results by itself: editing the length rescales them at constant per-km values (the root-locus sweep of a length does the same). A unit’s Rt/Lt are its own transformer – the element the power flow uses – converted from the transformer’s rating to the system base (operating_point.unit_transformer_rx), and the editor links the two: editing Rt/Lt edits the transformer and vice versa. The network setting MATLAB-compatible transformers (Network.units_use_first_transformer) restores the MATLAB tool’s convention of every unit using the first transformer, to reproduce its results. Breakers (g2elin_core.network.breakers): every line and transformer has one at each end (from_closed/to_closed, hv_closed/lv_closed), every load and unit one at its bus (closed); all closed by default. They are drawn on the diagram (filled square = closed, hollow red = open) and toggle on click — on the Network and Power Flow pages — or from the inspector/table checkboxes. A line or transformer with either breaker open, and a load or unit with its own open, is out of service; so is anything cut off from the slack’s bus (a de-energized island). The slack unit’s breaker can’t be opened. Out-of-service elements are drawn dashed / faded, validation lists them, and every analysis leaves them out: the power flow keeps them as pandapower in_service=False elements (so result tables keep their numbering; de-energized buses read null), and modal analysis, root locus and EMT are built from breakers.energized_network() — the network without them — whose model names keep the full network’s numbers (line #3 is always Ln_4, the second SM always SM_2; breakers.BlockLabels). A unit that is out of service takes its own transformer and terminal bus with it. Any breaker may be opened, the slack unit’s included. Open breakers split the network into islands, and each island that still holds a unit able to set a voltage and a frequency — a synchronous machine, a grid-forming converter or an infinite bus — is solved against its own reference (its own ext_grid), as every load-flow tool requires: the designated slack keeps the role in its island, otherwise the largest grid former takes it. An island left with only grid-following converters and loads is blacked out, since a grid-following converter can only follow a voltage, never start one (and anti-islanding protection would trip it). Only a network with no grid former left at all is an error. In the dynamic models this works because the reference frame is a block of its own (frame.py), one per island, so no unit is load-bearing for the model — EMT can trip the slack and watch the rest island and drift to its own frequency.

  • Power Flow — solver (nr, iwamoto_nr, fdbx, fdxb, gs, bfsw), max iterations, tolerance and initialisation, passed to pandapower.runpp. The network diagram is shown before any run; results colour it as a heatmap (buses on a blue→red diverging scale centred on 1 pu or 0°, lines/transformers light→dark by |P|, |Q|, losses or current) and every element’s own results appear on hover. Batch power flow opens sliders (0–200 %, default 100 %) for load P, load Q and each unit type’s P/Q setpoints; it solves a ramp of independent power flows from the base point to those targets, with a snapshot scrubber (and Play) over the diagram and overview charts. The Show selector sits directly above the results table; clicking an element on the diagram jumps to its row. Every figure and table can be taken out of the page (web/js/exports.js): each one gets a small toolbar in its corner with PNG and SVG for the figure and CSV for the numbers behind it – a chart’s own series, a result table’s rows, or, on the root-locus page, the selected mode’s participation across the sweep. A MutationObserver decorates figures as they are drawn, so no page has to opt in. The exported SVG carries its styles on the elements themselves (read off the live page with getComputedStyle), since the app’s rules are descendant selectors that stop matching once the figure stands on its own; the PNG is that SVG drawn onto a canvas at 2x.

In a chart’s legend, clicking an entry hides that signal and clicking it again brings it back; double-clicking shows only it, and double-clicking again restores the rest. A signal hidden this way also leaves the hover tooltip, and the PNG/SVG export follows what is on screen (the CSV always holds every series). Entries drive every chart in their own card, so a scope split over stacked subplots switches together.

Signals, everywhere they are picked, are organised by the element they belong to: a name’s trailing block (..._{SM_2}, ..._{Nd_4}, V_{bus4}, P_from_{line3}) is mapped back to the unit, bus, line or load it names, so a bus’s states and its measurements sit under the same entry (core.js’s elementCatalog()). A perturbation is chosen as element type → element → signal, and the “+ add signal” pickers ask the same way before listing anything. Buses, lines and loads have two states each and are rarely what a study is about, so a tick-box (on by default) keeps their states out of the lists — their measurements are unaffected, and whatever is already plotted or perturbed is never hidden away.

  • Modal Analysis — one linearisation per network version (cached server-side too, see analysis._cached), seven sub-pages: eigenvalue map (+ mode table, click to select a mode; markers grouped by what kind of mode it is), participation heatmap, single-mode participation, sensitivity heatmap, mode shape, free-motion response and step response.

    The sensitivity page answers one more question than its name suggests. ∂λ/∂A_ij says which entries of the state matrix a mode depends on, and an entry is not something anyone can change — it is an expression in the physical parameters. Find them (POST /api/network/modal/parameter_sensitivity) carries the sensitivity through to those parameters by the chain rule, ∂λ/∂p = Σ (∂λ/∂A_ij)(∂A_ij/∂p), and reports what a 1 % change in each one does to the mode’s frequency and damping. It also fills the Built from column: a parameter appears in A_ij exactly when ∂A_ij/∂p is non-zero, so the same pass that ranks parameters also says which ones each entry is made of — including for reduced-order models, where no printed symbolic matrix exists.

    The worked example is the one the algebra predicts. A machine’s swing equation divides by 2H, so H reaches A through that one row and every entry of it carries a factor 1/(2H). An electromechanical mode’s largest sensitivity therefore lands in the rotor-speed row, and the entry ∂(dw_r)/∂(P_m) — which is exactly 1/(2H·ω_r0) — comes back with H and nothing else. Checked against the MATLAB toolbox’s printed symbolic matrix: across all 80 entries and 34 parameters, the numerical detection and the symbolic content agree in every one of the 2720 cases. The last two hold channels — one perturbation (free motion) or one input step (step response) each — and every channel has any number of subplots, each with its own set of signals; crosshairs are synced across a channel’s subplots. Root locus sweeps any one parameter (network, bus, line, transformer, load, unit setpoint or any params.* value) over a range and step: POST /api/network/modal/sweep re-solves the power flow and the eigenvalues at each value (at most 201 values), streaming one NDJSON line per value. Eigenvalues are reordered step to step by a minimum-cost assignment (g2elin_api/sweep.py), so each mode draws as one locus, coloured by the parameter value, on symlog or linear axes; a table ranks the modes the parameter moves most. A value that doesn’t solve is reported for that step and the sweep continues. For GFM/GFL units the sweepable quantities also include each control loop’s response time t_r and damping ζ (and the droop loop’s inertia H and power-filter time constant), as tune.<loop>.<quantity> targets: at each value g2elin_core.tuning re-derives that loop’s Kp/Ki with the same pole-placement formulas as the defaults, holding the loop’s other quantity at its current value. Several parameters can be swept together (extra in the request): each moves from its own start to its stop in lockstep with the main parameter, so every step is their combined effect. Selecting a mode (on the plot or in the table) opens its participation beside the locus, moving: the same reading as the single-mode participation page, played through the sweep, so a mode’s composition can be watched changing with the parameter (one machine handing a swing mode over to another, an inner loop taking over as a gain rises). The bars keep a fixed order — each state’s largest participation over the whole sweep — so only their lengths move, a ring on the locus marks the value being shown, and hovering a point of that locus jumps the bars to it. The factors come from the sweep itself: every value’s eigendecomposition already computes them, so each solved line carries each mode’s top sweep.PARTICIPATION_TOP states (participation: false turns this off if the stream should stay small; about 14 kB per value for a 78-state model). Video records the sweep as it plays: one frame per solved value, the loci growing as the parameter moves, with a caption strip carrying the parameter’s value and — when a mode is selected — that mode’s frequency and damping, in red once it turns negative. The frames are the page’s own plot, serialized exactly as the PNG export is and painted onto a canvas that MediaRecorder captures: MP4 where the browser can write it (Chrome, Edge), WebM otherwise. The canvas is repainted while each value is held, since a canvas left untouched emits no frames and the video would come out a fraction of its intended length. The participation panel records the same way (Video beside its CSV): its bars are HTML rather than a figure, so those frames are drawn straight onto the canvas – the state names, the bars and their values, over the parameter, frequency and damping of that value.

  • Time-Domain Simulation — state offset, input step or network event at T0 = 0, integrated with the nonlinear model. Whether the run is an EMT or an RMS one follows from the model order set on the Network page (reduction — model order, and what a time-domain run means), and the page shows which it is; plots start 0.01 s before T0 (t_pre) so the initial point x0 is visible. Overlay the equivalent linearised response adds the linear model’s response to the same disturbance (linear_overlay: lsim of the full (A, B, C, D) around the same operating point, in absolute units) as dotted lines in the same colours. The network diagram is folded into the page’s network strip (closed by default, and breakers can be operated from it), and Trace live is on by default. The nonlinear model’s initial point is not an exact equilibrium (see timedomain — EMT (nonlinear) time-domain simulation), so the EMT traces include a small initial transient the linear response doesn’t have — the difference EMT(disturbed) − EMT(undisturbed) is what matches the linear response (checked in tests/test_analysis_options.py). Trace live streams the solver steps as before. Limits, and what happens at them. A one-shot run is limited to analysis.EMT_MAX_N_POINTS = 10 000 samples (each costs a Newton solve when inputs, outputs or measurements are plotted), and a live stream to EMT_LIVE_MAX_STEPS = 50 000 solver steps – a bound on what one stream may send, not a physical limit: how many steps an adaptive stiff solver takes depends on the network, and a stiff one (CIGRE) takes far more than a small one. Reaching the live limit is a normal end of the stream, not an error: the final line carries a truncated message saying where it stopped, and the page keeps every plot drawn so far with that message above it. The same holds when the solver fails part-way through: the partial trace stays, with the reason, and only a run that fails before its first step is shown as a bare error. Measurements (g2elin_core.timedomain.measurements, requested with plot_measurements, listed by .../states) are computed from each element’s own model variables at every sample: bus voltage magnitude (pu), angle (deg, equal to the power flow’s at t = 0), frequency (Hz – instantaneous, from the analytic derivative of the voltage angle, and “measured”, through a one-cycle first-order filter) and instantaneous 3-phase voltages (pu); P and Q (pu on the network base) at both ends of each line (π-line terminal flows, charging included), consumed by each load, and injected by each unit at its grid-side bus (which is also its transformer’s HV-side flow); and each unit’s own frequency (rotor, droop or PLL). At the operating point they reproduce the power flow (tests/test_measurements.py). A unit’s LV terminal bus is not a node of the dynamic model, so it has no bus measurements. When 3-phase voltages are plotted the page picks a timestep of about 40 samples per cycle. Network events (perturb_kind: "event", event: {...}; g2elin_core.timedomain.events): a breaker opening on a line, a unit transformer (trips its unit), a load or a unit; a load step (ΔP/ΔQ in % of the load, as a constant impedance at the operating-point voltage); or a phase jump at a bus (rotates that bus’s voltage phasor) or of the infinite-bus source (the grid-code phase-jump test). A breaker opening or load step integrates a post-event model built from the pre-event model’s own component instances — same parameters and setpoints, no new power flow — rewired without the element or with the new load impedance, starting from the pre-event state block by block; whatever the opening islands keeps running on its own. Signals of removed elements read null after T0 (power flows read 0). The slack can’t be tripped. Those two events change the model, so they have no linear overlay (the response says why in linear_note); a phase jump only moves the initial state, so it has one. Tested in tests/test_breakers.py.

Reference

FastAPI backend for G2ELin — P7 (web UI) first slice, plus a network editor / from-scratch builder layer.

Wraps the real g2elin_core compute functions (no mocking): static power flow, linear modal analysis, time-series load flow, and nonlinear EMT time-domain simulation. Two ways to get a Network to run these on:

  • /api/presets/{id}/... — one of the fixed, named presets in presets.py.

  • /api/network/... (network_routes.py) — an arbitrary Network JSON body, e.g. a preset the frontend cloned and edited, or one built from scratch in the drag-and-drop canvas. Nothing here is persisted server-side; the client holds the edited network and resends it whole each call.

Both sets of endpoints delegate to the same functions in analysis.py (each takes a Network and returns the exact response model its endpoint returns) so the actual analysis logic exists in exactly one place. This module’s own handlers are just “resolve preset_id -> Network (404 on unknown id), then call into analysis.py.”

Also serves the plain-JS frontend in web/ as static files alongside the JSON API, and the built Sphinx documentation (if present) at /manual for the frontend’s Documentation tab — see docs/sphinx/api_and_web.md’s “Mount layout” section for why /manual and not /docs (FastAPI’s own Swagger UI already lives there), and why mount order matters (specific routes/mounts before the catch-all /).

g2elin_api.main.list_presets()[source]
Return type:

list[PresetSummary]

g2elin_api.main.get_preset_network(preset_id)[source]

The preset’s own Network definition, verbatim – how the web UI clones a preset into an editable copy (see /api/network/* below).

Parameters:

preset_id (str)

Return type:

Network

g2elin_api.main.get_topology(preset_id)[source]
Parameters:

preset_id (str)

Return type:

TopologyResponse

g2elin_api.main.list_powerflow_algorithms()[source]

Solver ids accepted by PowerFlowOptions.algorithm -> display label.

Return type:

dict[str, str]

g2elin_api.main.list_solvers()[source]

The time-domain integrators this build offers, with what each one is good for, plus the default tolerances. Static, like /api/model-levels.

Return type:

SolversResponse

g2elin_api.main.list_model_levels()[source]

The model-order reduction catalogue: every element type’s named levels and the state groups they are built from. Static, so the web UI fetches it once and builds its level pickers and per-state controls from it rather than hard-coding either.

Return type:

ModelLevelsResponse

g2elin_api.main.unit_defaults(req)[source]

A unit type’s default electrical/control parameters for given base values – no network needed, so the editor can show and edit a unit’s parameters before it’s connected (or while the network is incomplete).

Parameters:

req (UnitDefaultsRequest)

Return type:

UnitDefaultsResponse

g2elin_api.main.unit_retune(req)[source]

A converter’s outer-loop parameters, re-expressed.

Every grid-forming law’s gains are written in terms of the same droop tuning, which is what makes them comparable. So the editor can offer mp, nq, wf and the equivalent inertia H on any of them – including the ones that carry no parameter by those names – and can swap a law without throwing the tuning away. The formulas live in operating_point and are not mirrored in the frontend.

Parameters:

req (UnitRetuneRequest)

Return type:

UnitRetuneResponse

g2elin_api.main.run_powerflow(preset_id, req=None)[source]
Parameters:
Return type:

PowerFlowResponse

g2elin_api.main.run_powerflow_batch(preset_id, req)[source]
Parameters:
Return type:

BatchPowerFlowResponse

g2elin_api.main.run_modal(preset_id)[source]
Parameters:

preset_id (str)

Return type:

ModalResponse

g2elin_api.main.run_modal_sensitivity(preset_id, req)[source]
Parameters:
Return type:

SensitivityResponse

g2elin_api.main.run_modal_mode_shape(preset_id, req)[source]
Parameters:
Return type:

ModeShapeResponse

g2elin_api.main.run_modal_free_response(preset_id, req)[source]
Parameters:
Return type:

FreeResponseResponse

g2elin_api.main.run_modal_step_response(preset_id, req)[source]
Parameters:
Return type:

StepResponseResponse

g2elin_api.main.run_timeseries(preset_id)[source]
Parameters:

preset_id (str)

Return type:

TimeSeriesResponse

g2elin_api.main.list_states(preset_id)[source]

State names for the nonlinear (EMT) model — lets the frontend populate its state pickers without hard-coding per-preset names (which differ: e.g. only synchronous machines have dw_r_* states, and which DER id is the slack varies by preset).

Parameters:

preset_id (str)

Return type:

StatesResponse

g2elin_api.main.run_emt(preset_id, req)[source]
Parameters:
Return type:

EmtResponse

g2elin_api.main.run_emt_live(preset_id, req, request)[source]

Newline-delimited JSON, one line per solver step – see analysis.emt_live_stream’s own docstring for the exact line shapes. Validation (prepare_emt_live) runs here, synchronously, before the streaming response is constructed, so a bad request still gets a normal 422 instead of a broken stream.

Parameters:
Return type:

StreamingResponse

Network-based analysis cores, shared by the preset-id-keyed endpoints in main.py and the arbitrary-Network-JSON endpoints in network_routes.py – both are “run this analysis on this Network,” differing only in how the Network is obtained (a fixed preset vs. an arbitrary POSTed body). Every function here takes an already-built Network and returns the exact response model its endpoint returns, so neither caller re-implements request validation or error handling.

g2elin_api.analysis.bus_rows(result)[source]
Parameters:

result (PowerFlowResult)

Return type:

list[BusRow]

g2elin_api.analysis.powerflow_tables(result)[source]

Shared by the powerflow and timeseries responses below – both wrap the exact same six other-pandapower-tables extraction, one per snapshot for the latter.

Parameters:

result (PowerFlowResult)

Return type:

dict

g2elin_api.analysis.topology_response(network)[source]

Bus/line/transformer layout only – no power flow needed, cheap.

Parameters:

network (Network)

Return type:

TopologyResponse

g2elin_api.analysis.validate_network_response(network)[source]

Every structural issue found (network.validation.validate_network()), not just the first – a pre-flight check the web UI can call before even attempting power flow/modal/EMT, so a hand-built network’s problems can all be shown at once instead of discovered one crash at a time. Doesn’t run power flow itself, so this is cheap.

Parameters:

network (Network)

Return type:

ValidateResponse

g2elin_api.analysis.energized_or_422(network)[source]

The network the dynamic models are built from: without whatever open breakers switch out (network.breakers.energized_network).

Parameters:

network (Network)

Return type:

Network

g2elin_api.analysis.runpp_kwargs(options)[source]

Validates options and maps them to pandapower.runpp keyword arguments (422 on anything out of range, before any solve runs).

Parameters:

options (PowerFlowOptions | None)

Return type:

dict

g2elin_api.analysis.solve_powerflow_or_422(network, kwargs)[source]

run_power_flow already maps non-convergence to converged=False; anything else pandapower raises here is a solver/network mismatch (e.g. backward/forward sweep on a meshed network), reported as a 422 with the solver’s own reason instead of a bare 500.

Parameters:
Return type:

PowerFlowResult

g2elin_api.analysis.powerflow_response(network, options=None)[source]
Parameters:
Return type:

PowerFlowResponse

g2elin_api.analysis.batch_powerflow_response(network, req)[source]

A ramp of req.steps + 1 power flows from the base operating point (every scale factor 1.0) to the requested targets, in equal increments – each snapshot an independent steady-state solve (no dynamics between them), like timeseries_response but with user-chosen load and DER setpoint scaling instead of a fixed load-only sweep.

Parameters:
Return type:

BatchPowerFlowResponse

g2elin_api.analysis.build_modal_from_network(network)[source]

Shared by every modal-analysis response below – all of them are different views over the same (system, modal) pair, not separate models. Memoized per network content (see _cached).

Parameters:

network (Network)

Return type:

tuple[AssembledSystem, ModalAnalysisResult]

g2elin_api.analysis.named_index_or_422(names, name, what)[source]
Parameters:
Return type:

int

g2elin_api.analysis.mode_or_422(modal, mode)[source]
Parameters:
Return type:

None

g2elin_api.analysis.modal_response(network)[source]
Parameters:

network (Network)

Return type:

ModalResponse

g2elin_api.analysis.modal_sensitivity_response(network, req)[source]
Parameters:
Return type:

SensitivityResponse

g2elin_api.analysis.modal_parameter_sensitivity_response(network, req)[source]

Which physical parameter moves the chosen mode, and which parameters the entries of A it is most sensitive to are built from.

Costs two component linearisations per parameter scanned, well under a millisecond each: a full 118-bus scan of 1674 parameters is a few seconds, less than this request’s own eigendecomposition. The request can still be narrowed to particular units or parameters when only those are of interest.

Parameters:
Return type:

ParameterSensitivityResponse

g2elin_api.analysis.modal_mode_shape_response(network, req)[source]
Parameters:
Return type:

ModeShapeResponse

g2elin_api.analysis.modal_free_response_response(network, req)[source]
Parameters:
Return type:

FreeResponseResponse

g2elin_api.analysis.modal_step_response_response(network, req)[source]
Parameters:
Return type:

StepResponseResponse

g2elin_api.analysis.timeseries_response(network)[source]
Parameters:

network (Network)

Return type:

TimeSeriesResponse

g2elin_api.analysis.build_nonlinear_model_from_network(network)[source]

Memoized per network content (see _cached).

Parameters:

network (Network)

Return type:

NonlinearNetworkModel

g2elin_api.analysis.find_state_or_422(model, name_contains)[source]
Parameters:
Return type:

int

g2elin_api.analysis.states_response(network)[source]

State/input/output names for the nonlinear (EMT) model – lets the frontend populate its pickers without hard-coding per-network names (which differ: e.g. only synchronous machines have dw_r_* states, and which DER id is the slack varies).

Parameters:

network (Network)

Return type:

StatesResponse

g2elin_api.analysis.measurement_set(network)[source]

The measurement outputs of network’s EMT model (memoized with it).

Parameters:

network (Network)

Return type:

MeasurementSet

g2elin_api.analysis.linear_overlay(network, run, t, plot_states, plot_inputs, plot_outputs)[source]

The linearized model’s response to the same disturbance the EMT run applied, around the same equilibrium, in absolute (not deviation) units so it overlays the nonlinear trajectories directly: x = x_eq + dx, y = y_eq + C dx + D du. A state perturbation is an initial condition dx(0); an input perturbation is a step du held from t=0 – both one lsim of the full (A, B, C, D).

Relies on the linear and nonlinear models sharing their state/input/ output naming and ordering (both are assembled from the same blocks by the same interconnection code) – checked, not assumed; outputs missing from the linear model are left out rather than guessed.

Parameters:
Return type:

LinearOverlay

g2elin_api.analysis.solvers_response()[source]

The integrator catalogue – what the time-domain page’s solver picker is built from, so it never hard-codes a method name or a default.

Return type:

SolversResponse

g2elin_api.analysis.emt_response(network, req)[source]
Parameters:
Return type:

EmtResponse

g2elin_api.analysis.prepare_emt_live(network, req)[source]

Resolves and validates everything a live EMT run needs – same t_final/dt bounds, same perturbation resolution, same plot_states/plot_inputs/plot_outputs name-checking as emt_response(), reusing the exact same helpers so the two paths can never validate differently. req.dt, when given, is read as a step-size ceiling here (max_step) rather than an exact output sample spacing – see timedomain.emt.simulate_steps’s own docstring for why that mostly controls redraw granularity, not runtime, for a stiff system.

Parameters:
Return type:

_EmtLivePlan

async g2elin_api.analysis.emt_live_stream(plan, perturb_kind, request)[source]

Newline-delimited JSON, one line per accepted solver step: {"t": ..., "states": {...}, "inputs": {...}, "outputs": {...}}, a final {"done": true, ...} line on success, or a single {"error": "..."} line (in place of done) if the coupled Newton solve fails partway through – there’s no partial result worth keeping at that point, matching emt_response()’s own 422 on the same failure, but a stream that’s already started can’t become an HTTP error status any more, so this is the best it can signal that over the wire.

Parameters:
  • plan (_EmtLivePlan)

  • perturb_kind (str)

  • request (Request)

Return type:

AsyncIterator[str]

g2elin_api.analysis.model_levels_response()[source]

The reduction catalogue. Static – it describes the models this build knows how to make, not anything about a particular network – so the web UI fetches it once at boot and never hard-codes a level name or a state group of its own.

Return type:

ModelLevelsResponse

g2elin_api.analysis.model_summary_response(network)[source]

What the network’s current settings add up to.

The model class is derived, never chosen: a run is EMT when the passive network still integrates and every unit keeps its own fast electrical states, RMS when neither does, and Mixed in between. Naming it here rather than in the UI keeps the one definition in one place – it is also what decides whether the time-domain page offers instantaneous 3-phase waveforms, which mean nothing in a quasi-stationary model.

Parameters:

network (Network)

Return type:

ModelSummaryResponse

g2elin_api.analysis.adequacy_response(network, band_hz)[source]

Judge the chosen reduction against this network’s own full-order model (g2elin_core.modal.adequacy). Builds two linearizations and two eigendecompositions, so it is the most expensive endpoint here – it is meant to be run once when a level is chosen, not on every page view.

Parameters:
Return type:

AdequacyResponse

/api/network/* — run any analysis on an arbitrary, client-provided Network instead of one of the fixed presets in presets.py.

Every handler here is a thin wrapper delegating to the same functions in analysis.py the preset-id-keyed endpoints in main.py use — the actual analysis logic lives in exactly one place; this module only differs in where the Network comes from (a POSTed body, not a preset lookup).

Nothing here is persisted server-side: the client (the web UI’s network editor / drag-and-drop builder) holds the Network and resends it whole on every call. A Network body can’t ride a GET request, so every endpoint here is POST, including the two (/topology, /states) whose preset-side counterparts are GET — an intentional, documented divergence, not an oversight.

pydantic validates the incoming network field against the exact same Network model g2elin_core uses (bus-reference integrity, exactly-one-slack, etc.) before any handler here runs, via FastAPI’s default request-body validation — an invalid network never reaches analysis.py at all, it 422s first with pydantic’s own per-field error list (see web/index.html’s formatDetail() for how the frontend renders that list as one readable message).

g2elin_api.network_routes.network_validate(req)[source]

Pre-flight structural check – every issue found, not just the first, and cheap (no power flow run). Meant to be called before the other endpoints, e.g. by the web UI’s network editor, so a hand-built network’s problems can be shown all at once.

Parameters:

req (NetworkRequest)

Return type:

ValidateResponse

g2elin_api.network_routes.network_model(req)[source]

What the network’s model-order settings add up to: the resulting model class (EMT / RMS / Mixed), each unit’s resolved level, and the state count against the full-order one.

Parameters:

req (NetworkRequest)

Return type:

ModelSummaryResponse

g2elin_api.network_routes.network_model_adequacy(req)[source]

Is this reduction safe for this network? Compares the chosen model against the same network at full order – which of the removed states mattered, and how far the surviving modes actually moved.

Parameters:

req (AdequacyRequest)

Return type:

AdequacyResponse

g2elin_api.network_routes.network_topology(req)[source]
Parameters:

req (NetworkRequest)

Return type:

TopologyResponse

g2elin_api.network_routes.network_powerflow(req)[source]
Parameters:

req (NetworkPowerFlowRequest)

Return type:

PowerFlowResponse

g2elin_api.network_routes.network_powerflow_batch(req)[source]
Parameters:

req (NetworkBatchPowerFlowRequest)

Return type:

BatchPowerFlowResponse

g2elin_api.network_routes.network_modal(req)[source]
Parameters:

req (NetworkRequest)

Return type:

ModalResponse

g2elin_api.network_routes.network_modal_sensitivity(req)[source]
Parameters:

req (NetworkSensitivityRequest)

Return type:

SensitivityResponse

g2elin_api.network_routes.network_modal_parameter_sensitivity(req)[source]

Which physical parameter moves this mode. The eigenvalue sensitivity says which entries of A matter; this says what those entries are made of, which is what anyone can actually change.

Parameters:

req (NetworkParameterSensitivityRequest)

Return type:

ParameterSensitivityResponse

g2elin_api.network_routes.network_modal_mode_shape(req)[source]
Parameters:

req (NetworkModeShapeRequest)

Return type:

ModeShapeResponse

g2elin_api.network_routes.network_modal_free_response(req)[source]
Parameters:

req (NetworkFreeResponseRequest)

Return type:

FreeResponseResponse

g2elin_api.network_routes.network_modal_step_response(req)[source]
Parameters:

req (NetworkStepResponseRequest)

Return type:

StepResponseResponse

g2elin_api.network_routes.network_timeseries(req)[source]
Parameters:

req (NetworkRequest)

Return type:

TimeSeriesResponse

g2elin_api.network_routes.network_states(req)[source]
Parameters:

req (NetworkRequest)

Return type:

StatesResponse

g2elin_api.network_routes.network_emt(req)[source]
Parameters:

req (NetworkEmtRequest)

Return type:

EmtResponse

g2elin_api.network_routes.network_emt_live(req, request)[source]

Newline-delimited JSON, one line per solver step – see main.py’s identical preset-side route and analysis.emt_live_stream’s own docstring for the line shapes.

Parameters:
Return type:

StreamingResponse

async g2elin_api.network_routes.network_modal_sweep(req, request)[source]

Root locus: eigenvalues while one parameter steps across a range, streamed as NDJSON (see sweep.sweep_stream for the line shapes). The target and the range are validated here, before streaming starts, so a bad request is a normal 422.

Parameters:
Return type:

StreamingResponse

The set of networks the API exposes.

Mirrors g2elin_core.network.presets — the API layer doesn’t add any new networks, it just exposes them over HTTP. A network editor (accepting arbitrary user-built Network instances) is added alongside this (/api/network/*, see network_routes.py); this module still owns the fixed, named preset catalog those endpoints’ “start from a preset” flow clones from.

class g2elin_api.presets.PresetInfo(id: 'str', name: 'str', description: 'str', build: 'Callable[[], Network]')[source]

Bases: object

Parameters:
id: str
name: str
description: str
build: Callable[[], Network]
g2elin_api.presets.get_preset(preset_id)[source]
Parameters:

preset_id (str)

Return type:

PresetInfo

HTTP response models. Kept separate from g2elin_core’s own types (PowerFlowResult wraps a live pandapower net, ModalAnalysisResult holds numpy arrays — neither is JSON-serializable directly) rather than serializing those internal types, so the wire format is a deliberate choice, not an accident of what happened to be easy to dict().

class g2elin_api.schemas.BusRow(*, bus, vm_pu, va_degree, p_net_gen_mw, q_net_gen_mvar)[source]

Bases: BaseModel

Parameters:
  • bus (int)

  • vm_pu (float | None)

  • va_degree (float | None)

  • p_net_gen_mw (float | None)

  • q_net_gen_mvar (float | None)

bus: int
vm_pu: float | None
va_degree: float | None
p_net_gen_mw: float | None
q_net_gen_mvar: float | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.PowerFlowOptions(*, algorithm='nr', max_iteration='auto', tolerance_mva=1e-08, init='auto')[source]

Bases: BaseModel

Solver settings forwarded to pandapower.runpp – see analysis.POWERFLOW_ALGORITHMS for the accepted algorithm values. Every field’s default is pandapower’s own default, so an empty PowerFlowOptions() solves exactly as before these options existed.

Parameters:
algorithm: str
max_iteration: int | str
tolerance_mva: float
init: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.PowerFlowRequest(*, options=PowerFlowOptions(algorithm='nr', max_iteration='auto', tolerance_mva=1e-08, init='auto'))[source]

Bases: BaseModel

Parameters:

options (PowerFlowOptions)

options: PowerFlowOptions
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.PowerFlowResponse(*, converged, buses, total_losses_mw, lines, transformers, loads, generators, static_generators, external_grid, algorithm='nr', iterations=None, solve_time_s=None)[source]

Bases: BaseModel

Parameters:
converged: bool
buses: list[BusRow]
total_losses_mw: float
lines: list[dict]
transformers: list[dict]
loads: list[dict]
generators: list[dict]
static_generators: list[dict]
external_grid: list[dict]
algorithm: str
iterations: int | None
solve_time_s: float | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModeCategoryInfo(*, id, label, note)[source]

Bases: BaseModel

One of the kinds a mode can be (g2elin_core.reduction.CATEGORIES).

Parameters:
id: str
label: str
note: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModeRow(*, mode, real, imag, undamped_hz, damped_hz, damping_pct, category='mixed', category_share=0.0, category_shares={}, state1, part1_pct, state2, part2_pct, state3, part3_pct)[source]

Bases: BaseModel

Parameters:
mode: int
real: float
imag: float
undamped_hz: float
damped_hz: float
damping_pct: float
category: str
category_share: float
category_shares: dict[str, float]
state1: str
part1_pct: float
state2: str
part2_pct: float
state3: str
part3_pct: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModalResponse(*, n_states, stable, max_real_part, modes, state_names, input_names, output_names, participation, categories=[], reference_modes=[])[source]

Bases: BaseModel

Parameters:
n_states: int
stable: bool
max_real_part: float
modes: list[ModeRow]
state_names: list[str]
input_names: list[str]
output_names: list[str]
participation: list[list[float]]
categories: list[ModeCategoryInfo]
reference_modes: list[int]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SensitivityRequest(*, mode)[source]

Bases: BaseModel

Parameters:

mode (int)

mode: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SensitivityEntryRow(*, row_state, col_state, value)[source]

Bases: BaseModel

Parameters:
row_state: str
col_state: str
value: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SensitivityResponse(*, mode, state_names, matrix, top)[source]

Bases: BaseModel

Parameters:
mode: int
state_names: list[str]
matrix: list[list[float]]
top: list[SensitivityEntryRow]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ParameterSensitivityRequest(*, mode, units=[], parameters=[], n_entries=8)[source]

Bases: BaseModel

Parameters:
mode: int
units: list[int]
parameters: list[str]
n_entries: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ParameterEffectRow(*, unit, unit_label, parameter, value, d_lambda_real, d_lambda_imag, d_freq_hz, d_damping_pct, magnitude)[source]

Bases: BaseModel

Parameters:
unit: int
unit_label: str
parameter: str
value: float
d_lambda_real: float
d_lambda_imag: float
d_freq_hz: float
d_damping_pct: float
magnitude: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.EntryParametersRow(*, row_state, col_state, sensitivity, parameters)[source]

Bases: BaseModel

One high-sensitivity entry of A, and the parameters it is built from.

Parameters:
row_state: str
col_state: str
sensitivity: float
parameters: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ParameterSensitivityResponse(*, mode, eigenvalue_real, eigenvalue_imag, effects, entries, notes=[])[source]

Bases: BaseModel

Parameters:
mode: int
eigenvalue_real: float
eigenvalue_imag: float
effects: list[ParameterEffectRow]
entries: list[EntryParametersRow]
notes: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModeShapeRequest(*, mode)[source]

Bases: BaseModel

Parameters:

mode (int)

mode: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModeShapeResponse(*, mode, states, angles_deg)[source]

Bases: BaseModel

Parameters:
mode: int
states: list[str]
angles_deg: list[float]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.FreeResponseRequest(*, perturb_state, offset=0.05, t_final=2.0, state_filter='dw_r', plot_states=[])[source]

Bases: BaseModel

Parameters:
perturb_state: str
offset: float
t_final: float
state_filter: str
plot_states: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.FreeResponseResponse(*, perturb_state, t, series)[source]

Bases: BaseModel

Parameters:
perturb_state: str
t: list[float]
series: dict[str, list[float]]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.StepResponseRequest(*, input_name, output_name=None, output_names=[], amplitude=0.1, t_final=2.0)[source]

Bases: BaseModel

Parameters:
input_name: str
output_name: str | None
output_names: list[str]
amplitude: float
t_final: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.StepResponseResponse(*, note='', t, y, series={})[source]

Bases: BaseModel

Parameters:
note: str
t: list[float]
y: list[float]
series: dict[str, list[float]]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.TimeSeriesSnapshot(*, label, converged, buses, total_losses_mw, lines, transformers, loads, generators, static_generators, external_grid)[source]

Bases: BaseModel

Parameters:
label: str
converged: bool
buses: list[BusRow]
total_losses_mw: float
lines: list[dict]
transformers: list[dict]
loads: list[dict]
generators: list[dict]
static_generators: list[dict]
external_grid: list[dict]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.TimeSeriesResponse(*, snapshots)[source]

Bases: BaseModel

Parameters:

snapshots (list[TimeSeriesSnapshot])

snapshots: list[TimeSeriesSnapshot]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.BatchPowerFlowRequest(*, options=PowerFlowOptions(algorithm='nr', max_iteration='auto', tolerance_mva=1e-08, init='auto'), load_p_scale=1.0, load_q_scale=1.0, der_scale={}, steps=10)[source]

Bases: PowerFlowRequest

A ramp of power-flow snapshots from the base operating point (every factor 1.0) to the target scale factors below, steps equal increments, base point included – steps + 1 snapshots in total. der_scale is keyed by DER unit type ("sm"/"gfm"/"gfl") and scales that type’s P and Q setpoints; a type left out stays at 1.0.

Parameters:
load_p_scale: float
load_q_scale: float
der_scale: dict[str, float]
steps: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.BatchSnapshot(*, label, converged, buses, total_losses_mw, lines, transformers, loads, generators, static_generators, external_grid, load_p_scale, load_q_scale, der_scale)[source]

Bases: TimeSeriesSnapshot

Parameters:
load_p_scale: float
load_q_scale: float
der_scale: dict[str, float]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.BatchPowerFlowResponse(*, snapshots)[source]

Bases: BaseModel

Parameters:

snapshots (list[BatchSnapshot])

snapshots: list[BatchSnapshot]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.MeasurementInfo(*, name, group, label, unit)[source]

Bases: BaseModel

Parameters:
name: str
group: str
label: str
unit: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.StatesResponse(*, state_names, input_names, output_names, measurements=[])[source]

Bases: BaseModel

Parameters:
state_names: list[str]
input_names: list[str]
output_names: list[str]
measurements: list[MeasurementInfo]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.BusNodeRow(*, id, name, vn_kv, x, y, unit_type, load_p_mw, der_info=None)[source]

Bases: BaseModel

Parameters:
id: int
name: str
vn_kv: float
x: float
y: float
unit_type: str | None
load_p_mw: float | None
der_info: dict | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.TopologyEdgeRow(*, kind, from_bus, to_bus, name, params={})[source]

Bases: BaseModel

Parameters:
kind: str
from_bus: int
to_bus: int
name: str
params: dict
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.TopologyResponse(*, nodes, edges)[source]

Bases: BaseModel

Parameters:
nodes: list[BusNodeRow]
edges: list[TopologyEdgeRow]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkEventSpec(*, kind, element='', index=0, dp_pct=0.0, dq_pct=0.0, bus=None, angle_deg=0.0)[source]

Bases: BaseModel

A network event applied at t = 0 (timedomain.events): a breaker opening, a load step, or a phase jump.

Parameters:
kind: str
element: str
index: int
dp_pct: float
dq_pct: float
bus: int | None
angle_deg: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.EmtRequest(*, perturb_kind='state', perturb_name='', event=None, perturb_offset=0.02, t_final=3.0, dt=None, plot_states=[], plot_inputs=[], plot_outputs=[], plot_measurements=[], t_pre=0.0, linear_overlay=False, stepping='variable', solver='Radau', rtol=0.0001, atol=1e-06, max_step=None, fixed_step=None)[source]

Bases: BaseModel

Parameters:
perturb_kind: str
perturb_name: str
event: NetworkEventSpec | None
perturb_offset: float
t_final: float
dt: float | None
plot_states: list[str]
plot_inputs: list[str]
plot_outputs: list[str]
plot_measurements: list[str]
t_pre: float
linear_overlay: bool
stepping: Literal['variable', 'fixed']
solver: str
rtol: float
atol: float
max_step: float | None
fixed_step: float | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SolverInfo(*, id, label, note, implicit)[source]

Bases: BaseModel

Parameters:
id: str
label: str
note: str
implicit: bool
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SolversResponse(*, solvers, default, default_rtol, default_atol)[source]

Bases: BaseModel

The integrators the time-domain page offers, and the defaults.

Parameters:
solvers: list[SolverInfo]
default: str
default_rtol: float
default_atol: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.LinearOverlay(*, t, series, inputs, outputs)[source]

Bases: BaseModel

Parameters:
t: list[float]
series: dict[str, list[float]]
inputs: dict[str, list[float]]
outputs: dict[str, list[float]]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.EmtResponse(*, perturbed, perturb_kind, dt, state_names, t, series, inputs, outputs, measurements={}, linear=None, linear_note=None)[source]

Bases: BaseModel

Parameters:
perturbed: str
perturb_kind: str
dt: float
state_names: list[str]
t: list[float]
series: dict[str, list[float | None]]
inputs: dict[str, list[float | None]]
outputs: dict[str, list[float | None]]
measurements: dict[str, list[float | None]]
linear: LinearOverlay | None
linear_note: str | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.PresetSummary(*, id, name, description, n_buses, n_lines, n_transformers, n_loads, n_der_units, der_unit_types)[source]

Bases: BaseModel

Parameters:
id: str
name: str
description: str
n_buses: int
n_lines: int
n_transformers: int
n_loads: int
n_der_units: int
der_unit_types: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ErrorResponse(*, detail)[source]

Bases: BaseModel

Parameters:

detail (str)

detail: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkRequest(*, network)[source]

Bases: BaseModel

Body for the network-based endpoints that take no other input today (topology, powerflow, modal, timeseries, states).

Parameters:

network (Network)

network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkPowerFlowRequest(*, options=PowerFlowOptions(algorithm='nr', max_iteration='auto', tolerance_mva=1e-08, init='auto'), network)[source]

Bases: PowerFlowRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkBatchPowerFlowRequest(*, options=PowerFlowOptions(algorithm='nr', max_iteration='auto', tolerance_mva=1e-08, init='auto'), load_p_scale=1.0, load_q_scale=1.0, der_scale={}, steps=10, network)[source]

Bases: BatchPowerFlowRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkParameterSensitivityRequest(*, mode, units=[], parameters=[], n_entries=8, network)[source]

Bases: ParameterSensitivityRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkSensitivityRequest(*, mode, network)[source]

Bases: SensitivityRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkModeShapeRequest(*, mode, network)[source]

Bases: ModeShapeRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkFreeResponseRequest(*, perturb_state, offset=0.05, t_final=2.0, state_filter='dw_r', plot_states=[], network)[source]

Bases: FreeResponseRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkStepResponseRequest(*, input_name, output_name=None, output_names=[], amplitude=0.1, t_final=2.0, network)[source]

Bases: StepResponseRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkEmtRequest(*, perturb_kind='state', perturb_name='', event=None, perturb_offset=0.02, t_final=3.0, dt=None, plot_states=[], plot_inputs=[], plot_outputs=[], plot_measurements=[], t_pre=0.0, linear_overlay=False, stepping='variable', solver='Radau', rtol=0.0001, atol=1e-06, max_step=None, fixed_step=None, network)[source]

Bases: EmtRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SweepTarget(*, element, key=None, field)[source]

Bases: BaseModel

One network parameter to sweep. element is “network”, “bus”, “line”, “transformer”, “load” or “unit”; key identifies the element (bus/unit id, or list index for lines/transformers/loads; unused for “network”); field is the element’s field name, or params.<name> for a unit’s control/electrical parameter (see DerUnit.params).

Parameters:
element: str
key: int | None
field: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SweepExtra(*, target, start, stop)[source]

Bases: BaseModel

A further parameter varied together with the main one: it goes from start to stop in lockstep with the main parameter’s progress, so every step of the sweep is the combined effect of all of them.

Parameters:
target: SweepTarget
start: float
stop: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.SweepRequest(*, target, start, stop, step, extra=[], participation=True)[source]

Bases: BaseModel

Parameters:
target: SweepTarget
start: float
stop: float
step: float
extra: list[SweepExtra]
participation: bool
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkSweepRequest(*, target, start, stop, step, extra=[], participation=True, network)[source]

Bases: SweepRequest

Parameters:
network: Network
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.UnitDefaultsRequest(*, unit_type, sn_mva, f_hz, un_kv, rt_pu=0.0, lt_pu=0.05, exciter=None, pss=None, governor=None, controller=None)[source]

Bases: BaseModel

Everything a unit’s default parameter set depends on – no network needed, so the editor can show a unit’s parameters before it’s wired up. rt_pu/lt_pu are the network’s first transformer (the models’ shared-transformer convention, see operating_point.py).

Parameters:
unit_type: str
sn_mva: float
f_hz: float
un_kv: float
rt_pu: float
lt_pu: float
exciter: str | None
pss: str | None
governor: str | None
controller: str | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.UnitRetuneRequest(*, controller, params, to_controller=None, tuning={})[source]

Bases: BaseModel

Re-express a grid-forming converter’s parameters for another control law, another tuning, or both. See operating_point.gfm_retuned.

Parameters:
controller: str
params: dict[str, float]
to_controller: str | None
tuning: dict[str, float]

Any of mp / nq / wf / H, to set before rebuilding. H wins over wf.

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.UnitRetuneResponse(*, params, tuning)[source]

Bases: BaseModel

Parameters:
params: dict[str, float]
tuning: dict[str, float]

mp, nq, wf and the equivalent inertia H.

Type:

What the result is tuned to

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.UnitDefaultsResponse(*, params)[source]

Bases: BaseModel

Parameters:

params (dict[str, float])

params: dict[str, float]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.NetworkIssueRow(*, severity, message, affects)[source]

Bases: BaseModel

Parameters:
severity: str
message: str
affects: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.IslandRow(*, buses, reference)[source]

Bases: BaseModel

Parameters:
buses: list[int]
reference: int | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ServiceInfo(*, references, islands, energized_buses, lines, transformers, loads, der_units)[source]

Bases: BaseModel

What open breakers leave in service (network.breakers.service_state).

Parameters:
references: list[int]
islands: list[IslandRow]
energized_buses: list[int]
lines: list[bool]
transformers: list[bool]
loads: list[bool]
der_units: dict[int, bool]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ValidateResponse(*, ok, issues, service=None)[source]

Bases: BaseModel

Parameters:
ok: bool
issues: list[NetworkIssueRow]
service: ServiceInfo | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.StateGroupInfo(*, id, label, states, allowed, default, locked, requires=[], note)[source]

Bases: BaseModel

One switchable approximation of an element type (g2elin_core.reduction).

Parameters:
id: str
label: str
states: list[str]
allowed: list[str]
default: str
locked: bool
requires: list[str]
note: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModelLevelInfo(*, id, label, note, modes)[source]

Bases: BaseModel

Parameters:
id: str
label: str
note: str
modes: dict[str, str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.RegulatorOptionInfo(*, id, label, group=None, groups=[])[source]

Bases: BaseModel

One AVR or PSS model a machine can carry, with the state group it contributes – what lets the UI redraw its per-group rows when the model is swapped.

Parameters:
id: str
label: str
group: StateGroupInfo | None

None for a model with no states of its own, such as “no governor”.

groups: list[StateGroupInfo]

Several, for a model that brings more than one group (a converter’s power-control law). Empty when group carries the single one.

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.RegulatorSlotInfo(*, id, label, default, options)[source]

Bases: BaseModel

Parameters:
id: str
label: str
default: str
options: list[RegulatorOptionInfo]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ElementModelInfo(*, kind, label, default_level, levels, groups, regulators=[])[source]

Bases: BaseModel

Parameters:
kind: str
label: str
default_level: str
levels: list[ModelLevelInfo]
groups: list[StateGroupInfo]
regulators: list[RegulatorSlotInfo]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModelLevelsResponse(*, elements)[source]

Bases: BaseModel

The whole reduction catalogue – what the web UI builds its level pickers and per-state-group controls from, so nothing about the available choices is hard-coded in the frontend.

Parameters:

elements (list[ElementModelInfo])

elements: list[ElementModelInfo]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.UnitModelRow(*, id, unit_type, label, level, modes, n_states, exciter=None, pss=None, governor=None, controller=None)[source]

Bases: BaseModel

One unit’s resolved model settings, after network defaults and the unit’s own overrides.

Parameters:
id: int
unit_type: str
label: str
level: str | None
modes: dict[str, str]
n_states: int
exciter: str | None
pss: str | None
governor: str | None
controller: str | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModelSummaryResponse(*, model_class, network_level, network_modes, network_frequency, units, n_states, n_states_full)[source]

Bases: BaseModel

What the current settings add up to: the model class the run will produce, and the size it will be.

Parameters:
model_class: str
network_level: str | None
network_modes: dict[str, str]
network_frequency: str
units: list[UnitModelRow]
n_states: int
n_states_full: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.StateRiskRow(*, state, group, mode_hz, mode_damping_pct, participation)[source]

Bases: BaseModel

Parameters:
state: str
group: str
mode_hz: float
mode_damping_pct: float
participation: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.ModePairRow(*, full_hz, full_damping_pct, reduced_hz, reduced_damping_pct, matched, expected_loss=False, removed_share=0.0, d_hz, d_damping_pct)[source]

Bases: BaseModel

Parameters:
full_hz: float
full_damping_pct: float
reduced_hz: float | None
reduced_damping_pct: float | None
matched: bool
expected_loss: bool
removed_share: float
d_hz: float | None
d_damping_pct: float | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.AdequacyRequest(*, network, band_hz=5.0)[source]

Bases: BaseModel

Parameters:
network: Network
band_hz: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_api.schemas.AdequacyResponse(*, verdict, band_hz, n_states_full, n_states_reduced, removed_states, risks, modes, max_d_hz, max_d_damping_pct, unmatched, stiffness_full, stiffness_reduced, notes)[source]

Bases: BaseModel

Whether this network’s chosen reduction is safe for this network – see g2elin_core.modal.adequacy.

Parameters:
verdict: str
band_hz: float
n_states_full: int
n_states_reduced: int
removed_states: list[str]
risks: list[StateRiskRow]
modes: list[ModePairRow]
max_d_hz: float
max_d_damping_pct: float
unmatched: int
stiffness_full: float
stiffness_reduced: float
notes: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Zooming a plot

Every time-series chart — free and step response, the time-domain traces — can be windowed: pick X, Y or Box above the plot and drag. The rubber band shows exactly what the release will keep, so a horizontal zoom draws full height rather than a box the user then finds was read differently. Reset, or a double-click on the plot, goes back.

A zoom is a window over the data’s measured extent, never a re-measure, so zooming out lands back exactly where it started. The traces are clipped to the plot area, and the crosshair keeps reading the full data underneath — the downsampling that keeps long traces cheap to draw is a drawing detail and never what the tooltip reports.

The power-flow batch chart is deliberately not zoomable: its x axis is a handful of discrete snapshots, and a window over it would say less than the whole thing already does.