use std::collections::{BTreeSet, HashMap};
use std::rc::Rc;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_activity::PhaseActivityModel;
pub use crate::Thermodynamics::ChemEquilibrium::equilibrium_component::{
EquilibriumComponentDescriptor, EquilibriumPhaseDescriptor,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_constant_cross_validation::EquilibriumConstantCrossValidationStatus;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_ids::PhaseIndex;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_log_moles::{
EquilibriumSolverSettings, GibbsFn,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_multiphase_domain::{
MultiphaseEquilibriumLayout, MultiphaseInitialComposition,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_prepared_runner::PreparedEquilibriumRunner;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::{
EquilibriumConditions, EquilibriumProblem, EquilibriumSolution, LogMolesInitialGuess,
TraceSpeciesSeedPolicy,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_rst_backend::{
RstPreparedProblem, prepare_rst_symbolic_problem_from_prepared,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_solver_policy::EquilibriumSolveReport;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_timing::{
EquilibriumTimingCollector, EquilibriumTimingMode, EquilibriumTimingReport,
EquilibriumTimingStage,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_workflows::PhaseManager;
use crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_solution::MultiphaseEquilibriumSolution;
use crate::Thermodynamics::ChemEquilibrium::prepared_phase_control_runner::PreparedPhaseControlRunner;
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 RustedSciThe::symbolic::symbolic_engine::Expr;
use nalgebra::DMatrix;
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SupportedPhaseModelPolicy {
#[default]
FixedPressureTemperatureV1,
}
#[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::new(
id,
index,
spec.physical_state(),
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,
lookup_report: ResolvedPhaseSystemReport,
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 lookup_report(&self) -> &ResolvedPhaseSystemReport {
&self.lookup_report
}
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(crate) fn at_conditions(
&self,
conditions: EquilibriumConditions,
gibbs: &[GibbsFn],
) -> Result<Self, ReactionExtentError> {
if gibbs.len() != self.components.len() {
return Err(ReactionExtentError::DimensionMismatch(format!(
"temperature report has {} components but {} Gibbs closures",
self.components.len(),
gibbs.len()
)));
}
let mut components = self.components.clone();
for (index, component) in components.iter_mut().enumerate() {
let value = gibbs[index](conditions.temperature());
if !value.is_finite() {
return Err(ReactionExtentError::InvalidProblem {
field: "standard_gibbs",
message: format!(
"component '{}' returned non-finite G0 at {} K",
component.component.label(),
conditions.temperature()
),
});
}
component.standard_gibbs_at_conditions = value;
}
Ok(Self {
conditions,
lookup_report: self.lookup_report.clone(),
layout_fingerprint: self.layout_fingerprint,
element_labels: self.element_labels.clone(),
element_totals: self.element_totals.clone(),
components,
})
}
}
pub struct PhaseEquilibriumProblemBundle {
problem: EquilibriumProblem,
metadata: PhaseEquilibriumMetadata,
report: PhaseEquilibriumBuildReport,
symbolic_standard_gibbs: Vec<Expr>,
thermo_payloads: Vec<SubsData>,
timing: EquilibriumTimingReport,
}
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 timing_report(&self) -> &EquilibriumTimingReport {
&self.timing
}
#[cfg(test)]
pub(crate) fn symbolic_standard_gibbs(&self) -> &[Expr] {
&self.symbolic_standard_gibbs
}
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 timing = EquilibriumTimingCollector::from_report(self.timing);
let prepared =
timing.measure(EquilibriumTimingStage::NumericalProblemPreparation, || {
crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::
PreparedEquilibriumProblem::new(self.problem)
})?;
let mut runner = PreparedEquilibriumRunner::new(prepared, self.symbolic_standard_gibbs)?;
configure(runner.configure());
let outcome = timing.measure(EquilibriumTimingStage::NonlinearSolve, || runner.solve())?;
timing.record(
EquilibriumTimingStage::Validation,
outcome.validation_duration,
);
Ok(PhaseEquilibriumSolutionBundle {
metadata: self.metadata,
build_report: self.report,
solution: outcome.solution,
solve_report: outcome.solve_report,
keq_validation_status: outcome.keq_validation_status,
timing: timing.finish(),
})
}
pub(crate) fn into_temperature_template(
self,
prepare_rst: bool,
) -> Result<PreparedPhaseEquilibriumTemplate, ReactionExtentError> {
let prepared = crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::
PreparedEquilibriumProblem::new(self.problem)?;
let rst_problem = if prepare_rst {
Some(prepare_rst_symbolic_problem_from_prepared(
&prepared,
&self.symbolic_standard_gibbs,
)?)
} else {
None
};
Ok(PreparedPhaseEquilibriumTemplate {
prepared,
metadata: self.metadata,
report: self.report,
symbolic_standard_gibbs: self.symbolic_standard_gibbs,
thermo_payloads: self.thermo_payloads,
rst_problem,
timing: self.timing,
last_symbolic_parameter_reused: false,
})
}
pub(crate) fn into_phase_control_template<F>(
self,
configure_phase_control: F,
) -> Result<PreparedPhaseControlTemplate, ReactionExtentError>
where
F: FnOnce(&mut PhaseManager),
{
let timing_enabled = self.timing.enabled();
let mut runner = PreparedPhaseControlRunner::new(
self.problem,
self.symbolic_standard_gibbs.clone(),
timing_enabled,
)?;
configure_phase_control(runner.configure_phase_control());
Ok(PreparedPhaseControlTemplate {
runner,
metadata: self.metadata,
report: self.report,
thermo_payloads: self.thermo_payloads,
timing: self.timing,
last_rst_symbolic_reused: false,
})
}
pub fn solve_with_bounded_phase_control<F, G>(
self,
configure_solver: F,
configure_phase_control: G,
) -> Result<MultiphaseEquilibriumSolution, ReactionExtentError>
where
F: FnOnce(&mut EquilibriumSolverSettings),
G: FnOnce(&mut PhaseManager),
{
let timing_enabled = self.timing.enabled();
let mut timing = EquilibriumTimingCollector::from_report(self.timing);
let mut runner =
timing.measure(EquilibriumTimingStage::NumericalProblemPreparation, || {
PreparedPhaseControlRunner::new(
self.problem,
self.symbolic_standard_gibbs,
timing_enabled,
)
})?;
configure_solver(runner.configure_solver());
configure_phase_control(runner.configure_phase_control());
let outcome = timing.measure(EquilibriumTimingStage::PhaseControl, || runner.solve())?;
timing.record(
EquilibriumTimingStage::ProjectionBuild,
outcome.projection_build,
);
timing.record(
EquilibriumTimingStage::Validation,
outcome.validation_duration,
);
let timing_report = timing.finish();
MultiphaseEquilibriumSolution::from_phase_control_parts(
self.metadata,
self.report,
outcome.solution,
outcome.solve_report,
outcome.keq_validation_status,
outcome.phase_control_report,
outcome.acceptance_report,
outcome.phase_statuses,
timing_report,
)
}
}
pub(crate) struct PreparedPhaseEquilibriumTemplate {
prepared:
crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::PreparedEquilibriumProblem,
metadata: PhaseEquilibriumMetadata,
report: PhaseEquilibriumBuildReport,
symbolic_standard_gibbs: Vec<Expr>,
thermo_payloads: Vec<SubsData>,
rst_problem: Option<RstPreparedProblem>,
timing: EquilibriumTimingReport,
last_symbolic_parameter_reused: bool,
}
pub(crate) struct PreparedPhaseControlTemplate {
runner: PreparedPhaseControlRunner,
metadata: PhaseEquilibriumMetadata,
report: PhaseEquilibriumBuildReport,
thermo_payloads: Vec<SubsData>,
timing: EquilibriumTimingReport,
last_rst_symbolic_reused: bool,
}
impl PreparedPhaseControlTemplate {
pub(crate) fn build_timing(&self) -> EquilibriumTimingReport {
self.timing
}
pub(crate) fn projection_cache_size(&self) -> usize {
self.runner.projection_cache_size()
}
pub(crate) fn prepared_cache_size(&self) -> usize {
self.runner.prepared_active_set_cache_size()
}
pub(crate) fn rst_cache_size(&self) -> usize {
self.runner.rst_prepared_cache_size()
}
pub(crate) fn last_rst_symbolic_reused(&self) -> bool {
self.last_rst_symbolic_reused
}
pub(crate) fn solve_at(
&mut self,
conditions: EquilibriumConditions,
seed: LogMolesInitialGuess,
settings: EquilibriumSolverSettings,
timing_mode: EquilibriumTimingMode,
continuation_phase_set: Option<
crate::Thermodynamics::ChemEquilibrium::equilibrium_workflows::PhaseSet,
>,
) -> Result<MultiphaseEquilibriumSolution, ReactionExtentError> {
let started = Instant::now();
let mut timing = EquilibriumTimingCollector::new(timing_mode);
let gibbs = self.refresh_gibbs(conditions.temperature(), &mut timing)?;
let symbolic = self.refresh_symbolic()?;
self.runner
.retarget(conditions, seed, gibbs.clone(), symbolic.clone())?;
if let Some(phase_set) = continuation_phase_set {
self.runner.set_continuation_phase_set(phase_set)?;
}
*self.runner.configure_solver() = settings;
let outcome =
timing.measure(EquilibriumTimingStage::PhaseControl, || self.runner.solve())?;
self.last_rst_symbolic_reused = outcome.rst_symbolic_reused;
timing.record(
EquilibriumTimingStage::ProjectionBuild,
outcome.projection_build,
);
timing.record(
EquilibriumTimingStage::Validation,
outcome.validation_duration,
);
timing.set_total(started.elapsed());
let report = self.report.at_conditions(conditions, &gibbs)?;
MultiphaseEquilibriumSolution::from_phase_control_parts(
self.metadata.clone(),
report,
outcome.solution,
outcome.solve_report,
outcome.keq_validation_status,
outcome.phase_control_report,
outcome.acceptance_report,
outcome.phase_statuses,
timing.finish(),
)
}
fn refresh_gibbs(
&mut self,
temperature: f64,
timing: &mut EquilibriumTimingCollector,
) -> Result<Vec<GibbsFn>, ReactionExtentError> {
let mut phase_functions = Vec::with_capacity(self.thermo_payloads.len());
for payload in &mut self.thermo_payloads {
timing.measure(EquilibriumTimingStage::ThermochemistryPreparation, || {
payload.extract_all_thermal_coeffs(temperature)
})?;
let functions = timing
.measure(EquilibriumTimingStage::NumericClosureConstruction, || {
payload.calculate_dG0_fun_one_phase()
})?;
phase_functions.push(
functions
.into_iter()
.map(|(substance, function)| {
(
substance,
std::sync::Arc::from(function)
as std::sync::Arc<dyn Fn(f64) -> f64 + Send + Sync>,
)
})
.collect::<HashMap<_, _>>(),
);
}
self.metadata
.components()
.iter()
.map(|component| {
let phase_index = self
.metadata
.phase_index(&component.id().phase)
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_phase",
message: format!("missing phase for component '{}'", component.label()),
})?
.index();
let function = phase_functions
.get(phase_index)
.and_then(|functions| functions.get(component.substance()))
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_gibbs",
message: format!("missing Gibbs closure for '{}'", component.label()),
})?;
let function = std::sync::Arc::clone(function);
Ok(Rc::new(move |temperature: f64| function(temperature)) as GibbsFn)
})
.collect()
}
fn refresh_symbolic(&mut self) -> Result<Vec<Expr>, ReactionExtentError> {
let phase_expressions = self
.thermo_payloads
.iter_mut()
.map(SubsData::calculate_dG0_sym_one_phase)
.collect::<Result<Vec<_>, _>>()?;
self.metadata
.components()
.iter()
.map(|component| {
let phase_index = self
.metadata
.phase_index(&component.id().phase)
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_phase",
message: format!("missing phase for component '{}'", component.label()),
})?
.index();
phase_expressions
.get(phase_index)
.and_then(|expressions| expressions.get(component.substance()))
.cloned()
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_symbolic_gibbs",
message: format!(
"missing symbolic Gibbs expression for '{}'",
component.label()
),
})
})
.collect()
}
}
impl PreparedPhaseEquilibriumTemplate {
pub(crate) fn build_timing(&self) -> EquilibriumTimingReport {
self.timing
}
pub(crate) fn symbolic_problem_reused(&self) -> bool {
self.rst_problem.is_some()
}
pub(crate) fn last_symbolic_parameter_reused(&self) -> bool {
self.last_symbolic_parameter_reused
}
pub(crate) fn solve_at(
&mut self,
conditions: EquilibriumConditions,
seed: LogMolesInitialGuess,
settings: EquilibriumSolverSettings,
timing_mode: EquilibriumTimingMode,
) -> Result<MultiphaseEquilibriumSolution, ReactionExtentError> {
let started = Instant::now();
let mut timing = EquilibriumTimingCollector::new(timing_mode);
if let Some(rst_problem) = self.rst_problem.as_mut() {
rst_problem.set_temperature(conditions.temperature())?;
}
let gibbs = self.refresh_gibbs(conditions.temperature(), &mut timing)?;
let prepared = self
.prepared
.retarget_with_gibbs(conditions, seed.clone(), gibbs)?;
if self.rst_problem.is_some() {
let symbolic = timing.measure(EquilibriumTimingStage::SymbolicConstruction, || {
self.refresh_symbolic()
})?;
if symbolic != self.symbolic_standard_gibbs {
self.symbolic_standard_gibbs = symbolic;
self.rst_problem = Some(prepare_rst_symbolic_problem_from_prepared(
&prepared,
&self.symbolic_standard_gibbs,
)?);
self.last_symbolic_parameter_reused = false;
} else {
self.last_symbolic_parameter_reused = true;
}
} else {
self.last_symbolic_parameter_reused = false;
}
let report = self
.report
.at_conditions(conditions, prepared.problem().gibbs())?;
let mut runner =
PreparedEquilibriumRunner::new(prepared, self.symbolic_standard_gibbs.clone())?;
*runner.configure() = settings.clone();
let outcome = timing.measure(EquilibriumTimingStage::NonlinearSolve, || {
match self.rst_problem.as_ref() {
Some(rst_problem) => runner.solve_from_seed_with_rst(seed, rst_problem),
None => runner.solve_from_seed(seed),
}
})?;
timing.record(
EquilibriumTimingStage::Validation,
outcome.validation_duration,
);
timing.set_total(started.elapsed());
let bundle = PhaseEquilibriumSolutionBundle {
metadata: self.metadata.clone(),
build_report: report,
solution: outcome.solution,
solve_report: outcome.solve_report,
keq_validation_status: outcome.keq_validation_status,
timing: timing.finish(),
};
bundle.into_multiphase_solution()
}
fn refresh_gibbs(
&mut self,
temperature: f64,
timing: &mut EquilibriumTimingCollector,
) -> Result<Vec<GibbsFn>, ReactionExtentError> {
let mut phase_functions = Vec::with_capacity(self.thermo_payloads.len());
for payload in &mut self.thermo_payloads {
timing.measure(EquilibriumTimingStage::ThermochemistryPreparation, || {
payload.extract_all_thermal_coeffs(temperature)
})?;
let functions = timing
.measure(EquilibriumTimingStage::NumericClosureConstruction, || {
payload.calculate_dG0_fun_one_phase()
})?;
phase_functions.push(
functions
.into_iter()
.map(|(substance, function)| {
(
substance,
std::sync::Arc::from(function)
as std::sync::Arc<dyn Fn(f64) -> f64 + Send + Sync>,
)
})
.collect::<HashMap<_, _>>(),
);
}
let mut gibbs = Vec::with_capacity(self.metadata.components().len());
for component in self.metadata.components() {
let phase_index = self
.metadata
.phase_index(&component.id().phase)
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_phase",
message: format!("missing phase for component '{}'", component.label()),
})?
.index();
let function = phase_functions
.get(phase_index)
.and_then(|functions| functions.get(component.substance()))
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_gibbs",
message: format!("missing Gibbs closure for '{}'", component.label()),
})?;
let function = std::sync::Arc::clone(function);
gibbs.push(Rc::new(move |temperature: f64| function(temperature)) as GibbsFn);
}
Ok(gibbs)
}
fn refresh_symbolic(&mut self) -> Result<Vec<Expr>, ReactionExtentError> {
let mut phase_expressions = Vec::with_capacity(self.thermo_payloads.len());
for payload in &mut self.thermo_payloads {
phase_expressions.push(payload.calculate_dG0_sym_one_phase()?);
}
let mut symbolic = Vec::with_capacity(self.metadata.components().len());
for component in self.metadata.components() {
let phase_index = self
.metadata
.phase_index(&component.id().phase)
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_phase",
message: format!("missing phase for component '{}'", component.label()),
})?
.index();
let expression = phase_expressions
.get(phase_index)
.and_then(|expressions| expressions.get(component.substance()))
.ok_or_else(|| ReactionExtentError::InvalidProblem {
field: "temperature_range_symbolic_gibbs",
message: format!(
"missing symbolic Gibbs expression for '{}'",
component.label()
),
})?;
symbolic.push(expression.clone());
}
Ok(symbolic)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PhaseEquilibriumSolutionBundle {
metadata: PhaseEquilibriumMetadata,
build_report: PhaseEquilibriumBuildReport,
solution: EquilibriumSolution,
solve_report: EquilibriumSolveReport,
keq_validation_status: Option<EquilibriumConstantCrossValidationStatus>,
timing: EquilibriumTimingReport,
}
impl PhaseEquilibriumSolutionBundle {
pub fn metadata(&self) -> &PhaseEquilibriumMetadata {
&self.metadata
}
pub fn build_report(&self) -> &PhaseEquilibriumBuildReport {
&self.build_report
}
pub fn lookup_report(&self) -> &ResolvedPhaseSystemReport {
self.build_report.lookup_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 timing_report(&self) -> &EquilibriumTimingReport {
&self.timing
}
pub fn into_multiphase_solution(
self,
) -> Result<MultiphaseEquilibriumSolution, ReactionExtentError> {
let timing_enabled = self.timing_report().enabled();
let started = std::time::Instant::now();
let solution = MultiphaseEquilibriumSolution::from_fixed_active_bundle(self)?;
if timing_enabled {
Ok(solution
.with_timing_stage(EquilibriumTimingStage::Postprocessing, started.elapsed()))
} else {
Ok(solution)
}
}
}
pub fn build_phase_equilibrium_problem(
request: PhaseEquilibriumBuildRequest<'_>,
) -> Result<PhaseEquilibriumProblemBundle, ReactionExtentError> {
build_phase_equilibrium_problem_with_timing(request, EquilibriumTimingMode::Disabled)
}
pub(crate) fn build_phase_equilibrium_problem_with_timing(
request: PhaseEquilibriumBuildRequest<'_>,
timing_mode: EquilibriumTimingMode,
) -> Result<PhaseEquilibriumProblemBundle, ReactionExtentError> {
let mut timing = EquilibriumTimingCollector::new(timing_mode);
let metadata = request.metadata.clone();
let conditions = request.conditions;
let mut phase_data = HashMap::with_capacity(metadata.phases().len());
let mut thermo_payloads = Vec::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()
),
})?;
thermo_payloads.push(payload.clone());
let prepared =
prepare_phase_thermochemistry(payload, request.conditions.temperature(), &mut timing)?;
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 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 = timing.measure(EquilibriumTimingStage::EquationConstruction, || {
EquilibriumProblem::new_with_phase_descriptors(
metadata.components().to_vec(),
initial_moles,
initial_log_moles,
element_composition,
gibbs,
metadata.phases().to_vec(),
conditions,
)
})?;
let report = PhaseEquilibriumBuildReport {
conditions,
lookup_report: metadata.provenance().clone(),
layout_fingerprint: metadata.layout_fingerprint(),
element_labels,
element_totals,
components: component_reports,
};
Ok(PhaseEquilibriumProblemBundle {
problem,
metadata,
report,
symbolic_standard_gibbs,
thermo_payloads,
timing: timing.finish(),
})
}
#[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,
temperature: f64,
timing: &mut EquilibriumTimingCollector,
) -> Result<PreparedPhaseThermochemistry, ReactionExtentError> {
let mut working = resolved_payload.clone();
timing.measure(EquilibriumTimingStage::ThermochemistryPreparation, || {
working.extract_all_thermal_coeffs(temperature)
})?;
let gibbs_functions = timing
.measure(EquilibriumTimingStage::NumericClosureConstruction, || {
working.calculate_dG0_fun_one_phase()
})?;
let symbolic_gibbs_functions = timing
.measure(EquilibriumTimingStage::SymbolicConstruction, || {
working.calculate_dG0_sym_one_phase()
})?;
let (_, compositions, _) = timing
.measure(EquilibriumTimingStage::ThermochemistryPreparation, || {
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,
}
}