use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use crate::Thermodynamics::User_PhaseOrSolution::{PhaseModel, PhaseSpec};
use crate::Thermodynamics::phase_layout::{PhaseComponentId, SystemLayout};
use crate::Thermodynamics::physical_state::PhysicalState;
use nalgebra::DMatrix;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiphaseEquilibriumLayout {
phase_specs: Vec<PhaseSpec>,
system_layout: SystemLayout,
fingerprint: u64,
}
impl MultiphaseEquilibriumLayout {
pub fn new(mut phase_specs: Vec<PhaseSpec>) -> Result<Self, ReactionExtentError> {
if phase_specs.is_empty() {
return Err(ReactionExtentError::InvalidProblem {
field: "phase_specs",
message: "a multiphase equilibrium problem requires at least one phase".into(),
});
}
phase_specs.sort_by(|left, right| left.id().cmp(right.id()));
let mut phase_ids = HashSet::with_capacity(phase_specs.len());
let mut gas_phase_count = 0usize;
for phase in &phase_specs {
let phase_id = phase.id().clone();
if !phase_ids.insert(phase_id.clone()) {
return Err(ReactionExtentError::InvalidProblem {
field: "phase_specs",
message: format!("duplicate phase identity {:?}", phase_id.as_option()),
});
}
match (phase.physical_state(), phase.model()) {
(PhysicalState::Gas, PhaseModel::IdealGas) => gas_phase_count += 1,
(_, PhaseModel::PureCondensed) if phase.components().len() == 1 => {}
(_, PhaseModel::PureCondensed) => {
return Err(ReactionExtentError::ValidationNotApplicable {
path: "multiphase equilibrium layout",
message: format!(
"pure condensed phase {:?} must contain exactly one component",
phase_id.as_option()
),
});
}
(state, model) => {
return Err(ReactionExtentError::ValidationNotApplicable {
path: "multiphase equilibrium layout",
message: format!(
"activity model {:?} is not supported for physical state {:?}",
model, state
),
});
}
}
}
if gas_phase_count > 1 {
return Err(ReactionExtentError::ValidationNotApplicable {
path: "multiphase equilibrium layout",
message: "at most one ideal-gas phase is currently implemented".into(),
});
}
let system_layout = layout_from_ordered_specs(&phase_specs);
let fingerprint = fingerprint_specs(&phase_specs);
Ok(Self {
phase_specs,
system_layout,
fingerprint,
})
}
pub fn phase_specs(&self) -> &[PhaseSpec] {
&self.phase_specs
}
pub fn system_layout(&self) -> &SystemLayout {
&self.system_layout
}
pub fn fingerprint(&self) -> u64 {
self.fingerprint
}
pub fn component_count(&self) -> usize {
self.system_layout.component_count()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MultiphaseInitialComposition {
layout_fingerprint: u64,
moles: Vec<f64>,
}
impl MultiphaseInitialComposition {
pub fn from_dense(
layout: &MultiphaseEquilibriumLayout,
moles: Vec<f64>,
) -> Result<Self, ReactionExtentError> {
if moles.len() != layout.component_count() {
return Err(ReactionExtentError::DimensionMismatch(format!(
"initial composition has {} entries but layout has {} components",
moles.len(),
layout.component_count()
)));
}
validate_moles(&moles)?;
Ok(Self {
layout_fingerprint: layout.fingerprint(),
moles,
})
}
pub fn from_sparse(
layout: &MultiphaseEquilibriumLayout,
entries: Vec<(PhaseComponentId, f64)>,
) -> Result<Self, ReactionExtentError> {
let mut moles = vec![0.0; layout.component_count()];
let mut assigned = HashSet::with_capacity(entries.len());
for (component, amount) in entries {
let Some(index) = layout.system_layout().component_index(&component) else {
return Err(ReactionExtentError::InvalidProblem {
field: "initial_composition",
message: format!(
"component '{}' is not in the phase layout",
component.label()
),
});
};
if !assigned.insert(component.clone()) {
return Err(ReactionExtentError::InvalidProblem {
field: "initial_composition",
message: format!(
"component '{}' is specified more than once",
component.label()
),
});
}
moles[index] = amount;
}
Self::from_dense(layout, moles)
}
pub fn moles(&self) -> &[f64] {
&self.moles
}
pub fn validate_for(
&self,
layout: &MultiphaseEquilibriumLayout,
) -> Result<(), ReactionExtentError> {
if self.layout_fingerprint != layout.fingerprint() {
return Err(ReactionExtentError::InvalidProblem {
field: "initial_composition",
message: "composition belongs to a different multiphase layout".into(),
});
}
Ok(())
}
pub fn element_totals(
&self,
layout: &MultiphaseEquilibriumLayout,
element_composition: &DMatrix<f64>,
) -> Result<Vec<f64>, ReactionExtentError> {
self.validate_for(layout)?;
if element_composition.nrows() != self.moles.len() {
return Err(ReactionExtentError::DimensionMismatch(format!(
"element composition has {} component rows but composition has {} entries",
element_composition.nrows(),
self.moles.len()
)));
}
if element_composition.iter().any(|value| !value.is_finite()) {
return Err(ReactionExtentError::InvalidProblem {
field: "element_composition",
message: "element composition must contain only finite values".into(),
});
}
let mut totals = vec![0.0; element_composition.ncols()];
for component in 0..element_composition.nrows() {
for element in 0..element_composition.ncols() {
totals[element] +=
self.moles[component] * element_composition[(component, element)];
}
}
Ok(totals)
}
}
fn validate_moles(moles: &[f64]) -> Result<(), ReactionExtentError> {
let mut has_positive_amount = false;
for (index, amount) in moles.iter().copied().enumerate() {
if !amount.is_finite() || amount < 0.0 {
return Err(ReactionExtentError::InvalidProblem {
field: "initial_composition",
message: format!("component {index} must have a finite non-negative amount"),
});
}
has_positive_amount |= amount > 0.0;
}
if !has_positive_amount {
return Err(ReactionExtentError::InvalidProblem {
field: "initial_composition",
message: "at least one component must have a positive physical mole amount".into(),
});
}
Ok(())
}
fn layout_from_ordered_specs(specs: &[PhaseSpec]) -> SystemLayout {
SystemLayout::from_ordered_phase_components(
specs
.iter()
.map(|spec| (spec.id().clone(), spec.components().to_vec()))
.collect(),
)
}
fn fingerprint_specs(specs: &[PhaseSpec]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x00000100000001b3;
let mut hash = FNV_OFFSET;
let mut write = |bytes: &[u8]| {
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
};
for spec in specs {
match spec.id().as_option() {
Some(id) => write(id.as_bytes()),
None => write(b"<anonymous>"),
}
write(&[0]);
write(&[match spec.physical_state() {
PhysicalState::Gas => 1,
PhysicalState::Liquid => 2,
PhysicalState::Solid => 3,
PhysicalState::Condensed => 4,
}]);
write(&[match spec.model() {
PhaseModel::IdealGas => 1,
PhaseModel::PureCondensed => 2,
}]);
for component in spec.components() {
write(component.as_bytes());
write(&[0]);
}
write(&[0xff]);
}
hash
}