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_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, TraceSpeciesSeedPolicy,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_solver_policy::{
SolverBackend, 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,
}
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(),
}
}
}
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(),
})
}
pub(crate) fn into_settings(self) -> EquilibriumSolverSettings {
self.settings
}
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_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,
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(),
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(),
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
}
#[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,
repository,
} = self;
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(),
)?;
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_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,
repository,
} = self;
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,
}
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(),
}
}
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
}
#[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();
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();
bundle
.solve_with(|configured| *configured = settings)
.and_then(|bundle| bundle.into_multiphase_solution())
.map(|solution| solution.with_timing_total(started.elapsed()))
}
PhaseEquilibriumSolveMode::BoundedPhaseControl(phase_control_policy) => {
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()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Thermodynamics::ChemEquilibrium::prelude::{
LegacyEquilibriumSolver, RustedSciTheSolver, SolverBackend, SolverPolicy,
};
use crate::Thermodynamics::User_PhaseOrSolution::{
SubstanceSystemSpecBuilder, SubstancesContainer,
};
use crate::Thermodynamics::phase_layout::{PhaseComponentId, PhaseId};
#[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 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);
}
}