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("output dimension mismatch: matrix is {n}x{n} but RHS has length {b_len}")]
43 DimensionMismatch { n: usize, b_len: usize },
44
45 #[error("case has no generators; DC-OPF requires an `mpc.gen` block")]
46 NoGenerators,
47
48 #[error("generator {gen_index} has no cost data")]
49 MissingGenCost { gen_index: usize },
50
51 #[error("default generator cost field `{field}` is not finite: {value}")]
52 NonFiniteGenCost { field: &'static str, value: f64 },
53
54 #[error("invalid generator cost patch row {row}: {reason}")]
55 InvalidGenCostPatch { row: usize, reason: String },
56
57 #[error(
58 "generator {gen_index} has an unsupported cost model (model {model}, ncost {ncost}); need polynomial model 2 with degree ≤ 2"
59 )]
60 UnsupportedCostModel {
61 gen_index: usize,
62 model: u8,
63 ncost: usize,
64 },
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("expected exactly one reference (slack) bus, found {found}")]
70 ReferenceBusCount { found: usize },
71
72 #[error("base MVA must be a positive, finite number, got {base}")]
73 InvalidBaseMva { base: f64 },
74
75 #[error("dimension mismatch: `{what}` expected length {expected}, got {got}")]
76 ShapeMismatch {
77 what: &'static str,
78 expected: usize,
79 got: usize,
80 },
81
82 #[error(
83 "DC sensitivity solve failed: the reference-grounded Laplacian is singular even though every component is grounded"
84 )]
85 SingularNetwork,
86
87 #[error(
88 "{components} connected component(s) have no reference (slack) bus to ground; DC sensitivities need at least one reference per island"
89 )]
90 UngroundedComponent { components: usize },
91
92 #[error(transparent)]
93 Io(#[from] std::io::Error),
94
95 #[error("matrix-market I/O: {0}")]
96 Mtx(String),
97
98 #[error("gridfm Parquet export: {0}")]
99 Parquet(String),
100
101 #[error("gridfm scenario batch is empty; provide at least one snapshot")]
102 EmptyScenarioBatch,
103
104 #[error("gridfm scenario id overflows i64 when numbering snapshot {index} from base {base}")]
105 ScenarioIdOverflow {
106 base: i64,
107 index: usize,
109 },
110
111 #[error(
112 "gridfm snapshot scenario {scenario} is normalized; gridfm export expects raw MW and degree fields"
113 )]
114 NormalizedGridfmSnapshot { scenario: i64 },
115
116 #[error(
117 "gridfm snapshot scenario {scenario} has non-finite {element} row {row} field `{field}`: {value}"
118 )]
119 NonFiniteGridfmValue {
120 scenario: i64,
121 element: &'static str,
122 row: usize,
123 field: &'static str,
124 value: f64,
125 },
126
127 #[error(
128 "gridfm snapshot {index} doesn't match the first snapshot's element set: {reason}; \
129 a scenario batch shares one base element set (same bus/branch/gen counts and bus-id order)"
130 )]
131 ScenarioShapeMismatch {
132 index: usize,
135 reason: ScenarioMismatch,
136 },
137
138 #[error("{format} read error: {message}")]
139 FormatRead {
140 format: &'static str,
141 message: String,
142 },
143
144 #[error("unknown or unsupported case format: {0}")]
145 UnknownFormat(String),
146
147 #[error("{format} is a read only format with no writer")]
151 WriteUnsupported { format: &'static str },
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum ErrorCategory {
165 Io,
167 UnknownFormat,
169 Parse,
171 Data,
173 Output,
175}
176
177impl Error {
178 pub fn category(&self) -> ErrorCategory {
182 use ErrorCategory as C;
183 match self {
184 Error::Io(_) => C::Io,
185 Error::UnknownFormat(_) | Error::WriteUnsupported { .. } => C::UnknownFormat,
189 Error::MissingField(_)
192 | Error::ShortRow { .. }
193 | Error::BadFloat { .. }
194 | Error::UnbalancedBrackets(_)
195 | Error::FormatRead { .. } => C::Parse,
196 Error::UnknownBus { .. }
201 | Error::ZeroImpedance { .. }
202 | Error::NonFiniteSusceptance { .. }
203 | Error::DimensionMismatch { .. }
204 | Error::NoGenerators
205 | Error::MissingGenCost { .. }
206 | Error::NonFiniteGenCost { .. }
207 | Error::InvalidGenCostPatch { .. }
208 | Error::UnsupportedCostModel { .. }
209 | Error::GenCostCountMismatch { .. }
210 | Error::ReferenceBusCount { .. }
211 | Error::InvalidBaseMva { .. }
212 | Error::ShapeMismatch { .. }
213 | Error::SingularNetwork
214 | Error::UngroundedComponent { .. }
215 | Error::EmptyScenarioBatch
216 | Error::ScenarioIdOverflow { .. }
217 | Error::NormalizedGridfmSnapshot { .. }
218 | Error::NonFiniteGridfmValue { .. }
219 | Error::ScenarioShapeMismatch { .. } => C::Data,
220 Error::Mtx(_) | Error::Parquet(_) => C::Output,
222 }
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub struct ElementCounts {
231 pub buses: usize,
232 pub branches: usize,
233 pub gens: usize,
234}
235
236impl std::fmt::Display for ElementCounts {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 write!(
239 f,
240 "{} buses, {} branches, {} gens",
241 self.buses, self.branches, self.gens
242 )
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum ScenarioMismatch {
255 Counts {
257 expected: ElementCounts,
258 got: ElementCounts,
259 },
260 BusOrder,
263}
264
265impl std::fmt::Display for ScenarioMismatch {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 match self {
268 Self::Counts { expected, got } => {
269 write!(f, "got ({got}) vs the first snapshot's ({expected})")
270 }
271 Self::BusOrder => {
272 write!(f, "counts match but the bus ids are in a different order")
273 }
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn category_pins_the_intended_buckets() {
284 use ErrorCategory::*;
285 assert_eq!(Error::MissingField("bus").category(), Parse);
287 assert_eq!(
288 Error::FormatRead {
289 format: "psse",
290 message: "bad record".into()
291 }
292 .category(),
293 Parse
294 );
295 assert_eq!(Error::NoGenerators.category(), Data);
299 assert_eq!(Error::InvalidBaseMva { base: 0.0 }.category(), Data);
300 assert_eq!(
301 Error::UngroundedComponent { components: 1 }.category(),
302 Data
303 );
304 assert_eq!(
305 Error::UnknownBus {
306 bus_id: BusId(7),
307 element_index: 0
308 }
309 .category(),
310 Data
311 );
312 assert_eq!(Error::EmptyScenarioBatch.category(), Data);
313 assert_eq!(
314 Error::ScenarioShapeMismatch {
315 index: 1,
316 reason: ScenarioMismatch::BusOrder
317 }
318 .category(),
319 Data
320 );
321 assert_eq!(Error::UnknownFormat("xyz".into()).category(), UnknownFormat);
323 assert_eq!(Error::Mtx("write failed".into()).category(), Output);
324 assert_eq!(
325 Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)).category(),
326 Io
327 );
328 }
329}