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 zero impedance (r=0, x=0); not representable in B'")]
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(
52 "generator {gen_index} has an unsupported cost model (model {model}, ncost {ncost}); need polynomial model 2 with degree ≤ 2"
53 )]
54 UnsupportedCostModel {
55 gen_index: usize,
56 model: u8,
57 ncost: usize,
58 },
59
60 #[error("`gen` has {gens} rows but `gencost` has {gencost}; expected {gens} (active only) or {} (active + reactive)", gens * 2)]
61 GenCostCountMismatch { gens: usize, gencost: usize },
62
63 #[error("expected exactly one reference (slack) bus, found {found}")]
64 ReferenceBusCount { found: usize },
65
66 #[error("base MVA must be a positive, finite number, got {base}")]
67 InvalidBaseMva { base: f64 },
68
69 #[error("dimension mismatch: `{what}` expected length {expected}, got {got}")]
70 ShapeMismatch {
71 what: &'static str,
72 expected: usize,
73 got: usize,
74 },
75
76 #[error(
77 "DC sensitivity solve failed: the reference-grounded Laplacian is singular even though every component is grounded"
78 )]
79 SingularNetwork,
80
81 #[error(
82 "{components} connected component(s) have no reference (slack) bus to ground; DC sensitivities need at least one reference per island"
83 )]
84 UngroundedComponent { components: usize },
85
86 #[error(transparent)]
87 Io(#[from] std::io::Error),
88
89 #[error("matrix-market I/O: {0}")]
90 Mtx(String),
91
92 #[error("gridfm Parquet export: {0}")]
93 Parquet(String),
94
95 #[error("gridfm scenario batch is empty; provide at least one snapshot")]
96 EmptyScenarioBatch,
97
98 #[error("gridfm scenario id overflows i64 when numbering snapshot {index} from base {base}")]
99 ScenarioIdOverflow {
100 base: i64,
101 index: usize,
103 },
104
105 #[error(
106 "gridfm snapshot scenario {scenario} is normalized; gridfm export expects raw MW and degree fields"
107 )]
108 NormalizedGridfmSnapshot { scenario: i64 },
109
110 #[error(
111 "gridfm snapshot scenario {scenario} has non-finite {element} row {row} field `{field}`: {value}"
112 )]
113 NonFiniteGridfmValue {
114 scenario: i64,
115 element: &'static str,
116 row: usize,
117 field: &'static str,
118 value: f64,
119 },
120
121 #[error(
122 "gridfm snapshot {index} doesn't match the first snapshot's element set: {reason}; \
123 a scenario batch shares one base element set (same bus/branch/gen counts and bus-id order)"
124 )]
125 ScenarioShapeMismatch {
126 index: usize,
129 reason: ScenarioMismatch,
130 },
131
132 #[error("{format} read error: {message}")]
133 FormatRead {
134 format: &'static str,
135 message: String,
136 },
137
138 #[error("unknown or unsupported case format: {0}")]
139 UnknownFormat(String),
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum ErrorCategory {
153 Io,
155 UnknownFormat,
157 Parse,
159 Data,
161 Output,
163}
164
165impl Error {
166 pub fn category(&self) -> ErrorCategory {
170 use ErrorCategory as C;
171 match self {
172 Error::Io(_) => C::Io,
173 Error::UnknownFormat(_) => C::UnknownFormat,
174 Error::MissingField(_)
177 | Error::ShortRow { .. }
178 | Error::BadFloat { .. }
179 | Error::UnbalancedBrackets(_)
180 | Error::FormatRead { .. } => C::Parse,
181 Error::UnknownBus { .. }
186 | Error::ZeroImpedance { .. }
187 | Error::NonFiniteSusceptance { .. }
188 | Error::DimensionMismatch { .. }
189 | Error::NoGenerators
190 | Error::MissingGenCost { .. }
191 | Error::UnsupportedCostModel { .. }
192 | Error::GenCostCountMismatch { .. }
193 | Error::ReferenceBusCount { .. }
194 | Error::InvalidBaseMva { .. }
195 | Error::ShapeMismatch { .. }
196 | Error::SingularNetwork
197 | Error::UngroundedComponent { .. }
198 | Error::EmptyScenarioBatch
199 | Error::ScenarioIdOverflow { .. }
200 | Error::NormalizedGridfmSnapshot { .. }
201 | Error::NonFiniteGridfmValue { .. }
202 | Error::ScenarioShapeMismatch { .. } => C::Data,
203 Error::Mtx(_) | Error::Parquet(_) => C::Output,
205 }
206 }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub struct ElementCounts {
214 pub buses: usize,
215 pub branches: usize,
216 pub gens: usize,
217}
218
219impl std::fmt::Display for ElementCounts {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 write!(
222 f,
223 "{} buses, {} branches, {} gens",
224 self.buses, self.branches, self.gens
225 )
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236#[non_exhaustive]
237pub enum ScenarioMismatch {
238 Counts {
240 expected: ElementCounts,
241 got: ElementCounts,
242 },
243 BusOrder,
246}
247
248impl std::fmt::Display for ScenarioMismatch {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 match self {
251 Self::Counts { expected, got } => {
252 write!(f, "got ({got}) vs the first snapshot's ({expected})")
253 }
254 Self::BusOrder => {
255 write!(f, "counts match but the bus ids are in a different order")
256 }
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn category_pins_the_intended_buckets() {
267 use ErrorCategory::*;
268 assert_eq!(Error::MissingField("bus").category(), Parse);
270 assert_eq!(
271 Error::FormatRead {
272 format: "psse",
273 message: "bad record".into()
274 }
275 .category(),
276 Parse
277 );
278 assert_eq!(Error::NoGenerators.category(), Data);
282 assert_eq!(Error::InvalidBaseMva { base: 0.0 }.category(), Data);
283 assert_eq!(
284 Error::UngroundedComponent { components: 1 }.category(),
285 Data
286 );
287 assert_eq!(
288 Error::UnknownBus {
289 bus_id: BusId(7),
290 element_index: 0
291 }
292 .category(),
293 Data
294 );
295 assert_eq!(Error::EmptyScenarioBatch.category(), Data);
296 assert_eq!(
297 Error::ScenarioShapeMismatch {
298 index: 1,
299 reason: ScenarioMismatch::BusOrder
300 }
301 .category(),
302 Data
303 );
304 assert_eq!(Error::UnknownFormat("xyz".into()).category(), UnknownFormat);
306 assert_eq!(Error::Mtx("write failed".into()).category(), Output);
307 assert_eq!(
308 Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)).category(),
309 Io
310 );
311 }
312}