use std::collections::BTreeMap;
use std::fmt;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_constant_cross_validation::EquilibriumConstantCrossValidationStatus;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::{
EquilibriumConditions, EquilibriumSolution,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_solver_policy::EquilibriumSolveReport;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_workflows::PhaseStatus;
use crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_problem::{
EquilibriumPhaseDescriptor, PhaseEquilibriumBuildReport, PhaseEquilibriumMetadata,
PhaseEquilibriumSolutionBundle,
};
use crate::Thermodynamics::phase_layout::{PhaseComponentId, PhaseId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiphaseEquilibriumSummaryRow {
pub section: &'static str,
pub label: String,
pub value: String,
}
impl fmt::Display for MultiphaseEquilibriumSummaryRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {} = {}", self.section, self.label, self.value)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MultiphaseEquilibriumSolution {
metadata: PhaseEquilibriumMetadata,
build_report: PhaseEquilibriumBuildReport,
accepted_solution: EquilibriumSolution,
phase_totals: Vec<f64>,
mole_fractions: Vec<f64>,
phase_statuses: Vec<PhaseStatus>,
solve_report: EquilibriumSolveReport,
keq_validation_status: Option<EquilibriumConstantCrossValidationStatus>,
}
impl MultiphaseEquilibriumSolution {
pub fn from_fixed_active_bundle(
bundle: PhaseEquilibriumSolutionBundle,
) -> Result<Self, ReactionExtentError> {
let metadata = bundle.metadata().clone();
let build_report = bundle.build_report().clone();
let accepted_solution = bundle.solution().clone();
let solve_report = bundle.solve_report().clone();
let keq_validation_status = bundle.keq_validation_status().cloned();
if metadata.layout_fingerprint() != build_report.layout_fingerprint() {
return Err(ReactionExtentError::InvalidCandidate {
field: "multiphase_solution_layout",
message: "accepted result and build report have different layout fingerprints"
.to_string(),
});
}
if accepted_solution.conditions() != build_report.conditions() {
return Err(ReactionExtentError::InvalidCandidate {
field: "multiphase_solution_conditions",
message: "accepted result and build report have different thermodynamic conditions"
.to_string(),
});
}
if metadata.components().len() != accepted_solution.moles().len()
|| metadata.components().len() != build_report.components().len()
{
return Err(ReactionExtentError::DimensionMismatch(format!(
"multiphase result has {} components, solution has {} moles, and build report has {} rows",
metadata.components().len(),
accepted_solution.moles().len(),
build_report.components().len(),
)));
}
let mut phase_totals = Vec::with_capacity(metadata.phases().len());
let mut mole_fractions = vec![0.0; metadata.components().len()];
for phase in metadata.phases() {
let range = phase.component_range();
let total = accepted_solution.moles()[range.clone()].iter().sum::<f64>();
if !total.is_finite() || total <= 0.0 {
return Err(ReactionExtentError::InvalidCandidate {
field: "multiphase_phase_total",
message: format!(
"phase {:?} has invalid accepted total {total:e}",
phase.id().as_option()
),
});
}
for component_index in range {
mole_fractions[component_index] =
accepted_solution.moles()[component_index] / total;
}
phase_totals.push(total);
}
Ok(Self {
phase_statuses: vec![PhaseStatus::Active; metadata.phases().len()],
metadata,
build_report,
accepted_solution,
phase_totals,
mole_fractions,
solve_report,
keq_validation_status,
})
}
pub fn conditions(&self) -> EquilibriumConditions {
self.accepted_solution.conditions()
}
pub fn metadata(&self) -> &PhaseEquilibriumMetadata {
&self.metadata
}
pub fn layout_fingerprint(&self) -> u64 {
self.metadata.layout_fingerprint()
}
pub fn build_report(&self) -> &PhaseEquilibriumBuildReport {
&self.build_report
}
pub fn accepted_solution(&self) -> &EquilibriumSolution {
&self.accepted_solution
}
pub fn component_moles(&self) -> &[f64] {
self.accepted_solution.moles()
}
pub fn moles_for(&self, component: &PhaseComponentId) -> Option<f64> {
self.metadata
.component_index(component)
.map(|index| self.accepted_solution.moles()[index])
}
pub fn mole_fraction_for(&self, component: &PhaseComponentId) -> Option<f64> {
self.metadata
.component_index(component)
.map(|index| self.mole_fractions[index])
}
pub fn phases(&self) -> &[EquilibriumPhaseDescriptor] {
self.metadata.phases()
}
pub fn phase_total(&self, phase: &PhaseId) -> Option<f64> {
self.metadata
.phase_index(phase)
.map(|index| self.phase_totals[index.index()])
}
pub fn phase_status(&self, phase: &PhaseId) -> Option<PhaseStatus> {
self.metadata
.phase_index(phase)
.map(|index| self.phase_statuses[index.index()])
}
pub fn aggregate_moles_by_substance(&self) -> BTreeMap<String, f64> {
let mut totals = BTreeMap::new();
for (descriptor, &moles) in self
.metadata
.components()
.iter()
.zip(self.accepted_solution.moles())
{
*totals
.entry(descriptor.substance().to_string())
.or_insert(0.0) += moles;
}
totals
}
pub fn solve_report(&self) -> &EquilibriumSolveReport {
&self.solve_report
}
pub fn keq_validation_status(&self) -> Option<&EquilibriumConstantCrossValidationStatus> {
self.keq_validation_status.as_ref()
}
pub fn summary_rows(&self) -> Vec<MultiphaseEquilibriumSummaryRow> {
let mut rows = vec![
MultiphaseEquilibriumSummaryRow {
section: "conditions",
label: "temperature_k".to_string(),
value: format!("{:.6}", self.conditions().temperature()),
},
MultiphaseEquilibriumSummaryRow {
section: "conditions",
label: "pressure_pa".to_string(),
value: format!("{:.6}", self.conditions().pressure()),
},
MultiphaseEquilibriumSummaryRow {
section: "layout",
label: "fingerprint".to_string(),
value: self.layout_fingerprint().to_string(),
},
MultiphaseEquilibriumSummaryRow {
section: "backend",
label: "accepted".to_string(),
value: format!("{:?}", self.solve_report.accepted_backend),
},
MultiphaseEquilibriumSummaryRow {
section: "validation",
label: "residual_l2_norm".to_string(),
value: format!(
"{:.6e}",
self.accepted_solution.validation().residual_l2_norm
),
},
];
for (index, phase) in self.metadata.phases().iter().enumerate() {
rows.push(MultiphaseEquilibriumSummaryRow {
section: "phase",
label: phase
.id()
.as_option()
.clone()
.unwrap_or_else(|| "single".to_string()),
value: format!(
"total={:.6e}, status={:?}",
self.phase_totals[index], self.phase_statuses[index]
),
});
}
for (index, component) in self.metadata.components().iter().enumerate() {
rows.push(MultiphaseEquilibriumSummaryRow {
section: "component",
label: component.label(),
value: format!(
"moles={:.6e}, x={:.6e}",
self.accepted_solution.moles()[index],
self.mole_fractions[index]
),
});
}
rows
}
}
impl fmt::Display for MultiphaseEquilibriumSolution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for row in self.summary_rows() {
writeln!(f, "{row}")?;
}
Ok(())
}
}