Skip to main content

powerio_tx/
diagnostics.rs

1//! The codes this crate emits, and the registry gates over them.
2//!
3//! The record, the code grammar, the severity ladder, and the stage family live
4//! in `powerio-core`. What lives here is the transmission side registry: one
5//! [`DiagnosticInfo`] per code, declared once, so an emission site names an
6//! entry rather than a loose string and every emitted code is registered by
7//! construction.
8//!
9//! Codes are families, not one per site: what differs between two sites of a
10//! family is which field or record it was, which belongs in `details` where a
11//! consumer can read it, rather than in a code nobody can enumerate.
12
13// The collector is crate-private implementation support, not API: each
14// emitting crate carries its own copy (src/collect.rs) and never exports it.
15pub(crate) use crate::collect::Diagnostics;
16
17pub use powerio_core::{
18    Diagnostic, DiagnosticCode, DiagnosticInfo, DiagnosticSeverity, DiagnosticStage, ErrorCategory,
19    check_registry, code_is_well_formed, render_diagnostic, render_diagnostics,
20};
21
22use crate::format::TargetFormat;
23
24/// The write side family every target shares.
25///
26/// A writer's fidelity losses are the same eleven questions for every target —
27/// what was dropped, what was defaulted, what was collapsed — so the family is
28/// declared once per target and the shared writer passes take the target's
29/// family rather than a label they cannot turn into a code.
30#[derive(Clone, Copy, Debug)]
31pub struct EmitFamily {
32    /// A field the target format has no column or key for.
33    pub field_dropped: DiagnosticInfo,
34    /// A whole element or record the target does not model.
35    pub record_dropped: DiagnosticInfo,
36    /// A value the target requires and the source never stated.
37    pub value_defaulted: DiagnosticInfo,
38    /// Richer structure reduced to what the target's one field can hold.
39    pub value_collapsed: DiagnosticInfo,
40    /// A stated value replaced by another the target can represent.
41    pub value_substituted: DiagnosticInfo,
42    /// A value shortened to the target's width, e.g. a cost curve order.
43    pub value_truncated: DiagnosticInfo,
44    /// A branch rating set beyond the target's rate_a/rate_b/rate_c.
45    pub rating_set_dropped: DiagnosticInfo,
46    /// Source format passthrough fields the writer does not replay.
47    pub extras_dropped: DiagnosticInfo,
48    /// The area table, which is typed rather than passthrough.
49    pub areas_dropped: DiagnosticInfo,
50    /// A non-finite value written as a sentinel or a JSON null.
51    pub not_a_number: DiagnosticInfo,
52    /// The network has no reference bus for the target's solver to key on.
53    pub reference_missing: DiagnosticInfo,
54    /// A normalized line lands in the target's transformer section.
55    pub element_relabeled: DiagnosticInfo,
56}
57
58macro_rules! emit_family {
59    ($name:ident, $scope:literal, $label:literal) => {
60        /// The write side family for this target.
61        pub const $name: EmitFamily = EmitFamily {
62            field_dropped: DiagnosticInfo::new(
63                concat!("EMIT.", $scope, ".FIELD_DROPPED"),
64                DiagnosticSeverity::Warning,
65                concat!("a field ", $label, " has no place for was dropped"),
66            ),
67            record_dropped: DiagnosticInfo::new(
68                concat!("EMIT.", $scope, ".RECORD_DROPPED"),
69                DiagnosticSeverity::Warning,
70                concat!("an element ", $label, " does not model was dropped"),
71            ),
72            value_defaulted: DiagnosticInfo::new(
73                concat!("EMIT.", $scope, ".VALUE_DEFAULTED"),
74                DiagnosticSeverity::Warning,
75                concat!("a value ", $label, " requires was synthesized"),
76            ),
77            value_collapsed: DiagnosticInfo::new(
78                concat!("EMIT.", $scope, ".VALUE_COLLAPSED"),
79                DiagnosticSeverity::Warning,
80                concat!("structure reduced to what ", $label, " can carry"),
81            ),
82            value_substituted: DiagnosticInfo::new(
83                concat!("EMIT.", $scope, ".VALUE_SUBSTITUTED"),
84                DiagnosticSeverity::Warning,
85                concat!("a stated value was replaced by one ", $label, " can hold"),
86            ),
87            value_truncated: DiagnosticInfo::new(
88                concat!("EMIT.", $scope, ".VALUE_TRUNCATED"),
89                DiagnosticSeverity::Warning,
90                concat!("a value was shortened to the width ", $label, " carries"),
91            ),
92            rating_set_dropped: DiagnosticInfo::new(
93                concat!("EMIT.", $scope, ".RATING_SET_DROPPED"),
94                DiagnosticSeverity::Warning,
95                concat!(
96                    "a branch rating set beyond rate_a/rate_b/rate_c was dropped: ",
97                    $label,
98                    " has no field for it"
99                ),
100            ),
101            extras_dropped: DiagnosticInfo::new(
102                concat!("EMIT.", $scope, ".EXTRAS_DROPPED"),
103                DiagnosticSeverity::Warning,
104                concat!(
105                    "source format passthrough fields the ",
106                    $label,
107                    " writer does not replay were dropped"
108                ),
109            ),
110            areas_dropped: DiagnosticInfo::new(
111                concat!("EMIT.", $scope, ".AREAS_DROPPED"),
112                DiagnosticSeverity::Warning,
113                concat!("the area table was dropped: ", $label, " emits none"),
114            ),
115            not_a_number: DiagnosticInfo::new(
116                concat!("EMIT.", $scope, ".NOT_A_NUMBER"),
117                DiagnosticSeverity::Warning,
118                concat!(
119                    "a non-finite value was written as the sentinel ",
120                    $label,
121                    " uses, because it has no Inf or NaN"
122                ),
123            ),
124            reference_missing: DiagnosticInfo::new(
125                concat!("EMIT.", $scope, ".REFERENCE_MISSING"),
126                DiagnosticSeverity::Warning,
127                concat!(
128                    "the network has no reference bus, which ",
129                    $label,
130                    " consumers reject"
131                ),
132            ),
133            element_relabeled: DiagnosticInfo::new(
134                concat!("EMIT.", $scope, ".ELEMENT_RELABELED"),
135                DiagnosticSeverity::Warning,
136                concat!(
137                    "a normalized line reads as a transformer in the ",
138                    $label,
139                    " layout"
140                ),
141            ),
142        };
143    };
144}
145
146impl EmitFamily {
147    /// Every entry, for the registry gates and the generated reference.
148    #[must_use]
149    pub fn entries(&'static self) -> [&'static DiagnosticInfo; 12] {
150        [
151            &self.field_dropped,
152            &self.record_dropped,
153            &self.value_defaulted,
154            &self.value_collapsed,
155            &self.value_substituted,
156            &self.value_truncated,
157            &self.rating_set_dropped,
158            &self.extras_dropped,
159            &self.areas_dropped,
160            &self.not_a_number,
161            &self.reference_missing,
162            &self.element_relabeled,
163        ]
164    }
165}
166
167/// One [`EmitFamily`] per write target, plus the codes a single reader or a
168/// single writer owns.
169pub mod codes {
170    use super::{DiagnosticInfo, DiagnosticSeverity, EmitFamily};
171
172    emit_family!(EMIT_MATPOWER, "MATPOWER", "MATPOWER .m");
173    emit_family!(EMIT_PSSE, "PSSE", "PSS/E .raw");
174    emit_family!(EMIT_PSLF, "PSLF", "PSLF .epc");
175    emit_family!(EMIT_PANDAPOWER, "PANDAPOWER", "pandapower JSON");
176    emit_family!(EMIT_PYPSA, "PYPSA", "the PyPSA CSV folder");
177    emit_family!(EMIT_POWERWORLD, "POWERWORLD", "PowerWorld .aux");
178    emit_family!(EMIT_POWERMODELS, "POWERMODELS", "PowerModels JSON");
179    emit_family!(EMIT_EGRET, "EGRET", "egret JSON");
180    emit_family!(EMIT_SURGE, "SURGE", "Surge JSON");
181    emit_family!(EMIT_XIIDM, "XIIDM", "XIIDM 1.17 XML");
182    emit_family!(EMIT_JIIDM, "JIIDM", "JIIDM 1.17 JSON");
183    emit_family!(EMIT_CGMES, "CGMES", "CGMES 3.0");
184    emit_family!(EMIT_UCTE, "UCTE", "UCTE-DEF .uct");
185    emit_family!(EMIT_UNSUPPORTED, "UNSUPPORTED", "a read only format");
186
187    powerio_core::diagnostic_codes! {
188        // PARSE: the source text could not be decoded as given.
189        PARSE_MATPOWER_MALFORMED = "PARSE.MATPOWER.MALFORMED", Error,
190            "a MATPOWER matrix is missing, short, unparseable, or unbalanced", category = Parse;
191        PARSE_SOURCE_MALFORMED = "PARSE.SOURCE.MALFORMED", Error,
192            "a format reader refused the source it was given", category = Parse;
193        PARSE_GOC3_MALFORMED = "PARSE.GOC3.MALFORMED", Error,
194            "a GO Challenge 3 data file is not well formed JSON", category = Parse;
195        PARSE_XIIDM_VERSION_UNSUPPORTED = "PARSE.XIIDM.VERSION_UNSUPPORTED", Error,
196            "the XIIDM namespace names a version PowerIO has not tested", category = Parse;
197
198        // READ: decoded, but not representable in the canonical model.
199        READ_PSSE_FIELD_DROPPED = "READ.PSSE.FIELD_DROPPED", Warning,
200            "a PSS/E field with no canonical home was dropped";
201        READ_PSSE_VALUE_SUBSTITUTED = "READ.PSSE.VALUE_SUBSTITUTED", Warning,
202            "a PSS/E value the record states could not be used as given";
203        READ_PSSE_VALUE_UNSUPPORTED = "READ.PSSE.VALUE_UNSUPPORTED", Warning,
204            "a PSS/E code word (CZ, CW, CM) outside the modeled set was read as the default";
205        READ_PSSE_REFERENCE_DROPPED = "READ.PSSE.REFERENCE_DROPPED", Warning,
206            "a PSS/E control pointer names a bus the case does not declare";
207        READ_PSSE_SECTION_UNSUPPORTED = "READ.PSSE.SECTION_UNSUPPORTED", Warning,
208            "a PSS/E section is preserved in a same-format echo only";
209        READ_PSSE_RETAINED_SOURCE_ONLY = "READ.PSSE.RETAINED_SOURCE_ONLY", Remark,
210            "a PSS/E field survives in extras rather than in a typed field";
211        READ_PSSE_VALUE_DEFAULTED = "READ.PSSE.VALUE_DEFAULTED", Warning,
212            "a PSS/E record ends before a field the typed model reads, so that field took its default";
213
214        READ_PSLF_VALUE_DEFAULTED = "READ.PSLF.VALUE_DEFAULTED", Warning,
215            "a PSLF value the model needs was not in the source and was defaulted";
216        READ_PSLF_VALUE_APPROXIMATED = "READ.PSLF.VALUE_APPROXIMATED", Warning,
217            "a PSLF value was read through an approximation the .epc model forces";
218        READ_PSLF_RECORD_DROPPED = "READ.PSLF.RECORD_DROPPED", Warning,
219            "a PSLF record could not be mapped and was dropped";
220        READ_PSLF_SOURCE_MALFORMED = "READ.PSLF.SOURCE_MALFORMED", Warning,
221            "a PSLF section header, count, or end marker disagrees with the records";
222        READ_PSLF_RETAINED_SOURCE_ONLY = "READ.PSLF.RETAINED_SOURCE_ONLY", Remark,
223            "a PSLF section survives in the retained source or in extras only";
224
225        READ_PANDAPOWER_FIELD_DROPPED = "READ.PANDAPOWER.FIELD_DROPPED", Warning,
226            "a pandapower field with no canonical home was dropped";
227        READ_CGMES_RECORD_UNMAPPED = "READ.CGMES.RECORD_UNMAPPED", Warning,
228            "a CGMES record has no representation in the balanced network model";
229        READ_CGMES_FIELD_UNMAPPED = "READ.CGMES.FIELD_UNMAPPED", Warning,
230            "a field on a mapped CGMES record has no representation in the balanced network model";
231        READ_CGMES_VALUE_DEFAULTED = "READ.CGMES.VALUE_DEFAULTED", Warning,
232            "a value absent from the CGMES profile set was defaulted";
233        READ_CGMES_VALUE_APPROXIMATED = "READ.CGMES.VALUE_APPROXIMATED", Warning,
234            "a CGMES value was represented through an explicit approximation";
235        READ_CGMES_TOPOLOGY_CALCULATED = "READ.CGMES.TOPOLOGY_CALCULATED", Remark,
236            "the set carries no TopologicalNode data, so buses were calculated from ConnectivityNodes and switch positions";
237        READ_CGMES_CONNECTIVITY_INSUFFICIENT = "READ.CGMES.CONNECTIVITY_INSUFFICIENT", Error,
238            "the set carries neither TopologicalNode data nor enough connectivity to calculate buses",
239            category = Parse;
240        READ_PANDAPOWER_VALUE_INFERRED = "READ.PANDAPOWER.VALUE_INFERRED", Warning,
241            "a value pandapower does not store was reconstructed on a declared convention";
242        READ_PANDAPOWER_TABLE_UNSUPPORTED = "READ.PANDAPOWER.TABLE_UNSUPPORTED", Warning,
243            "a pandapower table is not mapped into the canonical model";
244
245        READ_PYPSA_TABLE_UNSUPPORTED = "READ.PYPSA.TABLE_UNSUPPORTED", Warning,
246            "a PyPSA table is not mapped into the canonical model";
247        READ_PYPSA_VALUE_APPROXIMATED = "READ.PYPSA.VALUE_APPROXIMATED", Warning,
248            "a PyPSA element was read through the nearest canonical element";
249        READ_PYPSA_NAME_REMAPPED = "READ.PYPSA.NAME_REMAPPED", Warning,
250            "a PyPSA bus name collides with another and was keyed by its numeric id";
251
252        READ_POWERWORLD_VALUE_DEFAULTED = "READ.POWERWORLD.VALUE_DEFAULTED", Warning,
253            "a PowerWorld field this binary vintage does not locate was defaulted";
254        READ_POWERWORLD_RETAINED_SOURCE_ONLY = "READ.POWERWORLD.RETAINED_SOURCE_ONLY", Warning,
255            "a PowerWorld aux data block survives in the retained source only";
256
257        READ_POWERMODELS_RECORD_DROPPED = "READ.POWERMODELS.RECORD_DROPPED", Warning,
258            "a PowerModels document states more than the canonical snapshot holds";
259        READ_POWERMODELS_FIELD_DROPPED = "READ.POWERMODELS.FIELD_DROPPED", Warning,
260            "a PowerModels field the canonical model cannot state was dropped";
261
262        READ_GOC3_AMBIGUOUS_DOCUMENTS = "READ.GOC3.AMBIGUOUS_DOCUMENTS", Error,
263            "a GO Challenge 3 source contains more than one problem or solution data file",
264            category = Parse;
265        READ_GOC3_PROBLEM_REQUIRED = "READ.GOC3.PROBLEM_REQUIRED", Error,
266            "a GO Challenge 3 solution data file requires its matching problem data file",
267            category = Parse;
268        READ_GOC3_SOURCE_UNRECOGNIZED = "READ.GOC3.SOURCE_UNRECOGNIZED", Error,
269            "a declared GO Challenge 3 source contains no problem or solution data file",
270            category = Parse;
271        READ_GOC3_INVALID_DOCUMENT = "READ.GOC3.INVALID_DOCUMENT", Error,
272            "the GO Challenge 3 document decodes but is not a valid problem or solution file",
273            category = Parse;
274        READ_GOC3_VALUE_INFERRED = "READ.GOC3.VALUE_INFERRED", Warning,
275            "a GO Challenge 3 value the document never states was inferred";
276        READ_GOC3_OPTIONAL_FIELD_UNTYPED = "READ.GOC3.OPTIONAL_FIELD_UNTYPED", Remark,
277            "an optional GO Challenge 3 field is retained as untyped source metadata";
278        READ_GOC3_RETAINED_SOURCE_ONLY = "READ.GOC3.RETAINED_SOURCE_ONLY", Warning,
279            "a GO Challenge 3 section survives in the retained source only";
280
281        READ_OPFDATA_FIELD_DROPPED = "READ.OPFDATA.FIELD_DROPPED", Warning,
282            "an OPFData field outside the published schema is not in the snapshot";
283        READ_OPFDATA_VALUE_INFERRED = "READ.OPFDATA.VALUE_INFERRED", Warning,
284            "OPFData carries no identity or frequency, so the reader synthesized them";
285        READ_OPFDATA_RETAINED_SOURCE_ONLY = "READ.OPFDATA.RETAINED_SOURCE_ONLY", Warning,
286            "an OPFData generator's solver initial values are carried in the parsed solution instead of the network snapshot";
287
288        READ_XIIDM_FIELD_UNMAPPED = "READ.XIIDM.FIELD_UNMAPPED", Warning,
289            "an XIIDM field is not represented in the PowerIO model";
290        READ_XIIDM_ELEMENT_UNMAPPED = "READ.XIIDM.ELEMENT_UNMAPPED", Warning,
291            "an XIIDM element is not represented in the PowerIO model";
292        READ_XIIDM_CALCULATION_VIEW = "READ.XIIDM.CALCULATION_VIEW", Warning,
293            "an XIIDM value is retained in detailed connectivity but represented differently in the balanced calculation view";
294        READ_XIIDM_VALUE_DEFAULTED = "READ.XIIDM.VALUE_DEFAULTED", Warning,
295            "a PowerIO value absent from XIIDM was assigned a documented default";
296        READ_XIIDM_VERSION_COMPATIBILITY = "READ.XIIDM.VERSION_COMPATIBILITY", Remark,
297            "an older XIIDM input version was read; fresh XIIDM output uses 1.17";
298
299        READ_SURGE_RETAINED_SOURCE_ONLY = "READ.SURGE.RETAINED_SOURCE_ONLY", Warning,
300            "a Surge section survives in the retained source only";
301
302        READ_UCTE_VALUE_DEFAULTED = "READ.UCTE.VALUE_DEFAULTED", Warning,
303            "a UCTE-DEF value the model needs was not in the record and was defaulted";
304        READ_UCTE_VALUE_SUBSTITUTED = "READ.UCTE.VALUE_SUBSTITUTED", Warning,
305            "a UCTE-DEF value the record states could not be used as given";
306        READ_UCTE_REFERENCE_DROPPED = "READ.UCTE.REFERENCE_DROPPED", Warning,
307            "a UCTE-DEF regulation or special description names a transformer the case does not declare";
308        READ_UCTE_RECORD_IGNORED = "READ.UCTE.RECORD_IGNORED", Warning,
309            "a UCTE-DEF record names no usable electrical element and was ignored";
310        READ_UCTE_RETAINED_SOURCE_ONLY = "READ.UCTE.RETAINED_SOURCE_ONLY", Warning,
311            "a UCTE-DEF block survives in the retained source only";
312
313        PARSE_IEEE_CDF_MALFORMED = "PARSE.IEEE_CDF.MALFORMED", Error,
314            "an IEEE CDF title card or record could not be decoded", category = Parse;
315        READ_IEEE_CDF_RECORD_TRUNCATED = "READ.IEEE_CDF.RECORD_TRUNCATED", Warning,
316            "an IEEE CDF record ends before a mandatory field, which was read as zero";
317        READ_IEEE_CDF_VALUE_DEFAULTED = "READ.IEEE_CDF.VALUE_DEFAULTED", Warning,
318            "a value the balanced model needs is absent from the IEEE CDF and was defaulted";
319        READ_IEEE_CDF_VALUE_SUBSTITUTED = "READ.IEEE_CDF.VALUE_SUBSTITUTED", Warning,
320            "an IEEE CDF type or side code outside the documented set was read as the nearest documented value";
321        READ_IEEE_CDF_SOURCE_MALFORMED = "READ.IEEE_CDF.SOURCE_MALFORMED", Warning,
322            "an IEEE CDF section header, item count, terminator, record placement, or bus reference disagrees with the records";
323        READ_IEEE_CDF_RETAINED_SOURCE_ONLY = "READ.IEEE_CDF.RETAINED_SOURCE_ONLY", Remark,
324            "an IEEE CDF field or section survives in the retained source only";
325
326        READ_CON_STATEMENT_UNRECOGNIZED = "READ.CON.STATEMENT_UNRECOGNIZED", Warning,
327            "a PSS/E contingency statement outside the grammar was kept as text";
328        READ_CON_TEXT_AFTER_END = "READ.CON.TEXT_AFTER_END", Warning,
329            "a PSS/E contingency file states text after its file END";
330        READ_CON_NOTES_TRUNCATED = "READ.CON.NOTES_TRUNCATED", Warning,
331            "the PSS/E contingency reader stopped recording notes at its budget";
332        READ_CON_SOURCE_MALFORMED = "READ.CON.SOURCE_MALFORMED", Warning,
333            "a PSS/E contingency line could not be read as the statement its block requires";
334        READ_CON_NOT_TEXT = "READ.CON.NOT_TEXT", Error,
335            "a PSS/E contingency description file is not valid UTF-8 text", category = Parse;
336
337        READ_SUB_STATEMENT_UNRECOGNIZED = "READ.SUB.STATEMENT_UNRECOGNIZED", Warning,
338            "a PSS/E subsystem statement outside the grammar was kept as its original line";
339        READ_SUB_TEXT_AFTER_END = "READ.SUB.TEXT_AFTER_END", Warning,
340            "a PSS/E subsystem description file states text after its file END";
341        READ_SUB_NOTES_TRUNCATED = "READ.SUB.NOTES_TRUNCATED", Warning,
342            "the PSS/E subsystem reader stopped recording notes at its budget";
343        READ_SUB_SOURCE_MALFORMED = "READ.SUB.SOURCE_MALFORMED", Warning,
344            "a PSS/E subsystem selector states a keyword whose values are not the numbers it needs";
345        READ_SUB_NOT_TEXT = "READ.SUB.NOT_TEXT", Error,
346            "a PSS/E subsystem description file is not valid UTF-8 text", category = Parse;
347
348        READ_MON_STATEMENT_UNRECOGNIZED = "READ.MON.STATEMENT_UNRECOGNIZED", Warning,
349            "a PSS/E monitored element statement outside the grammar was kept as its original line";
350        READ_MON_TEXT_AFTER_END = "READ.MON.TEXT_AFTER_END", Warning,
351            "a PSS/E monitored element file states text after its file END";
352        READ_MON_NOTES_TRUNCATED = "READ.MON.NOTES_TRUNCATED", Warning,
353            "the PSS/E monitored element reader stopped recording notes at its budget";
354        READ_MON_SOURCE_MALFORMED = "READ.MON.SOURCE_MALFORMED", Warning,
355            "a line inside a PSS/E monitored element block states no branch";
356        READ_MON_NOT_TEXT = "READ.MON.NOT_TEXT", Error,
357            "a PSS/E monitored element file is not valid UTF-8 text", category = Parse;
358
359        READ_GEO_SOURCE_MALFORMED = "READ.GEO.SOURCE_MALFORMED", Warning,
360            "a geo layer row could not be read and was skipped";
361        READ_GEO_NOTES_TRUNCATED = "READ.GEO.NOTES_TRUNCATED", Warning,
362            "the geo reader stopped recording notes at its budget";
363        READ_GEO_NOT_TEXT = "READ.GEO.NOT_TEXT", Error,
364            "a geographic layer document is not valid UTF-8 text", category = Parse;
365
366        READ_IO_FAILED = "READ.IO.FAILED", Error,
367            "the case file could not be read", category = Io;
368
369        // CANONICALIZE: normalization of an already-read network.
370        CANONICALIZE_NORMALIZE_BOUNDS_CLAMPED = "CANONICALIZE.NORMALIZE.BOUNDS_CLAMPED", Remark,
371            "a branch angle difference bound was clamped into the modeled range";
372        CANONICALIZE_NORMALIZE_NO_REFERENCE_BUS = "CANONICALIZE.NORMALIZE.NO_REFERENCE_BUS", Error,
373            "no reference bus can be established: no bus hosts an in-service generator",
374            category = Data;
375        CANONICALIZE_NORMALIZE_REFERENCE_DESIGNATED =
376            "CANONICALIZE.NORMALIZE.REFERENCE_DESIGNATED", Warning,
377            "the case states no surviving reference bus, so normalization designated a slack";
378        CANONICALIZE_NORMALIZE_GEN_COST_ABSENT =
379            "CANONICALIZE.NORMALIZE.GEN_COST_ABSENT", Warning,
380            "the solver-ready copy has in-service generators and no cost data, so any cost objective built from it is zero";
381        CANONICALIZE_NORMALIZE_INVALID_OPTION = "CANONICALIZE.NORMALIZE.INVALID_OPTION", Error,
382            "a normalize option is outside the range it is defined on", category = Data;
383        CANONICALIZE_NORMALIZE_INVALID_BASE_MVA = "CANONICALIZE.NORMALIZE.INVALID_BASE_MVA", Error,
384            "the case base MVA is not a positive finite number", category = Data;
385
386        // BUILD: assembling a derived object from a network that already parsed.
387        BUILD_INDEX_UNKNOWN_BUS = "BUILD.INDEX.UNKNOWN_BUS", Error,
388            "an element references a bus id the case does not declare", category = Data;
389        BUILD_INDEX_REFERENCE_BUS_COUNT = "BUILD.INDEX.REFERENCE_BUS_COUNT", Error,
390            "the index needs exactly one reference bus", category = Data;
391        BUILD_INDEX_UNGROUNDED_COMPONENT = "BUILD.INDEX.UNGROUNDED_COMPONENT", Error,
392            "a connected component has no reference bus to ground", category = Data;
393        BUILD_BRANCH_ZERO_IMPEDANCE = "BUILD.BRANCH.ZERO_IMPEDANCE", Error,
394            "a branch has a zero matrix denominator under the selected build options",
395            category = Data;
396        BUILD_BRANCH_NOT_A_NUMBER = "BUILD.BRANCH.NOT_A_NUMBER", Error,
397            "a branch susceptance is not finite", category = Data;
398        BUILD_BRANCH_DEGENERATE_TAP = "BUILD.BRANCH.DEGENERATE_TAP", Error,
399            "a branch tap ratio is too small to divide by", category = Data;
400        BUILD_GEO_UNLOCATED_ELEMENTS = "BUILD.GEO.UNLOCATED_ELEMENTS", Error,
401            "a geo apply left elements with no location or route", category = Data;
402        BUILD_GEO_APPLY_SUMMARY = "BUILD.GEO.APPLY_SUMMARY", Remark,
403            "how many elements a geo apply located";
404        BUILD_GEO_UNMATCHED_FEATURE = "BUILD.GEO.UNMATCHED_FEATURE", Warning,
405            "a geo feature matched no element in the network";
406        BUILD_CON_CASE_UNRESOLVED = "BUILD.CON.CASE_UNRESOLVED", Warning,
407            "an action of a contingency case bound to no element, or named more than one where one was required";
408        BUILD_CON_NOTES_TRUNCATED = "BUILD.CON.NOTES_TRUNCATED", Warning,
409            "the contingency resolver stopped recording notes at its budget";
410        BUILD_CON_SUBSYSTEM_UNKNOWN = "BUILD.CON.SUBSYSTEM_UNKNOWN", Warning,
411            "an automatic contingency specification names a subsystem the subsystem set does not state";
412        BUILD_CON_SPECIFICATION_EMPTY = "BUILD.CON.SPECIFICATION_EMPTY", Warning,
413            "an automatic contingency specification names fewer in-service elements of its target family than its order needs";
414        BUILD_MON_STATEMENT_UNRESOLVED = "BUILD.MON.STATEMENT_UNRESOLVED", Warning,
415            "a monitored element statement did not bind to exactly one element, or names a subsystem the subsystem set does not state";
416
417        // VALIDATE: the case's own internal consistency.
418        /// Emitted by the stored document's payload validation in the facade;
419        /// declared here because this crate owns the balanced model.
420        VALIDATE_BALANCED_STRUCTURE = "VALIDATE.BALANCED.STRUCTURE", Error,
421            "a balanced payload's referential integrity does not hold";
422        VALIDATE_BALANCED_VALUE_DOMAIN = "VALIDATE.BALANCED.VALUE_DOMAIN", Warning,
423            "a balanced payload value is outside the domain the model states";
424        VALIDATE_BALANCED_PAYLOAD_IDENTITY = "VALIDATE.BALANCED.PAYLOAD_IDENTITY", Error,
425            "a balanced payload's uid identity does not hold";
426        VALIDATE_GEN_COST_MISSING = "VALIDATE.GEN_COST.MISSING", Error,
427            "a generator carries no cost data under a policy that requires one", category = Data;
428        VALIDATE_GEN_COST_NOT_A_NUMBER = "VALIDATE.GEN_COST.NOT_A_NUMBER", Error,
429            "a default generator cost field is not finite", category = Data;
430        VALIDATE_GEN_COST_PATCH_INVALID = "VALIDATE.GEN_COST.PATCH_INVALID", Error,
431            "a generator cost patch row is not usable", category = Data;
432        VALIDATE_GEN_COST_COUNT_MISMATCH = "VALIDATE.GEN_COST.COUNT_MISMATCH", Error,
433            "the cost table has neither one row per generator nor two", category = Data;
434        VALIDATE_DC_LINE_COST_COUNT_MISMATCH = "VALIDATE.DC_LINE_COST.COUNT_MISMATCH", Error,
435            "the dcline cost table has other than one row per dcline", category = Data;
436
437        // LOWER: a policy applied on the way into a target.
438        TRANSFORM_GEN_COST_POLICY_APPLIED = "TRANSFORM.GEN_COST.POLICY_APPLIED", Remark,
439            "a write time generator cost policy patched or synthesized costs";
440
441        // REQUEST: the call named something powerio does not provide.
442        REQUEST_FORMAT_UNKNOWN = "REQUEST.FORMAT.UNKNOWN", Error,
443            "the named case format is not one powerio reads", category = Request;
444        REQUEST_FORMAT_WRITE_UNSUPPORTED = "REQUEST.FORMAT.WRITE_UNSUPPORTED", Error,
445            "the named case format is read only and has no writer", category = Request;
446
447        EMIT_FORMAT_REQUIRED_VALUE_MISSING = "EMIT.FORMAT.REQUIRED_VALUE_MISSING", Error,
448            "the requested format requires a value the module does not contain", category = Output;
449
450        // Write side codes a single target owns.
451        /// The default `.raw` target is revision 33, so writing a newer source
452        /// through it re-emits the older layout.
453        EMIT_PSSE_DOWNGRADED = "EMIT.PSSE.DOWNGRADED", Warning,
454            "a newer PSS/E revision was written into an older layout";
455        EMIT_PSSE_RATING_SET_REMAPPED = "EMIT.PSSE.RATING_SET_REMAPPED", Remark,
456            "a named branch rating set was written into a PSS/E numbered rating slot";
457    }
458
459    /// Every write target's family, in the order [`super::registry`] reports
460    /// them.
461    pub const EMIT_FAMILIES: [&EmitFamily; 14] = [
462        &EMIT_MATPOWER,
463        &EMIT_PSSE,
464        &EMIT_PSLF,
465        &EMIT_PANDAPOWER,
466        &EMIT_PYPSA,
467        &EMIT_POWERWORLD,
468        &EMIT_POWERMODELS,
469        &EMIT_EGRET,
470        &EMIT_SURGE,
471        &EMIT_XIIDM,
472        &EMIT_JIIDM,
473        &EMIT_CGMES,
474        &EMIT_UCTE,
475        &EMIT_UNSUPPORTED,
476    ];
477}
478
479/// Every code this crate declares.
480#[must_use]
481pub fn registry() -> Vec<&'static DiagnosticInfo> {
482    let mut all: Vec<&'static DiagnosticInfo> = codes::ALL.to_vec();
483    for family in codes::EMIT_FAMILIES {
484        all.extend(family.entries());
485    }
486    all
487}
488
489impl TargetFormat {
490    /// The write side family for this target.
491    #[must_use]
492    pub fn emit_family(self) -> &'static EmitFamily {
493        match self {
494            TargetFormat::Matpower => &codes::EMIT_MATPOWER,
495            TargetFormat::Psse { .. } | TargetFormat::PsseRawx => &codes::EMIT_PSSE,
496            TargetFormat::Pslf => &codes::EMIT_PSLF,
497            TargetFormat::PandapowerJson => &codes::EMIT_PANDAPOWER,
498            TargetFormat::PowerWorld => &codes::EMIT_POWERWORLD,
499            TargetFormat::PowerModelsJson => &codes::EMIT_POWERMODELS,
500            TargetFormat::EgretJson => &codes::EMIT_EGRET,
501            TargetFormat::SurgeJson => &codes::EMIT_SURGE,
502            TargetFormat::Xiidm => &codes::EMIT_XIIDM,
503            TargetFormat::Jiidm => &codes::EMIT_JIIDM,
504            TargetFormat::Cgmes => &codes::EMIT_CGMES,
505            TargetFormat::Ucte => &codes::EMIT_UCTE,
506            // This transmission layer has no GOC3 problem or OPFData writer.
507            // The facade emits a complete GOC3 solution before reaching this
508            // branch. Other requests are refused before any family is
509            // consulted, and `REQUEST.FORMAT.WRITE_UNSUPPORTED` carries it.
510            TargetFormat::Goc3Json | TargetFormat::DeepMindOpfDataJson => &codes::EMIT_UNSUPPORTED,
511        }
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518
519    #[test]
520    fn the_registry_is_sound() {
521        let problems = check_registry(registry());
522        assert!(problems.is_empty(), "{problems:#?}");
523    }
524
525    #[test]
526    fn every_write_target_has_a_family_of_its_own() {
527        let mut scopes: Vec<&str> = codes::EMIT_FAMILIES
528            .iter()
529            .map(|f| f.field_dropped.code.split('.').nth(1).unwrap())
530            .collect();
531        scopes.sort_unstable();
532        scopes.dedup();
533        assert_eq!(scopes.len(), codes::EMIT_FAMILIES.len());
534        assert_eq!(
535            TargetFormat::Psse { rev: 33 }
536                .emit_family()
537                .field_dropped
538                .code,
539            "EMIT.PSSE.FIELD_DROPPED"
540        );
541    }
542}