1use thiserror::Error as ThisError;
9
10#[derive(Debug, ThisError)]
12#[non_exhaustive]
13pub enum Error {
14 #[error(transparent)]
16 Core(#[from] powerio::Error),
17
18 #[error(transparent)]
23 Io(#[from] std::io::Error),
24
25 #[error("output dimension mismatch: matrix is {n}x{n} but RHS has length {b_len}")]
26 DimensionMismatch { n: usize, b_len: usize },
27
28 #[error("dimension mismatch: `{what}` expected length {expected}, got {got}")]
29 ShapeMismatch {
30 what: &'static str,
31 expected: usize,
32 got: usize,
33 },
34
35 #[error(
36 "DC sensitivity solve failed: the reference-grounded Laplacian is singular even though every component is grounded"
37 )]
38 SingularNetwork,
39
40 #[error("invalid DC sensitivity option: {reason}")]
41 InvalidSensitivityOptions { reason: String },
42
43 #[error(
44 "DC sensitivity iterative solve did not converge after {iterations} iterations (relative residual {relative_residual:.3e})"
45 )]
46 SensitivitySolveDidNotConverge {
47 iterations: usize,
48 relative_residual: f64,
49 },
50
51 #[error("matrix-market I/O: {0}")]
52 Mtx(String),
53
54 #[error("gridfm Parquet export: {0}")]
55 Parquet(String),
56
57 #[error("gridfm scenario batch is empty; provide at least one snapshot")]
58 EmptyScenarioBatch,
59
60 #[error("gridfm scenario id overflows i64 when numbering snapshot {index} from base {base}")]
61 ScenarioIdOverflow {
62 base: i64,
63 index: usize,
65 },
66
67 #[error(
68 "gridfm snapshot scenario {scenario} is normalized; gridfm export expects raw MW and degree fields"
69 )]
70 NormalizedGridfmSnapshot { scenario: i64 },
71
72 #[error(
73 "gridfm snapshot scenario {scenario} has non-finite {element} row {row} field `{field}`: {value}"
74 )]
75 NonFiniteGridfmValue {
76 scenario: i64,
77 element: &'static str,
78 row: usize,
79 field: &'static str,
80 value: f64,
81 },
82
83 #[error(
84 "gridfm snapshot {index} doesn't match the first snapshot's element set: {reason}; \
85 a scenario batch shares one base element set (same bus/branch/gen counts and bus-id order)"
86 )]
87 ScenarioShapeMismatch {
88 index: usize,
91 reason: ScenarioMismatch,
92 },
93}
94
95impl Error {
96 #[must_use]
101 pub fn category(&self) -> powerio::ErrorCategory {
102 use powerio::ErrorCategory as C;
103 match self {
104 Error::Core(inner) => inner.category(),
105 Error::Io(_) => C::Io,
106 Error::DimensionMismatch { .. }
108 | Error::ShapeMismatch { .. }
109 | Error::SingularNetwork
110 | Error::InvalidSensitivityOptions { .. }
111 | Error::SensitivitySolveDidNotConverge { .. }
112 | Error::EmptyScenarioBatch
113 | Error::ScenarioIdOverflow { .. }
114 | Error::NormalizedGridfmSnapshot { .. }
115 | Error::NonFiniteGridfmValue { .. }
116 | Error::ScenarioShapeMismatch { .. } => C::Data,
117 Error::Mtx(_) | Error::Parquet(_) => C::Output,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct ElementCounts {
128 pub buses: usize,
129 pub branches: usize,
130 pub gens: usize,
131}
132
133impl std::fmt::Display for ElementCounts {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(
136 f,
137 "{} buses, {} branches, {} gens",
138 self.buses, self.branches, self.gens
139 )
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150#[non_exhaustive]
151pub enum ScenarioMismatch {
152 Counts {
154 expected: ElementCounts,
155 got: ElementCounts,
156 },
157 BusOrder,
160}
161
162impl std::fmt::Display for ScenarioMismatch {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 Self::Counts { expected, got } => {
166 write!(f, "got ({got}) vs the first snapshot's ({expected})")
167 }
168 Self::BusOrder => {
169 write!(f, "counts match but the bus ids are in a different order")
170 }
171 }
172 }
173}
174
175pub type Result<T> = std::result::Result<T, Error>;
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use powerio::ErrorCategory::{Data, Output, Parse};
182
183 #[test]
184 fn category_pins_the_intended_buckets() {
185 assert_eq!(Error::SingularNetwork.category(), Data);
186 assert_eq!(Error::EmptyScenarioBatch.category(), Data);
187 assert_eq!(Error::Mtx("write failed".into()).category(), Output);
188 assert_eq!(Error::Parquet("write failed".into()).category(), Output);
189 }
190
191 #[test]
192 fn a_wrapped_hub_error_keeps_its_own_category() {
193 let wrapped: Error = powerio::Error::MissingField("bus").into();
194 assert_eq!(wrapped.category(), Parse);
195 assert_eq!(
198 wrapped.to_string(),
199 powerio::Error::MissingField("bus").to_string()
200 );
201 }
202}