use crate::Thermodynamics::ChemEquilibrium::equilibrium_log_moles::Solvers;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_rst_backend::RustedSciTheSolver;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverBackend {
Legacy(Solvers),
RustedSciThe(RustedSciTheSolver),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SolverPolicy {
Single(SolverBackend),
Cascade(Vec<SolverBackend>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SolverCascadeBudget {
pub max_attempts: usize,
pub max_iterations_per_attempt: usize,
pub max_total_iterations: usize,
}
impl SolverCascadeBudget {
pub const fn new(
max_attempts: usize,
max_iterations_per_attempt: usize,
max_total_iterations: usize,
) -> Self {
Self {
max_attempts,
max_iterations_per_attempt,
max_total_iterations,
}
}
}
impl SolverPolicy {
pub fn legacy_default(preferred: Solvers) -> Self {
Self::Cascade(vec![
SolverBackend::Legacy(preferred),
SolverBackend::Legacy(Solvers::LM),
SolverBackend::Legacy(Solvers::NR),
SolverBackend::Legacy(Solvers::TR),
])
}
pub fn rusted_scithe_default() -> Self {
Self::Cascade(
RustedSciTheSolver::recommended_cascade()
.into_iter()
.map(SolverBackend::RustedSciThe)
.collect(),
)
}
pub fn ordered_backends(&self) -> Vec<SolverBackend> {
let requested = match self {
Self::Single(backend) => std::slice::from_ref(backend),
Self::Cascade(backends) => backends.as_slice(),
};
let mut unique = Vec::with_capacity(requested.len());
for &backend in requested {
if !unique.contains(&backend) {
unique.push(backend);
}
}
unique
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverAttemptFailureKind {
Solver,
Backend,
ResidualEvaluation,
JacobianEvaluation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SolverAttemptOutcome {
Accepted,
Failed {
kind: SolverAttemptFailureKind,
reason: String,
},
RejectedCandidate { reason: String },
Skipped { reason: String },
}
impl SolverAttemptOutcome {
pub fn is_accepted(&self) -> bool {
matches!(self, Self::Accepted)
}
pub fn is_skipped(&self) -> bool {
matches!(self, Self::Skipped { .. })
}
pub fn is_started(&self) -> bool {
!self.is_skipped()
}
pub fn reason(&self) -> Option<&str> {
match self {
Self::Accepted => None,
Self::Failed { reason, .. }
| Self::RejectedCandidate { reason }
| Self::Skipped { reason } => Some(reason.as_str()),
}
}
pub fn failure_kind(&self) -> Option<SolverAttemptFailureKind> {
match self {
Self::Failed { kind, .. } => Some(*kind),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverTermination {
Converged,
MaxIterations,
StepTooSmall,
Stagnation,
RejectedStepLimit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolverAttemptMetrics {
pub termination: SolverTermination,
pub backend_converged: bool,
pub iterations: usize,
pub residual_evaluations: usize,
pub jacobian_evaluations: usize,
pub linear_solves: usize,
pub elapsed_millis: u128,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolverAttemptReport {
pub backend: SolverBackend,
pub outcome: SolverAttemptOutcome,
pub metrics: Option<SolverAttemptMetrics>,
}
impl SolverAttemptReport {
pub fn is_accepted(&self) -> bool {
self.outcome.is_accepted()
}
pub fn is_skipped(&self) -> bool {
self.outcome.is_skipped()
}
pub fn is_started(&self) -> bool {
self.outcome.is_started()
}
pub fn failure_kind(&self) -> Option<SolverAttemptFailureKind> {
self.outcome.failure_kind()
}
pub fn summary(&self) -> String {
match &self.outcome {
SolverAttemptOutcome::Accepted => {
format!("backend={:?}, outcome=accepted", self.backend)
}
SolverAttemptOutcome::Failed { kind, reason } => {
format!(
"backend={:?}, outcome=failed({kind:?}): {reason}",
self.backend
)
}
SolverAttemptOutcome::RejectedCandidate { reason } => {
format!(
"backend={:?}, outcome=rejected_candidate: {reason}",
self.backend
)
}
SolverAttemptOutcome::Skipped { reason } => {
format!("backend={:?}, outcome=skipped: {reason}", self.backend)
}
}
}
}
impl fmt::Display for SolverAttemptReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EquilibriumSolveReport {
pub policy: SolverPolicy,
pub attempts: Vec<SolverAttemptReport>,
pub accepted_backend: SolverBackend,
}
impl EquilibriumSolveReport {
pub fn attempt_count(&self) -> usize {
self.attempts.len()
}
pub fn started_attempt_count(&self) -> usize {
self.attempts
.iter()
.filter(|attempt| !matches!(attempt.outcome, SolverAttemptOutcome::Skipped { .. }))
.count()
}
pub fn fallback_attempt_count(&self) -> usize {
self.started_attempt_count().saturating_sub(1)
}
pub fn skipped_attempt_count(&self) -> usize {
self.attempts
.iter()
.filter(|attempt| matches!(attempt.outcome, SolverAttemptOutcome::Skipped { .. }))
.count()
}
pub fn accepted_attempt_index(&self) -> Option<usize> {
self.attempts
.iter()
.position(|attempt| attempt.backend == self.accepted_backend)
}
pub fn accepted_attempt(&self) -> Option<&SolverAttemptReport> {
self.accepted_attempt_index()
.and_then(|index| self.attempts.get(index))
}
pub fn attempt(&self, index: usize) -> Option<&SolverAttemptReport> {
self.attempts.get(index)
}
pub fn attempt_backends(&self) -> Vec<SolverBackend> {
self.attempts
.iter()
.map(|attempt| attempt.backend)
.collect()
}
pub fn accepted_after_fallback(&self) -> bool {
self.started_attempt_count() > 1
}
pub fn summary(&self) -> String {
format!(
"policy={:?}, attempts={}, started={}, skipped={}, fallback={}, accepted_backend={:?}",
self.policy,
self.attempt_count(),
self.started_attempt_count(),
self.skipped_attempt_count(),
self.fallback_attempt_count(),
self.accepted_backend
)
}
}
impl fmt::Display for EquilibriumSolveReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_policy_keeps_preferred_backend_first_and_removes_duplicates() {
let policy = SolverPolicy::legacy_default(Solvers::NR);
assert_eq!(
policy.ordered_backends(),
vec![
SolverBackend::Legacy(Solvers::NR),
SolverBackend::Legacy(Solvers::LM),
SolverBackend::Legacy(Solvers::TR),
]
);
}
#[test]
fn single_policy_never_adds_a_fallback_backend() {
assert_eq!(
SolverPolicy::Single(SolverBackend::Legacy(Solvers::TR)).ordered_backends(),
vec![SolverBackend::Legacy(Solvers::TR)]
);
}
#[test]
fn solve_report_summarizes_attempts_without_manual_vector_traversal() {
let report = EquilibriumSolveReport {
policy: SolverPolicy::Single(SolverBackend::Legacy(Solvers::LM)),
attempts: vec![
SolverAttemptReport {
backend: SolverBackend::Legacy(Solvers::LM),
outcome: SolverAttemptOutcome::Failed {
kind: SolverAttemptFailureKind::Solver,
reason: "first".to_string(),
},
metrics: None,
},
SolverAttemptReport {
backend: SolverBackend::Legacy(Solvers::NR),
outcome: SolverAttemptOutcome::Accepted,
metrics: None,
},
SolverAttemptReport {
backend: SolverBackend::Legacy(Solvers::TR),
outcome: SolverAttemptOutcome::Skipped {
reason: "budget exhausted".to_string(),
},
metrics: None,
},
],
accepted_backend: SolverBackend::Legacy(Solvers::NR),
};
assert_eq!(report.attempt_count(), 3);
assert_eq!(report.started_attempt_count(), 2);
assert_eq!(report.fallback_attempt_count(), 1);
assert_eq!(report.skipped_attempt_count(), 1);
assert_eq!(report.accepted_attempt_index(), Some(1));
assert!(report.accepted_attempt().unwrap().is_accepted());
assert!(report.attempt(2).unwrap().is_skipped());
assert_eq!(
report.attempt_backends(),
vec![
SolverBackend::Legacy(Solvers::LM),
SolverBackend::Legacy(Solvers::NR),
SolverBackend::Legacy(Solvers::TR),
]
);
assert!(report.accepted_after_fallback());
assert!(report.summary().contains("attempts=3"));
assert!(report.to_string().contains("accepted_backend"));
assert!(report.attempt(0).unwrap().to_string().contains("failed"));
}
#[test]
fn attempt_outcome_exposes_reason_and_failure_kind() {
let outcome = SolverAttemptOutcome::Failed {
kind: SolverAttemptFailureKind::ResidualEvaluation,
reason: "temporary residual failure".to_string(),
};
assert!(outcome.is_started());
assert!(!outcome.is_skipped());
assert_eq!(
outcome.failure_kind(),
Some(SolverAttemptFailureKind::ResidualEvaluation)
);
assert_eq!(outcome.reason(), Some("temporary residual failure"));
let skipped = SolverAttemptOutcome::Skipped {
reason: "budget exhausted".to_string(),
};
assert!(skipped.is_skipped());
assert_eq!(skipped.reason(), Some("budget exhausted"));
}
}