IEEE C37.118.2 Synchrophasor Ingest + Phasor Data Concentrator - Technical Design¶
Status (verified 1.95.169): Partly implemented and experimental: the C37.118.2 DATA decoder, CONFIG-1/2 parser, phasor data concentrator, PDC-to-
MeasurementSetmapper, linear PMU-only estimator and hybrid SCADA/PMU scheduler exist as Core API with tests. Thepmu-ingestverb decodes a recorded multi-PMU capture file (technical ch. 33). There is no live frame transport, no stateful decoder and no per-PMU stream configuration type. Implemented in:src/GridSim.Core/Synchrophasor/C37118Decoder.cs(C37118Decoder,C37118DataFrame,FrameDecodeStatus),C37118ConfigFrame.cs(C37118ConfigParser,C37118ConfigFrame,PmuConfig),C37118Frame.cs(C37118FrameType,C37118DataFormat,Phasor,C37118CommonHeader,C37118Crc),PhasorDataConcentrator.cs(PmuSample,PdcSuperframe,PhasorDataConcentrator),PmuMeasurementSetMapper.cs(PmuChannelMap,PmuVoltageChannel,PmuCurrentChannel,PmuAngleReference,PmuMeasurementSetMapper);src/GridSim.Core/Estimation/LinearPmuStateEstimator.cs(LinearPmuStateEstimator,PmuVoltageMeasurement,PmuCurrentMeasurement,LinearPmuResult);src/GridSim.Core/Estimation/Measurement.cs(MeasurementKind.IAngDeg);src/GridSim.Core/Simulation/HybridMultiRateScheduler.cs(HybridMultiRateScheduler,ScheduledUpdate). Design-to-code name map:
Design name Implemented as C37118FrameType.cs,C37118DataFormat.cs,C37118Crc.cs,C37118CommonHeader.cs,Phasor.csall in C37118Frame.csConfigFrame2.cs,PmuConfig.csC37118ConfigFrame.cs(C37118ConfigFrame,PmuConfig; the parser isC37118ConfigParser)PhasorDescriptor.csnot implemented; PmuConfig.PhasorUnitsandPmuConfig.PhasorScale(index)carry the PHUNIT scalingDataFrame.cs,PmuData.csC37118DataFrameinC37118Decoder.cs(first PMU block only; analogue and digital channels are not decoded)PmuStatus.csnot implemented; the STAT word is the raw ushortC37118DataFrame.Statstateful C37118Decoderholding CONFIG-2not implemented; the static decoder takes the parsed PmuConfigper callPdcSuperframe.csPdcSuperframeinPhasorDataConcentrator.cs(Samplesby PMU id,Droppedlist)IPmuFrameSource.cs,FileReplayPmuFrameSource.csnot implemented PmuStreamConfig.csPmuChannelMapwithPmuVoltageChannel,PmuCurrentChannel,PmuAngleReferenceinPmuMeasurementSetMapper.csMeasurementKind.csMeasurementKindis declared inEstimation/Measurement.csThis is a design record. The current behaviour is documented in the technical manual chapter 18. Real-time, distribution and dynamic estimation (section 18.6).
Governing constraint: UnderstandingRestrictions.md (no forward inference) and
RestrictionReview.Reviewed (src/GridSim.Core/Diagnostics/RestrictionReview.cs) (the Dynamics->Estimation review gate). Read
those first; every decision below is subordinate to them.
This document specifies, to the level an implementer follows without further
research: (a) a byte-exact C37.118.2 frame decoder (CONFIG-2 and DATA frames),
(b) a Phasor Data Concentrator (PDC) that time-aligns several PMU streams with a
bounded wait-window and survives dropouts and latency, (c) the adapter that maps
concentrated phasors into the existing estimator as voltage-magnitude,
voltage-angle and current-phasor measurement rows, (d) a linear single-shot
PMU-only state estimator, and (e) a hybrid multi-rate SCADA(~5 s) + PMU(~0.5 s)
fusion scheduler that fits the RealtimeEstimatorEngine cadence. No C# is
written here - types are named and specified so they land in the existing
code style.
1. Restriction posture (read this before any code)¶
This feature ingests synchrophasors, which in the field are a live, sub-second telemetry stream - exactly the kind of data the one rule forbids GridSim from using to see the present or future of the real GB grid. The design is built so the capability is real (a validated C37.118 decoder + PDC + linear/hybrid PMU estimator) while the forbidden operation is structurally impossible.
The three things this design guarantees, mechanically:
- Every dated synchrophasor input passes
ForwardInferenceGuard.RejectFuturebefore it is estimated. A PMU frame carries an absolute UTC timestamp (SOC + FRACSEC, section 4.4). The PDC stamps each aligned superframe with that UTC instant and the mapping adapter (PmuMeasurementSetMapper, section 10) calls the guard on it. A frame newer thanUtcNow - 169 his recorded and skipped, never mapped, never estimated. Because a live PMU frame is by definition seconds old, the guard rejects every live frame by construction: there is no code path that turns a live synchrophasor into a state estimate. - The feature runs on replay or permitted-age captures only. The transport
layer (
IPmuFrameSource, section 9.1) is fed by a recorded C37.118 byte stream (a capture file / pcap payload replayed frame by frame). A live-socket source may exist as an interface implementation, but its frames still transit the guard, so it can only ever deliver frames that are >= 169 h old - which a live socket never produces. "Real-time" here means the engineering (streaming parse, resident PDC, sub-cycle cadence, resilience), never live sight, exactly asUnderstandingRestrictions.mdsec. 3 permits. - Every estimator introduced here is a filter, not a predictor. The linear
single-shot PMU estimator (section 11) and the hybrid scheduler (section 12)
are measurement-only: they resolve the state at the frame's own timestamp
from that frame's observations. They emit a state stamped at the (present/past)
measurement time, never a future-dated claim. Neither fuses a
Dynamics/process model, so per the review log they are static filters, unconditionally permitted and need noRestrictionReviewEntry. (RestrictionReview.Reviewedalready listslinear-PMUandbranch-currentamong the permitted static methods; this design realises the first of those.) If a future variant ever adds a dynamic predict step (e.g. a PMU-rate EKF tracker), it becomes a Dynamics->Estimation fusion and section 13.3's gate applies - stop and review.
Invariant mapping (against UnderstandingRestrictions.md "rules, as testable
invariants"): #1 - the PDC/mapper is a new dated ingestion seam behind the guard
(section 13); #2 - no output is stamped later than its newest input frame
(section 12.4); #4 - proven on replay/permitted-age only (sections 1.2, 15); #5 -
no Dynamics/ fusion, so the DSE gate is untouched (section 13.3).
2. Scope, non-goals, and standard baseline¶
In scope. C37.118.2-2011 mapping of the 2005 frame syntax: the common frame header, CONFIG-2 configuration frames, DATA frames, fixed (16-bit integer) and floating-point (32-bit IEEE-754) formats, phasors in rectangular and polar form, FREQ/DFREQ in integer and float, the STAT word and its time-quality/sync bits, CRC-CCITT frame integrity, and TVE-aware sigma assignment. A PDC that aligns N PMU streams by timestamp. Mapping to estimator rows. A linear PMU-only estimator and a hybrid multi-rate scheduler.
Out of scope (named as future seams, section 17). CONFIG-1 and CONFIG-3 frames (CONFIG-3 is decoded to the extent needed to reject it cleanly); HEADER frames (skipped, logged); the command channel (CMD frames) beyond a documented request enum - GridSim never drives a live PMU, it replays captures; analog and digital channels are parsed and carried but not mapped to estimator rows; TCP/UDP/multicast socket transport internals (the interface is defined; a file-replay source is the reference implementation).
Standard. IEEE Std C37.118.2-2011 "for Synchrophasor Data Transfer for Power
Systems". All multi-byte integers are network byte order (big-endian). This is
the single most common decoder mistake; use System.Buffers.Binary.BinaryPrimitives
big-endian readers throughout, never BitConverter (which is host-endian).
3. Namespace and file layout¶
New code lives under two existing conventions: transport/decoder/PDC in a new
GridSim.Core.Synchrophasor namespace (folder src/GridSim.Core/Synchrophasor/),
the estimator/scheduler additions in the existing GridSim.Core.Estimation and
GridSim.Core.Simulation namespaces so they sit beside WlsStateEstimator and
RealtimeEstimatorEngine.
src/GridSim.Core/Synchrophasor/
C37118FrameType.cs enum: Data, Header, Config1, Config2, Config3, Command
C37118DataFormat.cs [Flags] enum for the CONFIG FORMAT word (4 bits)
C37118Crc.cs static CRC-CCITT (poly 0x1021, init 0xFFFF)
C37118CommonHeader.cs readonly struct: Sync/Type/Version/FrameSize/IdCode/Soc/FracSec/TimeQuality
C37118Decoder.cs the parser: ReadOnlySpan<byte> -> parsed frames; stateful (holds CONFIG-2)
ConfigFrame2.cs sealed record: TimeBase, DataRate, PmuConfig[]
PmuConfig.cs sealed record: StationName, IdCode, Format, phasor/analog/digital descriptors, FNom, CfgCount
PhasorDescriptor.cs sealed record: Name, Kind(Voltage/Current), ConversionFactor, ScalePu
DataFrame.cs sealed record: CommonHeader-derived TimeUtc + PmuData[]
PmuData.cs sealed record: StatWord, Phasor[], FreqHz, RocofHzPerS, Analog[], Digital[]
Phasor.cs readonly struct: Real, Imag (engineering units) + Magnitude/AngleRad accessors
PmuStatus.cs readonly struct wrapping the STAT word (DataValid, SyncOk, UnlockedTime, ...)
PhasorDataConcentrator.cs the time-aligner (wait-window buffer -> PdcSuperframe stream)
PdcSuperframe.cs sealed record: TimeUtc + per-PMU aligned PmuData (+ per-PMU present/dropped flags)
IPmuFrameSource.cs transport seam: IEnumerable<ReadOnlyMemory<byte>> of raw frames
FileReplayPmuFrameSource.cs reference source: length-delimited capture file
PmuMeasurementSetMapper.cs PdcSuperframe -> MeasurementSet (guarded); PMU->bus/branch crosswalk
PmuStreamConfig.cs per-PMU crosswalk + base kV/A + performance class -> sigma policy
src/GridSim.Core/Estimation/
MeasurementKind.cs + IAngDeg member (branch current phase angle) - section 10.2
LinearPmuStateEstimator.cs rectangular constant-gain single-shot PMU-only WLS - section 11
src/GridSim.Core/Simulation/
HybridMultiRateScheduler.cs SCADA(~5 s) + PMU(~0.5 s) window interleaver - section 12
Style rules the new files follow (matching the existing tree): sealed record
for immutable value carriers, readonly struct for hot parse structs, XML doc
comments on every public member (the 0-warning gate requires them, section 16),
internal for anything the CLI/UI does not need, culture-invariant parsing,
no unsafe.
4. The C37.118.2 frame model¶
4.1 Common frame header (14 bytes, all frame types)¶
Every frame begins with the same header. Offsets are from frame start.
Off Size Field Meaning
0 2 SYNC byte0 = 0xAA (leading sync);
byte1: bit7 reserved(0);
bits6..4 = frame type (see 4.2);
bits3..0 = version number (1 = 2005 mapping, 2 = 2011)
2 2 FRAMESIZE total bytes in the frame, header..CHK inclusive (uint16 BE)
4 2 IDCODE data stream / PMU / PDC id (uint16 BE)
6 4 SOC second-of-century: UNIX time, seconds since 1970-01-01T00:00:00Z (uint32 BE)
10 4 FRACSEC bits31..24 = message time-quality flags;
bits23..0 = fraction-of-second, raw count of 1/TIME_BASE (from CONFIG)
... body ...
end-2 2 CHK CRC-CCITT over bytes [0 .. FRAMESIZE-3] (uint16 BE)
C37118CommonHeader is a readonly struct parsed by
C37118CommonHeader.Parse(ReadOnlySpan<byte> frame); it exposes FrameType,
Version, FrameSize, IdCode, Soc, FracSecRaw (lower 24 bits),
TimeQuality (top byte of FRACSEC). It does not convert to UTC (that needs
TIME_BASE from CONFIG - see 4.4).
4.2 Frame type discrimination¶
C37118FrameType from SYNC byte1 bits 6..4:
The decoder switches on this. Config-1 is treated as Config-2 syntactically (same layout for our fields); Config-3, Header and Command are decoded to a typed "unsupported/ignored" outcome (section 6.4), never a throw in the resident loop.
4.3 CRC-CCITT (frame integrity)¶
C37.118 uses CRC-CCITT: width 16, polynomial 0x1021, initial value 0xFFFF,
no input/output reflection, no final XOR. Computed over every byte of the
frame from SYNC through the last data byte (i.e. bytes [0 .. FRAMESIZE-3]), and
compared to the trailing 2-byte CHK.
crc = 0xFFFF
for each byte b in frame[0 .. FRAMESIZE-3]:
crc = crc XOR (b << 8)
repeat 8 times:
if (crc AND 0x8000) != 0: crc = (crc << 1) XOR 0x1021
else: crc = (crc << 1)
crc = crc AND 0xFFFF
return crc
C37118Crc.Compute(ReadOnlySpan<byte> bytes) -> ushort. The decoder verifies CHK
before trusting any field; a CRC mismatch yields FrameDecodeStatus.CrcMismatch
(section 6.4), and the PDC treats that PMU as dropped for that timestamp
(section 9.4). A table-driven variant is permitted for speed but must be
cross-checked bit-for-bit against the bitwise form above in a unit test.
4.4 Timestamp: SOC + FRACSEC -> UTC¶
fraction_seconds = FracSecRaw / TIME_BASE (TIME_BASE from CONFIG-2, typically 1_000_000)
TimeUtc = DateTime(1970,1,1,0,0,0, DateTimeKind.Utc)
+ TimeSpan.FromSeconds(Soc)
+ TimeSpan.FromSeconds(fraction_seconds)
Implemented with DateTime.UnixEpoch.AddSeconds(Soc).AddTicks(...); prefer ticks
math on the fraction to avoid double rounding (ticks = FracSecRaw * 10_000_000L /
TIME_BASE, integer). This UTC instant is the timestamp the PDC aligns on and the
guard checks. It is always absolute UTC - never local, never relative.
The FRACSEC top byte (time quality) and the STAT unlocked-time bits (section 6.3) feed the sigma policy: a frame whose PMU clock is unlocked gets inflated sigma or is dropped.
5. CONFIG-2 frame decoding¶
CONFIG-2 is stateful context: the decoder must hold the most recent CONFIG-2
per IDCODE, because DATA frames are self-descriptionless and are only decodable
against their CONFIG. C37118Decoder keeps Dictionary<ushort, ConfigFrame2>
keyed by IDCODE and refuses to decode a DATA frame for an IDCODE it has not yet
seen a CONFIG for (FrameDecodeStatus.NoConfig).
5.1 CONFIG-2 body layout (after the 14-byte common header)¶
Off Size Field Meaning
14 4 TIME_BASE resolution of FRACSEC; lower 24 bits significant (uint32 BE)
18 2 NUM_PMU number of PMU blocks that follow (uint16 BE)
20 ... PMU blocks NUM_PMU repetitions of the block below
... 2 DATA_RATE > 0: frames/second; < 0: seconds/frame (int16 BE)
end-2 2 CHK
Per-PMU block (repeated NUM_PMU times):
Size Field Meaning
16 STN station name, ASCII, space-padded
2 IDCODE this PMU's data-source id (uint16 BE)
2 FORMAT format word (section 5.2)
2 PHNMR number of phasors (uint16 BE)
2 ANNMR number of analog values (uint16 BE)
2 DGNMR number of digital status words (uint16 BE)
16*(PHNMR+ANNMR+16*DGNMR) CHNAM channel names, 16 ASCII bytes each
4*PHNMR PHUNIT per-phasor conversion factor (section 5.3)
4*ANNMR ANUNIT per-analog conversion factor
4*DGNMR DIGUNIT per-digital mask/valid words
2 FNOM nominal line frequency: bit0 0=60 Hz, 1=50 Hz
2 CFGCNT configuration change count (uint16 BE)
5.2 FORMAT word (C37118DataFormat, a [Flags] enum)¶
Lower 4 bits (bits 15..4 unused):
bit0 phasor number format: 0 = 16-bit integer, 1 = 32-bit IEEE float
bit1 analog number format: 0 = 16-bit integer, 1 = 32-bit IEEE float
bit2 freq/dfreq format: 0 = 16-bit integer, 1 = 32-bit IEEE float
bit3 phasor coordinate: 0 = rectangular, 1 = polar
The decoder reads FORMAT per PMU and dispatches phasor/freq parsing (section 6) on these four bits. All four combinations (int/float x rect/polar, int/float freq) must be supported and each covered by a decode test (section 15).
5.3 PHUNIT (phasor scale + type)¶
Each 4-byte PHUNIT: top byte = phasor type (0 = voltage, 1 = current); lower 3
bytes (24-bit uint BE) = scale factor. For integer phasor format the scale is
in units of 10^-5 volts or amperes per bit (multiply the raw integer by
scale * 1e-5 to get engineering volts/amps). For float format the phasor is
already in engineering units and the scale is informational (still carries the
voltage/current flag, which the mapper needs to decide bus-voltage vs
branch-current row). PhasorDescriptor records Kind (Voltage/Current from the
top byte), Name (from CHNAM), the raw ConversionFactor, and a mapper-supplied
ScalePu (section 8) that turns engineering units into per-unit.
FNOM sets each PMU's nominal frequency for FREQ reconstruction (section 6.2).
6. DATA frame decoding¶
A DATA frame body is a concatenation of per-PMU blocks in the exact order the
PMUs appear in the governing CONFIG-2. There are no delimiters; sizes come from
CONFIG (PHNMR/ANNMR/DGNMR and FORMAT). C37118Decoder.DecodeData(span) looks up
the CONFIG by IDCODE, then walks each PMU block.
6.1 Per-PMU DATA block¶
Size Field Meaning
2 STAT status word (section 6.3)
phasor-size * PHNMR PHASORS phasor-size = 8 (float) or 4 (int) bytes each
freq-size FREQ 4 (float) or 2 (int) bytes
freq-size DFREQ 4 (float) or 2 (int) bytes
analog-size * ANNMR ANALOG 4 (float) or 2 (int) bytes each
2 * DGNMR DIGITAL 16-bit words
6.2 Phasors, frequency, ROCOF¶
Phasor, rectangular integer (4 bytes): int16 BE real, int16 BE imag; engineering
value re = raw_re * scale * 1e-5, im = raw_im * scale * 1e-5.
Phasor, polar integer (4 bytes): uint16 BE magnitude, int16 BE angle;
mag_eng = raw_mag * scale * 1e-5, angle_rad = raw_angle * 1e-4 (the angle unit
is 10^-4 radians, range -pi..+pi). Convert to rectangular for the Phasor struct:
re = mag_eng * cos(angle_rad), im = mag_eng * sin(angle_rad).
Phasor, rectangular float (8 bytes): float32 BE real, float32 BE imag, already engineering units.
Phasor, polar float (8 bytes): float32 BE magnitude (engineering units), float32 BE angle (radians). Convert to rectangular as above.
Phasor always stores rectangular engineering units (Real, Imag) with
Magnitude => hypot(Real, Imag) and AngleRad => atan2(Imag, Real) accessors, so
downstream code is format-agnostic.
FREQ. Integer: int16 BE = deviation from nominal in millihertz; actual
Hz = FNom + FREQ_raw / 1000.0. Float: float32 BE = the actual frequency in Hz,
nominal included (IEEE C37.118.2-2011 Table 5, FREQ: fixed point is the deviation
from nominal in mHz, floating point is the actual frequency in Hz; the standard is
not on disk, so this is from the published table, as in pyPMU and openPDC);
Hz = FREQ_float. Store FreqHz absolute.
(N038, 1.95.239: this section used to say the float field is a deviation from
nominal and proposed a per-stream PmuStreamConfig.FreqIsAbsolute switch. That
was wrong against Table 5 and was never built; C37118Decoder reads the float
field as absolute Hz, which is the standard's rule.)
DFREQ (ROCOF). Integer: int16 BE = ROCOF * 100 in Hz/s; RocofHzPerS =
DFREQ_raw / 100.0. Float: float32 BE = ROCOF in Hz/s directly.
6.3 STAT word and TVE awareness (PmuStatus)¶
The 16-bit STAT word:
bit15 data valid 0 = good, 1 = invalid/error
bit14 PMU error 1 = device error
bit13 PMU sync 0 = in sync, 1 = out of sync
bit12 data sorting 0 = by timestamp, 1 = by arrival
bit11 PMU trigger detected
bit10 configuration change pending
bit09 data modified (post-processed)
bits08..06 PMU time quality
bits05..04 unlocked time: 00 sync locked / <10 s since unlock;
01 <10 s, 10 <100 s, 11 >100 s unlocked
bits03..00 trigger reason
PmuStatus exposes DataValid, SyncOk (bit13 clear), UnlockedTime (enum),
ConfigChangePending. These drive the sigma policy (section 8.3) and the PDC
drop rule (section 9.4).
TVE (Total Vector Error) awareness. C37.118.1 defines a phasor's accuracy by
TVE = sqrt((Xr_meas - Xr_true)^2 + (Xi_meas - Xi_true)^2) / |X_true|, with 1 %
the compliance limit. GridSim never measures TVE (there is no truth), but it
uses the TVE budget as the sigma model: a phasor at compliance carries at most
1 % magnitude error and about 0.01 rad (~0.573 deg) angle error, because for a
pure angle error TVE ~= |angle_error_rad|. So (section 8.3) magnitude sigma is
set to tve_budget * |V| and angle sigma to tve_budget radians, where
tve_budget comes from the declared performance class (P vs M) and is inflated
when STAT reports unlock/error. This is how "TVE awareness" enters the estimator:
as principled, class- and status-derived measurement weights, not as a discarded
diagnostic.
6.4 Decode outcome¶
C37118Decoder returns a discriminated outcome rather than throwing in the hot
loop:
enum FrameDecodeStatus { Ok, CrcMismatch, NoConfig, UnsupportedFrameType,
TruncatedFrame, VersionUnsupported }
readonly struct DecodeResult { FrameDecodeStatus Status; C37118FrameType Type;
DataFrame? Data; ConfigFrame2? Config; }
A TruncatedFrame (FRAMESIZE says more bytes than the buffer holds, or a PMU
block overruns) is a hard protocol violation for that frame only; it is recorded
and the frame skipped. Only genuinely un-parseable program states (a null
buffer) throw. This mirrors RealtimeEstimatorEngine's "record and skip, never
tear down the resident loop" resilience doctrine.
7. Parsed data structures (named to fit the tree)¶
sealed record ConfigFrame2(
uint TimeBase, double DataRateFramesPerSec, IReadOnlyList<PmuConfig> Pmus,
ushort IdCode, uint Soc, uint FracSecRaw);
sealed record PmuConfig(
string StationName, ushort IdCode, C37118DataFormat Format,
IReadOnlyList<PhasorDescriptor> Phasors, int AnalogCount, int DigitalCount,
double FNomHz, ushort ConfigCount);
sealed record PhasorDescriptor(string Name, PhasorKind Kind, uint ConversionFactor);
enum PhasorKind { Voltage, Current }
readonly struct Phasor { double Real; double Imag;
double Magnitude => Math.Sqrt(Real*Real + Imag*Imag);
double AngleRad => Math.Atan2(Imag, Real); }
readonly struct PmuStatus { ushort Raw; bool DataValid; bool SyncOk;
UnlockedTime UnlockedTime; bool ConfigChangePending; }
sealed record PmuData(ushort IdCode, PmuStatus Status, IReadOnlyList<Phasor> Phasors,
double FreqHz, double RocofHzPerS, IReadOnlyList<double> Analog,
IReadOnlyList<ushort> Digital);
sealed record DataFrame(DateTime TimeUtc, ushort IdCode, IReadOnlyList<PmuData> Pmus);
Engineering-unit values are stored; per-unit conversion happens at the mapper (section 8), keeping the decoder a pure, protocol-faithful layer that a decode test can pin against reference vectors independent of any grid model.
8. Phasor scaling and per-unit mapping (PmuStreamConfig)¶
A C37.118 stream carries engineering volts/amps (secondary or primary,
per-device), with no knowledge of the estimator's per-unit base or the network
topology. The bridge between the two is a curated, static crosswalk -
PmuStreamConfig - authored once per capture, exactly as the GDA GSP/BMU
crosswalk authors SCADA measurement sets today.
8.1 Crosswalk contents¶
sealed record PmuStreamConfig(
IReadOnlyList<PmuChannelMap> Channels, PmuSigmaPolicy Sigma);
sealed record PmuChannelMap(
ushort PmuIdCode, int PhasorIndex, // which stream, which phasor in it
PmuChannelTarget Target, // bus voltage, or branch current end
double PrimaryToBase); // engineering-units -> per-unit factor
// Target is one of:
// BusVoltage(int BusId)
// BranchCurrent(int From, int To, BranchEnd End, string? Circuit)
Target reuses the estimator's addressing exactly: bus id for voltages,
(From, To, End, Circuit) for branch currents - the same fields
Measurement/MeasurementSet.TryResolveBranch already validate, so a bad
crosswalk fails in the existing validation path with the existing diagnostics.
8.2 Per-unit factors¶
Voltage phasor -> pu: Vpu = V_eng * PrimaryToBase, where for a bus at base kV
V_base_LN (line-to-neutral), PrimaryToBase = 1 / (V_base_LN in the same unit as
V_eng). The mapper carries this as one authored scalar per channel so the design
does not assume a specific PT ratio.
Current phasor -> pu: Ipu = I_eng * PrimaryToBase, with base current
I_base = S_base_3ph / (sqrt(3) * V_base_LL) = BaseMva * 1e6 / (sqrt(3) *
V_base_LL_volts). PrimaryToBase = 1 / I_base. Per-unit current magnitude is
directly the IMagPu measurement unit the estimator already uses.
8.3 Sigma policy (TVE + status derived)¶
sealed record PmuSigmaPolicy(
double TveBudgetP, // performance-class P TVE budget, e.g. 0.01 (1 %)
double TveBudgetM, // performance-class M budget
double UnlockInflation, // multiply sigma when unlocked-time > 0, e.g. 10x
double DropOnInvalid); // if data invalid, drop the row (true) vs inflate
// Per phasor at magnitude |V| (pu) or |I| (pu):
// sigma_mag_pu = tve * |mag| (relative magnitude error)
// sigma_angle_rad = tve (angle error ~ TVE for small angles)
// tve = (class == M ? TveBudgetM : TveBudgetP) * (unlocked ? UnlockInflation : 1)
If PmuStatus.DataValid is false, the mapper drops that PMU's rows for that
timestamp (a NaN/garbage phasor must never reach the gain matrix - the same
doctrine as MeasurementSet.Validate). Angle-magnitude coupling (a large phasor
has a tighter absolute angle need) is captured adequately by the two
independent sigmas above; a full 2x2 phasor covariance is a documented future
refinement (section 17) - the estimator currently takes scalar per-row sigmas.
9. The Phasor Data Concentrator¶
The PDC turns several asynchronous single-PMU DATA streams into one time-aligned
sequence of PdcSuperframes, each carrying every PMU's data for one reporting
instant, with explicit present/dropped flags. It is the classic PDC
wait-window aligner, made deterministic.
9.1 Transport seam¶
The reference implementation FileReplayPmuFrameSource reads a length-delimited
capture file (each record = uint32 BE length + raw C37.118 frame bytes, optionally
a leading CONFIG-2 then DATA frames), yielding frames in file order. This is the
replay source that section 1.2 relies on. A socket source is a future
implementation of the same interface; its frames still pass the guard downstream,
so it cannot deliver a processable (>=169 h old) live frame.
9.2 Reporting grid and alignment key¶
Each PMU reports at a fixed rate (CONFIG DATA_RATE, e.g. 50 fps -> 20 ms; a
downsampled 0.5 s stream -> 2 fps). The PDC quantises every frame's TimeUtc
onto a common reporting grid of period T_report (the slowest common cadence
we align to - for the hybrid scheduler, the 0.5 s PMU tick). The alignment key is
Round-to-nearest (not truncate) so a frame arriving 1 ms early/late still lands in
its intended slot. Frames whose residual |TimeUtc - slot*T_report| exceeds
T_report/2 are logged as off-grid and dropped (a misconfigured rate).
9.3 Wait-window buffer¶
The PDC holds a bounded, ordered buffer of partially-filled superframes keyed by
slot. For each incoming decoded PmuData:
- Compute its
slot. Ifslotis older than the newest already-emitted slot (a late straggler past the window), recordLateArrivalsDropped++and discard. - Otherwise insert the PMU's data into the superframe for
slot(creating it if new). - A superframe is complete when all expected PMUs (from
PmuStreamConfig) are present.
Emission is driven by a wait window W = WaitSlots * T_report (author-set,
e.g. 2-3 report periods). A slot is emitted when either it is complete, or the
watermark (newest observed slot) has advanced beyond slot + WaitSlots - i.e. we
have waited W for the missing PMUs and give up. Emission is strictly in
increasing slot order, so downstream sees a monotonic timestamp sequence
(load-bearing for invariant #2 and warm-start chaining).
sealed record PdcSuperframe(
DateTime TimeUtc, // slot * T_report, absolute UTC
IReadOnlyList<PdcPmuSlot> Pmus); // one per expected PMU, in config order
sealed record PdcPmuSlot(ushort PmuIdCode, PmuData? Data, PmuSlotState State);
enum PmuSlotState { Present, Dropped, Late, CrcFailed, StatusInvalid }
9.4 Dropouts, latency, and quality¶
- Dropout (a PMU absent when the window closes):
PdcPmuSlot.State = Dropped,Data = null. The mapper simply authors no rows for it - the estimator handles missing measurements natively (that is the entire point of an estimator vs a power flow; a dropped PMU degrades redundancy, not correctness). If dropouts make the PMU-observable island unobservable, the linear estimator reportsUnobservablehonestly (section 11.4) and the hybrid scheduler falls back to the last SCADA state as prior (section 12.3). - Latency: bounded by
W. A frame later thanWis aLatestraggler, counted and dropped, never retro-applied to an emitted slot (that would emit a second, out-of-order superframe for a past instant - forbidden). ChoosingWtrades completeness against emission delay; it is an authored knob, not adaptive (determinism, section 14). - CRC / status quality: a frame that failed CRC at decode never reaches the
PDC as data; the PDC marks that slot
CrcFailedif it knows a frame was expected. A frame present butStatusInvalid(STAT data-invalid) is carried asStatusInvalidand dropped at the mapper per the sigma policy.
9.5 Determinism of alignment¶
Given the same multiset of decoded frames, the emitted PdcSuperframe sequence
is identical regardless of arrival interleaving, because slotting is a pure
function of TimeUtc and emission order is by slot. There is no wall-clock in
the alignment math; the "watermark" is the max observed frame slot, not real time.
A test feeds the same frames in several shuffled orders and asserts identical
superframe output (section 15.4).
10. Mapping concentrated phasors to estimator rows¶
PmuMeasurementSetMapper.Map(PdcSuperframe sf, PmuStreamConfig cfg, DateTime?
utcNow) -> MeasurementSet produces a gridsim-measurements/1 set stamped
TakenUtc = sf.TimeUtc. The guard is called here (section 13). The set is
then handed to the existing WlsStateEstimator (hybrid path) or converted to the
linear estimator's rectangular input (section 11).
10.1 Voltage phasor -> VmPu + VaDeg¶
Each voltage phasor mapped to a BusVoltage(busId) target authors two rows:
MeasurementKind.VmPu, value|V|_pu, sigmasigma_mag_pu,Bus = busId,Source = "pmu".MeasurementKind.VaDeg, value = the re-referenced angle in degrees, sigmasigma_angle_rad * 180/pi,Bus = busId,Source = "pmu".
Re-referencing (the reference problem). All synchrophasor angles in one
superframe share the C37.118 common time reference (a cosine at nominal frequency
at TimeUtc); the estimator's datum is the slack bus. They differ by a common,
slowly-drifting offset delta(t). Exactly as the Gridradar design note
prescribes, the mapper carries only angle differences against an authored datum
PMU bus, anchored to the estimator's own current view of that datum:
Va_prior(datum_bus) is the datum bus angle from the warm-start prior state the
scheduler already threads (section 12). Differences cancel delta(t) exactly; the
anchor puts the row in the slack frame. The estimator does not solve for a
per-feed offset state (one feed, few nodes: the offset would consume a
measurement of information for no gain). A VaDeg row on the slack bus is legal
and structurally empty (existing behaviour) and is skipped by the mapper as
uninformative.
10.2 Current phasor -> IMagPu + IAngDeg (one new MeasurementKind)¶
A current phasor carries information the existing IMagPu row alone discards: its
angle. To use the full phasor without abandoning the estimator's polar state and
its validated nonlinear kernels, add one measurement kind:
// added to MeasurementKind (Estimation/MeasurementKind.cs):
IAngDeg // Branch current phase angle into the measured end, degrees, slack-referenced.
// Pairs with IMagPu to represent a full current phasor. Addresses a branch
// (From/To/End/Circuit), like the flow/current-magnitude kinds.
h(x) for IAngDeg. The current at end a is I_a = conj(S_a / V_a), so its
angle is angle(I_a) = angle(V_a) - angle(S_a) = Va[a] - atan2(Q_a, P_a), where
P_a, Q_a are the branch's real/reactive flow into end a (per-unit) computed
by the existing FlowP/FlowQ kernels. Thus
This deliberately mirrors IMagPu (|I| = sqrt(P^2+Q^2)/Vm[a]), reusing the same
flow kernels - no copy-drift, the same discipline as CurrentMagRow.
Jacobian row d h_IAngDeg / dx. With S2 = P_a^2 + Q_a^2:
d/dx [ atan2(Q, P) ] = ( P * dQ/dx - Q * dP/dx ) / S2
=> row = e_{Va[a]} - ( P * dQ/dx - Q * dP/dx ) / S2
e_{Va[a]} is the unit entry in end a's angle column (omitted if a is the
slack - then the current-angle row is measured purely through the flow terms).
dP/dx and dQ/dx are the exact partials FlowRow already emits (captured via
the existing PairSink pattern, precisely as CurrentMagRow does). A branch
carrying no flow (S2 ~ 0) contributes a structural near-zero row and is skipped,
same guard as CurrentMagRow. StructuralColumns for IAngDeg is identical to
IMagPu/flow rows (the four columns: both end angles + both end Vm). The residual
back-conversion in WlsStateEstimator.Assemble gets one switch arm:
IAngDeg => 180/pi (it is an angle, like VaDeg).
The current phasor angle is re-referenced identically to the voltage angle
(section 10.1) because h already lives in the slack frame (Va[a] is
slack-referenced state): author I_ang_row = angle_pmu(I) - angle_pmu(Vdatum) +
Va_prior(datum_bus).
10.3 Why this mapping is the right seam¶
It keeps the estimator's state polar and its kernels untouched-but-reused, adds
exactly one measurement kind, and lets the same WlsStateEstimator fuse PMU and
SCADA rows in one solve (the hybrid path). Voltage phasors become the cheapest,
linear VaDeg/VmPu rows; current phasors reuse the flow algebra. Bad-data
detection, chi-square, observability and warm-start all apply unchanged.
11. Linear single-shot PMU state estimator¶
When a subnetwork is fully observed by PMUs (voltage and current phasors), the
measurement equation is linear in the rectangular bus-voltage state, so the
estimate is a single non-iterative solve of a constant-gain system - the classic
Phadke linear PMU estimator. This is LinearPmuStateEstimator
(Estimation/LinearPmuStateEstimator.cs), a static, measurement-only filter
(the linear-PMU method the review log already lists as unconditionally
permitted; no DSE entry).
11.1 State and measurement model (rectangular)¶
State: complex bus voltages of the PMU-observable island,
x = [Vr_1..Vr_p, Vi_1..Vi_p] (real then imaginary parts, p buses). All phasors
in a superframe are in one common reference (section 10.1), so no per-bus angle
datum is needed within the linear solve; the whole solution is rotated into the
slack frame afterward (section 11.3).
- Voltage phasor at bus
k: measuresV_kdirectly. Rows:Vr_k = Re(V_k),Vi_k = Im(V_k). Constant, trivially linear. - Current phasor on branch
k->m, measured endk: from the pi-model,I_km = (y_series + y_shunt_k) V_k - y_series V_m, complex-linear in the voltages. Withy_series = g + jb,y_shunt_k = jb_c/2(and tap/phase-shift folded into the sameyff/yftstampsMeasurementModelalready computes), the real/imag rows are linear combinations ofVr_k, Vi_k, Vr_m, Vi_m.
Stacking real and imaginary parts gives a real linear system z = H x + e
with a constant real matrix H (built once from the Y-bus stamps, reused for
every superframe - the standout property of linear PMU SE).
11.2 Weighted least squares (single shot)¶
Formed once: Gain = H^T W H (constant if the sigma structure is constant), and
per superframe only the right-hand side H^T W z changes. Solve with the existing
ILinearSolver (DenseLuSolver, or the sparse backend above the bus threshold) -
the same solver seam the WLS estimator pivots on, so the linear PMU estimator
inherits the validated linear algebra and its machine-epsilon behaviour. Because
H is constant, the gain can be factored once and back-substituted each
superframe (the natural real-time cadence); the design keeps a cached
factorisation keyed by the sigma vector, invalidated when the observed-PMU set or
sigma policy changes.
11.3 Output in the estimator's convention¶
Convert x_hat (rectangular, common reference) to (Vm, VaDeg) per bus:
Vm_k = hypot(Vr_k, Vi_k), Va_k = atan2(Vi_k, Vr_k). Rotate all angles by the
common offset so the datum bus matches the warm-start prior:
Va_k <- Va_k - Va_hat(datum) + Va_prior(datum_bus). Emit an EstimationResult
in the identical shape WlsStateEstimator produces (so replay/UI/export and
ToPowerFlowResult work unchanged), with Iterations = 1, MaxStateUpdate = 0
(single shot), and residuals/chi-square computed against the same declared sigmas.
11.4 Observability and honesty¶
If the PMU set does not observe every state direction, H^T W H is singular; the
estimator reports EstimationStatus.Unobservable (never NaN), exactly as the WLS
estimator does on a singular gain - the reported state is the prior, not an
estimate. This is what the hybrid scheduler uses to decide when the PMU-only fast
path is trustworthy vs when to defer to the last SCADA state.
12. Hybrid multi-rate SCADA(~5 s) + PMU(~0.5 s) fusion scheduler¶
HybridMultiRateScheduler (Simulation/HybridMultiRateScheduler.cs) produces the
IEnumerable<MeasurementSet> window stream that RealtimeEstimatorEngine.Run
already consumes - so it plugs into the resident engine without changing it. It
interleaves two cadences and threads one warm-start state through both, honouring
the guard on every window.
12.1 Cadences and window construction¶
- PMU tick every
T_pmu(~0.5 s): drain the PDC of any newly-complete/expiredPdcSuperframes, map each to aMeasurementSet(section 10) tagged with source"pmu", timestamp = superframe UTC. - SCADA tick every
T_scada(~5 s, a multiple ofT_pmu): pull the aligned SCADA measurement window (the existing GDA/SCADA row set) for that instant.
The scheduler emits windows in strict timestamp order. It supports two fusion modes (author-selected):
Mode A - single hybrid WLS (recommended default). At each SCADA tick, emit one
combined MeasurementSet containing SCADA rows and the concurrent PMU rows
(VmPu/VaDeg/IMagPu/IAngDeg), estimated in one nonlinear WlsStateEstimator solve.
Between SCADA ticks, at each PMU tick emit a PMU-only window; the engine runs the
linear PMU estimator on it (via a scheduler-selected estimator, section 12.2)
for the PMU-observable buses, warm-started from the last full state so unobserved
buses simply retain the prior. This gives full-network accuracy at 5 s and fast
angle/observed-bus tracking at 0.5 s.
Mode B - two-stage / hierarchical. SCADA WLS produces the full state at 5 s; each PMU tick runs the linear estimator on the PMU island and corrects only those buses, leaving the rest at the SCADA state. Simpler, but the PMU update does not re-weight against SCADA rows; documented as the lighter option.
12.2 Estimator selection per window¶
The window carries a tag (MeasurementSet.Note or a scheduler side-channel)
saying whether it is PMU-only or hybrid. The scheduler drives estimation by
choosing the estimator per window: PMU-only -> LinearPmuStateEstimator; hybrid
-> WlsStateEstimator with PMU + SCADA rows. Both are static filters; neither
touches the DSE gate. (If a future variant wants the resident RealtimeEstimator-
Engine to own the selection, extend it with an IStateEstimator seam - section
17 - rather than branching inside the engine.)
12.3 Warm-start threading and dropout fallback¶
Each converged estimate becomes the next window's warm-start prior (via
ToPowerFlowResult), exactly as RealtimeEstimatorEngine already chains. The
prior also supplies the angle anchor Va_prior(datum_bus) the mapper needs
(section 10.1). If a PMU tick is Unobservable (dropouts), the scheduler does not
emit a bad state: it carries the prior forward unchanged and marks the frame
skipped, so the fast path degrades gracefully to "hold last state until the next
SCADA solve".
12.4 Cadence, timestamps, and the no-future guarantee¶
Every emitted window's TakenUtc is a real measurement instant (a PDC slot or a
SCADA instant), always <= the newest input frame's time - the scheduler never
fabricates a future-dated window, and never interpolates ahead of data (it may
hold the last state, which is a present/past claim, never a forward one). The
0.5 s "real-time" cadence is wall-clock pacing done by the host loop; the
scheduler and engine process each window as a pure function of its (already-
historical, guard-checked) contents, so the whole pipeline is deterministic and
replay-testable. This is invariant #2 (no output stamped later than its newest
input) and #4 (real-time proven on replay/permitted-age), realised in the
scheduler.
13. Horizon guard integration and the guarded-seam registry¶
13.1 Where the guard is called¶
The synchrophasor pipeline introduces one new dated ingestion seam:
PmuMeasurementSetMapper.Map, which calls
ForwardInferenceGuard.RejectFuture(sf.TimeUtc, utcNow, "PMU superframe") before
producing any MeasurementSet. Because the produced set carries
TakenUtc = sf.TimeUtc, MeasurementSet.Load's existing guard also fires if the
set is ever round-tripped through disk. Belt and braces: a PMU observation cannot
reach an estimator without passing the horizon at least once, by construction.
The PDC itself (PhasorDataConcentrator) may additionally guard at slot emission
as defence in depth (reject a superframe whose slot time is newer than the
horizon), but the mapper is the canonical, tested seam.
13.2 Registry updates (must ship with the code)¶
The seam table of technical ch. 31 lists the guarded
seams. Add PmuMeasurementSetMapper.Map (and, if implemented, the PDC emission
guard) to that list. Add a per-seam test to RestrictionInvariantTests /
ForwardInferenceGuardTests that feeds a superframe stamped UtcNow - 1 h and
asserts ForwardInferenceException (the same shape as the existing
GdaReplayLoopTests seam pin), and one stamped UtcNow - 200 h that passes.
13.3 DSE gate: explicitly not triggered¶
The linear PMU estimator and the hybrid scheduler consume no Dynamics/ process
model - they are measurement-only filters. Per RestrictionReview.AssertPermitted,
a method that does not use a dynamics model is permitted unconditionally, so
no RestrictionReviewEntry is added and RestrictionReview.Reviewed is
unchanged. The design note for reviewers: should anyone later add a PMU-rate
recursive tracker (an EKF/UKF predicting between frames), that does fuse a
process model, and section "How a new DSE method gets added" in the review log
applies - a row plus a RestrictionReviewEntry before it may run. This design
deliberately stops short of that line.
14. Determinism requirements¶
The whole pipeline is a pure function of (raw frame bytes, crosswalk, sigma
policy, injected utcNow). No wall clock, no ambient culture, no floating-point
nondeterminism beyond IEEE-754.
- No wall-clock in the math path.
utcNowis always injected (asRealtimeEstimatorOptions.UtcNowalready is) so the guard boundary is fixed in tests. PDC slotting uses frame timestamps only; the watermark is the max observed slot, not real time. - Fixed iteration structure. The linear estimator is single-shot; the WLS path
has its existing deterministic loop. No adaptive wait-window, no random tie-break
in alignment (
slotorder is total; ties within a slot resolve by PMU config order). - Byte-exact parsing. Big-endian reads via
BinaryPrimitives; polar->rect and int-scale conversions are fixed formulae; float32 read is exact IEEE-754. Given the same bytes, the parsedDataFrameis bit-identical across runs and machines. - Deterministic ordering. Measurement rows are authored in a fixed order (PMU
config order, then phasor index, VmPu before VaDeg, IMagPu before IAngDeg), so
the gain matrix assembly order - and thus the last-bit summation - is stable
(the same bit-for-bit doctrine
MeasurementModelalready upholds).
15. Validation and test plan¶
All new tests live beside the existing suites in tests/GridSim.Tests. The bar is
the project's: machine-epsilon cross-checks against an independent reference, and
determinism.
15.1 Decoder conformance (independent reference vectors)¶
- Reference frame vectors. Decode CONFIG-2 and DATA byte vectors taken from an
independent encoder (IEEE C37.118.2 Annex examples and/or an openPDC / pypmu
capture checked into
data/synchrophasor/). Assert every parsed field matches the reference to the bit (int fields exactly; float fields exactly, since the bytes are exact IEEE-754). This is the independent cross-check that the decoder is standard-faithful, not self-consistent. - CRC. Cross-check
C37118Crc.Computeagainst the CHK embedded in every reference frame, and cross-check the fast table-driven form (if used) against the bitwise form over 10k random buffers - byte-exact. - Format matrix. One decode test per FORMAT combination (int/float x rect/polar, int/float freq), asserting the same engineering phasor within rounding for int forms and exactly for float forms.
- Timestamp. SOC/FRACSEC -> UTC checked against hand-computed instants for several TIME_BASE values; assert no double-rounding drift (ticks form).
- Resilience. Truncated frame, wrong CRC, DATA-before-CONFIG, unsupported
frame type each yield the correct
FrameDecodeStatusand never throw.
15.2 Encode->decode->estimate round-trip (machine epsilon)¶
The end-to-end truth test. Take a solved PowerFlowResult on a small case
(case9 / case14). Synthesise a C37.118 DATA frame whose voltage phasors are the
solved bus voltages and whose current phasors are the solved branch currents
(computed from the same Y-bus stamps), with a chosen TIME_BASE/FORMAT. Decode it,
concentrate it, map it (section 10). Then:
- Linear PMU estimator on a fully-PMU-observed case must recover the solved
state to ~1e-9 (the analogue of the existing
Voltage_only_pmu_set_recovers_the_state_exactly, extended to include current phasors). This is the machine-epsilon cross-check. - Hybrid WLS with the same PMU rows plus a redundant SCADA row set must recover the same state to solver tolerance, and the residuals of the exact rows must be ~0.
- Cross-estimator agreement. On a case observable by both the linear PMU
estimator and the nonlinear WLS (PMU rows fed to
WlsStateEstimatorvia the newIAngDeg/IMagPurows), the two estimates must agree to ~1e-8 - an independent numerical cross-check of the new Jacobian against the linear formulation.
15.3 New Jacobian correctness (IAngDeg)¶
Finite-difference h_IAngDeg(x) against the analytic Jacobian row (section 10.2)
at several random feasible states on case14/case118; assert max abs error <~1e-6
(central differences), matching how the other measurement-kind Jacobians are
pinned. Also assert StructuralColumns(IAngDeg) exactly equals the columns the
analytic row writes (no missing/extra structural entry - the sparse backend
depends on it).
15.4 PDC alignment determinism¶
- Feed the same multiset of decoded frames to
PhasorDataConcentratorin several shuffled arrival orders; assert byte-identicalPdcSuperframesequences. - Drop one PMU for a slot; assert the slot emits at window close with that PMU
Droppedand the restPresent, and that a late straggler for an already- emitted slot is counted and discarded (no out-of-order emission). - Assert emitted superframe timestamps are strictly increasing.
15.5 Restriction invariants¶
- A superframe stamped
UtcNow - 1 hfed toPmuMeasurementSetMapper.MapthrowsForwardInferenceException; one atUtcNow - 200 hmaps cleanly. (New rows inForwardInferenceGuardTests/RestrictionInvariantTests, section 13.2.) RealtimeEstimatorEngine.Runover aHybridMultiRateSchedulerwindow stream containing one too-fresh window records it asWindowsRejectedFreshand never estimates it (reuses the engine's existing behaviour - assert the summary).- Assert no emitted
RealtimeEstimateFrame.StateTimeUtcexceeds the newest input frame time (invariant #2). - Assert
RestrictionReview.Reviewedis unchanged (the static-filter methods add no DSE entry) and thatRestrictionInvariantTestsstill passes green.
15.6 Determinism regression¶
Run the full replay pipeline twice with a fixed utcNow; assert bit-identical
EstimationResults (Vm, VaDeg, ObjectiveJ) across runs.
16. 0-warning build gate compliance¶
The new code compiles clean under the project's warnings-as-errors gate:
- Nullable reference types enabled and honoured; the decode outcome uses the
DecodeResult/FrameDecodeStatusstruct rather than nullable returns for the hot path, andPmuData? Dataon a dropped slot is explicitly nullable and null-checked at every use. - XML docs on every public member (types, records, enum members, public
methods) - the decoder, PDC, mapper, estimator and scheduler all expose only
documented public surface; internals are
internal. - No
unsafe, noBitConverterfor multi-byte reads -BinaryPrimitivesbig-endian readers only, so there is no host-endianness or alignment warning surface. - Culture-invariant parsing/formatting; ASCII station/channel names decoded with an explicit encoding.
- No unused usings, no unreachable arms; the FORMAT/kind switches are
exhaustive with an explicit
defaultthat throwsInvalidOperationExceptionnaming the unhandled case (the patternMeasurementModeluses). - No analyser suppressions introduced; if a span-based parser trips a CA1062/CA1031-style rule, the code is restructured, not suppressed.
- Deterministic, allocation-conscious parsing (
ReadOnlySpan<byte>,stackallocfor small fixed buffers) so no perf-analyser noise.
The design adds no dependency: C37.118 is decoded with the BCL
(System.Buffers.Binary, System.Numerics for phasors) only - no external NuGet,
consistent with the self-contained core.
17. Seams to extend (future work, explicitly deferred)¶
- CONFIG-3 (fractional scaling, long names, baseline PMU coordinates): decode
path parallels CONFIG-2;
C37118Decoderalready discriminates the type and rejects it cleanly, so support drops in behind the same interface. - Command channel / live socket source:
IPmuFrameSourcehas a socket implementation shape; it remains guard-bounded (cannot deliver processable live frames). A CMD-frame request enum is documented but GridSim never drives a live device - it replays captures. - Full 2x2 phasor covariance (magnitude/angle correlation) instead of two scalar sigmas: needs a block-weighted WLS row; a documented refinement of the sigma policy, not a change to the mapping.
- Rectangular current-phasor kinds (
IRePu/IImPu) as an alternative toIMagPu+IAngDegfor better conditioning near zero flow: an additive measurement-kind option, same kernel reuse. IStateEstimatorseam soRealtimeEstimatorEnginecan own linear-vs-WLS selection internally rather than the scheduler choosing; a clean refactor when a third estimator (e.g. branch-current DSSE) arrives.- PMU-rate recursive tracker (EKF/UKF): the moment a predict step integrates a process model this becomes a Dynamics->Estimation fusion - it must go through the review-log gate (section 13.3) before it can run.
18. Implementation checklist (order of work)¶
C37118Crc+ CRC tests against reference frames.C37118CommonHeader,C37118FrameType,C37118DataFormat, SOC/FRACSEC->UTC + tests.ConfigFrame2decode (C37118Decoderconfig path) + config-vector tests.DataFrame/PmuData/Phasor/PmuStatusdecode (all four FORMAT combos) + format-matrix tests.PhasorDataConcentrator+PdcSuperframe+ alignment/determinism/dropout tests.MeasurementKind.IAngDeg+MeasurementModelh/Jacobian/StructuralColumns armsAssembleresidual arm + finite-difference Jacobian test.PmuStreamConfig/PmuMeasurementSetMapper(with the guard call) + mapping and guard-seam tests; update the seam table of technical ch. 31.LinearPmuStateEstimator+ machine-epsilon recovery and cross-estimator agreement tests.HybridMultiRateScheduler+ end-to-end round-trip and no-future invariant tests overRealtimeEstimatorEngine.FileReplayPmuFrameSource+ a small checked-in capture for the round-trip replay test.
Each step lands with its tests green and the 0-warning gate intact before the next begins.