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