Technical manual — chapter list

Data model and per-unit conventions

Everything the solvers touch is defined in one file: src/GridSim.Core/Model/Model.cs. The types are immutable - records with value equality and with-expression copies, except GridModel itself, which is a sealed class with init-only properties over IReadOnlyList<T> collections. Immutability is load-bearing: a model can be shared across parallel N-1 solves without defensive copies, and "modifying" a network (an outage, a tap move) means cloning with a changed element list (src/GridSim.Core/Dispatch/Dispatcher.cs:Dispatcher.Clone).

GridModel

Property Type Meaning
BaseMva double (required) The system MVA base. Every per-unit power conversion in the solvers divides by this.
Buses, Generators, Branches required lists The network.
Oltcs list, default empty On-load tap changers (drive the SteadyStateSolver outer loop).
Boundaries list, default empty Transmission boundary cuts with transfer limits.
FrequencyHz double, default 50.0 Nominal system frequency.
Name string, default "grid" Case label carried into exports.
BusCount computed Buses.Count.
SystemInertiaGvaS computed sum H*MBase / 1000 over the generators - the headline GVA.s figure for frequency work.

Bus

Bus(Id, Type, Pd, Qd, Gs, Bs, BaseKv, Vm, VaDeg, Name, X, Y). Loads are folded onto the bus, MATPOWER-style - there is no separate load type.

Field Units / default Meaning
Id - External bus number. Ids are arbitrary; solvers build an id -> index map and results carry BusIds so you can always translate back. Must be unique (validation error otherwise).
Type BusType Slack (Vm and Va fixed, absorbs the power balance), Pv (P and Vm fixed, Q solved), Pq (P and Q fixed, V solved). This enum drives the whole mismatch/Jacobian partition.
Pd, Qd MW, MVAr, default 0 Demand at the bus.
Gs, Bs MW, MVAr at 1.0 pu, default 0 Fixed shunt admittance (e.g. a capacitor bank), entered as the power at 1.0 pu voltage. Stamped on the Y-bus diagonal as (Gs + jBs)/BaseMva (src/GridSim.Core/PowerFlow/YBus.cs:YBus.Build). With that stamp the complex power drawn at voltage V is |V|^2*(Gs - jBs): positive Gs draws MW, positive Bs injects MVAr (capacitive) - the MATPOWER sign convention.
BaseKv kV, default 0 The bus's nominal voltage base. It never enters the solve (all impedances are already per-unit); it exists to recover engineering units afterwards.
Vm pu, default 1.0 Initial voltage magnitude. A start point only: at a voltage-controlled bus with a generator, the generator's Vg setpoint takes precedence; FlatStart overrides the rest with 1.0.
VaDeg degrees, default 0 Initial/fixed voltage angle. Angles are degrees at the model and result surfaces and radians inside the solvers.
Name optional Asset identity, carried through exports for replay.
X, Y 0..1, default 0 Layout coordinates for visualisation (GB bounding box). FastSchematic draws a bus at (X, 1-Y); all-zero coordinates stack every bus at one corner, which is why the bridge topology carries x/y. Never used by any solver.

Actual kV recovery. A solved result stores Vm in per-unit. The physical voltage at result index i is Result.Vm[i] * bus.BaseKv, where the bus is found via Result.BusIds[i]. There is no other place voltage bases are applied.

Generator

Generator(Bus, Pg, Qg, Vg, Qmin, Qmax, Pmin, Pmax, MBase, H, Name, Type).

Field Units / default Meaning
Bus - The bus it sits on. Multiple generators per bus are aggregated by the solvers (NewtonShell.BuildBusSpec sums Pg/Qg/Qmin/Qmax per bus; the last generator's Vg wins as the bus setpoint).
Pg, Qg MW, MVAr Dispatched output. Qg is the nominal reactive point; at PV/slack buses the solve determines the actual Q.
Vg pu, default 1.0 Voltage setpoint, used at Slack and PV buses.
Qmin, Qmax MVAr, default +/-inf Reactive capability. +/-Infinity means "unlimited" and is deliberately exempt from the finite-input validation. Aggregated per bus for the Q-limit outer loop.
Pmin, Pmax MW, defaults 0 / +inf Real-power capability (dispatch, not load flow).
MBase MVA, default 100 Machine base for H.
H MW.s/MVA on MBase, default 0 Inertia constant. Not used by the load flow at all - it feeds the frequency dynamics, where system inertia is sum H*MBase.
Type string Fuel/technology tag, classified by Fuels.Classify for the dashboards.

Branch

Branch(From, To, R, X, B, TapRatio, PhaseShiftDeg, RateAMva, Name, IsTransformer)

  • a line or transformer as a standard pi-model.
Field Units / default Meaning
R, X pu Series impedance on the system base.
B pu, default 0 Total line-charging susceptance; the Y-bus stamp splits it half to each end.
TapRatio default 1.0 Off-nominal tap magnitude. The Y-bus treats TapRatio <= 0 as 1.0. 1.0 with zero shift is a plain line.
PhaseShiftDeg degrees, default 0 Phase-shifting angle; combined with TapRatio into one complex tap.
RateAMva MVA, default 0 Thermal rating. 0 means unrated: LineFlow.LoadingPercent reports 0 rather than dividing by zero.
Name optional Identity; an Oltc references its transformer by this string.
IsTransformer default false Display/classification flag only - the electrical model is entirely TapRatio/PhaseShiftDeg.

Oltc

Oltc(Transformer, ControlsBus, TargetVpu, MinTap, MaxTap, StepPu, DeadbandPu)

  • an on-load tap changer that nudges a transformer's tap to hold a controlled bus near a target, the way a Grid Supply Point holds its distribution-side voltage.
Field Default Meaning
Transformer - The Branch.Name of the transformer it moves.
ControlsBus - The bus whose voltage it regulates; validation expects this to be the transformer's LV (lower-BaseKv) winding.
TargetVpu 1.0 Voltage target.
MinTap, MaxTap 0.9, 1.1 Tap range.
StepPu 0.0125 One tap step.
DeadbandPu 0.01 No movement while |Vm - Target| <= Deadband.

The regulation loop itself lives in SteadyStateSolver (03-power-flow.md); the sign convention is that lowering the tap raises the controlled bus voltage.

Boundary and BoundaryBranch

BoundaryBranch(Branch, Direction = 1) names one branch's contribution to a cut and the sign that counts as positive transfer. Boundary(Name, LimitMw, Branches) is the cut itself (e.g. an ETYS-style B6 Scotland-England boundary): the flow across it is the signed sum of the named branches' solved flows, compared against LimitMw by src/GridSim.Core/Analysis/BoundaryAnalysis.cs.

Per-unit conventions

  • One system base. All power quantities are converted with BaseMva alone: the specified injection at a bus is Pspec = (sum Pg - Pd) / BaseMva, Qspec = (sum Qg - Qd) / BaseMva (src/GridSim.Core/PowerFlow/NewtonShell.cs:NewtonShell.BuildBusSpec). Branch R/X/B are entered already per-unit on that base (and on the branch's own voltage base, as in MATPOWER), so no impedance conversion happens anywhere in the code.
  • Shunts are entered in engineering units (MW/MVAr at 1.0 pu) and divided by BaseMva at stamp time - the only place a bus field is converted.
  • Voltages are per-unit throughout; BaseKv only converts back to kV for display (see the recovery rule above).
  • Angles are degrees in Bus.VaDeg and PowerFlowResult.VaDeg, radians inside every solver loop.
  • Convergence tolerances are per-unit power mismatches (default 1e-8, which on a 100 MVA base is a ~1 W power balance).
  • Results return to engineering units at assembly: PInjMw = P_pu x BaseMva, line flows likewise (NewtonShell.AssembleResult, NewtonShell.ComputeLineFlows).

Y-bus construction

Two builders produce identical stamps: dense (src/GridSim.Core/PowerFlow/YBus.cs:YBus.Build, a Complex[n,n]) and sparse (src/GridSim.Core/PowerFlow/SparseNewtonRaphsonPowerFlow.cs:SparseNewtonRaphsonPowerFlow.BuildY, a ComplexCsr). For each branch:

ys  = 1 / (R + jX)                      series admittance
bc  = j*B/2                             half the line charging, per end
a   = TapRatio (<= 0 treated as 1.0)
tap = a * e^{j*PhaseShiftDeg*pi/180}     complex tap, on the FROM side

Yff = (ys + bc) / a^2
Ytt =  ys + bc
Yft = -ys / conj(tap)
Ytf = -ys / tap

stamped as Y[f,f] += Yff, Y[t,t] += Ytt, Y[f,t] += Yft, Y[t,f] += Ytf. Then each bus shunt adds (Gs + jBs)/BaseMva to its diagonal. Points worth auditing:

  • The ideal transformer sits on the from side (MATPOWER convention). The from-end self term - including the from half of the charging - is referred through the tap by 1/a^2; the to-end self term is unscaled.
  • The phase shift never appears in the self terms (|tap|^2 = a^2); it rotates only the mutual terms, asymmetrically (conj(tap) vs tap), which is what makes a phase shifter shift flow.
  • Parallel branches accumulate - stamps add, so duplicated circuits between the same pair of buses are simply summed admittances.
  • The same four formulas are reused wherever branch flows are needed: NewtonShell.ComputeLineFlows, the BFS result assembly, and the estimator's flow-measurement model - one pi+tap model, no copy-drift.

Model validation

GridModel.Validate() (src/GridSim.Core/Model/Model.cs:GridModel.Validate) structurally checks a model and returns a list of GridDiagnostics; it never throws. Severity semantics:

Errors (would break the solver - entry-point loaders hard-fail on these; the GDA merge pipeline collects them as notes instead):

  1. Duplicate bus ids - duplicates would silently overwrite the solvers' id -> index map, resolving later elements to the wrong physical bus. Reported once per duplicated id.
  2. No slack bus - the power flow would be singular.
  3. A branch endpoint (From/To) that references a nonexistent bus.
  4. A generator on a nonexistent bus.
  5. Non-finite numeric inputs: Pd/Qd/Gs/Bs per bus, R/X/B/TapRatio/ PhaseShiftDeg per branch, Pg/Qg/Vg per generator. A NaN/Inf here feeds the Y-bus/Jacobian and poisons the entire solve, so it must be caught as a model fault, not discovered as a non-finite result. Generator Qmin/Qmax/Pmin/Pmax are deliberately excluded - +/-Infinity is the "no limit" default.

Warnings (informational or a benign misconfiguration):

  • More than one slack - treated as a multi-island system, one reference per island, not an error.
  • An OLTC whose ControlsBus does not exist, whose Transformer names no branch, or which controls the HV-side (higher-BaseKv) winding or a non-endpoint bus. A bad OLTC simply fails to regulate (it is skipped by the loop); when the two winding kV values are equal or unknown the winding check is skipped.

ToString() on a diagnostic renders error: ... / warning: ..., which is the format the CLI prints.

See also