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
BaseMvaalone: the specified injection at a bus isPspec = (sum Pg - Pd) / BaseMva,Qspec = (sum Qg - Qd) / BaseMva(src/GridSim.Core/PowerFlow/NewtonShell.cs:NewtonShell.BuildBusSpec). BranchR/X/Bare 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
BaseMvaat stamp time - the only place a bus field is converted. - Voltages are per-unit throughout;
BaseKvonly converts back to kV for display (see the recovery rule above). - Angles are degrees in
Bus.VaDegandPowerFlowResult.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)vstap), 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):
- 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.
- No slack bus - the power flow would be singular.
- A branch endpoint (
From/To) that references a nonexistent bus. - A generator on a nonexistent bus.
- Non-finite numeric inputs:
Pd/Qd/Gs/Bsper bus,R/X/B/TapRatio/ PhaseShiftDegper branch,Pg/Qg/Vgper 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. GeneratorQmin/Qmax/Pmin/Pmaxare 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
ControlsBusdoes not exist, whoseTransformernames 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
- 01-architecture.md - where models come from and go.
- 03-power-flow.md - how the solvers consume these fields.
- 04-sparse-and-gpu-numerics.md - the sparse Y-bus/Jacobian storage.
- ../manual/02-loading-networks.md - the case formats in practice.
- ../manual/15-exports-and-formats.md - how model fields appear in exports.