use cobre_io::LoadError;
use cobre_solver::SolverError;
use cobre_stochastic::StochasticError;
use crate::estimation::EstimationError;
use crate::fpha_fitting::FphaFittingError;
#[derive(Debug, thiserror::Error)]
pub enum SddpError {
#[error("solver error: {0}")]
Solver(#[from] SolverError),
#[error("communication error: {0}")]
Communication(#[from] cobre_comm::CommError),
#[error("stochastic error: {0}")]
Stochastic(#[from] StochasticError),
#[error("I/O error: {0}")]
Io(#[from] LoadError),
#[error("configuration validation error: {0}")]
Validation(String),
#[error("infeasible subproblem at stage {stage}, iteration {iteration}, scenario {scenario}")]
Infeasible {
stage: usize,
iteration: u64,
scenario: usize,
},
#[error("simulation error: {0}")]
Simulation(String),
}
impl From<EstimationError> for SddpError {
fn from(err: EstimationError) -> Self {
match err {
EstimationError::Load(load_err) => Self::Io(load_err),
EstimationError::Stochastic(stoch_err) => Self::Stochastic(stoch_err),
}
}
}
impl From<FphaFittingError> for SddpError {
fn from(err: FphaFittingError) -> Self {
Self::Validation(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::SddpError;
use cobre_comm::CommError;
use cobre_io::LoadError;
use cobre_solver::SolverError;
use cobre_stochastic::StochasticError;
use std::path::PathBuf;
use crate::fpha_fitting::FphaFittingError;
fn assert_send_sync_static<E: std::error::Error + Send + Sync + 'static>() {}
#[test]
fn sddp_error_is_send_sync_static() {
assert_send_sync_static::<SddpError>();
}
#[test]
fn display_solver_variant_contains_solver_and_underlying_message() {
let inner = SolverError::Infeasible;
let err = SddpError::Solver(inner);
let msg = err.to_string();
assert!(msg.contains("solver"), "{msg}");
assert!(msg.contains("infeasible"), "{msg}");
}
#[test]
fn display_communication_variant_contains_message() {
let err = SddpError::Communication(CommError::CollectiveFailed {
operation: "allgatherv",
mpi_error_code: 1,
message: "timed out".to_string(),
});
let msg = err.to_string();
assert!(msg.contains("communication"), "{msg}");
assert!(msg.contains("allgatherv"), "{msg}");
}
#[test]
fn display_stochastic_variant_contains_stochastic_and_underlying_message() {
let inner = StochasticError::InsufficientData {
context: "hydro 7 has only 2 observations".to_string(),
};
let err = SddpError::Stochastic(inner);
let msg = err.to_string();
assert!(msg.contains("stochastic"), "{msg}");
assert!(msg.contains("insufficient data"), "{msg}");
}
#[test]
fn display_io_variant_contains_io_and_underlying_message() {
let inner = LoadError::ConstraintError {
description: "hydro cascade contains a cycle".to_string(),
};
let err = SddpError::Io(inner);
let msg = err.to_string();
assert!(
msg.to_lowercase().contains("i/o") || msg.to_lowercase().contains("io"),
"{msg}"
);
assert!(msg.contains("hydro cascade contains a cycle"), "{msg}");
}
#[test]
fn display_validation_variant_contains_message() {
let err = SddpError::Validation("forward_passes must be greater than zero".to_string());
let msg = err.to_string();
assert!(msg.contains("validation"), "{msg}");
assert!(
msg.contains("forward_passes must be greater than zero"),
"{msg}"
);
}
#[test]
fn display_infeasible_variant_contains_stage_iteration_scenario() {
let err = SddpError::Infeasible {
stage: 5,
iteration: 42,
scenario: 3,
};
let msg = err.to_string();
assert!(msg.contains('5'), "{msg}");
assert!(msg.contains("42"), "{msg}");
assert!(msg.contains('3'), "{msg}");
}
#[test]
fn from_solver_error() {
let inner = SolverError::InternalError {
message: "test".to_string(),
error_code: Some(99),
};
let err: SddpError = inner.into();
assert!(matches!(err, SddpError::Solver(_)));
}
#[test]
fn from_stochastic_error() {
let inner = StochasticError::SeedDerivationError {
reason: "hash overflow".to_string(),
};
let err: SddpError = inner.into();
assert!(matches!(err, SddpError::Stochastic(_)));
}
#[test]
fn from_load_error() {
let inner = LoadError::SchemaError {
path: PathBuf::from("system/buses.json"),
field: "voltage".to_string(),
message: "must be positive".to_string(),
};
let err: SddpError = inner.into();
assert!(matches!(err, SddpError::Io(_)));
}
#[test]
fn from_comm_error_wraps_directly() {
let inner = CommError::InvalidCommunicator;
let err: SddpError = inner.into();
assert!(matches!(
err,
SddpError::Communication(CommError::InvalidCommunicator)
));
let msg = err.to_string();
assert!(msg.contains("MPI"), "{msg}");
}
#[test]
fn from_fpha_fitting_error_wraps_as_validation() {
let inner = FphaFittingError::InsufficientPoints {
hydro_name: "Itaipu".to_string(),
count: 1,
};
let display_msg = inner.to_string();
let err: SddpError = inner.into();
assert!(
matches!(err, SddpError::Validation(ref msg) if *msg == display_msg),
"expected Validation wrapping the FphaFittingError display output, got {err:?}"
);
}
#[test]
fn sddp_error_satisfies_std_error_trait() {
let variants: Vec<SddpError> = vec![
SddpError::Solver(SolverError::Infeasible),
SddpError::Communication(CommError::InvalidCommunicator),
SddpError::Stochastic(StochasticError::InsufficientData {
context: "no data".to_string(),
}),
SddpError::Io(LoadError::ConstraintError {
description: "cycle".to_string(),
}),
SddpError::Validation("bad config".to_string()),
SddpError::Infeasible {
stage: 0,
iteration: 1,
scenario: 0,
},
SddpError::Simulation("simulation phase failed".to_string()),
];
for err in &variants {
let _: &dyn std::error::Error = err;
}
}
#[test]
fn all_variants_debug_non_empty() {
let variants: Vec<SddpError> = vec![
SddpError::Solver(SolverError::Unbounded),
SddpError::Communication(CommError::InvalidCommunicator),
SddpError::Stochastic(StochasticError::InvalidCorrelation {
profile_name: "test".to_string(),
reason: "bad value".to_string(),
}),
SddpError::Io(LoadError::ConstraintError {
description: "test".to_string(),
}),
SddpError::Validation("test validation".to_string()),
SddpError::Infeasible {
stage: 1,
iteration: 2,
scenario: 3,
},
SddpError::Simulation("test simulation error".to_string()),
];
for err in &variants {
assert!(!format!("{err:?}").is_empty());
}
}
}