use std::collections::{BTreeSet, HashMap};
use std::ops::Range;
use std::rc::Rc;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_activity::PhaseActivityModel;
pub use crate::Thermodynamics::ChemEquilibrium::equilibrium_component::EquilibriumComponentDescriptor;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_constant_cross_validation::EquilibriumConstantCrossValidationStatus;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_ids::PhaseIndex;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_log_moles::{
EquilibriumLogMoles, EquilibriumSolverSettings, GibbsFn, Phase,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_multiphase_domain::{
MultiphaseEquilibriumLayout, MultiphaseInitialComposition,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::{
EquilibriumConditions, EquilibriumProblem, EquilibriumSolution, LogMolesInitialGuess,
TraceSpeciesSeedPolicy,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_solver_policy::EquilibriumSolveReport;
use crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_solution::MultiphaseEquilibriumSolution;
use crate::Thermodynamics::User_PhaseOrSolution::{
PhaseModel, ResolvedPhaseSystem, ResolvedPhaseSystemReport,
};
use crate::Thermodynamics::User_substances::SubsData;
use crate::Thermodynamics::User_substances2::SearchSummaryRow;
use crate::Thermodynamics::phase_layout::{
PhaseComponentId, PhaseId as SemanticPhaseId, SystemLayout,
};
use crate::Thermodynamics::physical_state::PhysicalState;
use RustedSciThe::symbolic::symbolic_engine::Expr;
use nalgebra::DMatrix;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SupportedPhaseModelPolicy {
#[default]
FixedPressureTemperatureV1,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EquilibriumPhaseDescriptor {
id: SemanticPhaseId,
index: PhaseIndex,
physical_state: PhysicalState,
phase_model: PhaseModel,
activity_model: PhaseActivityModel,
component_range: Range<usize>,
}
impl EquilibriumPhaseDescriptor {
pub fn id(&self) -> &SemanticPhaseId {
&self.id
}
pub fn index(&self) -> PhaseIndex {
self.index
}
pub fn physical_state(&self) -> PhysicalState {
self.physical_state
}
pub fn phase_model(&self) -> PhaseModel {
self.phase_model
}
pub fn activity_model(&self) -> PhaseActivityModel {
self.activity_model
}
pub fn component_range(&self) -> Range<usize> {
self.component_range.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhaseEquilibriumMetadata {
layout: SystemLayout,
layout_fingerprint: u64,
components: Vec<EquilibriumComponentDescriptor>,
phases: Vec<EquilibriumPhaseDescriptor>,
component_indices: HashMap<PhaseComponentId, usize>,
phase_indices: HashMap<SemanticPhaseId, PhaseIndex>,
provenance: ResolvedPhaseSystemReport,
}
impl PhaseEquilibriumMetadata {
pub fn from_resolved(
resolved: &ResolvedPhaseSystem,
policy: SupportedPhaseModelPolicy,
) -> Result<Self, ReactionExtentError> {
match policy {
SupportedPhaseModelPolicy::FixedPressureTemperatureV1 => {}
}
let multiphase_layout = MultiphaseEquilibriumLayout::new(resolved.phase_specs().to_vec())?;
if multiphase_layout.system_layout() != resolved.layout() {
return Err(ReactionExtentError::InvalidProblem {
field: "resolved_phase_layout",
message:
"resolved phase data and phase specifications use different component order"
.to_string(),
});
}
let layout = resolved.layout().clone();
let phase_count = resolved.phase_specs().len();
let mut phases = Vec::with_capacity(phase_count);
let mut components = Vec::with_capacity(layout.component_count());
let mut phase_indices = HashMap::with_capacity(phase_count);
let mut component_indices = HashMap::with_capacity(layout.component_count());
for (phase_position, spec) in resolved.phase_specs().iter().enumerate() {
let id = spec.id().clone();
let index = PhaseIndex::new(phase_position, phase_count)?;
let component_range = layout.phase_component_range(&id).cloned().ok_or_else(|| {
ReactionExtentError::InvalidProblem {
field: "resolved_phase_layout",
message: format!("phase {:?} has no component range", id.as_option()),
}
})?;
let activity_model = activity_model_for(spec.model());
if phase_indices.insert(id.clone(), index).is_some() {
return Err(ReactionExtentError::InvalidProblem {
field: "resolved_phase_layout",
message: format!("duplicate phase identity {:?}", id.as_option()),
});
}
for component_position in component_range.clone() {
let id = layout.components()[component_position].clone();
if component_indices
.insert(id.clone(), component_position)
.is_some()
{
return Err(ReactionExtentError::InvalidProblem {
field: "resolved_phase_layout",
message: format!("duplicate component identity '{}'", id.label()),
});
}
components.push(EquilibriumComponentDescriptor::new(
id,
spec.physical_state(),
spec.model(),
activity_model,
));
}
phases.push(EquilibriumPhaseDescriptor {
id,
index,
physical_state: spec.physical_state(),
phase_model: spec.model(),
activity_model,
component_range,
});
}
if components.len() != layout.component_count() {
return Err(ReactionExtentError::InvalidProblem {
field: "resolved_phase_layout",
message: "not every resolved component belongs to a declared phase".to_string(),
});
}
Ok(Self {
layout,
layout_fingerprint: multiphase_layout.fingerprint(),
components,
phases,
component_indices,
phase_indices,
provenance: resolved.report().clone(),
})
}
pub fn layout(&self) -> &SystemLayout {
&self.layout
}
pub fn layout_fingerprint(&self) -> u64 {
self.layout_fingerprint
}
pub fn components(&self) -> &[EquilibriumComponentDescriptor] {
&self.components
}
pub fn phases(&self) -> &[EquilibriumPhaseDescriptor] {
&self.phases
}
pub fn component_index(&self, id: &PhaseComponentId) -> Option<usize> {
self.component_indices.get(id).copied()
}
pub fn phase_index(&self, id: &SemanticPhaseId) -> Option<PhaseIndex> {
self.phase_indices.get(id).copied()
}
pub fn provenance(&self) -> &ResolvedPhaseSystemReport {
&self.provenance
}
}
#[derive(Debug)]
pub struct PhaseEquilibriumBuildRequest<'a> {
resolved: &'a ResolvedPhaseSystem,
conditions: EquilibriumConditions,
initial_composition: MultiphaseInitialComposition,
trace_seed_policy: TraceSpeciesSeedPolicy,
model_policy: SupportedPhaseModelPolicy,
metadata: PhaseEquilibriumMetadata,
}
impl<'a> PhaseEquilibriumBuildRequest<'a> {
pub fn new(
resolved: &'a ResolvedPhaseSystem,
conditions: EquilibriumConditions,
initial_composition: MultiphaseInitialComposition,
trace_seed_policy: TraceSpeciesSeedPolicy,
model_policy: SupportedPhaseModelPolicy,
) -> Result<Self, ReactionExtentError> {
let multiphase_layout = MultiphaseEquilibriumLayout::new(resolved.phase_specs().to_vec())?;
initial_composition.validate_for(&multiphase_layout)?;
let metadata = PhaseEquilibriumMetadata::from_resolved(resolved, model_policy)?;
Ok(Self {
resolved,
conditions,
initial_composition,
trace_seed_policy,
model_policy,
metadata,
})
}
pub fn resolved(&self) -> &ResolvedPhaseSystem {
self.resolved
}
pub fn conditions(&self) -> EquilibriumConditions {
self.conditions
}
pub fn initial_composition(&self) -> &MultiphaseInitialComposition {
&self.initial_composition
}
pub fn trace_seed_policy(&self) -> TraceSpeciesSeedPolicy {
self.trace_seed_policy
}
pub fn model_policy(&self) -> SupportedPhaseModelPolicy {
self.model_policy
}
pub fn metadata(&self) -> &PhaseEquilibriumMetadata {
&self.metadata
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EquilibriumBridgeComponentReport {
component: EquilibriumComponentDescriptor,
initial_moles: f64,
standard_gibbs_at_conditions: f64,
thermo_source: SearchSummaryRow,
}
impl EquilibriumBridgeComponentReport {
pub fn component(&self) -> &EquilibriumComponentDescriptor {
&self.component
}
pub fn initial_moles(&self) -> f64 {
self.initial_moles
}
pub fn standard_gibbs_at_conditions(&self) -> f64 {
self.standard_gibbs_at_conditions
}
pub fn thermo_source(&self) -> &SearchSummaryRow {
&self.thermo_source
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PhaseEquilibriumBuildReport {
conditions: EquilibriumConditions,
layout_fingerprint: u64,
element_labels: Vec<String>,
element_totals: Vec<f64>,
components: Vec<EquilibriumBridgeComponentReport>,
}
impl PhaseEquilibriumBuildReport {
pub fn conditions(&self) -> EquilibriumConditions {
self.conditions
}
pub fn layout_fingerprint(&self) -> u64 {
self.layout_fingerprint
}
pub fn element_labels(&self) -> &[String] {
&self.element_labels
}
pub fn element_totals(&self) -> &[f64] {
&self.element_totals
}
pub fn components(&self) -> &[EquilibriumBridgeComponentReport] {
&self.components
}
}
pub struct PhaseEquilibriumProblemBundle {
problem: EquilibriumProblem,
metadata: PhaseEquilibriumMetadata,
report: PhaseEquilibriumBuildReport,
symbolic_standard_gibbs: Vec<Expr>,
}
impl PhaseEquilibriumProblemBundle {
pub fn problem(&self) -> &EquilibriumProblem {
&self.problem
}
pub fn metadata(&self) -> &PhaseEquilibriumMetadata {
&self.metadata
}
pub fn report(&self) -> &PhaseEquilibriumBuildReport {
&self.report
}
pub fn into_problem(self) -> EquilibriumProblem {
self.problem
}
pub fn solve(self) -> Result<PhaseEquilibriumSolutionBundle, ReactionExtentError> {
self.solve_with(|_| {})
}
pub fn solve_with<F>(
self,
configure: F,
) -> Result<PhaseEquilibriumSolutionBundle, ReactionExtentError>
where
F: FnOnce(&mut EquilibriumSolverSettings),
{
let mut solver = EquilibriumLogMoles::from_problem(self.problem)?;
solver.gibbs_sym = self.symbolic_standard_gibbs;
configure(&mut solver.solver_settings);
solver.solve()?;
let solution = solver.accepted_solution()?;
let solve_report = solver.last_solve_report.clone().ok_or_else(|| {
ReactionExtentError::InvalidCandidate {
field: "phase_equilibrium_solution",
message: "accepted bridge solve did not publish a backend report".to_string(),
}
})?;
Ok(PhaseEquilibriumSolutionBundle {
metadata: self.metadata,
build_report: self.report,
solution,
solve_report,
keq_validation_status: solver.last_keq_validation_status.clone(),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PhaseEquilibriumSolutionBundle {
metadata: PhaseEquilibriumMetadata,
build_report: PhaseEquilibriumBuildReport,
solution: EquilibriumSolution,
solve_report: EquilibriumSolveReport,
keq_validation_status: Option<EquilibriumConstantCrossValidationStatus>,
}
impl PhaseEquilibriumSolutionBundle {
pub fn metadata(&self) -> &PhaseEquilibriumMetadata {
&self.metadata
}
pub fn build_report(&self) -> &PhaseEquilibriumBuildReport {
&self.build_report
}
pub fn solution(&self) -> &EquilibriumSolution {
&self.solution
}
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 into_multiphase_solution(
self,
) -> Result<MultiphaseEquilibriumSolution, ReactionExtentError> {
MultiphaseEquilibriumSolution::from_fixed_active_bundle(self)
}
}
pub fn build_phase_equilibrium_problem(
request: PhaseEquilibriumBuildRequest<'_>,
) -> Result<PhaseEquilibriumProblemBundle, ReactionExtentError> {
let metadata = request.metadata.clone();
let conditions = request.conditions;
let mut phase_data = HashMap::with_capacity(metadata.phases().len());
let mut all_elements = BTreeSet::new();
for phase in metadata.phases() {
let payload = request
.resolved
.phase_data()
.get(phase.id().as_option())
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "resolved_phase_data",
message: format!(
"missing data payload for phase {:?}",
phase.id().as_option()
),
})?;
let prepared = prepare_phase_thermochemistry(payload)?;
all_elements.extend(
prepared
.element_compositions
.values()
.flat_map(|composition| composition.keys().cloned()),
);
phase_data.insert(phase.id().clone(), prepared);
}
if all_elements.is_empty() {
return Err(ReactionExtentError::InvalidProblem {
field: "element_composition",
message: "resolved phase system contains no elemental composition".to_string(),
});
}
let element_labels = all_elements.into_iter().collect::<Vec<_>>();
let component_count = metadata.components().len();
let mut element_composition = DMatrix::zeros(component_count, element_labels.len());
let mut gibbs = Vec::with_capacity(component_count);
let mut symbolic_standard_gibbs = Vec::with_capacity(component_count);
let mut component_reports = Vec::with_capacity(component_count);
let mut composition_by_substance = HashMap::<String, HashMap<String, f64>>::new();
for (index, component) in metadata.components().iter().enumerate() {
let phase = phase_data.get(&component.id().phase).ok_or_else(|| {
ReactionExtentError::InvalidProblem {
field: "resolved_phase_data",
message: format!(
"missing prepared phase data for component '{}'",
component.label()
),
}
})?;
let composition = phase
.element_compositions
.get(component.substance())
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "element_composition",
message: format!(
"missing elemental composition for component '{}'",
component.label()
),
})?;
if let Some(reference) = composition_by_substance.get(component.substance()) {
if reference != composition {
return Err(ReactionExtentError::InvalidProblem {
field: "element_composition",
message: format!(
"component '{}' has a molecular composition inconsistent with another phase record",
component.substance()
),
});
}
} else {
composition_by_substance.insert(component.substance().to_string(), composition.clone());
}
for (element_index, element) in element_labels.iter().enumerate() {
element_composition[(index, element_index)] =
composition.get(element).copied().unwrap_or(0.0);
}
let thermo_source = thermo_source_for(&metadata, component)?;
let function = phase
.gibbs_functions
.get(component.substance())
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "standard_gibbs",
message: format!(
"missing standard Gibbs function for component '{}' from {}:{}",
component.label(),
thermo_source.library(),
thermo_source.record_key()
),
})?;
let standard_gibbs_at_conditions = function(conditions.temperature());
if !standard_gibbs_at_conditions.is_finite() {
return Err(ReactionExtentError::InvalidProblem {
field: "standard_gibbs",
message: format!(
"component '{}' from {}:{} returned non-finite G0 at {} K",
component.label(),
thermo_source.library(),
thermo_source.record_key(),
conditions.temperature()
),
});
}
let function = std::sync::Arc::clone(function);
gibbs.push(Rc::new(move |temperature: f64| function(temperature)) as GibbsFn);
let symbolic = phase
.symbolic_gibbs_functions
.get(component.substance())
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "symbolic_standard_gibbs",
message: format!(
"missing symbolic standard Gibbs expression for component '{}' from {}:{}",
component.label(),
thermo_source.library(),
thermo_source.record_key()
),
})?;
symbolic_standard_gibbs.push(symbolic.clone());
component_reports.push(EquilibriumBridgeComponentReport {
component: component.clone(),
initial_moles: request.initial_composition.moles()[index],
standard_gibbs_at_conditions,
thermo_source,
});
}
let phases = metadata
.phases()
.iter()
.map(|phase| Phase {
kind: phase.activity_model(),
species: phase.component_range().collect(),
})
.collect::<Vec<_>>();
let initial_moles = request.initial_composition.moles().to_vec();
let initial_log_moles =
LogMolesInitialGuess::from_moles_with_policy(&initial_moles, request.trace_seed_policy)?;
let element_totals = element_totals(&initial_moles, &element_composition);
let problem = EquilibriumProblem::new(
metadata.components().to_vec(),
initial_moles,
initial_log_moles,
element_composition,
gibbs,
phases,
conditions,
)?;
let report = PhaseEquilibriumBuildReport {
conditions,
layout_fingerprint: metadata.layout_fingerprint(),
element_labels,
element_totals,
components: component_reports,
};
Ok(PhaseEquilibriumProblemBundle {
problem,
metadata,
report,
symbolic_standard_gibbs,
})
}
#[derive(Clone)]
struct PreparedPhaseThermochemistry {
gibbs_functions: HashMap<String, std::sync::Arc<dyn Fn(f64) -> f64 + Send + Sync>>,
symbolic_gibbs_functions: HashMap<String, Expr>,
element_compositions: HashMap<String, HashMap<String, f64>>,
}
fn prepare_phase_thermochemistry(
resolved_payload: &SubsData,
) -> Result<PreparedPhaseThermochemistry, ReactionExtentError> {
let mut working = resolved_payload.clone();
let gibbs_functions = working.calculate_dG0_fun_one_phase()?;
let symbolic_gibbs_functions = working.calculate_dG0_sym_one_phase()?;
let (_, compositions, _) =
SubsData::calculate_elem_composition_and_molar_mass_local(&mut working, None)?;
if compositions.len() != working.substances().len() {
return Err(ReactionExtentError::DimensionMismatch(format!(
"phase thermochemistry produced {} compositions for {} substances",
compositions.len(),
working.substances().len()
)));
}
let mut element_compositions = HashMap::with_capacity(compositions.len());
for (substance, composition) in working.substances().iter().cloned().zip(compositions) {
if element_compositions
.insert(substance.clone(), composition)
.is_some()
{
return Err(ReactionExtentError::InvalidProblem {
field: "phase_thermochemistry",
message: format!("duplicate local substance '{substance}'"),
});
}
}
Ok(PreparedPhaseThermochemistry {
gibbs_functions: gibbs_functions
.into_iter()
.map(|(substance, function)| {
(
substance,
std::sync::Arc::from(function)
as std::sync::Arc<dyn Fn(f64) -> f64 + Send + Sync>,
)
})
.collect(),
symbolic_gibbs_functions,
element_compositions,
})
}
fn thermo_source_for(
metadata: &PhaseEquilibriumMetadata,
component: &EquilibriumComponentDescriptor,
) -> Result<SearchSummaryRow, ReactionExtentError> {
metadata
.provenance()
.phase(&component.id().phase)
.and_then(|phase| {
phase.search().rows().iter().find(|row| {
row.substance() == component.substance()
&& row.property() == "Thermo"
&& row.state() == "Found"
})
})
.cloned()
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "thermochemical_provenance",
message: format!(
"component '{}' has no resolved thermochemical provenance row",
component.label()
),
})
}
fn element_totals(initial_moles: &[f64], element_composition: &DMatrix<f64>) -> Vec<f64> {
(0..element_composition.ncols())
.map(|element| {
initial_moles
.iter()
.enumerate()
.map(|(component, amount)| amount * element_composition[(component, element)])
.sum()
})
.collect()
}
fn activity_model_for(model: PhaseModel) -> PhaseActivityModel {
match model {
PhaseModel::IdealGas => PhaseActivityModel::IdealGas,
PhaseModel::PureCondensed => PhaseActivityModel::IdealSolution,
}
}