1use thiserror::Error;
2
3use crate::network::BusId;
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum Error {
10 #[error("missing required MATPOWER field `{0}`")]
11 MissingField(&'static str),
12
13 #[error(
14 "malformed MATPOWER `{field}` row {row}: expected at least {expected} columns, got {got}"
15 )]
16 ShortRow {
17 field: &'static str,
18 row: usize,
19 expected: usize,
20 got: usize,
21 },
22
23 #[error("could not parse `{field}` row {row} value `{value}` as f64")]
24 BadFloat {
25 field: &'static str,
26 row: usize,
27 value: String,
28 },
29
30 #[error("unbalanced brackets in MATPOWER `{0}` matrix")]
31 UnbalancedBrackets(&'static str),
32
33 #[error("element references unknown bus id {bus_id} (in-service index {element_index})")]
34 UnknownBus { bus_id: BusId, element_index: usize },
35
36 #[error("branch row {row} has a zero matrix denominator under the selected build options")]
37 ZeroImpedance { row: usize },
38
39 #[error("branch row {row} has non-finite DC susceptance b = 1/x (x is NaN, Inf, or denormal)")]
40 NonFiniteSusceptance { row: usize },
41
42 #[error("branch row {row} has a tap ratio of {tap} too small to divide by")]
45 DegenerateTap { row: usize, tap: f64 },
46
47 #[error("generator {gen_index} has no cost data")]
48 MissingGenCost { gen_index: usize },
49
50 #[error("default generator cost field `{field}` is not finite: {value}")]
51 NonFiniteGenCost { field: &'static str, value: f64 },
52
53 #[error("invalid generator cost patch row {row}: {reason}")]
54 InvalidGenCostPatch { row: usize, reason: String },
55
56 #[error("`gen` has {gens} rows but `gencost` has {gencost}; expected {gens} (active only) or {} (active + reactive)", gens * 2)]
57 GenCostCountMismatch { gens: usize, gencost: usize },
58
59 #[error("expected exactly one reference (slack) bus, found {found}")]
60 ReferenceBusCount { found: usize },
61
62 #[error("base MVA must be a positive, finite number, got {base}")]
63 InvalidBaseMva { base: f64 },
64
65 #[error("invalid normalize option `{field}`: {value}")]
66 InvalidNormalizeOption { field: &'static str, value: f64 },
67
68 #[error(
69 "{components} connected component(s) have no reference (slack) bus to ground; DC sensitivities need at least one reference per island"
70 )]
71 UngroundedComponent { components: usize },
72
73 #[error(transparent)]
74 Io(#[from] std::io::Error),
75
76 #[error(
77 "geo apply left {buses} bus(es) with no location and {branches} branch(es) with no route"
78 )]
79 UnlocatedElements { buses: usize, branches: usize },
80
81 #[error("{format} read error: {message}")]
82 FormatRead {
83 format: &'static str,
84 message: String,
85 },
86
87 #[error("unknown or unsupported case format: {0}")]
88 UnknownFormat(String),
89
90 #[error("{format} is a read only format with no writer")]
94 WriteUnsupported { format: &'static str },
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum ErrorCategory {
108 Io,
110 UnknownFormat,
112 Parse,
114 Data,
116 Output,
118}
119
120impl ErrorCategory {
121 #[must_use]
124 pub fn token(self) -> &'static str {
125 match self {
126 ErrorCategory::Io => "io",
127 ErrorCategory::UnknownFormat => "unknown_format",
128 ErrorCategory::Parse => "parse",
129 ErrorCategory::Data => "data",
130 ErrorCategory::Output => "output",
131 }
132 }
133
134 pub const TOKENS: [&'static str; 5] = ["io", "unknown_format", "parse", "data", "output"];
138}
139
140impl Error {
141 pub fn category(&self) -> ErrorCategory {
145 use ErrorCategory as C;
146 match self {
147 Error::Io(_) => C::Io,
148 Error::UnknownFormat(_) | Error::WriteUnsupported { .. } => C::UnknownFormat,
152 Error::MissingField(_)
155 | Error::ShortRow { .. }
156 | Error::BadFloat { .. }
157 | Error::UnbalancedBrackets(_)
158 | Error::FormatRead { .. } => C::Parse,
159 Error::UnknownBus { .. }
164 | Error::ZeroImpedance { .. }
165 | Error::NonFiniteSusceptance { .. }
166 | Error::DegenerateTap { .. }
167 | Error::MissingGenCost { .. }
168 | Error::NonFiniteGenCost { .. }
169 | Error::InvalidGenCostPatch { .. }
170 | Error::GenCostCountMismatch { .. }
171 | Error::ReferenceBusCount { .. }
172 | Error::InvalidBaseMva { .. }
173 | Error::InvalidNormalizeOption { .. }
174 | Error::UngroundedComponent { .. }
175 | Error::UnlocatedElements { .. } => C::Data,
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn category_pins_the_intended_buckets() {
186 use ErrorCategory::{Data, Io, Parse, UnknownFormat};
187 assert_eq!(Error::MissingField("bus").category(), Parse);
189 assert_eq!(
190 Error::FormatRead {
191 format: "psse",
192 message: "bad record".into()
193 }
194 .category(),
195 Parse
196 );
197 assert_eq!(Error::InvalidBaseMva { base: 0.0 }.category(), Data);
201 assert_eq!(
202 Error::UngroundedComponent { components: 1 }.category(),
203 Data
204 );
205 assert_eq!(
206 Error::UnknownBus {
207 bus_id: BusId(7),
208 element_index: 0
209 }
210 .category(),
211 Data
212 );
213 assert_eq!(Error::UnknownFormat("xyz".into()).category(), UnknownFormat);
217 assert_eq!(
218 Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)).category(),
219 Io
220 );
221 }
222}
223
224#[cfg(test)]
225mod category_token_tests {
226 use super::ErrorCategory;
227
228 #[test]
231 fn tokens_lists_every_category_exactly_once() {
232 let every = [
233 ErrorCategory::Io,
234 ErrorCategory::UnknownFormat,
235 ErrorCategory::Parse,
236 ErrorCategory::Data,
237 ErrorCategory::Output,
238 ];
239 let from_tokens: Vec<&str> = every.iter().map(|c| c.token()).collect();
240 assert_eq!(from_tokens, ErrorCategory::TOKENS.to_vec());
241 }
242}