use crate::composition::{Composition, Element};
use crate::error::{GugenError, Result, require_finite};
use std::collections::BTreeMap;
pub(crate) const COMPOSITION_CONSERVATION_TOLERANCE: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ReactionSpecies {
pub composition: Composition,
coefficient: u64,
}
impl ReactionSpecies {
pub fn new(composition: Composition, coefficient: u64) -> Result<Self> {
if coefficient == 0 {
return Err(GugenError::ZeroCoefficient);
}
Ok(Self {
composition,
coefficient,
})
}
pub fn coefficient(&self) -> u64 {
self.coefficient
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ReactionSpecies {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Raw {
composition: Composition,
coefficient: u64,
}
let raw = Raw::deserialize(deserializer)?;
ReactionSpecies::new(raw.composition, raw.coefficient).map_err(serde::de::Error::custom)
}
}
pub(crate) fn check_element_conservation(
reactants: &[ReactionSpecies],
products: &[ReactionSpecies],
) -> Result<()> {
let mut residual: BTreeMap<Element, f64> = BTreeMap::new();
for species in reactants {
for (element, amount) in species.composition.iter() {
*residual.entry(element).or_insert(0.0) += species.coefficient as f64 * amount;
}
}
for species in products {
for (element, amount) in species.composition.iter() {
*residual.entry(element).or_insert(0.0) -= species.coefficient as f64 * amount;
}
}
for (element, imbalance) in residual {
if imbalance.abs() > COMPOSITION_CONSERVATION_TOLERANCE {
return Err(GugenError::UnbalancedReaction {
element: element.symbol().to_string(),
imbalance,
});
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct BalancedReaction {
reactants: Vec<ReactionSpecies>,
products: Vec<ReactionSpecies>,
}
impl BalancedReaction {
pub fn new(reactants: Vec<ReactionSpecies>, products: Vec<ReactionSpecies>) -> Result<Self> {
if reactants.is_empty() || products.is_empty() {
return Err(GugenError::EmptyReaction);
}
if reactants
.iter()
.chain(products.iter())
.any(|s| s.coefficient == 0)
{
return Err(GugenError::ZeroCoefficient);
}
check_element_conservation(&reactants, &products)?;
Ok(Self {
reactants,
products,
})
}
pub fn reactants(&self) -> &[ReactionSpecies] {
&self.reactants
}
pub fn products(&self) -> &[ReactionSpecies] {
&self.products
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for BalancedReaction {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Raw {
reactants: Vec<ReactionSpecies>,
products: Vec<ReactionSpecies>,
}
let raw = Raw::deserialize(deserializer)?;
BalancedReaction::new(raw.reactants, raw.products).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ThermodynamicConditions {
pub temperature_celsius: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ReactionEnergy {
value_ev_per_atom: f64,
}
impl ReactionEnergy {
pub fn new(value_ev_per_atom: f64) -> Result<Self> {
require_finite("value_ev_per_atom", value_ev_per_atom)?;
Ok(Self { value_ev_per_atom })
}
pub fn value_ev_per_atom(&self) -> f64 {
self.value_ev_per_atom
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ReactionEnergy {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Raw {
value_ev_per_atom: f64,
}
let raw = Raw::deserialize(deserializer)?;
ReactionEnergy::new(raw.value_ev_per_atom).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CompetingPhase {
pub composition: Composition,
formation_energy_ev_per_atom: f64,
}
impl CompetingPhase {
pub fn new(composition: Composition, formation_energy_ev_per_atom: f64) -> Result<Self> {
require_finite("formation_energy_ev_per_atom", formation_energy_ev_per_atom)?;
Ok(Self {
composition,
formation_energy_ev_per_atom,
})
}
pub fn formation_energy_ev_per_atom(&self) -> f64 {
self.formation_energy_ev_per_atom
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for CompetingPhase {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Raw {
composition: Composition,
formation_energy_ev_per_atom: f64,
}
let raw = Raw::deserialize(deserializer)?;
CompetingPhase::new(raw.composition, raw.formation_energy_ev_per_atom)
.map_err(serde::de::Error::custom)
}
}