use std::fmt;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_constraints::TemperatureBounds;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_log_moles::GibbsFn;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use crate::Thermodynamics::DBhandlers::thermo_api::ThermoCalculator;
use crate::Thermodynamics::User_PhaseOrSolution::ResolvedPhaseSystem;
use crate::Thermodynamics::User_substances::{CalculatorType, DataType, SubsData, WhatIsFound};
use crate::Thermodynamics::phase_layout::PhaseComponentId;
use RustedSciThe::symbolic::symbolic_engine::Expr;
pub type MolarEnthalpyFunction<'a> =
Arc<dyn Fn(f64) -> Result<f64, ReactionExtentError> + Send + Sync + 'a>;
pub type MolarThermoFunction =
Arc<dyn Fn(f64) -> Result<f64, ReactionExtentError> + Send + Sync + 'static>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThermochemistryProvenance {
component: PhaseComponentId,
library: String,
record_key: String,
state: String,
}
#[derive(Clone, Debug)]
pub(crate) struct ResolvedSymbolicThermochemistry {
standard_gibbs: Vec<Expr>,
enthalpy: Vec<Expr>,
}
impl ResolvedSymbolicThermochemistry {
pub(crate) fn new(
standard_gibbs: Vec<Expr>,
enthalpy: Vec<Expr>,
) -> Result<Self, ReactionExtentError> {
if standard_gibbs.is_empty() || standard_gibbs.len() != enthalpy.len() {
return Err(ReactionExtentError::DimensionMismatch(format!(
"symbolic thermochemistry has {} Gibbs and {} enthalpy expressions",
standard_gibbs.len(),
enthalpy.len()
)));
}
Ok(Self {
standard_gibbs,
enthalpy,
})
}
pub(crate) fn standard_gibbs(&self) -> &[Expr] {
&self.standard_gibbs
}
pub(crate) fn enthalpy(&self) -> &[Expr] {
&self.enthalpy
}
fn subset(&self, indices: &[usize]) -> Result<Self, ReactionExtentError> {
let mut standard_gibbs = Vec::with_capacity(indices.len());
let mut enthalpy = Vec::with_capacity(indices.len());
for &index in indices {
let gibbs = self.standard_gibbs.get(index).ok_or_else(|| {
ReactionExtentError::DimensionMismatch(format!(
"symbolic P,H active-set index {index} exceeds Gibbs capability count {}",
self.standard_gibbs.len()
))
})?;
let heat = self.enthalpy.get(index).ok_or_else(|| {
ReactionExtentError::DimensionMismatch(format!(
"symbolic P,H active-set index {index} exceeds enthalpy capability count {}",
self.enthalpy.len()
))
})?;
standard_gibbs.push(gibbs.clone());
enthalpy.push(heat.clone());
}
Self::new(standard_gibbs, enthalpy)
}
}
impl ThermochemistryProvenance {
pub fn new(
component: PhaseComponentId,
library: impl Into<String>,
record_key: impl Into<String>,
state: impl Into<String>,
) -> Self {
Self {
component,
library: library.into(),
record_key: record_key.into(),
state: state.into(),
}
}
pub fn component(&self) -> &PhaseComponentId {
&self.component
}
pub fn library(&self) -> &str {
&self.library
}
pub fn record_key(&self) -> &str {
&self.record_key
}
pub fn state(&self) -> &str {
&self.state
}
}
#[derive(Clone)]
pub struct ResolvedThermochemistry {
provenance: Vec<ThermochemistryProvenance>,
temperature_bounds: TemperatureBounds,
gibbs: Vec<MolarThermoFunction>,
enthalpy: Vec<MolarThermoFunction>,
heat_capacity: Vec<Option<MolarThermoFunction>>,
symbolic: Option<ResolvedSymbolicThermochemistry>,
symbolic_sources: Option<std::collections::HashMap<Option<String>, SubsData>>,
}
impl fmt::Debug for ResolvedThermochemistry {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ResolvedThermochemistry")
.field("provenance", &self.provenance)
.field("temperature_bounds", &self.temperature_bounds)
.field("component_count", &self.len())
.finish_non_exhaustive()
}
}
impl PartialEq for ResolvedThermochemistry {
fn eq(&self, other: &Self) -> bool {
self.provenance == other.provenance && self.temperature_bounds == other.temperature_bounds
}
}
impl ResolvedThermochemistry {
pub fn from_resolved_system(
resolved: &ResolvedPhaseSystem,
) -> Result<Self, ReactionExtentError> {
struct PhaseSource {
data: Arc<Mutex<SubsData>>,
gibbs: Arc<Mutex<PhaseGibbsSource>>,
provenance: std::collections::HashMap<String, ThermochemistryProvenance>,
}
let mut phase_sources = std::collections::HashMap::new();
let mut symbolic_sources = std::collections::HashMap::new();
let mut common_bounds: Option<TemperatureBounds> = None;
for (phase, original) in resolved.phase_data() {
let local = original.clone();
let data = Arc::new(Mutex::new(local));
let gibbs = Arc::new(Mutex::new(PhaseGibbsSource::new(original.clone())));
let substances = original.substances().to_vec();
let mut phase_bounds: Option<TemperatureBounds> = None;
let mut provenance = std::collections::HashMap::new();
for substance in substances {
let source = data
.lock()
.map_err(|_| ReactionExtentError::InvalidProblem {
field: "thermochemistry_source",
message: "private SubsData source was poisoned during preparation"
.to_string(),
})?;
let record = source
.get_search_result(&substance, WhatIsFound::Thermo)
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "thermochemistry_provenance",
message: format!("no thermochemistry record for '{substance}'"),
})?;
let mut calculator = match record.calculator().cloned() {
Some(CalculatorType::Thermo(calculator)) => calculator,
Some(CalculatorType::Transport(_)) => {
return Err(ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: format!(
"transport calculator cannot provide thermochemistry for '{substance}'"
),
});
}
None => {
return Err(ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: format!("calculator is missing for '{substance}'"),
});
}
};
calculator.parse_coefficients().map_err(|error| {
ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: format!(
"failed to parse temperature domain for '{substance}': {error}"
),
}
})?;
let interval = calculator.valid_temperature_interval().map_err(|error| {
ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: format!(
"failed to read temperature domain for '{substance}': {error}"
),
}
})?;
let interval = TemperatureBounds::new(interval.0, interval.1)?;
phase_bounds = Some(match phase_bounds {
Some(current) => current.intersect(interval).map_err(|_| {
ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: format!(
"temperature domains do not overlap in phase {phase:?}"
),
}
})?,
None => interval,
});
let state = resolved
.report()
.phase(&crate::Thermodynamics::phase_layout::PhaseId::new(
phase.clone(),
))
.and_then(|summary| {
summary.search().rows().iter().find(|row| {
row.substance() == substance
&& row.library() == record.library()
&& row.record_key() == record.record_key()
})
})
.map(|row| row.state().to_string())
.unwrap_or_else(|| "resolved".to_string());
provenance.insert(
substance.clone(),
ThermochemistryProvenance::new(
PhaseComponentId::new(
crate::Thermodynamics::phase_layout::PhaseId::new(phase.clone()),
substance.clone(),
),
record.library(),
record.record_key(),
state,
),
);
}
let bounds = phase_bounds.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: format!("phase {phase:?} has no thermochemistry components"),
})?;
common_bounds =
Some(match common_bounds {
Some(current) => current.intersect(bounds).map_err(|_| {
ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: "selected records have no common temperature domain"
.to_string(),
}
})?,
None => bounds,
});
symbolic_sources.insert(phase.clone(), original.clone());
phase_sources.insert(
phase.clone(),
PhaseSource {
data,
gibbs,
provenance,
},
);
}
let count = resolved.layout().component_count();
let mut provenance = Vec::with_capacity(count);
let mut gibbs = Vec::with_capacity(count);
let mut enthalpy = Vec::with_capacity(count);
let mut heat_capacity = Vec::with_capacity(count);
for component in resolved.layout().components() {
let phase = phase_sources
.get_mut(component.phase.as_option())
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "thermochemistry_layout",
message: format!("missing thermochemistry phase for '{}'", component.label()),
})?;
let row = phase
.provenance
.get(&component.substance)
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "thermochemistry_provenance",
message: format!("missing provenance for '{}'", component.label()),
})?
.clone();
provenance.push(row);
let source = Arc::clone(&phase.data);
let substance = component.substance.clone();
gibbs.push(gibbs_function(Arc::clone(&phase.gibbs), substance.clone()));
enthalpy.push(property_function(
Arc::clone(&source),
substance.clone(),
DataType::dH_fun,
));
heat_capacity.push(Some(property_function(source, substance, DataType::Cp_fun)));
}
let mut thermochemistry = Self::from_functions(
provenance,
common_bounds.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "thermochemistry_interval",
message: "resolved system has no thermochemistry domain".to_string(),
})?,
gibbs,
enthalpy,
heat_capacity,
)?;
thermochemistry.symbolic_sources = Some(symbolic_sources);
Ok(thermochemistry)
}
pub fn from_functions(
provenance: Vec<ThermochemistryProvenance>,
temperature_bounds: TemperatureBounds,
gibbs: Vec<MolarThermoFunction>,
enthalpy: Vec<MolarThermoFunction>,
heat_capacity: Vec<Option<MolarThermoFunction>>,
) -> Result<Self, ReactionExtentError> {
let count = provenance.len();
if count == 0 {
return Err(ReactionExtentError::InvalidProblem {
field: "thermochemistry_bundle",
message: "at least one thermochemical component is required".to_string(),
});
}
if gibbs.len() != count || enthalpy.len() != count || heat_capacity.len() != count {
return Err(ReactionExtentError::DimensionMismatch(format!(
"thermochemistry bundle has {} provenance rows, {} Gibbs, {} enthalpy, and {} Cp capabilities",
count,
gibbs.len(),
enthalpy.len(),
heat_capacity.len()
)));
}
Ok(Self {
provenance,
temperature_bounds,
gibbs,
enthalpy,
heat_capacity,
symbolic: None,
symbolic_sources: None,
})
}
pub fn len(&self) -> usize {
self.provenance.len()
}
pub fn is_empty(&self) -> bool {
self.provenance.is_empty()
}
pub fn temperature_bounds(&self) -> TemperatureBounds {
self.temperature_bounds
}
pub fn provenance(&self) -> &[ThermochemistryProvenance] {
&self.provenance
}
pub(crate) fn subset(&self, indices: &[usize]) -> Result<Self, ReactionExtentError> {
let mut provenance = Vec::with_capacity(indices.len());
let mut gibbs = Vec::with_capacity(indices.len());
let mut enthalpy = Vec::with_capacity(indices.len());
let mut heat_capacity = Vec::with_capacity(indices.len());
for &index in indices {
let row = self.provenance.get(index).ok_or_else(|| {
ReactionExtentError::DimensionMismatch(format!(
"P,H active-set index {index} exceeds thermochemistry component count {}",
self.len()
))
})?;
provenance.push(row.clone());
gibbs.push(self.gibbs[index].clone());
enthalpy.push(self.enthalpy[index].clone());
heat_capacity.push(self.heat_capacity[index].clone());
}
let mut subset = Self::from_functions(
provenance,
self.temperature_bounds,
gibbs,
enthalpy,
heat_capacity,
)?;
subset.symbolic = self
.symbolic
.as_ref()
.map(|symbolic| symbolic.subset(indices))
.transpose()?;
subset.symbolic_sources = self.symbolic_sources.clone();
Ok(subset)
}
pub(crate) fn symbolic_for_bounds(
&self,
bounds: TemperatureBounds,
) -> Result<Option<ResolvedSymbolicThermochemistry>, String> {
if let Some(symbolic) = &self.symbolic {
return Ok(Some(symbolic.clone()));
}
let Some(sources) = &self.symbolic_sources else {
return Ok(None);
};
let mut substances_by_phase: std::collections::HashMap<Option<String>, Vec<String>> =
std::collections::HashMap::new();
for row in &self.provenance {
substances_by_phase
.entry(row.component().phase.as_option().clone())
.or_default()
.push(row.component().substance.clone());
}
let mut expressions_by_phase = std::collections::HashMap::new();
for (phase, source) in sources {
let substances = substances_by_phase
.get(phase)
.ok_or_else(|| format!("missing symbolic component selection for '{phase:?}'"))?;
let expressions = symbolic_phase_capabilities(source, substances, bounds)?;
expressions_by_phase.insert(phase.clone(), expressions);
}
let mut standard_gibbs = Vec::with_capacity(self.len());
let mut enthalpy = Vec::with_capacity(self.len());
for row in &self.provenance {
let phase = row.component().phase.as_option();
let substance = &row.component().substance;
let (gibbs_by_substance, enthalpy_by_substance) = expressions_by_phase
.get(phase)
.ok_or_else(|| format!("missing symbolic phase source for '{phase:?}'"))?;
let gibbs = gibbs_by_substance.get(substance).ok_or_else(|| {
format!(
"missing symbolic standard Gibbs expression for '{}'",
row.component().label()
)
})?;
let heat = enthalpy_by_substance.get(substance).ok_or_else(|| {
format!(
"missing symbolic enthalpy expression for '{}'",
row.component().label()
)
})?;
standard_gibbs.push(gibbs.clone());
enthalpy.push(heat.clone());
}
ResolvedSymbolicThermochemistry::new(standard_gibbs, enthalpy)
.map(Some)
.map_err(|error| error.to_string())
}
#[cfg(test)]
pub(crate) fn with_symbolic_expressions(
mut self,
standard_gibbs: Vec<Expr>,
enthalpy: Vec<Expr>,
) -> Result<Self, ReactionExtentError> {
if standard_gibbs.len() != self.len() || enthalpy.len() != self.len() {
return Err(ReactionExtentError::DimensionMismatch(format!(
"symbolic test capability has {} Gibbs and {} enthalpy expressions for {} components",
standard_gibbs.len(),
enthalpy.len(),
self.len()
)));
}
self.symbolic = Some(ResolvedSymbolicThermochemistry::new(
standard_gibbs,
enthalpy,
)?);
Ok(self)
}
pub fn evaluate_gibbs(&self, temperature: f64) -> Result<Vec<f64>, ReactionExtentError> {
self.evaluate_functions("standard_gibbs", &self.gibbs, temperature)
}
pub(crate) fn gibbs_snapshot_for_legacy_boundary(
&self,
temperature: f64,
) -> Result<Vec<GibbsFn>, ReactionExtentError> {
let values = self.evaluate_gibbs(temperature)?;
Ok(values
.into_iter()
.map(|value| Rc::new(move |_| value) as GibbsFn)
.collect())
}
pub fn evaluate_enthalpy(&self, temperature: f64) -> Result<Vec<f64>, ReactionExtentError> {
self.evaluate_functions("molar_enthalpy", &self.enthalpy, temperature)
}
pub fn evaluate_heat_capacity(
&self,
temperature: f64,
) -> Result<Vec<Option<f64>>, ReactionExtentError> {
self.temperature_bounds
.contains(temperature)
.then_some(())
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature",
message: format!(
"temperature {temperature} K is outside the common thermochemistry interval"
),
})?;
self.heat_capacity
.iter()
.enumerate()
.map(|(index, function)| {
function
.as_ref()
.map(|function| {
let value = function(temperature)?;
if !value.is_finite() {
return Err(ReactionExtentError::InvalidProblem {
field: "heat_capacity",
message: format!("Cp function {index} returned a non-finite value"),
});
}
Ok(value)
})
.transpose()
})
.collect()
}
pub(crate) fn enthalpy_functions(&self) -> Vec<MolarEnthalpyFunction<'static>> {
self.enthalpy.clone()
}
pub(crate) fn heat_capacity_functions(
&self,
) -> Vec<Option<MolarEnthalpyFunction<'static>>> {
self.heat_capacity.clone()
}
fn evaluate_functions(
&self,
field: &'static str,
functions: &[MolarThermoFunction],
temperature: f64,
) -> Result<Vec<f64>, ReactionExtentError> {
self.temperature_bounds
.contains(temperature)
.then_some(())
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature",
message: format!(
"temperature {temperature} K is outside the common thermochemistry interval"
),
})?;
functions
.iter()
.enumerate()
.map(|(index, function)| {
let value = function(temperature)?;
if !value.is_finite() {
return Err(ReactionExtentError::InvalidProblem {
field,
message: format!(
"thermochemistry function {index} returned a non-finite value"
),
});
}
Ok(value)
})
.collect()
}
}
fn property_function(
data: Arc<Mutex<SubsData>>,
substance: String,
data_type: DataType,
) -> MolarThermoFunction {
Arc::new(move |temperature| {
let mut data = data
.lock()
.map_err(|_| ReactionExtentError::InvalidProblem {
field: "thermochemistry_source",
message: "private SubsData source was poisoned during evaluation".to_string(),
})?;
data.extract_thermal_coeffs(&substance, temperature)
.map_err(|error| ReactionExtentError::InvalidProblem {
field: "thermochemistry_property",
message: format!("failed to select coefficients for '{substance}': {error}"),
})?;
let (cp, dh, _) = data
.calculate_thermo_properties(&substance, temperature)
.map_err(|error| ReactionExtentError::InvalidProblem {
field: "thermochemistry_property",
message: format!("failed to evaluate '{substance}': {error}"),
})?;
match data_type {
DataType::Cp_fun => Ok(cp),
DataType::dH_fun => Ok(dh),
_ => Err(ReactionExtentError::InvalidProblem {
field: "thermochemistry_property",
message: "unsupported property requested by the P,H bundle".to_string(),
}),
}
})
}
fn symbolic_phase_capabilities(
original: &SubsData,
selected_substances: &[String],
bounds: TemperatureBounds,
) -> Result<
(
std::collections::HashMap<String, Expr>,
std::collections::HashMap<String, Expr>,
),
String,
> {
let mut working = original.clone();
working.substances = selected_substances.to_vec();
working
.extract_all_thermal_coeffs(bounds.lower())
.map_err(|error| error.to_string())?;
for substance in working.substances().to_vec() {
let remains_in_native_interval = working
.is_coeffs_valid_for_T(&substance, bounds.upper())
.map_err(|error| error.to_string())?;
if !remains_in_native_interval {
return Err(format!(
"symbolic P,H interval [{}, {}] crosses a native thermochemical coefficient boundary for '{substance}'",
bounds.lower(),
bounds.upper()
));
}
}
working
.calculate_therm_map_of_sym()
.map_err(|error| error.to_string())?;
let standard_gibbs = working
.calculate_dG0_sym_one_phase()
.map_err(|error| error.to_string())?;
let mut enthalpy = std::collections::HashMap::new();
for substance in working.substances().iter() {
let expression = working
.get_thermo_symbolic(substance, DataType::dH_sym)
.ok_or_else(|| format!("missing symbolic dH expression for '{substance}'"))?;
enthalpy.insert(substance.clone(), expression.clone());
}
Ok((standard_gibbs, enthalpy))
}
struct PhaseGibbsSource {
data: SubsData,
selected_temperature: Option<f64>,
functions: std::collections::HashMap<String, Box<dyn Fn(f64) -> f64 + Send + Sync>>,
}
impl PhaseGibbsSource {
fn new(data: SubsData) -> Self {
Self {
data,
selected_temperature: None,
functions: std::collections::HashMap::new(),
}
}
fn evaluate(
&mut self,
substance: &str,
temperature: f64,
) -> Result<f64, ReactionExtentError> {
if self.selected_temperature != Some(temperature) {
self.data
.extract_all_thermal_coeffs(temperature)
.map_err(|error| ReactionExtentError::InvalidProblem {
field: "thermochemistry_gibbs",
message: format!(
"failed to select phase coefficients at {temperature} K: {error}"
),
})?;
self.functions = self.data.calculate_dG0_fun_one_phase().map_err(|error| {
ReactionExtentError::InvalidProblem {
field: "thermochemistry_gibbs",
message: format!(
"failed to build phase standard Gibbs functions at {temperature} K: {error}"
),
}
})?;
self.selected_temperature = Some(temperature);
}
let function = self.functions.get(substance).ok_or_else(|| {
ReactionExtentError::InvalidProblem {
field: "thermochemistry_gibbs",
message: format!("missing standard Gibbs function for '{substance}'"),
}
})?;
let value = function(temperature);
if !value.is_finite() {
return Err(ReactionExtentError::InvalidProblem {
field: "thermochemistry_gibbs",
message: format!(
"standard Gibbs function for '{substance}' returned a non-finite value"
),
});
}
Ok(value)
}
}
fn gibbs_function(data: Arc<Mutex<PhaseGibbsSource>>, substance: String) -> MolarThermoFunction {
Arc::new(move |temperature| {
let mut data = data
.lock()
.map_err(|_| ReactionExtentError::InvalidProblem {
field: "thermochemistry_source",
message: "private SubsData source was poisoned during evaluation".to_string(),
})?;
data.evaluate(&substance, temperature)
})
}