#![allow(clippy::indexing_slicing, clippy::needless_range_loop)]
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::Error;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PhaseEntry {
pub name: String,
pub composition: BTreeMap<String, f64>,
pub energy: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PhasePoint {
pub name: String,
pub composition: BTreeMap<String, f64>,
pub formation_energy: f64,
pub hull_energy: f64,
pub distance: f64,
pub stable: bool,
pub decomposition: Vec<(usize, f64)>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConvexHull {
pub elements: Vec<String>,
pub references: BTreeMap<String, f64>,
pub points: Vec<PhasePoint>,
}
pub fn convex_hull(
entries: &[PhaseEntry],
references: Option<&BTreeMap<String, f64>>,
) -> Result<ConvexHull, Error> {
if entries.is_empty() {
return Err(Error::InvalidInput(
"the convex hull needs at least one phase".to_owned(),
));
}
let mut elements = BTreeSet::new();
for entry in entries {
if entry.composition.is_empty() {
return Err(Error::InvalidInput(format!(
"phase {} has an empty composition",
entry.name
)));
}
if !entry.energy.is_finite() {
return Err(Error::InvalidInput(format!(
"phase {} has a non-finite energy",
entry.name
)));
}
for (element, amount) in &entry.composition {
if !amount.is_finite() || *amount < 0.0 {
return Err(Error::InvalidInput(format!(
"phase {} has an invalid amount of {element}",
entry.name
)));
}
if *amount > 0.0 {
elements.insert(element.clone());
}
}
}
let elements: Vec<String> = elements.into_iter().collect();
let reference_map = match references {
Some(map) => {
let mut checked = BTreeMap::new();
for element in &elements {
let Some(value) = map.get(element) else {
return Err(Error::InvalidInput(format!(
"no reference energy for element {element}"
)));
};
if !value.is_finite() {
return Err(Error::InvalidInput(format!(
"reference energy for {element} is not finite"
)));
}
checked.insert(element.clone(), *value);
}
checked
}
None => {
let mut derived = BTreeMap::new();
for entry in entries {
let total: f64 = entry.composition.values().sum();
let non_zero: Vec<&String> = entry
.composition
.iter()
.filter(|(_, amount)| **amount > 0.0)
.map(|(element, _)| element)
.collect();
if non_zero.len() == 1
&& total > 0.0
&& let Some(element) = non_zero.first()
{
derived
.entry((*element).clone())
.or_insert(entry.energy / total);
}
}
for element in &elements {
if !derived.contains_key(element) {
return Err(Error::InvalidInput(format!(
"no pure-element phase to derive the chemical potential of {element}"
)));
}
}
derived
}
};
let mut fractions: Vec<BTreeMap<String, f64>> = Vec::with_capacity(entries.len());
let mut formation = Vec::with_capacity(entries.len());
for entry in entries {
let total: f64 = entry.composition.values().sum();
if total <= 0.0 {
return Err(Error::InvalidInput(format!(
"phase {} has a zero formula unit",
entry.name
)));
}
let mut fraction = BTreeMap::new();
for element in &elements {
fraction.insert(
element.clone(),
entry.composition.get(element).copied().unwrap_or(0.0) / total,
);
}
let reference_energy: f64 = entry
.composition
.iter()
.map(|(element, amount)| reference_map.get(element).copied().unwrap_or(0.0) * amount)
.sum();
formation.push((entry.energy - reference_energy) / total);
fractions.push(fraction);
}
let mut points = Vec::with_capacity(entries.len());
for index in 0..entries.len() {
let target = &fractions[index];
let (hull_energy, decomposition) = solve_hull(&fractions, &formation, target, &elements)?;
let distance = formation[index] - hull_energy;
points.push(PhasePoint {
name: entries[index].name.clone(),
composition: target.clone(),
formation_energy: formation[index],
hull_energy,
distance,
stable: distance <= 1e-6,
decomposition,
});
}
Ok(ConvexHull {
elements,
references: reference_map,
points,
})
}
fn solve_hull(
fractions: &[BTreeMap<String, f64>],
energies: &[f64],
target: &BTreeMap<String, f64>,
elements: &[String],
) -> Result<(f64, Vec<(usize, f64)>), Error> {
let rows = elements.len().max(1);
let columns = fractions.len();
let mut matrix = vec![vec![0.0; columns]; rows];
let mut vector = vec![0.0; rows];
for (row, element) in elements.iter().enumerate() {
for column in 0..columns {
matrix[row][column] = fractions[column].get(element).copied().unwrap_or(0.0);
}
vector[row] = target.get(element).copied().unwrap_or(0.0);
}
if elements.is_empty() {
matrix[0][0] = 1.0;
vector[0] = 1.0;
}
let solution = simplex(&matrix, &vector, energies)?;
let mut decomposition = Vec::new();
let mut energy = 0.0;
for (index, weight) in solution.iter().enumerate() {
if *weight > 1e-9 {
decomposition.push((index, *weight));
energy += weight * energies[index];
}
}
Ok((energy, decomposition))
}
fn simplex(matrix: &[Vec<f64>], vector: &[f64], costs: &[f64]) -> Result<Vec<f64>, Error> {
let rows = matrix.len();
let columns = costs.len();
if rows == 0 || columns == 0 {
return Err(Error::InvalidInput(
"the simplex needs a non-empty system".to_owned(),
));
}
let total = columns + rows;
let mut tableau = vec![vec![0.0; total + 1]; rows + 1];
let mut basis = vec![0usize; rows];
for row in 0..rows {
for column in 0..columns {
tableau[row][column] = matrix[row][column];
}
tableau[row][columns + row] = 1.0;
tableau[row][total] = vector[row];
basis[row] = columns + row;
}
for column in 0..total {
let mut sum = 0.0;
for row in 0..rows {
sum += tableau[row][column];
}
tableau[rows][column] = if column < columns { -sum } else { 1.0 - sum };
}
let mut sum = 0.0;
for row in 0..rows {
sum += vector[row];
}
tableau[rows][total] = -sum;
pivot_until_done(&mut tableau, &mut basis, rows, total, Some(columns))?;
if -tableau[rows][total] > 1e-8 {
return Err(Error::InvalidInput(
"the convex hull is infeasible".to_owned(),
));
}
for column in 0..total {
let mut sum = 0.0;
for (row, &index) in basis.iter().enumerate() {
let cost = if index < columns { costs[index] } else { 0.0 };
sum += cost * tableau[row][column];
}
tableau[rows][column] = if column < columns {
costs[column] - sum
} else {
1e9
};
}
let mut sum = 0.0;
for (row, &index) in basis.iter().enumerate() {
if index < columns {
sum += costs[index] * tableau[row][total];
}
}
tableau[rows][total] = -sum;
pivot_until_done(&mut tableau, &mut basis, rows, total, Some(columns))?;
let mut solution = vec![0.0; columns];
for (row, &index) in basis.iter().enumerate() {
if index < columns {
solution[index] = tableau[row][total];
}
}
Ok(solution)
}
fn pivot_until_done(
tableau: &mut [Vec<f64>],
basis: &mut [usize],
rows: usize,
total: usize,
allowed: Option<usize>,
) -> Result<(), Error> {
let limit = allowed.unwrap_or(total);
for _ in 0..10_000 {
let mut entering = None;
let mut best = -1e-10;
for column in 0..limit {
let value = tableau[rows][column];
if value < best {
best = value;
entering = Some(column);
}
}
let Some(column) = entering else {
return Ok(());
};
let mut leaving = None;
let mut ratio = f64::INFINITY;
for row in 0..rows {
let coefficient = tableau[row][column];
if coefficient > 1e-12 {
let candidate = tableau[row][total] / coefficient;
if candidate < ratio - 1e-12 {
ratio = candidate;
leaving = Some(row);
}
}
}
let Some(row) = leaving else {
return Err(Error::InvalidInput(
"the convex-hull linear program is unbounded".to_owned(),
));
};
basis[row] = column;
let pivot = tableau[row][column];
for value in 0..=total {
tableau[row][value] /= pivot;
}
for other in 0..=rows {
if other == row {
continue;
}
let factor = tableau[other][column];
if factor.abs() < 1e-18 {
continue;
}
for value in 0..=total {
tableau[other][value] -= factor * tableau[row][value];
}
}
}
Err(Error::InvalidInput(
"the convex-hull simplex did not converge".to_owned(),
))
}