1use powerio_core::DiagnosticInfo;
9use thiserror::Error as ThisError;
10
11use crate::diagnostics::codes;
12
13#[derive(Debug, ThisError)]
15#[non_exhaustive]
16pub enum Error {
17 #[error(transparent)]
19 Transmission(#[from] powerio_tx::Error),
20
21 #[error(transparent)]
27 Io(#[from] std::io::Error),
28
29 #[error(transparent)]
33 Commit(#[from] powerio_core::Error),
34
35 #[error("output dimension mismatch: matrix is {n}x{n} but RHS has length {b_len}")]
36 DimensionMismatch { n: usize, b_len: usize },
37
38 #[error("dimension mismatch: `{what}` expected length {expected}, got {got}")]
39 ShapeMismatch {
40 what: &'static str,
41 expected: usize,
42 got: usize,
43 },
44
45 #[error("unsupported OPF objective: {reason}")]
46 UnsupportedOpfObjective { reason: String },
47
48 #[error("unsupported AC power flow bus specification")]
49 UnsupportedAcPfSpecification,
50
51 #[error("invalid LinDist3Flow coefficient input: {reason}")]
52 InvalidLinDist3FlowCoefficients { reason: String },
53
54 #[error("{family} constraint selection names unknown identity `{identity}`")]
55 UnknownConstraintIdentity {
56 family: &'static str,
57 identity: String,
58 },
59
60 #[error("{family} has duplicate stable identity `{identity}`")]
61 DuplicateElementIdentity {
62 family: &'static str,
63 identity: String,
64 },
65
66 #[error(
67 "DC sensitivity solve failed: the reference-grounded Laplacian is singular even though every component is grounded"
68 )]
69 SingularNetwork,
70
71 #[error("invalid DC sensitivity option: {reason}")]
72 InvalidSensitivityOptions { reason: String },
73
74 #[error("case has no generators; DC-OPF requires an `mpc.gen` block")]
75 NoGenerators,
76
77 #[error(
78 "generator {gen_index} has an unsupported cost model (model {model}, ncost {ncost}); need polynomial model 2 with degree ≤ 2"
79 )]
80 UnsupportedCostModel {
81 gen_index: usize,
82 model: u8,
83 ncost: usize,
84 },
85
86 #[error("generator {gen_index} has an invalid piecewise linear cost: {reason}")]
87 InvalidPiecewiseCost {
88 gen_index: usize,
89 reason: PiecewiseCostInvalidity,
90 },
91
92 #[error(
93 "generator {gen_index} has a nonconvex piecewise linear cost: segment {segment} has a lower slope than the preceding segment"
94 )]
95 NonconvexPiecewiseCost { gen_index: usize, segment: usize },
96
97 #[error(
98 "generator {gen_index} has a piecewise linear cost that cannot be projected to one nodal quadratic cost"
99 )]
100 PiecewiseNodalCost { gen_index: usize },
101
102 #[error(
103 "generator {gen_index} has a concave cost row (c2 = {c2}); need a nonnegative quadratic coefficient"
104 )]
105 ConcaveCost { gen_index: usize, c2: f64 },
106
107 #[error("matrix-market I/O: {0}")]
108 Mtx(String),
109
110 #[error("gridfm Parquet export: {0}")]
111 Parquet(String),
112
113 #[error("gridfm scenario batch is empty; provide at least one snapshot")]
114 EmptyScenarioBatch,
115
116 #[error("gridfm scenario id overflows i64 when numbering snapshot {index} from base {base}")]
117 ScenarioIdOverflow {
118 base: i64,
119 index: usize,
121 },
122
123 #[error(
124 "gridfm snapshot scenario {scenario} is normalized; gridfm export expects raw MW and degree fields"
125 )]
126 NormalizedGridfmSnapshot { scenario: i64 },
127
128 #[error(
129 "gridfm snapshot scenario {scenario} has non-finite {element} row {row} field `{field}`: {value}"
130 )]
131 NonFiniteGridfmValue {
132 scenario: i64,
133 element: &'static str,
134 row: usize,
135 field: &'static str,
136 value: f64,
137 },
138
139 #[error(
140 "gridfm snapshot {index} doesn't align with the scenario batch: {reason}; \
141 a batch requires unique scenario ids, one system base, and fixed bus/branch/gen row identities"
142 )]
143 ScenarioShapeMismatch {
144 index: usize,
147 reason: ScenarioMismatch,
148 },
149}
150
151impl Error {
152 #[must_use]
158 pub fn code(&self) -> &'static DiagnosticInfo {
159 match self {
160 Error::Transmission(inner) => inner.code(),
161 Error::Commit(inner) => inner.info().unwrap_or(&codes::EMIT_MTX_FAILED),
162 Error::Io(_) => &codes::READ_MATRIX_IO_FAILED,
163 Error::DimensionMismatch { .. } | Error::ShapeMismatch { .. } => {
164 &codes::BUILD_MATRIX_SHAPE_MISMATCH
165 }
166 Error::UnsupportedOpfObjective { .. } => &codes::BUILD_OPF_OBJECTIVE_UNSUPPORTED,
167 Error::UnsupportedAcPfSpecification => &codes::BUILD_AC_PF_SPECIFICATION_UNSUPPORTED,
168 Error::InvalidLinDist3FlowCoefficients { .. } => {
169 &codes::BUILD_LINDIST3FLOW_COEFFICIENT_INVALID
170 }
171 Error::UnknownConstraintIdentity { .. } => {
172 &codes::BUILD_OPF_CONSTRAINT_IDENTITY_UNKNOWN
173 }
174 Error::DuplicateElementIdentity { .. } => &codes::BUILD_OPF_ELEMENT_IDENTITY_DUPLICATE,
175 Error::SingularNetwork => &codes::BUILD_SENSITIVITY_SINGULAR,
176 Error::InvalidSensitivityOptions { .. } => &codes::BUILD_SENSITIVITY_INVALID_OPTION,
177 Error::EmptyScenarioBatch => &codes::BUILD_GRIDFM_EMPTY_BATCH,
178 Error::ScenarioIdOverflow { .. } => &codes::BUILD_GRIDFM_SCENARIO_ID_OVERFLOW,
179 Error::NormalizedGridfmSnapshot { .. } => &codes::BUILD_GRIDFM_NORMALIZED_SNAPSHOT,
180 Error::NonFiniteGridfmValue { .. } => &codes::BUILD_GRIDFM_NOT_A_NUMBER,
181 Error::ScenarioShapeMismatch { .. } => &codes::BUILD_GRIDFM_SCENARIO_SHAPE_MISMATCH,
182 Error::NoGenerators => &powerio_prob::diagnostics::codes::BUILD_INSTANCE_NO_GENERATORS,
183 Error::UnsupportedCostModel { .. } => {
184 &powerio_prob::diagnostics::codes::BUILD_INSTANCE_UNSUPPORTED_COST_MODEL
185 }
186 Error::InvalidPiecewiseCost { .. } => {
187 &powerio_prob::diagnostics::codes::BUILD_INSTANCE_PIECEWISE_COST_INVALID
188 }
189 Error::NonconvexPiecewiseCost { .. } => {
190 &powerio_prob::diagnostics::codes::BUILD_INSTANCE_PIECEWISE_COST_NONCONVEX
191 }
192 Error::PiecewiseNodalCost { .. } => &codes::BUILD_OPF_NODAL_COST_UNSUPPORTED,
193 Error::ConcaveCost { .. } => {
194 &powerio_prob::diagnostics::codes::BUILD_INSTANCE_CONCAVE_COST
195 }
196 Error::Mtx(_) => &codes::EMIT_MTX_FAILED,
197 Error::Parquet(_) => &codes::EMIT_PARQUET_FAILED,
198 }
199 }
200
201 #[must_use]
206 pub fn category(&self) -> powerio_tx::ErrorCategory {
207 use powerio_tx::ErrorCategory as C;
208 match self {
209 Error::Transmission(inner) => inner.category(),
210 Error::Commit(inner) => inner.category(),
211 Error::Io(_) => C::Io,
212 Error::DimensionMismatch { .. }
214 | Error::ShapeMismatch { .. }
215 | Error::UnsupportedOpfObjective { .. }
216 | Error::UnsupportedAcPfSpecification
217 | Error::InvalidLinDist3FlowCoefficients { .. }
218 | Error::UnknownConstraintIdentity { .. }
219 | Error::DuplicateElementIdentity { .. }
220 | Error::SingularNetwork
221 | Error::InvalidSensitivityOptions { .. }
222 | Error::EmptyScenarioBatch
223 | Error::ScenarioIdOverflow { .. }
224 | Error::NormalizedGridfmSnapshot { .. }
225 | Error::NonFiniteGridfmValue { .. }
226 | Error::ScenarioShapeMismatch { .. }
227 | Error::NoGenerators
228 | Error::UnsupportedCostModel { .. }
229 | Error::InvalidPiecewiseCost { .. }
230 | Error::NonconvexPiecewiseCost { .. }
231 | Error::ConcaveCost { .. } => C::Data,
232 Error::PiecewiseNodalCost { .. } => C::Request,
233 Error::Mtx(_) | Error::Parquet(_) => C::Output,
235 }
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241#[non_exhaustive]
242pub enum PiecewiseCostInvalidity {
243 FewerThanTwoBreakpoints { declared: usize },
244 Truncated { expected_values: usize, got: usize },
245 NonFinitePoint { point: usize },
246 NonIncreasingPower { point: usize },
247}
248
249impl std::fmt::Display for PiecewiseCostInvalidity {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 match self {
252 Self::FewerThanTwoBreakpoints { declared } => {
253 write!(f, "need at least two breakpoints, got {declared}")
254 }
255 Self::Truncated {
256 expected_values,
257 got,
258 } => write!(
259 f,
260 "declared breakpoints require {expected_values} values, got {got}"
261 ),
262 Self::NonFinitePoint { point } => {
263 write!(f, "breakpoint {point} has a non-finite coordinate")
264 }
265 Self::NonIncreasingPower { point } => write!(
266 f,
267 "breakpoint {point} does not have greater power than its predecessor"
268 ),
269 }
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub struct ElementCounts {
278 pub buses: usize,
279 pub branches: usize,
280 pub gens: usize,
281}
282
283impl std::fmt::Display for ElementCounts {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 write!(
286 f,
287 "{} buses, {} branches, {} gens",
288 self.buses, self.branches, self.gens
289 )
290 }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299#[non_exhaustive]
300pub enum ScenarioMismatch {
301 Counts {
303 expected: ElementCounts,
304 got: ElementCounts,
305 },
306 BusOrder,
309 BranchOrder,
311 GeneratorOrder,
313 BaseMva,
315 DuplicateScenarioId { scenario: i64, first_index: usize },
317}
318
319impl std::fmt::Display for ScenarioMismatch {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 match self {
322 Self::Counts { expected, got } => {
323 write!(f, "got ({got}) vs the first snapshot's ({expected})")
324 }
325 Self::BusOrder => {
326 write!(f, "counts match but the bus ids are in a different order")
327 }
328 Self::BranchOrder => write!(
329 f,
330 "counts match but branch endpoints or identities differ by row"
331 ),
332 Self::GeneratorOrder => write!(
333 f,
334 "counts match but generator buses or identities differ by row"
335 ),
336 Self::BaseMva => write!(f, "base_mva differs from the first snapshot"),
337 Self::DuplicateScenarioId {
338 scenario,
339 first_index,
340 } => write!(
341 f,
342 "scenario id {scenario} is already used by snapshot {first_index}"
343 ),
344 }
345 }
346}
347
348pub type Result<T> = std::result::Result<T, Error>;
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use powerio_tx::ErrorCategory::{Data, Output, Parse, Request};
355
356 #[test]
357 fn category_pins_the_intended_buckets() {
358 assert_eq!(Error::SingularNetwork.category(), Data);
359 assert_eq!(Error::EmptyScenarioBatch.category(), Data);
360 assert_eq!(Error::Mtx("write failed".into()).category(), Output);
361 assert_eq!(Error::Parquet("write failed".into()).category(), Output);
362 }
363
364 #[test]
367 fn every_error_code_publishes_the_category_the_variant_reports() {
368 let every: Vec<Error> = vec![
369 powerio_tx::Error::MissingField("bus").into(),
370 std::io::Error::from(std::io::ErrorKind::NotFound).into(),
371 Error::DimensionMismatch { n: 2, b_len: 3 },
372 Error::ShapeMismatch {
373 what: "p",
374 expected: 2,
375 got: 3,
376 },
377 Error::UnsupportedOpfObjective {
378 reason: "term".into(),
379 },
380 Error::UnknownConstraintIdentity {
381 family: "branches",
382 identity: "missing".into(),
383 },
384 Error::DuplicateElementIdentity {
385 family: "branches",
386 identity: "duplicate".into(),
387 },
388 Error::SingularNetwork,
389 Error::InvalidSensitivityOptions {
390 reason: "tol".into(),
391 },
392 Error::EmptyScenarioBatch,
393 Error::ScenarioIdOverflow { base: 1, index: 0 },
394 Error::NormalizedGridfmSnapshot { scenario: 1 },
395 Error::NonFiniteGridfmValue {
396 scenario: 1,
397 element: "bus",
398 row: 0,
399 field: "vm",
400 value: f64::NAN,
401 },
402 Error::ScenarioShapeMismatch {
403 index: 1,
404 reason: ScenarioMismatch::BusOrder,
405 },
406 Error::NoGenerators,
407 Error::UnsupportedCostModel {
408 gen_index: 0,
409 model: 1,
410 ncost: 4,
411 },
412 Error::InvalidPiecewiseCost {
413 gen_index: 0,
414 reason: PiecewiseCostInvalidity::FewerThanTwoBreakpoints { declared: 1 },
415 },
416 Error::NonconvexPiecewiseCost {
417 gen_index: 0,
418 segment: 1,
419 },
420 Error::PiecewiseNodalCost { gen_index: 0 },
421 Error::ConcaveCost {
422 gen_index: 0,
423 c2: -0.5,
424 },
425 Error::Mtx("write failed".into()),
426 Error::Parquet("write failed".into()),
427 ];
428 for error in &every {
429 assert_eq!(
430 error.code().category,
431 Some(error.category()),
432 "{}",
433 error.code().code
434 );
435 }
436 }
437
438 #[test]
439 fn piecewise_cost_failures_keep_distinct_diagnostic_meanings() {
440 let malformed = Error::InvalidPiecewiseCost {
441 gen_index: 2,
442 reason: PiecewiseCostInvalidity::FewerThanTwoBreakpoints { declared: 1 },
443 };
444 let nonconvex = Error::NonconvexPiecewiseCost {
445 gen_index: 2,
446 segment: 1,
447 };
448 let nodal_projection = Error::PiecewiseNodalCost { gen_index: 2 };
449
450 assert_eq!(
451 malformed.code().code,
452 "BUILD.INSTANCE.PIECEWISE_COST_INVALID"
453 );
454 assert_eq!(
455 nonconvex.code().code,
456 "BUILD.INSTANCE.PIECEWISE_COST_NONCONVEX"
457 );
458 assert_eq!(
459 nodal_projection.code().code,
460 "BUILD.OPF.NODAL_COST_UNSUPPORTED"
461 );
462 assert_eq!(malformed.category(), Data);
463 assert_eq!(nonconvex.category(), Data);
464 assert_eq!(nodal_projection.category(), Request);
465 }
466
467 #[test]
468 fn a_wrapped_hub_error_keeps_its_own_category() {
469 let wrapped: Error = powerio_tx::Error::MissingField("bus").into();
470 assert_eq!(wrapped.category(), Parse);
471 assert_eq!(
474 wrapped.to_string(),
475 powerio_tx::Error::MissingField("bus").to_string()
476 );
477 }
478}