use thiserror::Error;
use crate::diagnostics::{DiagnosticInfo, codes};
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("malformed MATPOWER `{field}` row {row}: {message}")]
BadId {
field: &'static str,
row: usize,
message: 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 a zero matrix denominator under the selected build options")]
ZeroImpedance { row: usize },
#[error(
"branch row {row} has a non-finite susceptance (r or x is NaN or Inf, or the four terminal admittances overflow)"
)]
NonFiniteSusceptance { row: usize },
#[error("branch row {row} has a tap ratio of {tap} too small to divide by")]
DegenerateTap { row: usize, tap: f64 },
#[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("`gen` has {gens} rows but `gencost` has {gencost}; expected {gens} (active only) or {} (active + reactive)", gens * 2)]
GenCostCountMismatch { gens: usize, gencost: usize },
#[error(
"`dcline` has {dclines} rows but `dclinecost` has {dclinecost}; expected one cost row per dcline"
)]
DcLineCostCountMismatch { dclines: usize, dclinecost: usize },
#[error(
"cannot establish a reference bus: a reference bus must host an in-service generator, and this case has none"
)]
NoReferenceBus,
#[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("invalid normalize option `{field}`: {value}")]
InvalidNormalizeOption { field: &'static str, value: f64 },
#[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(
"geo apply left {buses} bus(es) with no location and {branches} branch(es) with no route"
)]
UnlocatedElements { buses: usize, branches: usize },
#[error("{format} read error: {message}")]
FormatRead {
format: &'static str,
message: String,
},
#[error("unknown or unsupported case format: {0}")]
UnknownFormat(String),
#[error("{format} is a read only format with no writer")]
WriteUnsupported { format: &'static str },
}
pub use powerio_core::ErrorCategory;
impl Error {
#[must_use]
pub fn reference_bus_count(found: usize) -> Self {
Error::ReferenceBusCount { found }
}
pub fn code(&self) -> &'static DiagnosticInfo {
match self {
Error::MissingField(_)
| Error::ShortRow { .. }
| Error::BadFloat { .. }
| Error::BadId { .. }
| Error::UnbalancedBrackets(_) => &codes::PARSE_MATPOWER_MALFORMED,
Error::FormatRead { .. } => &codes::PARSE_SOURCE_MALFORMED,
Error::Io(_) => &codes::READ_IO_FAILED,
Error::UnknownBus { .. } => &codes::BUILD_INDEX_UNKNOWN_BUS,
Error::ZeroImpedance { .. } => &codes::BUILD_BRANCH_ZERO_IMPEDANCE,
Error::NonFiniteSusceptance { .. } => &codes::BUILD_BRANCH_NOT_A_NUMBER,
Error::DegenerateTap { .. } => &codes::BUILD_BRANCH_DEGENERATE_TAP,
Error::MissingGenCost { .. } => &codes::VALIDATE_GEN_COST_MISSING,
Error::NonFiniteGenCost { .. } => &codes::VALIDATE_GEN_COST_NOT_A_NUMBER,
Error::InvalidGenCostPatch { .. } => &codes::VALIDATE_GEN_COST_PATCH_INVALID,
Error::GenCostCountMismatch { .. } => &codes::VALIDATE_GEN_COST_COUNT_MISMATCH,
Error::DcLineCostCountMismatch { .. } => &codes::VALIDATE_DC_LINE_COST_COUNT_MISMATCH,
Error::NoReferenceBus => &codes::CANONICALIZE_NORMALIZE_NO_REFERENCE_BUS,
Error::ReferenceBusCount { .. } => &codes::BUILD_INDEX_REFERENCE_BUS_COUNT,
Error::InvalidBaseMva { .. } => &codes::CANONICALIZE_NORMALIZE_INVALID_BASE_MVA,
Error::InvalidNormalizeOption { .. } => &codes::CANONICALIZE_NORMALIZE_INVALID_OPTION,
Error::UngroundedComponent { .. } => &codes::BUILD_INDEX_UNGROUNDED_COMPONENT,
Error::UnlocatedElements { .. } => &codes::BUILD_GEO_UNLOCATED_ELEMENTS,
Error::UnknownFormat(_) => &codes::REQUEST_FORMAT_UNKNOWN,
Error::WriteUnsupported { .. } => &codes::REQUEST_FORMAT_WRITE_UNSUPPORTED,
}
}
pub fn category(&self) -> ErrorCategory {
use ErrorCategory as C;
match self {
Error::Io(_) => C::Io,
Error::UnknownFormat(_) | Error::WriteUnsupported { .. } => C::Request,
Error::MissingField(_)
| Error::ShortRow { .. }
| Error::BadFloat { .. }
| Error::BadId { .. }
| Error::UnbalancedBrackets(_)
| Error::FormatRead { .. } => C::Parse,
Error::UnknownBus { .. }
| Error::ZeroImpedance { .. }
| Error::NonFiniteSusceptance { .. }
| Error::DegenerateTap { .. }
| Error::MissingGenCost { .. }
| Error::NonFiniteGenCost { .. }
| Error::InvalidGenCostPatch { .. }
| Error::GenCostCountMismatch { .. }
| Error::DcLineCostCountMismatch { .. }
| Error::NoReferenceBus
| Error::ReferenceBusCount { .. }
| Error::InvalidBaseMva { .. }
| Error::InvalidNormalizeOption { .. }
| Error::UngroundedComponent { .. }
| Error::UnlocatedElements { .. } => C::Data,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_error_code_publishes_the_category_the_variant_reports() {
let every: Vec<Error> = vec![
Error::MissingField("bus"),
Error::ShortRow {
field: "bus",
row: 1,
expected: 13,
got: 3,
},
Error::BadFloat {
field: "bus",
row: 1,
value: "x".into(),
},
Error::BadId {
field: "bus",
row: 1,
message: "`BUS_I` value 1e300 is outside the id range 0..2^63".into(),
},
Error::UnbalancedBrackets("bus"),
Error::FormatRead {
format: "psse",
message: "bad record".into(),
},
Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)),
Error::UnknownBus {
bus_id: BusId(7),
element_index: 0,
},
Error::ZeroImpedance { row: 1 },
Error::NonFiniteSusceptance { row: 1 },
Error::DegenerateTap { row: 1, tap: 0.0 },
Error::MissingGenCost { gen_index: 0 },
Error::NonFiniteGenCost {
field: "c2",
value: f64::NAN,
},
Error::InvalidGenCostPatch {
row: 1,
reason: "empty".into(),
},
Error::GenCostCountMismatch {
gens: 2,
gencost: 3,
},
Error::DcLineCostCountMismatch {
dclines: 1,
dclinecost: 2,
},
Error::NoReferenceBus,
Error::reference_bus_count(2),
Error::InvalidBaseMva { base: 0.0 },
Error::InvalidNormalizeOption {
field: "angle_bound_pad",
value: 0.0,
},
Error::UngroundedComponent { components: 1 },
Error::UnlocatedElements {
buses: 1,
branches: 0,
},
Error::UnknownFormat("xyz".into()),
Error::WriteUnsupported { format: "goc3" },
];
for error in &every {
let info = error.code();
assert_eq!(
info.category,
Some(error.category()),
"{} publishes {:?} but the variant reports {:?}",
info.code,
info.category,
error.category()
);
}
}
#[test]
fn the_two_reference_bus_stages_carry_different_codes() {
assert_eq!(
Error::NoReferenceBus.code().code,
"CANONICALIZE.NORMALIZE.NO_REFERENCE_BUS"
);
assert_eq!(
Error::reference_bus_count(2).code().code,
"BUILD.INDEX.REFERENCE_BUS_COUNT"
);
assert!(Error::NoReferenceBus.to_string().contains("generator"));
}
#[test]
fn category_pins_the_intended_buckets() {
use ErrorCategory::{Data, Io, Parse, Request};
assert_eq!(Error::MissingField("bus").category(), Parse);
assert_eq!(
Error::FormatRead {
format: "psse",
message: "bad record".into()
}
.category(),
Parse
);
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::UnknownFormat("xyz".into()).category(), Request);
assert_eq!(
Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound)).category(),
Io
);
}
}
#[cfg(test)]
mod category_token_tests {
use super::ErrorCategory;
#[test]
fn tokens_lists_every_category_exactly_once() {
let every = [
ErrorCategory::Io,
ErrorCategory::Request,
ErrorCategory::Parse,
ErrorCategory::Data,
ErrorCategory::Output,
];
let from_tokens: Vec<&str> = every.iter().map(|c| c.as_str()).collect();
assert_eq!(from_tokens, ErrorCategory::TOKENS.to_vec());
}
}