1use thiserror::Error;
2
3use crate::diagnostics::{DiagnosticInfo, codes};
4use crate::network::BusId;
5
6pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum Error {
11 #[error("missing required MATPOWER field `{0}`")]
12 MissingField(&'static str),
13
14 #[error(
15 "malformed MATPOWER `{field}` row {row}: expected at least {expected} columns, got {got}"
16 )]
17 ShortRow {
18 field: &'static str,
19 row: usize,
20 expected: usize,
21 got: usize,
22 },
23
24 #[error("could not parse `{field}` row {row} value `{value}` as f64")]
25 BadFloat {
26 field: &'static str,
27 row: usize,
28 value: String,
29 },
30
31 #[error("malformed MATPOWER `{field}` row {row}: {message}")]
32 BadId {
33 field: &'static str,
34 row: usize,
35 message: String,
36 },
37
38 #[error("unbalanced brackets in MATPOWER `{0}` matrix")]
39 UnbalancedBrackets(&'static str),
40
41 #[error("element references unknown bus id {bus_id} (in-service index {element_index})")]
42 UnknownBus { bus_id: BusId, element_index: usize },
43
44 #[error("branch row {row} has a zero matrix denominator under the selected build options")]
45 ZeroImpedance { row: usize },
46
47 #[error(
48 "branch row {row} has a non-finite susceptance (r or x is NaN or Inf, or the four terminal admittances overflow)"
49 )]
50 NonFiniteSusceptance { row: usize },
51
52 #[error("branch row {row} has a tap ratio of {tap} too small to divide by")]
55 DegenerateTap { row: usize, tap: f64 },
56
57 #[error("generator {gen_index} has no cost data")]
58 MissingGenCost { gen_index: usize },
59
60 #[error("default generator cost field `{field}` is not finite: {value}")]
61 NonFiniteGenCost { field: &'static str, value: f64 },
62
63 #[error("invalid generator cost patch row {row}: {reason}")]
64 InvalidGenCostPatch { row: usize, reason: String },
65
66 #[error("`gen` has {gens} rows but `gencost` has {gencost}; expected {gens} (active only) or {} (active + reactive)", gens * 2)]
67 GenCostCountMismatch { gens: usize, gencost: usize },
68
69 #[error(
70 "`dcline` has {dclines} rows but `dclinecost` has {dclinecost}; expected one cost row per dcline"
71 )]
72 DcLineCostCountMismatch { dclines: usize, dclinecost: usize },
73
74 #[error(
79 "cannot establish a reference bus: a reference bus must host an in-service generator, and this case has none"
80 )]
81 NoReferenceBus,
82
83 #[error("expected exactly one reference (slack) bus, found {found}")]
85 ReferenceBusCount { found: usize },
86
87 #[error("base MVA must be a positive, finite number, got {base}")]
88 InvalidBaseMva { base: f64 },
89
90 #[error("invalid normalize option `{field}`: {value}")]
91 InvalidNormalizeOption { field: &'static str, value: f64 },
92
93 #[error(
94 "{components} connected component(s) have no reference (slack) bus to ground; DC sensitivities need at least one reference per island"
95 )]
96 UngroundedComponent { components: usize },
97
98 #[error(
105 "{closed} closed switch(es) join distinct buses (the first, switch row {row}, joins buses {from} and {to}), and this calculation does not model switches; merge their buses first: to_normalized under the default ClosedSwitchPolicy::Merge or BusMergeRule::closed_switches() with merge_buses in Rust, to_normalized(closed_switches=\"merge\") or merge_buses(closed_switches=True) in Python, or --merge-buses switches on the command line"
106 )]
107 ClosedSwitch {
108 row: usize,
109 from: BusId,
110 to: BusId,
111 closed: usize,
112 },
113
114 #[error("invalid bus merge rule: {message}")]
116 BusMergeRule { message: String },
117
118 #[error("merged flow input `{what}` has {got} entries; the merge needs {expected}")]
120 MergedFlowShape {
121 what: &'static str,
122 expected: usize,
123 got: usize,
124 },
125
126 #[error(transparent)]
127 Io(#[from] std::io::Error),
128
129 #[error(
130 "geo apply left {buses} bus(es) with no location and {branches} branch(es) with no route"
131 )]
132 UnlocatedElements { buses: usize, branches: usize },
133
134 #[error("{format} read error: {message}")]
135 FormatRead {
136 format: &'static str,
137 message: String,
138 },
139
140 #[error("{format} emission failed: {message}")]
141 Emit {
142 format: &'static str,
143 message: String,
144 },
145
146 #[error("unknown or unsupported case format: {0}")]
147 UnknownFormat(String),
148
149 #[error("{format} is a read only format with no writer")]
153 WriteUnsupported { format: &'static str },
154}
155
156pub use powerio_core::ErrorCategory;
160
161impl Error {
162 #[must_use]
165 pub fn reference_bus_count(found: usize) -> Self {
166 Error::ReferenceBusCount { found }
167 }
168
169 pub fn code(&self) -> &'static DiagnosticInfo {
173 match self {
174 Error::MissingField(_)
175 | Error::ShortRow { .. }
176 | Error::BadFloat { .. }
177 | Error::BadId { .. }
178 | Error::UnbalancedBrackets(_) => &codes::PARSE_MATPOWER_MALFORMED,
179 Error::FormatRead {
180 format: "sincal", ..
181 } => &codes::PARSE_SINCAL_MALFORMED,
182 Error::FormatRead { .. } => &codes::PARSE_SOURCE_MALFORMED,
183 Error::Emit { .. } => &codes::EMIT_FORMAT_REQUIRED_VALUE_MISSING,
184 Error::Io(_) => &codes::READ_IO_FAILED,
185 Error::UnknownBus { .. } => &codes::BUILD_INDEX_UNKNOWN_BUS,
186 Error::ZeroImpedance { .. } => &codes::BUILD_BRANCH_ZERO_IMPEDANCE,
187 Error::NonFiniteSusceptance { .. } => &codes::BUILD_BRANCH_NOT_A_NUMBER,
188 Error::DegenerateTap { .. } => &codes::BUILD_BRANCH_DEGENERATE_TAP,
189 Error::MissingGenCost { .. } => &codes::VALIDATE_GEN_COST_MISSING,
190 Error::NonFiniteGenCost { .. } => &codes::VALIDATE_GEN_COST_NOT_A_NUMBER,
191 Error::InvalidGenCostPatch { .. } => &codes::VALIDATE_GEN_COST_PATCH_INVALID,
192 Error::GenCostCountMismatch { .. } => &codes::VALIDATE_GEN_COST_COUNT_MISMATCH,
193 Error::DcLineCostCountMismatch { .. } => &codes::VALIDATE_DC_LINE_COST_COUNT_MISMATCH,
194 Error::NoReferenceBus => &codes::CANONICALIZE_NORMALIZE_NO_REFERENCE_BUS,
195 Error::ReferenceBusCount { .. } => &codes::BUILD_INDEX_REFERENCE_BUS_COUNT,
196 Error::InvalidBaseMva { .. } => &codes::CANONICALIZE_NORMALIZE_INVALID_BASE_MVA,
197 Error::InvalidNormalizeOption { .. } => &codes::CANONICALIZE_NORMALIZE_INVALID_OPTION,
198 Error::UngroundedComponent { .. } => &codes::BUILD_INDEX_UNGROUNDED_COMPONENT,
199 Error::ClosedSwitch { .. } => &codes::BUILD_SWITCH_CLOSED,
200 Error::BusMergeRule { .. } => &codes::CANONICALIZE_MERGE_INVALID_RULE,
201 Error::MergedFlowShape { .. } => &codes::CANONICALIZE_MERGE_FLOW_SHAPE_MISMATCH,
202 Error::UnlocatedElements { .. } => &codes::BUILD_GEO_UNLOCATED_ELEMENTS,
203 Error::UnknownFormat(_) => &codes::REQUEST_FORMAT_UNKNOWN,
204 Error::WriteUnsupported { .. } => &codes::REQUEST_FORMAT_WRITE_UNSUPPORTED,
205 }
206 }
207
208 pub fn category(&self) -> ErrorCategory {
212 use ErrorCategory as C;
213 match self {
214 Error::Io(_) => C::Io,
215 Error::UnknownFormat(_) | Error::WriteUnsupported { .. } => C::Request,
219 Error::Emit { .. } => C::Output,
220 Error::MissingField(_)
223 | Error::ShortRow { .. }
224 | Error::BadFloat { .. }
225 | Error::BadId { .. }
226 | Error::UnbalancedBrackets(_)
227 | Error::FormatRead { .. } => C::Parse,
228 Error::UnknownBus { .. }
233 | Error::ZeroImpedance { .. }
234 | Error::NonFiniteSusceptance { .. }
235 | Error::DegenerateTap { .. }
236 | Error::MissingGenCost { .. }
237 | Error::NonFiniteGenCost { .. }
238 | Error::InvalidGenCostPatch { .. }
239 | Error::GenCostCountMismatch { .. }
240 | Error::DcLineCostCountMismatch { .. }
241 | Error::NoReferenceBus
242 | Error::ReferenceBusCount { .. }
243 | Error::InvalidBaseMva { .. }
244 | Error::InvalidNormalizeOption { .. }
245 | Error::UngroundedComponent { .. }
246 | Error::ClosedSwitch { .. }
247 | Error::BusMergeRule { .. }
248 | Error::MergedFlowShape { .. }
249 | Error::UnlocatedElements { .. } => C::Data,
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
262 fn every_error_code_publishes_the_category_the_variant_reports() {
263 let every: Vec<Error> = vec![
264 Error::MissingField("bus"),
265 Error::ShortRow {
266 field: "bus",
267 row: 1,
268 expected: 13,
269 got: 3,
270 },
271 Error::BadFloat {
272 field: "bus",
273 row: 1,
274 value: "x".into(),
275 },
276 Error::BadId {
277 field: "bus",
278 row: 1,
279 message: "`BUS_I` value 1e300 is outside the id range 0..2^63".into(),
280 },
281 Error::UnbalancedBrackets("bus"),
282 Error::FormatRead {
283 format: "psse",
284 message: "bad record".into(),
285 },
286 Error::Emit {
287 format: "xiidm",
288 message: "missing nominalV".into(),
289 },
290 Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)),
291 Error::UnknownBus {
292 bus_id: BusId(7),
293 element_index: 0,
294 },
295 Error::ZeroImpedance { row: 1 },
296 Error::NonFiniteSusceptance { row: 1 },
297 Error::DegenerateTap { row: 1, tap: 0.0 },
298 Error::MissingGenCost { gen_index: 0 },
299 Error::NonFiniteGenCost {
300 field: "c2",
301 value: f64::NAN,
302 },
303 Error::InvalidGenCostPatch {
304 row: 1,
305 reason: "empty".into(),
306 },
307 Error::GenCostCountMismatch {
308 gens: 2,
309 gencost: 3,
310 },
311 Error::DcLineCostCountMismatch {
312 dclines: 1,
313 dclinecost: 2,
314 },
315 Error::NoReferenceBus,
316 Error::reference_bus_count(2),
317 Error::InvalidBaseMva { base: 0.0 },
318 Error::InvalidNormalizeOption {
319 field: "angle_bound_pad",
320 value: 0.0,
321 },
322 Error::UngroundedComponent { components: 1 },
323 Error::ClosedSwitch {
324 row: 0,
325 from: BusId(1),
326 to: BusId(2),
327 closed: 1,
328 },
329 Error::BusMergeRule {
330 message: "threshold".into(),
331 },
332 Error::MergedFlowShape {
333 what: "branch_p_from",
334 expected: 2,
335 got: 1,
336 },
337 Error::UnlocatedElements {
338 buses: 1,
339 branches: 0,
340 },
341 Error::UnknownFormat("xyz".into()),
342 Error::WriteUnsupported { format: "goc3" },
343 ];
344 for error in &every {
345 let info = error.code();
346 assert_eq!(
347 info.category,
348 Some(error.category()),
349 "{} publishes {:?} but the variant reports {:?}",
350 info.code,
351 info.category,
352 error.category()
353 );
354 }
355 }
356
357 #[test]
358 fn the_two_reference_bus_stages_carry_different_codes() {
359 assert_eq!(
360 Error::NoReferenceBus.code().code,
361 "CANONICALIZE.NORMALIZE.NO_REFERENCE_BUS"
362 );
363 assert_eq!(
364 Error::reference_bus_count(2).code().code,
365 "BUILD.INDEX.REFERENCE_BUS_COUNT"
366 );
367 assert!(Error::NoReferenceBus.to_string().contains("generator"));
370 }
371
372 #[test]
373 fn category_pins_the_intended_buckets() {
374 use ErrorCategory::{Data, Io, Output, Parse, Request};
375 assert_eq!(Error::MissingField("bus").category(), Parse);
377 assert_eq!(
378 Error::FormatRead {
379 format: "psse",
380 message: "bad record".into()
381 }
382 .category(),
383 Parse
384 );
385 assert_eq!(Error::InvalidBaseMva { base: 0.0 }.category(), Data);
389 assert_eq!(
390 Error::UngroundedComponent { components: 1 }.category(),
391 Data
392 );
393 assert_eq!(
394 Error::UnknownBus {
395 bus_id: BusId(7),
396 element_index: 0
397 }
398 .category(),
399 Data
400 );
401 assert_eq!(Error::UnknownFormat("xyz".into()).category(), Request);
404 assert_eq!(
405 Error::Emit {
406 format: "xiidm",
407 message: "missing nominalV".into()
408 }
409 .category(),
410 Output
411 );
412 assert_eq!(
413 Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)).category(),
414 Io
415 );
416 }
417}
418
419#[cfg(test)]
420mod category_token_tests {
421 use super::ErrorCategory;
422
423 #[test]
426 fn tokens_lists_every_category_exactly_once() {
427 let every = [
428 ErrorCategory::Io,
429 ErrorCategory::Request,
430 ErrorCategory::Parse,
431 ErrorCategory::Data,
432 ErrorCategory::Output,
433 ];
434 let from_tokens: Vec<&str> = every.iter().map(|c| c.as_str()).collect();
435 assert_eq!(from_tokens, ErrorCategory::TOKENS.to_vec());
436 }
437}