use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MolecularStructure {
pub atoms: Vec<MolAtom>,
pub bonds: Vec<MolBond>,
pub formula: String,
pub molecular_weight: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct MolAtom {
pub atomic_number: u8,
pub x: f32,
pub y: f32,
pub radius: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct MolBond {
pub atom_a: usize,
pub atom_b: usize,
pub order: u8,
}
impl MolecularStructure {
#[must_use]
pub fn from_molecule(mol: &crate::molecule::Molecule) -> Self {
let mut atoms = Vec::new();
let mut idx = 0usize;
for atom in &mol.atoms {
for _ in 0..atom.count {
let angle = idx as f32 * std::f32::consts::TAU
/ mol.atoms.iter().map(|a| a.count).sum::<u32>().max(1) as f32;
let r = 1.0;
atoms.push(MolAtom {
atomic_number: atom.element_number,
x: r * angle.cos(),
y: r * angle.sin(),
radius: covalent_radius(atom.element_number),
});
idx += 1;
}
}
let bonds: Vec<MolBond> = if atoms.len() > 1 {
(0..atoms.len() - 1)
.map(|i| MolBond {
atom_a: i,
atom_b: i + 1,
order: 1,
})
.collect()
} else {
Vec::new()
};
Self {
atoms,
bonds,
formula: mol.formula().unwrap_or_default(),
molecular_weight: mol.molecular_weight().unwrap_or(0.0),
}
}
}
fn covalent_radius(atomic_number: u8) -> f32 {
match atomic_number {
1 => 0.15, 6 => 0.35, 7 => 0.30, 8 => 0.28, 16 => 0.45, _ => 0.30, }
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReactionNetworkVisualization {
pub species: Vec<String>,
pub reactions: Vec<ReactionEdge>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReactionEdge {
pub reactants: Vec<usize>,
pub products: Vec<usize>,
pub rate_constant: f64,
pub label: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpectrumVisualization {
pub x_values: Vec<f64>,
pub y_values: Vec<f64>,
pub x_label: String,
pub y_label: String,
pub peaks: Vec<f64>,
}
impl SpectrumVisualization {
#[must_use]
pub fn from_data(
x_values: Vec<f64>,
y_values: Vec<f64>,
x_label: &str,
y_label: &str,
peak_threshold: f64,
) -> Self {
let peak_indices = crate::spectroscopy::find_peaks(&y_values, peak_threshold);
let peaks = peak_indices
.iter()
.filter_map(|&i| x_values.get(i).copied())
.collect();
Self {
x_values,
y_values,
x_label: x_label.to_string(),
y_label: y_label.to_string(),
peaks,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PhaseDiagramVisualization {
pub boundaries: Vec<PhaseBoundary>,
pub critical_point: Option<[f64; 2]>,
pub triple_point: Option<[f64; 2]>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PhaseBoundary {
pub points: Vec<[f64; 2]>,
pub label: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn molecular_structure_water() {
let mol = crate::molecule::Molecule::new(&[(1, 2), (8, 1)]);
let viz = MolecularStructure::from_molecule(&mol);
assert_eq!(viz.atoms.len(), 3); assert!(!viz.bonds.is_empty());
assert!(viz.formula.contains('H'));
assert!(viz.formula.contains('O'));
assert!(viz.molecular_weight > 17.0 && viz.molecular_weight < 19.0);
}
#[test]
fn molecular_structure_single_atom() {
let mol = crate::molecule::Molecule::new(&[(2, 1)]); let viz = MolecularStructure::from_molecule(&mol);
assert_eq!(viz.atoms.len(), 1);
assert!(viz.bonds.is_empty());
}
#[test]
fn covalent_radius_known() {
assert!((covalent_radius(1) - 0.15).abs() < 0.01); assert!((covalent_radius(6) - 0.35).abs() < 0.01); }
#[test]
fn reaction_network_manual() {
let net = ReactionNetworkVisualization {
species: vec!["A".into(), "B".into(), "C".into()],
reactions: vec![ReactionEdge {
reactants: vec![0],
products: vec![1, 2],
rate_constant: 0.05,
label: "k₁".into(),
}],
};
assert_eq!(net.species.len(), 3);
assert_eq!(net.reactions.len(), 1);
}
#[test]
fn spectrum_from_data() {
let x = vec![400.0, 450.0, 500.0, 550.0, 600.0];
let y = vec![0.1, 0.5, 0.9, 0.3, 0.1];
let spec = SpectrumVisualization::from_data(x, y, "wavelength (nm)", "absorbance", 0.4);
assert_eq!(spec.x_values.len(), 5);
assert!(!spec.peaks.is_empty()); }
#[test]
fn spectrum_empty() {
let spec = SpectrumVisualization::from_data(vec![], vec![], "nm", "A", 0.5);
assert!(spec.peaks.is_empty());
}
#[test]
fn phase_diagram_serializes() {
let pd = PhaseDiagramVisualization {
boundaries: vec![PhaseBoundary {
points: vec![[273.15, 611.657], [373.15, 101325.0]],
label: "liquid-gas".into(),
}],
critical_point: Some([647.096, 22064000.0]),
triple_point: Some([273.16, 611.657]),
};
let json = serde_json::to_string(&pd);
assert!(json.is_ok());
}
}