use std::fmt;
use std::sync::Arc;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_candidate_selection::{
EquilibriumCandidatePhasePlan, EquilibriumCandidateSelectionReport,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_constant_validation::EquilibriumConstantValidationMode;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_execution::{
EquilibriumExecutionControl, EquilibriumProgressEvent, EquilibriumProgressStage,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_log_moles::{
EquilibriumSolverSettings, Solvers,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_multiphase_domain::{
MultiphaseEquilibriumLayout, MultiphaseInitialComposition,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::{
EquilibriumConditions, LogMolesInitialGuess, TraceSpeciesSeedPolicy,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_solver_policy::{
SolverBackend, SolverCascadeBudget, SolverPolicy,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_temperature_range::{
TemperatureGrid, TemperatureRangeRequest, TemperatureRangeSolution,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_timing::{
EquilibriumTimingMode, EquilibriumTimingReport, EquilibriumTimingStage,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_workflows::InitialPhaseSet;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_workflows::PhaseManager;
use crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_problem::PhaseEquilibriumBuildRequest;
use crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_problem::SupportedPhaseModelPolicy;
use crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_solution::MultiphaseEquilibriumSolution;
use crate::Thermodynamics::User_PhaseOrSolution::{
ResolvedPhaseSystem, ResolvedPhaseSystemReport, SubstanceSystemFactory,
SubstanceSystemFactoryError, SubstanceSystemSpec,
};
use crate::Thermodynamics::phase_layout::PhaseComponentId;
use crate::Thermodynamics::thermo_lib_api::ThermoRepository;
#[derive(Debug, Clone)]
pub enum PhaseEquilibriumSolveMode {
FixedDeclaredPhases,
BoundedPhaseControl(PhaseControlPolicy),
}
impl Default for PhaseEquilibriumSolveMode {
fn default() -> Self {
Self::FixedDeclaredPhases
}
}
impl PhaseEquilibriumSolveMode {
pub fn fixed_declared_phases() -> Self {
Self::FixedDeclaredPhases
}
pub fn bounded_phase_control(policy: PhaseControlPolicy) -> Self {
Self::BoundedPhaseControl(policy)
}
}
#[derive(Clone)]
pub struct EquilibriumSolveOptions {
settings: EquilibriumSolverSettings,
timing_mode: EquilibriumTimingMode,
execution_control: Option<EquilibriumExecutionControl>,
}
impl fmt::Debug for EquilibriumSolveOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EquilibriumSolveOptions")
.finish_non_exhaustive()
}
}
impl Default for EquilibriumSolveOptions {
fn default() -> Self {
Self {
settings: EquilibriumSolverSettings::default(),
timing_mode: EquilibriumTimingMode::default(),
execution_control: None,
}
}
}
impl EquilibriumSolveOptions {
pub fn new() -> Self {
Self::default()
}
#[cfg(test)]
pub(crate) fn from_settings(
settings: EquilibriumSolverSettings,
) -> Result<Self, ReactionExtentError> {
settings.validate()?;
Ok(Self {
settings,
timing_mode: EquilibriumTimingMode::default(),
execution_control: None,
})
}
pub(crate) fn into_settings(self) -> EquilibriumSolverSettings {
let mut settings = self.settings;
settings.execution_control = self.execution_control;
settings
}
pub fn with_execution_control(mut self, control: EquilibriumExecutionControl) -> Self {
self.execution_control = Some(control);
self
}
pub fn execution_control(&self) -> Option<&EquilibriumExecutionControl> {
self.execution_control.as_ref()
}
pub fn with_production_cascade(mut self) -> Self {
self.settings.solver_policy = Some(SolverPolicy::production_default(self.settings.solver));
self
}
pub fn with_max_iterations(mut self, max_iter: usize) -> Result<Self, ReactionExtentError> {
self.settings.solver_params.max_iter = max_iter;
self.settings.validate()?;
Ok(self)
}
pub fn with_solver_budget(
mut self,
budget: SolverCascadeBudget,
) -> Result<Self, ReactionExtentError> {
self.settings.solver_budget = Some(budget);
self.settings.validate()?;
Ok(self)
}
pub fn with_tolerance(mut self, tolerance: f64) -> Result<Self, ReactionExtentError> {
self.settings.solver_params.tol = tolerance;
self.settings.validate()?;
Ok(self)
}
pub fn with_scaling(mut self, enabled: bool) -> Self {
self.settings.scaling_flag = enabled;
self
}
pub fn with_solver_backend(mut self, solver: Solvers) -> Self {
self.settings.solver = solver;
self.settings.solver_policy = None;
self
}
pub fn with_solver_policy(mut self, policy: SolverPolicy) -> Result<Self, ReactionExtentError> {
self.settings.solver_policy = Some(policy);
self.settings.validate()?;
Ok(self)
}
pub fn with_trace_seed_policy(mut self, policy: TraceSpeciesSeedPolicy) -> Self {
self.settings.trace_seed_policy = policy;
self
}
pub fn trace_seed_policy(&self) -> TraceSpeciesSeedPolicy {
self.settings.trace_seed_policy
}
pub fn with_keq_validation_mode(mut self, mode: EquilibriumConstantValidationMode) -> Self {
self.settings.keq_validation_mode = mode;
self
}
pub fn with_timing_mode(mut self, mode: EquilibriumTimingMode) -> Self {
self.timing_mode = mode;
self
}
pub fn timing_mode(&self) -> EquilibriumTimingMode {
self.timing_mode
}
pub(crate) fn prepares_rst_backend(&self) -> bool {
self.settings
.solver_policy
.as_ref()
.map(|policy| {
policy
.ordered_backends()
.iter()
.any(|backend| matches!(backend, SolverBackend::RustedSciThe(_)))
})
.unwrap_or(true)
}
}
#[derive(Clone)]
pub struct PhaseControlPolicy {
phase_manager: PhaseManager,
}
impl fmt::Debug for PhaseControlPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PhaseControlPolicy").finish_non_exhaustive()
}
}
impl Default for PhaseControlPolicy {
fn default() -> Self {
Self {
phase_manager: PhaseManager::default(),
}
}
}
impl PhaseControlPolicy {
pub(crate) fn new(phase_manager: PhaseManager) -> Result<Self, ReactionExtentError> {
phase_manager.validate()?;
Ok(Self { phase_manager })
}
pub fn with_explicit_hysteresis(
phase_eps: f64,
dg_create: f64,
dg_keep: f64,
) -> Result<Self, ReactionExtentError> {
let phase_manager = PhaseManager::new(phase_eps, dg_create, dg_keep);
Self::new(phase_manager)
}
pub fn with_temperature_scaled_hysteresis(
phase_eps: f64,
create_rt_factor: f64,
keep_rt_factor: f64,
) -> Result<Self, ReactionExtentError> {
let phase_manager = PhaseManager::with_temperature_scaled_hysteresis(
phase_eps,
create_rt_factor,
keep_rt_factor,
);
Self::new(phase_manager)
}
pub(crate) fn into_phase_manager(self) -> PhaseManager {
self.phase_manager
}
pub fn with_phase_epsilon(mut self, phase_eps: f64) -> Result<Self, ReactionExtentError> {
self.phase_manager.phase_eps = phase_eps;
self.phase_manager.validate()?;
Ok(self)
}
pub fn with_max_phase_iterations(
mut self,
iterations: usize,
) -> Result<Self, ReactionExtentError> {
self.phase_manager.max_phase_iterations = iterations;
self.phase_manager.validate()?;
Ok(self)
}
pub fn with_initial_phase_set(
mut self,
initial_phase_set: InitialPhaseSet,
) -> Result<Self, ReactionExtentError> {
self.phase_manager.initial_phase_set = initial_phase_set;
self.phase_manager.validate()?;
Ok(self)
}
}
#[derive(Debug)]
pub enum PhaseEquilibriumPipelineError {
Resolve(SubstanceSystemFactoryError),
Solve(ReactionExtentError),
}
impl fmt::Display for PhaseEquilibriumPipelineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Resolve(error) => write!(f, "phase-system resolution failed: {error}"),
Self::Solve(error) => write!(f, "phase-equilibrium solve failed: {error}"),
}
}
}
impl std::error::Error for PhaseEquilibriumPipelineError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Resolve(error) => Some(error),
Self::Solve(error) => Some(error),
}
}
}
impl From<SubstanceSystemFactoryError> for PhaseEquilibriumPipelineError {
fn from(value: SubstanceSystemFactoryError) -> Self {
Self::Resolve(value)
}
}
impl From<ReactionExtentError> for PhaseEquilibriumPipelineError {
fn from(value: ReactionExtentError) -> Self {
Self::Solve(value)
}
}
#[derive(Debug, Clone)]
enum PipelineInitialComposition {
Dense(Vec<f64>),
Sparse(Vec<(PhaseComponentId, f64)>),
}
#[derive(Clone)]
pub struct PhaseEquilibriumPipelineRequest {
spec: SubstanceSystemSpec,
initial_composition: PipelineInitialComposition,
conditions: EquilibriumConditions,
model_policy: SupportedPhaseModelPolicy,
solve_options: EquilibriumSolveOptions,
solve_mode: PhaseEquilibriumSolveMode,
multi_start_seeds: Vec<LogMolesInitialGuess>,
repository: Option<Arc<ThermoRepository>>,
}
impl PhaseEquilibriumPipelineRequest {
pub fn new(
spec: SubstanceSystemSpec,
initial_moles: Vec<f64>,
conditions: EquilibriumConditions,
) -> Self {
Self {
spec,
initial_composition: PipelineInitialComposition::Dense(initial_moles),
conditions,
model_policy: SupportedPhaseModelPolicy::default(),
solve_options: EquilibriumSolveOptions::default(),
solve_mode: PhaseEquilibriumSolveMode::fixed_declared_phases(),
multi_start_seeds: Vec::new(),
repository: None,
}
}
pub fn from_candidate_selection(
repository: Arc<ThermoRepository>,
selection: &EquilibriumCandidateSelectionReport,
phase_plan: &EquilibriumCandidatePhasePlan,
initial_moles: Vec<f64>,
conditions: EquilibriumConditions,
) -> Result<Self, SubstanceSystemFactoryError> {
let spec = phase_plan.build_spec(selection)?;
Ok(Self::new(spec, initial_moles, conditions).with_repository(repository))
}
pub fn new_with_sparse_initial_composition(
spec: SubstanceSystemSpec,
entries: Vec<(PhaseComponentId, f64)>,
conditions: EquilibriumConditions,
) -> Self {
Self {
spec,
initial_composition: PipelineInitialComposition::Sparse(entries),
conditions,
model_policy: SupportedPhaseModelPolicy::default(),
solve_options: EquilibriumSolveOptions::default(),
solve_mode: PhaseEquilibriumSolveMode::fixed_declared_phases(),
multi_start_seeds: Vec::new(),
repository: None,
}
}
pub fn with_repository(mut self, repository: Arc<ThermoRepository>) -> Self {
self.repository = Some(repository);
self
}
pub fn with_trace_seed_policy(mut self, policy: TraceSpeciesSeedPolicy) -> Self {
self.solve_options = self.solve_options.with_trace_seed_policy(policy);
self
}
pub fn with_model_policy(mut self, policy: SupportedPhaseModelPolicy) -> Self {
self.model_policy = policy;
self
}
pub fn with_phase_control_policy(mut self, policy: PhaseControlPolicy) -> Self {
self.solve_mode = PhaseEquilibriumSolveMode::BoundedPhaseControl(policy);
self
}
pub fn with_fixed_declared_phases(mut self) -> Self {
self.solve_mode = PhaseEquilibriumSolveMode::fixed_declared_phases();
self
}
pub fn with_solve_options(mut self, options: EquilibriumSolveOptions) -> Self {
self.solve_options = options;
self
}
pub fn solve_options(&self) -> &EquilibriumSolveOptions {
&self.solve_options
}
pub fn with_multi_start_seeds(mut self, seeds: Vec<LogMolesInitialGuess>) -> Self {
self.multi_start_seeds = seeds;
self
}
#[deprecated(note = "use with_fixed_declared_phases() or with_phase_control_policy() instead")]
pub fn with_solve_mode(mut self, mode: PhaseEquilibriumSolveMode) -> Self {
self.solve_mode = mode;
self
}
pub fn resolve(self) -> Result<ResolvedPhaseSystem, PhaseEquilibriumPipelineError> {
let Self {
spec, repository, ..
} = self;
let resolved = match repository {
Some(repository) => {
SubstanceSystemFactory::resolve_phase_system_with_repository(spec, repository)?
}
None => SubstanceSystemFactory::resolve_phase_system(spec)?,
};
Ok(resolved)
}
pub fn solve(self) -> Result<ResolvedPhaseEquilibriumOutcome, PhaseEquilibriumPipelineError> {
let started = std::time::Instant::now();
let Self {
spec,
initial_composition,
conditions,
model_policy,
solve_options,
solve_mode,
multi_start_seeds,
repository,
} = self;
let execution_control = solve_options.execution_control().cloned();
if let Some(control) = &execution_control {
control.check_cancelled()?;
}
let lookup_started = std::time::Instant::now();
let resolved = match repository {
Some(repository) => {
SubstanceSystemFactory::resolve_phase_system_with_repository(spec, repository)?
}
None => SubstanceSystemFactory::resolve_phase_system(spec)?,
};
let layout = crate::Thermodynamics::ChemEquilibrium::equilibrium_multiphase_domain::MultiphaseEquilibriumLayout::new(
resolved.phase_specs().to_vec(),
)?;
if let Some(control) = &execution_control {
control.report(EquilibriumProgressEvent::new(
EquilibriumProgressStage::RepositoryLookup,
None,
None,
Some(conditions.temperature()),
));
control.check_cancelled()?;
control.report(EquilibriumProgressEvent::new(
EquilibriumProgressStage::FormulationPreparation,
None,
None,
Some(conditions.temperature()),
));
}
let initial_composition = match initial_composition {
PipelineInitialComposition::Dense(initial_moles) => {
MultiphaseInitialComposition::from_dense(&layout, initial_moles)?
}
PipelineInitialComposition::Sparse(entries) => {
MultiphaseInitialComposition::from_sparse(&layout, entries)?
}
};
let lookup_elapsed = lookup_started.elapsed();
let request =
ResolvedPhaseEquilibriumRequest::new(&resolved, conditions, initial_composition)
.with_model_policy(model_policy)
.with_solve_options(solve_options)
.with_multi_start_seeds(multi_start_seeds)
.with_fixed_declared_phases();
let request = match solve_mode {
PhaseEquilibriumSolveMode::FixedDeclaredPhases => request,
PhaseEquilibriumSolveMode::BoundedPhaseControl(policy) => {
request.with_phase_control_policy(policy)
}
};
let solution = solve_resolved_pt(request)?
.with_timing_stage(EquilibriumTimingStage::RepositoryLookup, lookup_elapsed)
.with_timing_total(started.elapsed());
Ok(ResolvedPhaseEquilibriumOutcome { resolved, solution })
}
pub fn solve_temperature_range(
self,
temperatures: TemperatureGrid,
) -> Result<TemperatureRangeSolution, PhaseEquilibriumPipelineError> {
let Self {
spec,
initial_composition,
conditions,
model_policy,
solve_options,
solve_mode,
multi_start_seeds,
repository,
} = self;
if let Some(control) = solve_options.execution_control() {
control.check_cancelled()?;
}
if !multi_start_seeds.is_empty() {
return Err(PhaseEquilibriumPipelineError::Solve(
ReactionExtentError::InvalidProblem {
field: "multi_start_temperature_range",
message: "explicit multi-start is supported only for one fixed-P,T solve"
.to_string(),
},
));
}
let phase_control_policy = match solve_mode {
PhaseEquilibriumSolveMode::FixedDeclaredPhases => None,
PhaseEquilibriumSolveMode::BoundedPhaseControl(policy) => Some(policy),
};
let resolved = match repository {
Some(repository) => {
SubstanceSystemFactory::resolve_phase_system_with_repository(spec, repository)?
}
None => SubstanceSystemFactory::resolve_phase_system(spec)?,
};
let layout = MultiphaseEquilibriumLayout::new(resolved.phase_specs().to_vec())?;
let initial_composition = match initial_composition {
PipelineInitialComposition::Dense(initial_moles) => {
MultiphaseInitialComposition::from_dense(&layout, initial_moles)?
}
PipelineInitialComposition::Sparse(entries) => {
MultiphaseInitialComposition::from_sparse(&layout, entries)?
}
};
let request = TemperatureRangeRequest::new(
&resolved,
initial_composition,
conditions.pressure(),
conditions.reference_pressure(),
temperatures,
)
.map(|request| {
request
.with_model_policy(model_policy)
.with_solve_options(solve_options)
})?;
let request = match phase_control_policy {
Some(policy) => request.with_phase_control_policy(policy),
None => request,
};
request.solve().map_err(PhaseEquilibriumPipelineError::from)
}
}
#[derive(Debug, Clone)]
pub struct ResolvedPhaseEquilibriumOutcome {
resolved: ResolvedPhaseSystem,
solution: MultiphaseEquilibriumSolution,
}
impl ResolvedPhaseEquilibriumOutcome {
pub fn resolved(&self) -> &ResolvedPhaseSystem {
&self.resolved
}
pub fn lookup_report(&self) -> &ResolvedPhaseSystemReport {
self.resolved.report()
}
pub fn solution(&self) -> &MultiphaseEquilibriumSolution {
&self.solution
}
pub fn timing_report(&self) -> &EquilibriumTimingReport {
self.solution.timing_report()
}
pub fn into_solution(self) -> MultiphaseEquilibriumSolution {
self.solution
}
}
#[derive(Clone)]
pub struct ResolvedPhaseEquilibriumRequest<'a> {
resolved: &'a ResolvedPhaseSystem,
conditions: EquilibriumConditions,
initial_composition: MultiphaseInitialComposition,
model_policy: SupportedPhaseModelPolicy,
solve_options: EquilibriumSolveOptions,
solve_mode: PhaseEquilibriumSolveMode,
multi_start_seeds: Vec<LogMolesInitialGuess>,
}
impl<'a> ResolvedPhaseEquilibriumRequest<'a> {
pub fn new(
resolved: &'a ResolvedPhaseSystem,
conditions: EquilibriumConditions,
initial_composition: MultiphaseInitialComposition,
) -> Self {
Self {
resolved,
conditions,
initial_composition,
model_policy: SupportedPhaseModelPolicy::default(),
solve_options: EquilibriumSolveOptions::default(),
solve_mode: PhaseEquilibriumSolveMode::fixed_declared_phases(),
multi_start_seeds: Vec::new(),
}
}
pub fn with_trace_seed_policy(mut self, policy: TraceSpeciesSeedPolicy) -> Self {
self.solve_options = self.solve_options.with_trace_seed_policy(policy);
self
}
pub fn with_model_policy(mut self, policy: SupportedPhaseModelPolicy) -> Self {
self.model_policy = policy;
self
}
pub fn with_phase_control_policy(mut self, policy: PhaseControlPolicy) -> Self {
self.solve_mode = PhaseEquilibriumSolveMode::BoundedPhaseControl(policy);
self
}
pub fn with_fixed_declared_phases(mut self) -> Self {
self.solve_mode = PhaseEquilibriumSolveMode::fixed_declared_phases();
self
}
pub fn with_solve_options(mut self, options: EquilibriumSolveOptions) -> Self {
self.solve_options = options;
self
}
pub fn with_multi_start_seeds(mut self, seeds: Vec<LogMolesInitialGuess>) -> Self {
self.multi_start_seeds = seeds;
self
}
#[deprecated(note = "use with_fixed_declared_phases() or with_phase_control_policy() instead")]
pub fn with_solve_mode(mut self, mode: PhaseEquilibriumSolveMode) -> Self {
self.solve_mode = mode;
self
}
pub fn resolved(&self) -> &'a ResolvedPhaseSystem {
self.resolved
}
pub fn conditions(&self) -> EquilibriumConditions {
self.conditions
}
pub fn initial_composition(&self) -> &MultiphaseInitialComposition {
&self.initial_composition
}
pub fn solve_options(&self) -> &EquilibriumSolveOptions {
&self.solve_options
}
}
pub fn solve_resolved_pt(
request: ResolvedPhaseEquilibriumRequest<'_>,
) -> Result<MultiphaseEquilibriumSolution, ReactionExtentError> {
let timing_mode = request.solve_options.timing_mode();
let started = std::time::Instant::now();
let execution_control = request.solve_options.execution_control().cloned();
if let Some(control) = &execution_control {
control.check_cancelled()?;
control.report(EquilibriumProgressEvent::new(
EquilibriumProgressStage::PointStarted,
Some(0),
Some(1),
Some(request.conditions.temperature()),
));
}
let solved = match request.solve_mode {
PhaseEquilibriumSolveMode::FixedDeclaredPhases => {
let trace_seed_policy = request.solve_options.trace_seed_policy();
let bundle = crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_problem::
build_phase_equilibrium_problem_with_timing(PhaseEquilibriumBuildRequest::new(
request.resolved,
request.conditions,
request.initial_composition,
trace_seed_policy,
request.model_policy,
)?, timing_mode)?;
let settings = request.solve_options.into_settings();
let solved = if request.multi_start_seeds.is_empty() {
bundle.solve_with(|configured| *configured = settings)
} else {
bundle.solve_with_initial_guesses(request.multi_start_seeds, |configured| {
*configured = settings
})
};
solved
.and_then(|bundle| bundle.into_multiphase_solution())
.map(|solution| solution.with_timing_total(started.elapsed()))
}
PhaseEquilibriumSolveMode::BoundedPhaseControl(phase_control_policy) => {
if !request.multi_start_seeds.is_empty() {
return Err(ReactionExtentError::InvalidProblem {
field: "multi_start_phase_control",
message: "explicit multi-start is supported only for fixed declared phases"
.to_string(),
});
}
let trace_seed_policy = request.solve_options.trace_seed_policy();
let bundle = crate::Thermodynamics::ChemEquilibrium::phase_equilibrium_problem::
build_phase_equilibrium_problem_with_timing(PhaseEquilibriumBuildRequest::new(
request.resolved,
request.conditions,
request.initial_composition,
trace_seed_policy,
request.model_policy,
)?, timing_mode)?;
let settings = request.solve_options.into_settings();
bundle
.solve_with_bounded_phase_control(
|configured| *configured = settings,
|configured| *configured = phase_control_policy.into_phase_manager(),
)
.map(|solution| solution.with_timing_total(started.elapsed()))
}
};
if let Some(control) = &execution_control {
control.check_cancelled()?;
control.report(EquilibriumProgressEvent::new(
EquilibriumProgressStage::PointAccepted,
Some(0),
Some(1),
Some(request.conditions.temperature()),
));
}
solved
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Thermodynamics::ChemEquilibrium::prelude::{
LegacyEquilibriumSolver, LogMolesInitialGuess, RustedSciTheSolver, SolverBackend,
SolverPolicy,
};
use crate::Thermodynamics::User_PhaseOrSolution::{
SubstanceSystemSpecBuilder, SubstancesContainer,
};
use crate::Thermodynamics::phase_layout::{PhaseComponentId, PhaseId};
#[test]
fn solve_options_round_trip_explicit_cascade_budget() {
let budget = SolverCascadeBudget::new(2, 10, 1);
let options = EquilibriumSolveOptions::new()
.with_solver_budget(budget)
.expect("positive cascade budget must validate");
assert_eq!(options.into_settings().solver_budget, Some(budget));
let error = EquilibriumSolveOptions::new()
.with_solver_budget(SolverCascadeBudget::new(0, 10, 10))
.expect_err("zero attempt budget must be rejected");
assert!(matches!(
error,
ReactionExtentError::InvalidProblem {
field: "solver_budget",
..
}
));
}
#[test]
fn cancelled_pipeline_stops_before_repository_resolution_for_point_and_range() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(false)
.build()
.unwrap();
let conditions = EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap();
let control =
crate::Thermodynamics::ChemEquilibrium::prelude::EquilibriumExecutionControl::new();
control.request_cancel();
let options = EquilibriumSolveOptions::new().with_execution_control(control);
let point_error = PhaseEquilibriumPipelineRequest::new(spec.clone(), vec![1.0], conditions)
.with_solve_options(options.clone())
.solve()
.expect_err("cancelled point must not enter repository lookup");
assert!(matches!(
point_error,
PhaseEquilibriumPipelineError::Solve(ReactionExtentError::Cancelled)
));
let range_error = PhaseEquilibriumPipelineRequest::new(spec, vec![1.0], conditions)
.with_solve_options(options)
.solve_temperature_range(TemperatureGrid::new(vec![500.0, 600.0]).unwrap())
.expect_err("cancelled range must not enter repository lookup");
assert!(matches!(
range_error,
PhaseEquilibriumPipelineError::Solve(ReactionExtentError::Cancelled)
));
}
#[test]
fn production_prelude_exposes_complete_backend_policy_contract() {
let rst = SolverPolicy::Single(SolverBackend::RustedSciThe(
RustedSciTheSolver::MinpackLevenbergMarquardt,
));
let legacy = SolverPolicy::Single(SolverBackend::Legacy(LegacyEquilibriumSolver::NR));
EquilibriumSolveOptions::new()
.with_solver_policy(rst)
.expect("RST policy exported by the production prelude must validate");
EquilibriumSolveOptions::new()
.with_solver_policy(legacy)
.expect("legacy fallback policy exported by the production prelude must validate");
}
#[test]
fn enabled_timing_is_published_on_pipeline_solution() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(false)
.build()
.unwrap();
let options =
EquilibriumSolveOptions::new().with_timing_mode(EquilibriumTimingMode::Enabled);
let outcome = PhaseEquilibriumPipelineRequest::new(
spec,
vec![0.79, 0.21],
EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap(),
)
.with_solve_options(options)
.solve()
.unwrap();
let timing = outcome.timing_report();
assert!(timing.enabled());
assert!(timing.total() > std::time::Duration::ZERO);
assert!(timing.thermochemistry_preparation() > std::time::Duration::ZERO);
assert!(timing.numeric_closure_construction() > std::time::Duration::ZERO);
assert!(timing.symbolic_construction() > std::time::Duration::ZERO);
assert!(timing.equation_construction() > std::time::Duration::ZERO);
assert!(timing.nonlinear_solve() > std::time::Duration::ZERO);
assert!(timing.validation() >= std::time::Duration::ZERO);
assert!(timing.postprocessing() > std::time::Duration::ZERO);
}
#[test]
fn resolve_and_solve_pipeline_round_trips_one_gas_phase_with_two_species() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(true)
.build()
.unwrap();
let request = PhaseEquilibriumPipelineRequest::new(
spec,
vec![0.79, 0.21],
EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap(),
);
let outcome = request.solve().unwrap();
assert_eq!(outcome.resolved().phase_specs().len(), 1);
assert_eq!(outcome.solution().component_moles().len(), 2);
assert_eq!(outcome.solution().build_report().components().len(), 2);
}
#[test]
fn pipeline_and_resolved_pt_facades_publish_the_same_fixed_solution() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(false)
.build()
.unwrap();
let conditions = EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap();
let options = EquilibriumSolveOptions::new()
.with_solver_policy(SolverPolicy::Single(SolverBackend::Legacy(Solvers::NR)))
.expect("single legacy backend policy must validate");
let pipeline = PhaseEquilibriumPipelineRequest::new(
spec,
vec![0.79, 0.21],
conditions,
)
.with_solve_options(options.clone());
let resolved = pipeline
.clone()
.resolve()
.expect("pipeline resolution must succeed");
let layout = MultiphaseEquilibriumLayout::new(resolved.phase_specs().to_vec()).unwrap();
let composition = MultiphaseInitialComposition::from_dense(&layout, vec![0.79, 0.21])
.expect("direct facade composition must match the resolved layout");
let pipeline_solution = pipeline
.solve()
.expect("pipeline facade must solve")
.into_solution();
let direct_solution = solve_resolved_pt(
ResolvedPhaseEquilibriumRequest::new(&resolved, conditions, composition)
.with_solve_options(options),
)
.expect("resolved facade must solve");
assert_eq!(
pipeline_solution.component_moles().len(),
direct_solution.component_moles().len()
);
for (pipeline_moles, direct_moles) in pipeline_solution
.component_moles()
.iter()
.zip(direct_solution.component_moles())
{
assert!((pipeline_moles - direct_moles).abs() <= 1e-8);
}
assert!(
(pipeline_solution
.accepted_solution()
.validation()
.residual_l2_norm
- direct_solution
.accepted_solution()
.validation()
.residual_l2_norm)
.abs()
<= 1e-12
);
assert_eq!(
pipeline_solution.build_report().layout_fingerprint(),
direct_solution.build_report().layout_fingerprint()
);
}
#[test]
fn fixed_pipeline_publishes_explicit_multi_start_evidence() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(false)
.build()
.unwrap();
let conditions = EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap();
let seed = LogMolesInitialGuess::from_initial_moles(&[0.79, 0.21]).unwrap();
let outcome = PhaseEquilibriumPipelineRequest::new(spec, vec![0.79, 0.21], conditions)
.with_multi_start_seeds(vec![seed.clone(), seed])
.solve()
.unwrap();
let report = outcome
.solution()
.multi_start_report()
.expect("fixed multi-start must publish its comparison report");
assert_eq!(report.attempts.len(), 2);
assert!(report.attempts.iter().all(|attempt| attempt.accepted));
assert!(report.selected_start < report.attempts.len());
}
#[test]
fn multi_start_is_rejected_for_bounded_phase_control() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(false)
.build()
.unwrap();
let seed = LogMolesInitialGuess::from_initial_moles(&[0.79, 0.21]).unwrap();
let error = PhaseEquilibriumPipelineRequest::new(
spec,
vec![0.79, 0.21],
EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap(),
)
.with_phase_control_policy(PhaseControlPolicy::default())
.with_multi_start_seeds(vec![seed])
.solve()
.unwrap_err();
assert!(matches!(
error,
PhaseEquilibriumPipelineError::Solve(ReactionExtentError::InvalidProblem {
field: "multi_start_phase_control",
..
})
));
}
#[test]
fn multi_start_rejects_a_seed_with_the_wrong_component_dimension() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(false)
.build()
.unwrap();
let bad_seed = LogMolesInitialGuess::from_initial_moles(&[1.0]).unwrap();
let error = PhaseEquilibriumPipelineRequest::new(
spec,
vec![0.79, 0.21],
EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap(),
)
.with_multi_start_seeds(vec![bad_seed])
.solve()
.unwrap_err();
assert!(matches!(
error,
PhaseEquilibriumPipelineError::Solve(ReactionExtentError::InvalidProblem {
field: "multi_start_seed_dimension",
..
})
));
}
#[test]
fn sparse_pipeline_inventory_matches_dense_ordered_inventory() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(true)
.build()
.unwrap();
let conditions = EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap();
let dense =
PhaseEquilibriumPipelineRequest::new(spec.clone(), vec![0.79, 0.21], conditions)
.solve()
.unwrap();
let sparse = PhaseEquilibriumPipelineRequest::new_with_sparse_initial_composition(
spec,
vec![
(PhaseComponentId::new(PhaseId::new(None), "N2"), 0.79),
(PhaseComponentId::new(PhaseId::new(None), "O2"), 0.21),
],
conditions,
)
.solve()
.unwrap();
assert_eq!(
dense.solution().component_moles(),
sparse.solution().component_moles()
);
assert_eq!(
dense.solution().layout_fingerprint(),
sparse.solution().layout_fingerprint()
);
}
#[test]
fn sparse_pipeline_inventory_rejects_duplicate_phase_components() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(true)
.build()
.unwrap();
let duplicate = PhaseComponentId::new(PhaseId::new(None), "N2");
let error = PhaseEquilibriumPipelineRequest::new_with_sparse_initial_composition(
spec,
vec![(duplicate.clone(), 0.79), (duplicate, 0.21)],
EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap(),
)
.solve()
.unwrap_err();
assert!(matches!(
error,
PhaseEquilibriumPipelineError::Solve(ReactionExtentError::InvalidProblem {
field: "initial_composition",
..
})
));
}
#[test]
fn phase_control_policy_rejects_an_invalid_hysteresis_order() {
assert!(PhaseControlPolicy::with_explicit_hysteresis(1e-6, 1.0, 0.5).is_err());
}
#[test]
fn phase_control_policy_rejects_invalid_scalar_limits_at_construction() {
let mut manager = PhaseManager::default();
manager.phase_eps = 0.0;
assert!(PhaseControlPolicy::new(manager).is_err());
let mut manager = PhaseManager::default();
manager.max_phase_iterations = 0;
assert!(PhaseControlPolicy::new(manager).is_err());
}
#[test]
fn typed_policy_builders_reject_invalid_limits_before_solving() {
assert!(
EquilibriumSolveOptions::default()
.with_max_iterations(0)
.is_err()
);
assert!(
EquilibriumSolveOptions::default()
.with_tolerance(0.0)
.is_err()
);
assert!(
PhaseControlPolicy::default()
.with_phase_epsilon(0.0)
.is_err()
);
assert!(
PhaseControlPolicy::default()
.with_max_phase_iterations(0)
.is_err()
);
}
#[test]
fn trace_seed_policy_has_one_typed_source_of_truth() {
let policy = TraceSpeciesSeedPolicy::RelativeToLargestInitialMole {
fraction: 1e-8,
minimum_floor: 1e-16,
};
let options = EquilibriumSolveOptions::default().with_trace_seed_policy(policy);
assert_eq!(options.trace_seed_policy(), policy);
}
#[test]
fn phase_control_policy_rejects_indices_after_resolved_phase_count_is_known() {
let manager = PhaseManager {
initial_phase_set: crate::Thermodynamics::ChemEquilibrium::equilibrium_workflows::
InitialPhaseSet::Explicit {
active: vec![crate::Thermodynamics::ChemEquilibrium::equilibrium_ids::PhaseIndex::new(2, 3).unwrap()],
excluded: Vec::new(),
},
..PhaseManager::default()
};
PhaseControlPolicy::new(manager)
.unwrap()
.into_phase_manager()
.validate_for_phase_count(2)
.expect_err("resolved phase bounds must be checked before active-set construction");
}
#[test]
fn selecting_a_preferred_backend_clears_an_explicit_policy() {
let settings = EquilibriumSolverSettings {
solver_policy: Some(SolverPolicy::Single(SolverBackend::Legacy(Solvers::NR))),
..EquilibriumSolverSettings::default()
};
let options = EquilibriumSolveOptions::from_settings(settings)
.unwrap()
.with_solver_backend(Solvers::TR);
let settings = options.into_settings();
assert_eq!(settings.solver, Solvers::TR);
assert!(settings.solver_policy.is_none());
}
#[test]
fn pipeline_outcome_exposes_the_original_lookup_provenance() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(true)
.build()
.unwrap();
let request = PhaseEquilibriumPipelineRequest::new(
spec,
vec![0.79, 0.21],
EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap(),
);
let outcome = request.solve().unwrap();
assert_eq!(outcome.lookup_report(), outcome.resolved().report());
assert_eq!(
outcome.solution().build_report().lookup_report(),
outcome.lookup_report()
);
}
#[test]
fn solve_mode_defaults_to_the_explicit_fixed_declared_phase_path() {
assert!(matches!(
PhaseEquilibriumSolveMode::default(),
PhaseEquilibriumSolveMode::FixedDeclaredPhases
));
assert!(matches!(
PhaseEquilibriumSolveMode::fixed_declared_phases(),
PhaseEquilibriumSolveMode::FixedDeclaredPhases
));
}
#[test]
fn request_can_explicitly_reset_to_the_fixed_declared_phase_path() {
let spec = SubstanceSystemSpecBuilder::new(SubstancesContainer::SinglePhase(vec![
"N2".to_string(),
"O2".to_string(),
]))
.with_library_priorities(vec!["NASA_gas".to_string()])
.with_search_in_nist(true)
.build()
.unwrap();
let request = PhaseEquilibriumPipelineRequest::new(
spec,
vec![0.79, 0.21],
EquilibriumConditions::new(500.0, 101_325.0, 101_325.0).unwrap(),
)
.with_phase_control_policy(PhaseControlPolicy::default())
.with_fixed_declared_phases();
let outcome = request.solve().unwrap();
assert_eq!(outcome.resolved().phase_specs().len(), 1);
assert_eq!(outcome.solution().component_moles().len(), 2);
}
}