use crate::Thermodynamics::ChemEquilibrium::equilibrium_presentation::{
EquilibriumComponentPresentationRow, EquilibriumPhasePresentationRow,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EquilibriumNumberStyle {
Scientific { decimals: usize },
Fixed { decimals: usize },
EngineeringSi { decimals: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MoleFractionDisplay {
Fraction,
Percent,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EquilibriumDisplayPolicy {
trace_moles: f64,
number_style: EquilibriumNumberStyle,
mole_fraction_display: MoleFractionDisplay,
}
impl Default for EquilibriumDisplayPolicy {
fn default() -> Self {
Self {
trace_moles: 0.0,
number_style: EquilibriumNumberStyle::Scientific { decimals: 6 },
mole_fraction_display: MoleFractionDisplay::Fraction,
}
}
}
impl EquilibriumDisplayPolicy {
pub fn new(
trace_moles: f64,
number_style: EquilibriumNumberStyle,
mole_fraction_display: MoleFractionDisplay,
) -> Result<Self, DisplayPolicyError> {
if !trace_moles.is_finite() || trace_moles < 0.0 {
return Err(DisplayPolicyError::InvalidTraceThreshold { trace_moles });
}
Ok(Self {
trace_moles,
number_style,
mole_fraction_display,
})
}
pub fn trace_moles(&self) -> f64 {
self.trace_moles
}
pub fn number_style(&self) -> EquilibriumNumberStyle {
self.number_style
}
pub fn mole_fraction_display(&self) -> MoleFractionDisplay {
self.mole_fraction_display
}
pub fn visible_components<'a>(
&self,
components: &'a [EquilibriumComponentPresentationRow],
) -> Vec<&'a EquilibriumComponentPresentationRow> {
components
.iter()
.filter(|row| row.physical_moles >= self.trace_moles)
.collect()
}
pub fn visible_amount_columns(
&self,
labels: &[String],
rows: &[Vec<f64>],
) -> Result<Vec<usize>, DisplayPolicyError> {
if labels.is_empty() || rows.iter().any(|row| row.len() != labels.len()) {
return Err(DisplayPolicyError::InvalidAmountSeriesShape);
}
Ok((0..labels.len())
.filter(|&column| rows.iter().any(|row| row[column] >= self.trace_moles))
.collect())
}
pub fn format_moles(&self, moles: f64) -> String {
self.format_quantity(moles, "mol")
}
pub fn format_standard_gibbs(&self, joules_per_mol: f64) -> String {
self.format_quantity(joules_per_mol, "J/mol")
}
pub fn format_mole_fraction(&self, fraction: f64) -> String {
match self.mole_fraction_display {
MoleFractionDisplay::Fraction => self.format_dimensionless(fraction, "-"),
MoleFractionDisplay::Percent => self.format_dimensionless(fraction * 100.0, "%"),
}
}
pub fn format_quantity(&self, value: f64, unit: &str) -> String {
format_quantity(value, unit, self.number_style)
}
fn format_dimensionless(&self, value: f64, unit: &str) -> String {
let style = match self.number_style {
EquilibriumNumberStyle::EngineeringSi { decimals } => {
EquilibriumNumberStyle::Fixed { decimals }
}
style => style,
};
format_quantity(value, unit, style)
}
pub fn format_phases(
&self,
phases: &[EquilibriumPhasePresentationRow],
) -> Vec<EquilibriumFormattedPhaseRow> {
phases
.iter()
.map(|row| EquilibriumFormattedPhaseRow {
phase: row.phase.clone(),
status: row.status.clone(),
physical_total_moles: self.format_moles(row.physical_total_moles),
numerical_total_moles: self.format_moles(row.numerical_total_moles),
})
.collect()
}
pub fn format_visible_components(
&self,
components: &[EquilibriumComponentPresentationRow],
) -> Vec<EquilibriumFormattedComponentRow> {
self.visible_components(components)
.into_iter()
.map(|row| EquilibriumFormattedComponentRow {
component: row.component.clone(),
phase: row.phase.clone(),
substance: row.substance.clone(),
physical_moles: self.format_moles(row.physical_moles),
numerical_moles: self.format_moles(row.numerical_moles),
mole_fraction: self.format_mole_fraction(row.mole_fraction),
initial_moles: self.format_moles(row.initial_moles),
standard_gibbs: self.format_standard_gibbs(row.standard_gibbs_j_per_mol),
library: row.library.clone(),
record_key: row.record_key.clone(),
lookup_priority: row.lookup_priority.clone(),
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EquilibriumFormattedPhaseRow {
pub phase: String,
pub status: String,
pub physical_total_moles: String,
pub numerical_total_moles: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EquilibriumFormattedComponentRow {
pub component: String,
pub phase: String,
pub substance: String,
pub physical_moles: String,
pub numerical_moles: String,
pub mole_fraction: String,
pub initial_moles: String,
pub standard_gibbs: String,
pub library: String,
pub record_key: String,
pub lookup_priority: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DisplayPolicyError {
InvalidTraceThreshold { trace_moles: f64 },
InvalidAmountSeriesShape,
}
impl std::fmt::Display for DisplayPolicyError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidTraceThreshold { trace_moles } => write!(
formatter,
"trace display threshold must be finite and non-negative, got {trace_moles}"
),
Self::InvalidAmountSeriesShape => write!(
formatter,
"amount-series labels and row widths must be non-empty and aligned"
),
}
}
}
impl std::error::Error for DisplayPolicyError {}
fn format_quantity(value: f64, unit: &str, style: EquilibriumNumberStyle) -> String {
if value.is_nan() {
return append_unit("NaN".to_string(), unit);
}
if value == f64::INFINITY {
return append_unit("+inf".to_string(), unit);
}
if value == f64::NEG_INFINITY {
return append_unit("-inf".to_string(), unit);
}
match style {
EquilibriumNumberStyle::Scientific { decimals } => {
append_unit(format!("{value:.decimals$e}"), unit)
}
EquilibriumNumberStyle::Fixed { decimals } => {
append_unit(format!("{value:.decimals$}"), unit)
}
EquilibriumNumberStyle::EngineeringSi { decimals } => {
let (scaled, prefix) = engineering_scale(value);
append_unit(format!("{scaled:.decimals$}"), &format!("{prefix}{unit}"))
}
}
}
fn append_unit(value: String, unit: &str) -> String {
if unit.is_empty() || unit == "-" {
value
} else {
format!("{value} {unit}")
}
}
fn engineering_scale(value: f64) -> (f64, &'static str) {
if value == 0.0 {
return (0.0, "");
}
const SCALES: &[(i32, &str)] = &[
(-12, "p"),
(-9, "n"),
(-6, "u"),
(-3, "m"),
(0, ""),
(3, "k"),
(6, "M"),
(9, "G"),
(12, "T"),
];
let exponent = value.abs().log10().floor() as i32;
let requested = exponent.div_euclid(3) * 3;
let (scale, prefix) = SCALES
.iter()
.copied()
.min_by_key(|(candidate, _)| (candidate - requested).abs())
.unwrap_or((0, ""));
(value / 10_f64.powi(scale), prefix)
}
#[cfg(test)]
mod tests {
use super::*;
fn component(name: &str, moles: f64, fraction: f64) -> EquilibriumComponentPresentationRow {
EquilibriumComponentPresentationRow {
component: format!("gas::{name}"),
phase: "gas".to_string(),
substance: name.to_string(),
physical_moles: moles,
numerical_moles: moles.max(1e-30),
mole_fraction: fraction,
initial_moles: moles,
standard_gibbs_j_per_mol: -12_500.0,
library: "NASA_gas".to_string(),
record_key: name.to_string(),
lookup_priority: "NASA_gas".to_string(),
}
}
#[test]
fn trace_filter_never_mutates_or_drops_the_source_rows() {
let rows = vec![
component("major", 1.0, 0.999),
component("trace", 1e-12, 1e-12),
];
let policy = EquilibriumDisplayPolicy::new(
1e-9,
EquilibriumNumberStyle::Scientific { decimals: 3 },
MoleFractionDisplay::Fraction,
)
.unwrap();
let visible = policy.visible_components(&rows);
assert_eq!(visible.len(), 1);
assert_eq!(visible[0].substance, "major");
assert_eq!(rows.len(), 2);
assert_eq!(rows[1].physical_moles, 1e-12);
}
#[test]
fn unit_and_fraction_formatting_are_presentation_only() {
let policy = EquilibriumDisplayPolicy::new(
0.0,
EquilibriumNumberStyle::EngineeringSi { decimals: 3 },
MoleFractionDisplay::Percent,
)
.unwrap();
assert_eq!(policy.format_moles(0.00125), "1.250 mmol");
assert_eq!(policy.format_mole_fraction(0.0025), "0.250 %");
assert_eq!(policy.format_standard_gibbs(-12_500.0), "-12.500 kJ/mol");
assert!(EquilibriumDisplayPolicy::new(
-1.0,
EquilibriumNumberStyle::Fixed { decimals: 2 },
MoleFractionDisplay::Fraction,
)
.is_err());
}
#[test]
fn range_column_filter_returns_indices_without_rewriting_raw_series() {
let labels = vec!["major".to_string(), "trace".to_string()];
let rows = vec![vec![1.0, 1e-12], vec![0.8, 2e-12]];
let policy = EquilibriumDisplayPolicy::new(
1e-9,
EquilibriumNumberStyle::Scientific { decimals: 3 },
MoleFractionDisplay::Fraction,
)
.unwrap();
assert_eq!(
policy.visible_amount_columns(&labels, &rows).unwrap(),
vec![0]
);
assert_eq!(rows[0][1], 1e-12);
assert!(policy
.visible_amount_columns(&labels, &[vec![1.0]])
.is_err());
}
}