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(transparent)]
99 Io(#[from] std::io::Error),
100
101 #[error(
102 "geo apply left {buses} bus(es) with no location and {branches} branch(es) with no route"
103 )]
104 UnlocatedElements { buses: usize, branches: usize },
105
106 #[error("{format} read error: {message}")]
107 FormatRead {
108 format: &'static str,
109 message: String,
110 },
111
112 #[error("{format} emission failed: {message}")]
113 Emit {
114 format: &'static str,
115 message: String,
116 },
117
118 #[error("unknown or unsupported case format: {0}")]
119 UnknownFormat(String),
120
121 #[error("{format} is a read only format with no writer")]
125 WriteUnsupported { format: &'static str },
126}
127
128pub use powerio_core::ErrorCategory;
132
133impl Error {
134 #[must_use]
137 pub fn reference_bus_count(found: usize) -> Self {
138 Error::ReferenceBusCount { found }
139 }
140
141 pub fn code(&self) -> &'static DiagnosticInfo {
145 match self {
146 Error::MissingField(_)
147 | Error::ShortRow { .. }
148 | Error::BadFloat { .. }
149 | Error::BadId { .. }
150 | Error::UnbalancedBrackets(_) => &codes::PARSE_MATPOWER_MALFORMED,
151 Error::FormatRead { .. } => &codes::PARSE_SOURCE_MALFORMED,
152 Error::Emit { .. } => &codes::EMIT_FORMAT_REQUIRED_VALUE_MISSING,
153 Error::Io(_) => &codes::READ_IO_FAILED,
154 Error::UnknownBus { .. } => &codes::BUILD_INDEX_UNKNOWN_BUS,
155 Error::ZeroImpedance { .. } => &codes::BUILD_BRANCH_ZERO_IMPEDANCE,
156 Error::NonFiniteSusceptance { .. } => &codes::BUILD_BRANCH_NOT_A_NUMBER,
157 Error::DegenerateTap { .. } => &codes::BUILD_BRANCH_DEGENERATE_TAP,
158 Error::MissingGenCost { .. } => &codes::VALIDATE_GEN_COST_MISSING,
159 Error::NonFiniteGenCost { .. } => &codes::VALIDATE_GEN_COST_NOT_A_NUMBER,
160 Error::InvalidGenCostPatch { .. } => &codes::VALIDATE_GEN_COST_PATCH_INVALID,
161 Error::GenCostCountMismatch { .. } => &codes::VALIDATE_GEN_COST_COUNT_MISMATCH,
162 Error::DcLineCostCountMismatch { .. } => &codes::VALIDATE_DC_LINE_COST_COUNT_MISMATCH,
163 Error::NoReferenceBus => &codes::CANONICALIZE_NORMALIZE_NO_REFERENCE_BUS,
164 Error::ReferenceBusCount { .. } => &codes::BUILD_INDEX_REFERENCE_BUS_COUNT,
165 Error::InvalidBaseMva { .. } => &codes::CANONICALIZE_NORMALIZE_INVALID_BASE_MVA,
166 Error::InvalidNormalizeOption { .. } => &codes::CANONICALIZE_NORMALIZE_INVALID_OPTION,
167 Error::UngroundedComponent { .. } => &codes::BUILD_INDEX_UNGROUNDED_COMPONENT,
168 Error::UnlocatedElements { .. } => &codes::BUILD_GEO_UNLOCATED_ELEMENTS,
169 Error::UnknownFormat(_) => &codes::REQUEST_FORMAT_UNKNOWN,
170 Error::WriteUnsupported { .. } => &codes::REQUEST_FORMAT_WRITE_UNSUPPORTED,
171 }
172 }
173
174 pub fn category(&self) -> ErrorCategory {
178 use ErrorCategory as C;
179 match self {
180 Error::Io(_) => C::Io,
181 Error::UnknownFormat(_) | Error::WriteUnsupported { .. } => C::Request,
185 Error::Emit { .. } => C::Output,
186 Error::MissingField(_)
189 | Error::ShortRow { .. }
190 | Error::BadFloat { .. }
191 | Error::BadId { .. }
192 | Error::UnbalancedBrackets(_)
193 | Error::FormatRead { .. } => C::Parse,
194 Error::UnknownBus { .. }
199 | Error::ZeroImpedance { .. }
200 | Error::NonFiniteSusceptance { .. }
201 | Error::DegenerateTap { .. }
202 | Error::MissingGenCost { .. }
203 | Error::NonFiniteGenCost { .. }
204 | Error::InvalidGenCostPatch { .. }
205 | Error::GenCostCountMismatch { .. }
206 | Error::DcLineCostCountMismatch { .. }
207 | Error::NoReferenceBus
208 | Error::ReferenceBusCount { .. }
209 | Error::InvalidBaseMva { .. }
210 | Error::InvalidNormalizeOption { .. }
211 | Error::UngroundedComponent { .. }
212 | Error::UnlocatedElements { .. } => C::Data,
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
225 fn every_error_code_publishes_the_category_the_variant_reports() {
226 let every: Vec<Error> = vec![
227 Error::MissingField("bus"),
228 Error::ShortRow {
229 field: "bus",
230 row: 1,
231 expected: 13,
232 got: 3,
233 },
234 Error::BadFloat {
235 field: "bus",
236 row: 1,
237 value: "x".into(),
238 },
239 Error::BadId {
240 field: "bus",
241 row: 1,
242 message: "`BUS_I` value 1e300 is outside the id range 0..2^63".into(),
243 },
244 Error::UnbalancedBrackets("bus"),
245 Error::FormatRead {
246 format: "psse",
247 message: "bad record".into(),
248 },
249 Error::Emit {
250 format: "xiidm",
251 message: "missing nominalV".into(),
252 },
253 Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)),
254 Error::UnknownBus {
255 bus_id: BusId(7),
256 element_index: 0,
257 },
258 Error::ZeroImpedance { row: 1 },
259 Error::NonFiniteSusceptance { row: 1 },
260 Error::DegenerateTap { row: 1, tap: 0.0 },
261 Error::MissingGenCost { gen_index: 0 },
262 Error::NonFiniteGenCost {
263 field: "c2",
264 value: f64::NAN,
265 },
266 Error::InvalidGenCostPatch {
267 row: 1,
268 reason: "empty".into(),
269 },
270 Error::GenCostCountMismatch {
271 gens: 2,
272 gencost: 3,
273 },
274 Error::DcLineCostCountMismatch {
275 dclines: 1,
276 dclinecost: 2,
277 },
278 Error::NoReferenceBus,
279 Error::reference_bus_count(2),
280 Error::InvalidBaseMva { base: 0.0 },
281 Error::InvalidNormalizeOption {
282 field: "angle_bound_pad",
283 value: 0.0,
284 },
285 Error::UngroundedComponent { components: 1 },
286 Error::UnlocatedElements {
287 buses: 1,
288 branches: 0,
289 },
290 Error::UnknownFormat("xyz".into()),
291 Error::WriteUnsupported { format: "goc3" },
292 ];
293 for error in &every {
294 let info = error.code();
295 assert_eq!(
296 info.category,
297 Some(error.category()),
298 "{} publishes {:?} but the variant reports {:?}",
299 info.code,
300 info.category,
301 error.category()
302 );
303 }
304 }
305
306 #[test]
307 fn the_two_reference_bus_stages_carry_different_codes() {
308 assert_eq!(
309 Error::NoReferenceBus.code().code,
310 "CANONICALIZE.NORMALIZE.NO_REFERENCE_BUS"
311 );
312 assert_eq!(
313 Error::reference_bus_count(2).code().code,
314 "BUILD.INDEX.REFERENCE_BUS_COUNT"
315 );
316 assert!(Error::NoReferenceBus.to_string().contains("generator"));
319 }
320
321 #[test]
322 fn category_pins_the_intended_buckets() {
323 use ErrorCategory::{Data, Io, Output, Parse, Request};
324 assert_eq!(Error::MissingField("bus").category(), Parse);
326 assert_eq!(
327 Error::FormatRead {
328 format: "psse",
329 message: "bad record".into()
330 }
331 .category(),
332 Parse
333 );
334 assert_eq!(Error::InvalidBaseMva { base: 0.0 }.category(), Data);
338 assert_eq!(
339 Error::UngroundedComponent { components: 1 }.category(),
340 Data
341 );
342 assert_eq!(
343 Error::UnknownBus {
344 bus_id: BusId(7),
345 element_index: 0
346 }
347 .category(),
348 Data
349 );
350 assert_eq!(Error::UnknownFormat("xyz".into()).category(), Request);
353 assert_eq!(
354 Error::Emit {
355 format: "xiidm",
356 message: "missing nominalV".into()
357 }
358 .category(),
359 Output
360 );
361 assert_eq!(
362 Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)).category(),
363 Io
364 );
365 }
366}
367
368#[cfg(test)]
369mod category_token_tests {
370 use super::ErrorCategory;
371
372 #[test]
375 fn tokens_lists_every_category_exactly_once() {
376 let every = [
377 ErrorCategory::Io,
378 ErrorCategory::Request,
379 ErrorCategory::Parse,
380 ErrorCategory::Data,
381 ErrorCategory::Output,
382 ];
383 let from_tokens: Vec<&str> = every.iter().map(|c| c.as_str()).collect();
384 assert_eq!(from_tokens, ErrorCategory::TOKENS.to_vec());
385 }
386}