use thiserror::Error;
use crate::network::BusId;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("missing required MATPOWER field `{0}`")]
MissingField(&'static str),
#[error(
"malformed MATPOWER `{field}` row {row}: expected at least {expected} columns, got {got}"
)]
ShortRow {
field: &'static str,
row: usize,
expected: usize,
got: usize,
},
#[error("could not parse `{field}` row {row} value `{value}` as f64")]
BadFloat {
field: &'static str,
row: usize,
value: String,
},
#[error("unbalanced brackets in MATPOWER `{0}` matrix")]
UnbalancedBrackets(&'static str),
#[error("element references unknown bus id {bus_id} (in-service index {element_index})")]
UnknownBus { bus_id: BusId, element_index: usize },
#[error("branch row {row} has zero impedance (r=0, x=0); not representable in B'")]
ZeroImpedance { row: usize },
#[error("branch row {row} has non-finite DC susceptance b = 1/x (x is NaN, Inf, or denormal)")]
NonFiniteSusceptance { row: usize },
#[error("output dimension mismatch: matrix is {n}x{n} but RHS has length {b_len}")]
DimensionMismatch { n: usize, b_len: usize },
#[error("case has no generators; DC-OPF requires an `mpc.gen` block")]
NoGenerators,
#[error("generator {gen_index} has no cost data")]
MissingGenCost { gen_index: usize },
#[error("default generator cost field `{field}` is not finite: {value}")]
NonFiniteGenCost { field: &'static str, value: f64 },
#[error("invalid generator cost patch row {row}: {reason}")]
InvalidGenCostPatch { row: usize, reason: String },
#[error(
"generator {gen_index} has an unsupported cost model (model {model}, ncost {ncost}); need polynomial model 2 with degree ≤ 2"
)]
UnsupportedCostModel {
gen_index: usize,
model: u8,
ncost: usize,
},
#[error("`gen` has {gens} rows but `gencost` has {gencost}; expected {gens} (active only) or {} (active + reactive)", gens * 2)]
GenCostCountMismatch { gens: usize, gencost: usize },
#[error("expected exactly one reference (slack) bus, found {found}")]
ReferenceBusCount { found: usize },
#[error("base MVA must be a positive, finite number, got {base}")]
InvalidBaseMva { base: f64 },
#[error("dimension mismatch: `{what}` expected length {expected}, got {got}")]
ShapeMismatch {
what: &'static str,
expected: usize,
got: usize,
},
#[error(
"DC sensitivity solve failed: the reference-grounded Laplacian is singular even though every component is grounded"
)]
SingularNetwork,
#[error(
"{components} connected component(s) have no reference (slack) bus to ground; DC sensitivities need at least one reference per island"
)]
UngroundedComponent { components: usize },
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("matrix-market I/O: {0}")]
Mtx(String),
#[error("gridfm Parquet export: {0}")]
Parquet(String),
#[error("gridfm scenario batch is empty; provide at least one snapshot")]
EmptyScenarioBatch,
#[error("gridfm scenario id overflows i64 when numbering snapshot {index} from base {base}")]
ScenarioIdOverflow {
base: i64,
index: usize,
},
#[error(
"gridfm snapshot scenario {scenario} is normalized; gridfm export expects raw MW and degree fields"
)]
NormalizedGridfmSnapshot { scenario: i64 },
#[error(
"gridfm snapshot scenario {scenario} has non-finite {element} row {row} field `{field}`: {value}"
)]
NonFiniteGridfmValue {
scenario: i64,
element: &'static str,
row: usize,
field: &'static str,
value: f64,
},
#[error(
"gridfm snapshot {index} doesn't match the first snapshot's element set: {reason}; \
a scenario batch shares one base element set (same bus/branch/gen counts and bus-id order)"
)]
ScenarioShapeMismatch {
index: usize,
reason: ScenarioMismatch,
},
#[error("{format} read error: {message}")]
FormatRead {
format: &'static str,
message: String,
},
#[error("unknown or unsupported case format: {0}")]
UnknownFormat(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Io,
UnknownFormat,
Parse,
Data,
Output,
}
impl Error {
pub fn category(&self) -> ErrorCategory {
use ErrorCategory as C;
match self {
Error::Io(_) => C::Io,
Error::UnknownFormat(_) => C::UnknownFormat,
Error::MissingField(_)
| Error::ShortRow { .. }
| Error::BadFloat { .. }
| Error::UnbalancedBrackets(_)
| Error::FormatRead { .. } => C::Parse,
Error::UnknownBus { .. }
| Error::ZeroImpedance { .. }
| Error::NonFiniteSusceptance { .. }
| Error::DimensionMismatch { .. }
| Error::NoGenerators
| Error::MissingGenCost { .. }
| Error::NonFiniteGenCost { .. }
| Error::InvalidGenCostPatch { .. }
| Error::UnsupportedCostModel { .. }
| Error::GenCostCountMismatch { .. }
| Error::ReferenceBusCount { .. }
| Error::InvalidBaseMva { .. }
| Error::ShapeMismatch { .. }
| Error::SingularNetwork
| Error::UngroundedComponent { .. }
| Error::EmptyScenarioBatch
| Error::ScenarioIdOverflow { .. }
| Error::NormalizedGridfmSnapshot { .. }
| Error::NonFiniteGridfmValue { .. }
| Error::ScenarioShapeMismatch { .. } => C::Data,
Error::Mtx(_) | Error::Parquet(_) => C::Output,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElementCounts {
pub buses: usize,
pub branches: usize,
pub gens: usize,
}
impl std::fmt::Display for ElementCounts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} buses, {} branches, {} gens",
self.buses, self.branches, self.gens
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScenarioMismatch {
Counts {
expected: ElementCounts,
got: ElementCounts,
},
BusOrder,
}
impl std::fmt::Display for ScenarioMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Counts { expected, got } => {
write!(f, "got ({got}) vs the first snapshot's ({expected})")
}
Self::BusOrder => {
write!(f, "counts match but the bus ids are in a different order")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn category_pins_the_intended_buckets() {
use ErrorCategory::*;
assert_eq!(Error::MissingField("bus").category(), Parse);
assert_eq!(
Error::FormatRead {
format: "psse",
message: "bad record".into()
}
.category(),
Parse
);
assert_eq!(Error::NoGenerators.category(), Data);
assert_eq!(Error::InvalidBaseMva { base: 0.0 }.category(), Data);
assert_eq!(
Error::UngroundedComponent { components: 1 }.category(),
Data
);
assert_eq!(
Error::UnknownBus {
bus_id: BusId(7),
element_index: 0
}
.category(),
Data
);
assert_eq!(Error::EmptyScenarioBatch.category(), Data);
assert_eq!(
Error::ScenarioShapeMismatch {
index: 1,
reason: ScenarioMismatch::BusOrder
}
.category(),
Data
);
assert_eq!(Error::UnknownFormat("xyz".into()).category(), UnknownFormat);
assert_eq!(Error::Mtx("write failed".into()).category(), Output);
assert_eq!(
Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)).category(),
Io
);
}
}