#![allow(clippy::too_many_arguments)]
#![allow(clippy::needless_range_loop)]
pub mod alignment;
pub mod ani;
pub mod canonical_smiles;
pub mod charges;
pub mod charges_eeq;
pub mod clustering;
pub mod conformer;
pub mod dipole;
pub mod dispersion;
pub mod distgeom;
pub mod dos;
pub mod dynamics;
pub mod eht;
pub mod esp;
pub mod etkdg;
pub mod experimental_status;
#[doc(hidden)]
pub mod fixture_io;
pub mod forcefield;
pub mod gpu;
pub mod graph;
pub mod hf;
pub mod ir;
pub mod materials;
pub mod mesh;
pub mod ml;
pub mod nmr;
pub mod optimization;
pub mod periodic;
pub mod pm3;
pub mod population;
pub mod potentials;
pub mod reactivity;
pub mod rings;
pub mod scf;
pub mod smarts;
pub mod smiles;
pub mod smirks;
pub mod solvation;
pub mod solvation_alpb;
pub mod spectroscopy;
pub mod stereo;
pub mod surface;
pub mod topology;
pub mod transport;
pub mod xtb;
#[cfg(any(
feature = "alpha-cga",
feature = "alpha-gsm",
feature = "alpha-sdr",
feature = "alpha-edl",
feature = "alpha-periodic-linear",
feature = "alpha-kinetics",
feature = "alpha-render-bridge",
feature = "alpha-reaction-dynamics"
))]
pub mod alpha;
#[cfg(any(
feature = "beta-kpm",
feature = "beta-mbh",
feature = "beta-cpm",
feature = "beta-randnla",
feature = "beta-riemannian"
))]
pub mod beta;
#[cfg(feature = "alpha-dynamics-live")]
pub mod dynamics_live;
#[cfg(feature = "alpha-dft")]
pub mod dft;
#[cfg(feature = "alpha-mlff")]
pub mod mlff {
pub use crate::ml::inference::*;
pub use crate::ml::mlff::*;
pub use crate::ml::symmetry_functions::*;
}
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformerResult {
pub smiles: String,
pub num_atoms: usize,
pub coords: Vec<f64>,
pub elements: Vec<u8>,
pub bonds: Vec<(usize, usize, String)>,
pub error: Option<String>,
pub time_ms: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformerConfig {
pub seed: u64,
pub num_threads: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformerEnsembleMember {
pub seed: u64,
pub cluster_id: Option<usize>,
pub coords: Vec<f64>,
pub energy_kcal_mol: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformerClusterSummary {
pub cluster_id: usize,
pub representative_seed: u64,
pub size: usize,
pub member_seeds: Vec<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformerSearchResult {
pub generated: usize,
pub unique: usize,
pub rotatable_bonds: usize,
pub conformers: Vec<ConformerEnsembleMember>,
pub clusters: Vec<ConformerClusterSummary>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodCapability {
pub available: bool,
pub confidence: eht::SupportLevel,
pub unsupported_elements: Vec<u8>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ScientificMethod {
Embed,
Uff,
Eht,
Pm3,
Xtb,
Mmff94,
Ani,
Hf3c,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PropertyRequest {
Geometry,
ForceFieldEnergy,
Orbitals,
Population,
OrbitalGrid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodMetadata {
pub method: ScientificMethod,
pub available: bool,
pub confidence: eht::SupportLevel,
pub confidence_score: f64,
pub limitations: Vec<String>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertyMethodPlan {
pub property: PropertyRequest,
pub recommended: Option<ScientificMethod>,
pub fallback: Option<ScientificMethod>,
pub rationale: Vec<String>,
pub methods: Vec<MethodMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMethodPlan {
pub capabilities: SystemCapabilities,
pub geometry: PropertyMethodPlan,
pub force_field_energy: PropertyMethodPlan,
pub orbitals: PropertyMethodPlan,
pub population: PropertyMethodPlan,
pub orbital_grid: PropertyMethodPlan,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemCapabilities {
pub embed: MethodCapability,
pub uff: MethodCapability,
pub eht: MethodCapability,
pub population: MethodCapability,
pub orbital_grid: MethodCapability,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum ElectronicWorkflowResult {
Eht {
result: eht::EhtResult,
},
UffFallback {
energy_kcal_mol: f64,
reason: String,
support: eht::EhtSupport,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MethodComparisonStatus {
Success,
Unavailable,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MethodComparisonPayload {
Eht {
homo_energy: f64,
lumo_energy: f64,
gap: f64,
support: eht::EhtSupport,
},
Uff {
energy_kcal_mol: f64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodComparisonEntry {
pub method: ScientificMethod,
pub status: MethodComparisonStatus,
pub available: bool,
pub confidence: eht::SupportLevel,
pub confidence_score: f64,
pub warnings: Vec<String>,
pub limitations: Vec<String>,
pub payload: Option<MethodComparisonPayload>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodComparisonResult {
pub plan: SystemMethodPlan,
pub comparisons: Vec<MethodComparisonEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AromaticityAnalysis {
pub aromatic_atoms: Vec<bool>,
pub aromatic_bonds: Vec<(usize, usize)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StereocenterAnalysis {
pub tagged_tetrahedral_centers: Vec<usize>,
pub inferred_tetrahedral_centers: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphFeatureAnalysis {
pub aromaticity: AromaticityAnalysis,
pub stereocenters: StereocenterAnalysis,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GiaoNmrConfig {
pub charge: i32,
pub multiplicity: u32,
pub max_scf_iterations: usize,
pub use_parallel_eri: bool,
pub allow_basis_fallback: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GiaoNmrEntry {
pub atom_index: usize,
pub element: u8,
pub tensor: [[f64; 3]; 3],
pub isotropic: f64,
pub anisotropy: f64,
pub chemical_shift: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GiaoNmrResult {
pub nucleus: String,
pub target_atomic_number: u8,
pub method: String,
pub basis_set: String,
pub charge: i32,
pub multiplicity: u32,
pub scf_converged: bool,
pub scf_iterations: usize,
pub total_energy_hartree: f64,
pub n_basis: usize,
pub n_target_atoms: usize,
pub chemical_shifts: Vec<f64>,
pub shieldings: Vec<GiaoNmrEntry>,
pub fallback_elements: Vec<u8>,
pub notes: Vec<String>,
}
impl Default for ConformerConfig {
fn default() -> Self {
Self {
seed: 42,
num_threads: 0,
}
}
}
impl Default for GiaoNmrConfig {
fn default() -> Self {
Self {
charge: 0,
multiplicity: 1,
max_scf_iterations: 100,
use_parallel_eri: false,
allow_basis_fallback: false,
}
}
}
pub fn version() -> String {
format!("sci-form {}", env!("CARGO_PKG_VERSION"))
}
pub fn get_eht_support(elements: &[u8]) -> eht::EhtSupport {
eht::analyze_eht_support(elements)
}
fn is_uff_element_supported(z: u8) -> bool {
matches!(
z,
1 | 5
| 6
| 7
| 8
| 9
| 14
| 15
| 16
| 17
| 22
| 23
| 24
| 25
| 26
| 27
| 28
| 29
| 30
| 32
| 33
| 34
| 35
| 42
| 46
| 47
| 50
| 51
| 52
| 53
| 78
| 79
)
}
fn unique_sorted_unsupported(elements: &[u8], pred: impl Fn(u8) -> bool) -> Vec<u8> {
let mut out: Vec<u8> = elements.iter().copied().filter(|&z| !pred(z)).collect();
out.sort_unstable();
out.dedup();
out
}
pub fn get_system_capabilities(elements: &[u8]) -> SystemCapabilities {
let eht_support = get_eht_support(elements);
let uff_unsupported = unique_sorted_unsupported(elements, is_uff_element_supported);
let embed = MethodCapability {
available: !elements.is_empty(),
confidence: eht::SupportLevel::Experimental,
unsupported_elements: Vec::new(),
warnings: vec![
"Embed capability is inferred from element presence only; final success still depends on full molecular graph and geometry constraints.".to_string(),
],
};
let uff = if uff_unsupported.is_empty() {
MethodCapability {
available: true,
confidence: eht::SupportLevel::Supported,
unsupported_elements: Vec::new(),
warnings: Vec::new(),
}
} else {
MethodCapability {
available: false,
confidence: eht::SupportLevel::Unsupported,
unsupported_elements: uff_unsupported.clone(),
warnings: vec![format!(
"UFF atom typing is unavailable for elements {:?}.",
uff_unsupported
)],
}
};
let eht = MethodCapability {
available: eht_support.unsupported_elements.is_empty(),
confidence: eht_support.level,
unsupported_elements: eht_support.unsupported_elements.clone(),
warnings: eht_support.warnings.clone(),
};
let population = MethodCapability {
available: eht.available,
confidence: eht.confidence,
unsupported_elements: eht.unsupported_elements.clone(),
warnings: eht.warnings.clone(),
};
let orbital_grid = MethodCapability {
available: eht.available,
confidence: eht.confidence,
unsupported_elements: eht.unsupported_elements.clone(),
warnings: eht.warnings.clone(),
};
SystemCapabilities {
embed,
uff,
eht,
population,
orbital_grid,
}
}
fn confidence_score_for_method(method: ScientificMethod, capability: &MethodCapability) -> f64 {
if !capability.available {
return 0.0;
}
match method {
ScientificMethod::Embed => 0.8,
ScientificMethod::Uff | ScientificMethod::Mmff94 => match capability.confidence {
eht::SupportLevel::Supported => 0.95,
eht::SupportLevel::Experimental => 0.75,
eht::SupportLevel::Unsupported => 0.0,
},
ScientificMethod::Eht | ScientificMethod::Pm3 | ScientificMethod::Xtb => {
match capability.confidence {
eht::SupportLevel::Supported => 0.95,
eht::SupportLevel::Experimental => 0.6,
eht::SupportLevel::Unsupported => 0.0,
}
}
ScientificMethod::Ani => match capability.confidence {
eht::SupportLevel::Supported => 0.90,
eht::SupportLevel::Experimental => 0.7,
eht::SupportLevel::Unsupported => 0.0,
},
ScientificMethod::Hf3c => match capability.confidence {
eht::SupportLevel::Supported => 0.85,
eht::SupportLevel::Experimental => 0.65,
eht::SupportLevel::Unsupported => 0.0,
},
}
}
fn build_method_metadata(
method: ScientificMethod,
capability: &MethodCapability,
extra_limitations: &[&str],
) -> MethodMetadata {
let mut limitations: Vec<String> = extra_limitations.iter().map(|s| s.to_string()).collect();
if !capability.unsupported_elements.is_empty() {
limitations.push(format!(
"Unsupported elements for this method: {:?}.",
capability.unsupported_elements
));
}
if matches!(method, ScientificMethod::Eht)
&& matches!(capability.confidence, eht::SupportLevel::Experimental)
{
limitations.push(
"Transition-metal EHT parameters remain provisional and should be treated as experimental."
.to_string(),
);
}
MethodMetadata {
method,
available: capability.available,
confidence: capability.confidence,
confidence_score: confidence_score_for_method(method, capability),
limitations,
warnings: capability.warnings.clone(),
}
}
fn build_property_plan(
property: PropertyRequest,
recommended: Option<ScientificMethod>,
fallback: Option<ScientificMethod>,
rationale: Vec<String>,
methods: Vec<MethodMetadata>,
) -> PropertyMethodPlan {
PropertyMethodPlan {
property,
recommended,
fallback,
rationale,
methods,
}
}
pub fn get_system_method_plan(elements: &[u8]) -> SystemMethodPlan {
let capabilities = get_system_capabilities(elements);
let geometry_method = build_method_metadata(
ScientificMethod::Embed,
&capabilities.embed,
&["Geometry generation still depends on graph topology, stereochemistry, and embedding constraints."],
);
let geometry = build_property_plan(
PropertyRequest::Geometry,
geometry_method.available.then_some(ScientificMethod::Embed),
None,
vec!["Embedding is the top-level geometry generation path in sci-form.".to_string()],
vec![geometry_method],
);
let uff_method = build_method_metadata(
ScientificMethod::Uff,
&capabilities.uff,
&["This recommendation applies to force-field energy evaluation, not molecular orbital analysis."],
);
let force_field_energy = build_property_plan(
PropertyRequest::ForceFieldEnergy,
uff_method.available.then_some(ScientificMethod::Uff),
None,
vec![
"UFF is the top-level force-field energy path when atom typing is available."
.to_string(),
],
vec![uff_method],
);
let eht_method = build_method_metadata(
ScientificMethod::Eht,
&capabilities.eht,
&["EHT is the only current top-level orbital method in sci-form."],
);
let orbitals = build_property_plan(
PropertyRequest::Orbitals,
eht_method.available.then_some(ScientificMethod::Eht),
None,
vec!["Orbital energies and MO coefficients are produced by the EHT workflow.".to_string()],
vec![eht_method.clone()],
);
let population_method = build_method_metadata(
ScientificMethod::Eht,
&capabilities.population,
&["Population analysis is derived from the EHT density and overlap matrices."],
);
let population = build_property_plan(
PropertyRequest::Population,
population_method.available.then_some(ScientificMethod::Eht),
None,
vec!["Population analysis currently requires a successful EHT calculation.".to_string()],
vec![population_method],
);
let orbital_grid_method = build_method_metadata(
ScientificMethod::Eht,
&capabilities.orbital_grid,
&["Orbital-grid rendering currently depends on EHT molecular-orbital coefficients."],
);
let orbital_grid = build_property_plan(
PropertyRequest::OrbitalGrid,
orbital_grid_method
.available
.then_some(ScientificMethod::Eht),
None,
vec![
"Orbital-grid generation currently requires a successful EHT calculation.".to_string(),
],
vec![orbital_grid_method],
);
SystemMethodPlan {
capabilities,
geometry,
force_field_energy,
orbitals,
population,
orbital_grid,
}
}
pub fn embed(smiles: &str, seed: u64) -> ConformerResult {
#[cfg(not(target_arch = "wasm32"))]
let start = std::time::Instant::now();
let mol = match graph::Molecule::from_smiles(smiles) {
Ok(m) => m,
Err(e) => {
return ConformerResult {
smiles: smiles.to_string(),
num_atoms: 0,
coords: vec![],
elements: vec![],
bonds: vec![],
error: Some(e),
#[cfg(not(target_arch = "wasm32"))]
time_ms: start.elapsed().as_secs_f64() * 1000.0,
#[cfg(target_arch = "wasm32")]
time_ms: 0.0,
};
}
};
let n = mol.graph.node_count();
let elements: Vec<u8> = (0..n)
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
.collect();
let bonds: Vec<(usize, usize, String)> = mol
.graph
.edge_indices()
.map(|e| {
let (a, b) = mol.graph.edge_endpoints(e).unwrap();
let order = match mol.graph[e].order {
graph::BondOrder::Single => "SINGLE",
graph::BondOrder::Double => "DOUBLE",
graph::BondOrder::Triple => "TRIPLE",
graph::BondOrder::Aromatic => "AROMATIC",
graph::BondOrder::Unknown => "UNKNOWN",
};
(a.index(), b.index(), order.to_string())
})
.collect();
let max_seed_retries = 3u64;
let mut last_err = String::new();
for retry in 0..max_seed_retries {
let current_seed = seed.wrapping_add(retry.wrapping_mul(997));
match conformer::generate_3d_conformer(&mol, current_seed) {
Ok(coords) => {
let mut flat = Vec::with_capacity(n * 3);
for i in 0..n {
flat.push(coords[(i, 0)] as f64);
flat.push(coords[(i, 1)] as f64);
flat.push(coords[(i, 2)] as f64);
}
return ConformerResult {
smiles: smiles.to_string(),
num_atoms: n,
coords: flat,
elements,
bonds,
error: None,
#[cfg(not(target_arch = "wasm32"))]
time_ms: start.elapsed().as_secs_f64() * 1000.0,
#[cfg(target_arch = "wasm32")]
time_ms: 0.0,
};
}
Err(e) => {
last_err = e;
}
}
}
ConformerResult {
smiles: smiles.to_string(),
num_atoms: n,
coords: vec![],
elements,
bonds,
error: Some(last_err),
#[cfg(not(target_arch = "wasm32"))]
time_ms: start.elapsed().as_secs_f64() * 1000.0,
#[cfg(target_arch = "wasm32")]
time_ms: 0.0,
}
}
#[cfg(feature = "parallel")]
pub fn embed_batch(smiles_list: &[&str], config: &ConformerConfig) -> Vec<ConformerResult> {
use rayon::prelude::*;
if config.num_threads > 0 {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(config.num_threads)
.build()
.unwrap();
pool.install(|| {
smiles_list
.par_iter()
.map(|smi| embed(smi, config.seed))
.collect()
})
} else {
smiles_list
.par_iter()
.map(|smi| embed(smi, config.seed))
.collect()
}
}
#[cfg(not(feature = "parallel"))]
pub fn embed_batch(smiles_list: &[&str], config: &ConformerConfig) -> Vec<ConformerResult> {
smiles_list
.iter()
.map(|smi| embed(smi, config.seed))
.collect()
}
pub fn embed_diverse(
smiles: &str,
n_conformers: usize,
rmsd_cutoff: f64,
base_seed: u64,
) -> Vec<ConformerResult> {
let all_results: Vec<ConformerResult> = (0..n_conformers as u64)
.map(|i| embed(smiles, base_seed.wrapping_add(i)))
.collect();
let successful: Vec<(usize, &ConformerResult)> = all_results
.iter()
.enumerate()
.filter(|(_, r)| r.error.is_none() && !r.coords.is_empty())
.collect();
if successful.len() <= 1 {
return all_results
.into_iter()
.filter(|r| r.error.is_none())
.collect();
}
let coords_vecs: Vec<Vec<f64>> = successful.iter().map(|(_, r)| r.coords.clone()).collect();
let cluster_result = clustering::butina_cluster(&coords_vecs, rmsd_cutoff);
cluster_result
.centroid_indices
.iter()
.map(|&ci| {
let (orig_idx, _) = successful[ci];
all_results[orig_idx].clone()
})
.collect()
}
pub fn parse(smiles: &str) -> Result<graph::Molecule, String> {
graph::Molecule::from_smiles(smiles)
}
pub fn compute_charges(smiles: &str) -> Result<charges::gasteiger::ChargeResult, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let n = mol.graph.node_count();
let elements: Vec<u8> = (0..n)
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
.collect();
let formal_charges: Vec<i8> = (0..n)
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].formal_charge)
.collect();
let bonds: Vec<(usize, usize)> = mol
.graph
.edge_indices()
.map(|e| {
let (a, b) = mol.graph.edge_endpoints(e).unwrap();
(a.index(), b.index())
})
.collect();
charges::gasteiger::gasteiger_marsili_charges(&elements, &bonds, &formal_charges, 6)
}
pub fn compute_sasa(
elements: &[u8],
coords_flat: &[f64],
probe_radius: Option<f64>,
) -> Result<surface::sasa::SasaResult, String> {
if coords_flat.len() != elements.len() * 3 {
return Err(format!(
"coords length {} != 3 * elements {}",
coords_flat.len(),
elements.len()
));
}
let positions: Vec<[f64; 3]> = coords_flat
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect();
#[cfg(feature = "parallel")]
{
Ok(surface::sasa::compute_sasa_parallel(
elements,
&positions,
probe_radius,
None,
))
}
#[cfg(not(feature = "parallel"))]
{
Ok(surface::sasa::compute_sasa(
elements,
&positions,
probe_radius,
None,
))
}
}
pub fn compute_population(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<population::PopulationResult, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
Ok(population::compute_population(
elements,
positions,
&eht_result.coefficients,
eht_result.n_electrons,
))
}
pub fn compute_dipole(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<dipole::DipoleResult, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
Ok(dipole::compute_dipole_from_eht(
elements,
positions,
&eht_result.coefficients,
eht_result.n_electrons,
))
}
pub fn compute_frontier_descriptors(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<reactivity::FrontierDescriptors, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
Ok(reactivity::compute_frontier_descriptors(
elements,
positions,
&eht_result,
))
}
pub fn compute_fukui_descriptors(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<reactivity::FukuiDescriptors, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
Ok(reactivity::compute_fukui_descriptors(
elements,
positions,
&eht_result,
))
}
pub fn compute_reactivity_ranking(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<reactivity::ReactivityRanking, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
let fukui = reactivity::compute_fukui_descriptors(elements, positions, &eht_result);
let pop = population::compute_population(
elements,
positions,
&eht_result.coefficients,
eht_result.n_electrons,
);
Ok(reactivity::rank_reactivity_sites(
&fukui,
&pop.mulliken_charges,
))
}
pub fn compute_uv_vis_spectrum(
elements: &[u8],
positions: &[[f64; 3]],
sigma: f64,
e_min: f64,
e_max: f64,
n_points: usize,
) -> Result<reactivity::UvVisSpectrum, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
Ok(reactivity::compute_uv_vis_like_spectrum(
&eht_result,
sigma,
e_min,
e_max,
n_points,
))
}
pub fn compute_bond_orders(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<population::BondOrderResult, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
Ok(population::compute_bond_orders(
elements,
positions,
&eht_result.coefficients,
eht_result.n_electrons,
))
}
pub fn compute_topology(
elements: &[u8],
positions: &[[f64; 3]],
) -> topology::TopologyAnalysisResult {
topology::analyze_topology(elements, positions)
}
pub fn analyze_graph_features(smiles: &str) -> Result<GraphFeatureAnalysis, String> {
use petgraph::visit::EdgeRef;
let mol = parse(smiles)?;
let n_atoms = mol.graph.node_count();
let mut aromatic_atoms = vec![false; n_atoms];
let mut aromatic_bonds = Vec::new();
for edge in mol.graph.edge_references() {
if matches!(edge.weight().order, graph::BondOrder::Aromatic) {
let i = edge.source().index();
let j = edge.target().index();
aromatic_atoms[i] = true;
aromatic_atoms[j] = true;
aromatic_bonds.push((i, j));
}
}
let mut tagged_tetrahedral_centers = Vec::new();
let mut inferred_tetrahedral_centers = Vec::new();
for i in 0..n_atoms {
let idx = petgraph::graph::NodeIndex::new(i);
let atom = &mol.graph[idx];
if matches!(
atom.chiral_tag,
graph::ChiralType::TetrahedralCW | graph::ChiralType::TetrahedralCCW
) {
tagged_tetrahedral_centers.push(i);
}
let neighbors: Vec<_> = mol.graph.neighbors(idx).collect();
if neighbors.len() == 4 && matches!(atom.hybridization, graph::Hybridization::SP3) {
let mut signature: Vec<u8> = neighbors.iter().map(|n| mol.graph[*n].element).collect();
signature.sort_unstable();
signature.dedup();
if signature.len() >= 3 {
inferred_tetrahedral_centers.push(i);
}
}
}
Ok(GraphFeatureAnalysis {
aromaticity: AromaticityAnalysis {
aromatic_atoms,
aromatic_bonds,
},
stereocenters: StereocenterAnalysis {
tagged_tetrahedral_centers,
inferred_tetrahedral_centers,
},
})
}
pub fn compute_eht_or_uff_fallback(
smiles: &str,
elements: &[u8],
positions: &[[f64; 3]],
allow_experimental_eht: bool,
) -> Result<ElectronicWorkflowResult, String> {
let support = get_eht_support(elements);
let should_fallback = match support.level {
eht::SupportLevel::Unsupported => true,
eht::SupportLevel::Experimental => !allow_experimental_eht,
eht::SupportLevel::Supported => false,
};
if should_fallback {
let coords_flat: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
let energy = compute_uff_energy(smiles, &coords_flat).map_err(|e| {
format!(
"EHT is not appropriate for this system (support: {:?}) and UFF fallback failed: {}",
support.level, e
)
})?;
return Ok(ElectronicWorkflowResult::UffFallback {
energy_kcal_mol: energy,
reason: if matches!(support.level, eht::SupportLevel::Unsupported) {
"EHT unsupported for one or more elements; routed to UFF-only workflow.".to_string()
} else {
"EHT confidence is experimental and experimental mode is disabled; routed to UFF-only workflow."
.to_string()
},
support,
});
}
let result = eht::solve_eht(elements, positions, None)?;
Ok(ElectronicWorkflowResult::Eht { result })
}
pub fn compare_methods(
smiles: &str,
elements: &[u8],
positions: &[[f64; 3]],
allow_experimental_eht: bool,
) -> MethodComparisonResult {
let plan = get_system_method_plan(elements);
let mut comparisons = Vec::new();
let coords_flat: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
{
let meta = build_method_metadata(
ScientificMethod::Uff,
&plan.capabilities.uff,
&["Comparison uses UFF force-field energy as the UFF observable."],
);
if !meta.available {
comparisons.push(MethodComparisonEntry {
method: ScientificMethod::Uff,
status: MethodComparisonStatus::Unavailable,
available: false,
confidence: meta.confidence,
confidence_score: meta.confidence_score,
warnings: meta.warnings,
limitations: meta.limitations,
payload: None,
error: Some("UFF is unavailable for this element set.".to_string()),
});
} else {
match compute_uff_energy(smiles, &coords_flat) {
Ok(energy) => comparisons.push(MethodComparisonEntry {
method: ScientificMethod::Uff,
status: MethodComparisonStatus::Success,
available: true,
confidence: meta.confidence,
confidence_score: meta.confidence_score,
warnings: meta.warnings,
limitations: meta.limitations,
payload: Some(MethodComparisonPayload::Uff {
energy_kcal_mol: energy,
}),
error: None,
}),
Err(err) => comparisons.push(MethodComparisonEntry {
method: ScientificMethod::Uff,
status: MethodComparisonStatus::Error,
available: true,
confidence: meta.confidence,
confidence_score: meta.confidence_score,
warnings: meta.warnings,
limitations: meta.limitations,
payload: None,
error: Some(err),
}),
}
}
}
{
let meta = build_method_metadata(
ScientificMethod::Eht,
&plan.capabilities.eht,
&["Comparison uses frontier orbital energies and gap as the EHT observable."],
);
if !meta.available {
comparisons.push(MethodComparisonEntry {
method: ScientificMethod::Eht,
status: MethodComparisonStatus::Unavailable,
available: false,
confidence: meta.confidence,
confidence_score: meta.confidence_score,
warnings: meta.warnings,
limitations: meta.limitations,
payload: None,
error: Some("EHT is unavailable for this element set.".to_string()),
});
} else if matches!(meta.confidence, eht::SupportLevel::Experimental)
&& !allow_experimental_eht
{
comparisons.push(MethodComparisonEntry {
method: ScientificMethod::Eht,
status: MethodComparisonStatus::Unavailable,
available: true,
confidence: meta.confidence,
confidence_score: meta.confidence_score,
warnings: meta.warnings,
limitations: meta.limitations,
payload: None,
error: Some(
"EHT confidence is experimental and allow_experimental_eht=false.".to_string(),
),
});
} else {
match eht::solve_eht(elements, positions, None) {
Ok(result) => comparisons.push(MethodComparisonEntry {
method: ScientificMethod::Eht,
status: MethodComparisonStatus::Success,
available: true,
confidence: meta.confidence,
confidence_score: meta.confidence_score,
warnings: meta.warnings,
limitations: meta.limitations,
payload: Some(MethodComparisonPayload::Eht {
homo_energy: result.homo_energy,
lumo_energy: result.lumo_energy,
gap: result.gap,
support: result.support,
}),
error: None,
}),
Err(err) => comparisons.push(MethodComparisonEntry {
method: ScientificMethod::Eht,
status: MethodComparisonStatus::Error,
available: true,
confidence: meta.confidence,
confidence_score: meta.confidence_score,
warnings: meta.warnings,
limitations: meta.limitations,
payload: None,
error: Some(err),
}),
}
}
}
MethodComparisonResult { plan, comparisons }
}
pub fn compute_esp(
elements: &[u8],
positions: &[[f64; 3]],
spacing: f64,
padding: f64,
) -> Result<esp::EspGrid, String> {
let pop = compute_population(elements, positions)?;
let (grid, _report) = gpu::esp_grid_gpu::compute_esp_grid_with_report(
elements,
positions,
&pop.mulliken_charges,
spacing,
padding,
);
Ok(grid)
}
pub fn compute_dos(
elements: &[u8],
positions: &[[f64; 3]],
sigma: f64,
e_min: f64,
e_max: f64,
n_points: usize,
) -> Result<dos::DosResult, String> {
let eht_result = eht::solve_eht(elements, positions, None)?;
let flat: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
#[cfg(feature = "parallel")]
{
Ok(dos::compute_pdos_parallel(
elements,
&flat,
&eht_result.energies,
&eht_result.coefficients,
eht_result.n_electrons,
sigma,
e_min,
e_max,
n_points,
))
}
#[cfg(not(feature = "parallel"))]
{
Ok(dos::compute_pdos(
elements,
&flat,
&eht_result.energies,
&eht_result.coefficients,
eht_result.n_electrons,
sigma,
e_min,
e_max,
n_points,
))
}
}
pub fn compute_rmsd(coords: &[f64], reference: &[f64]) -> f64 {
alignment::compute_rmsd(coords, reference)
}
pub fn compute_uff_energy(smiles: &str, coords: &[f64]) -> Result<f64, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let n = mol.graph.node_count();
if coords.len() != n * 3 {
return Err(format!("coords length {} != 3 * atoms {}", coords.len(), n));
}
let ff = forcefield::builder::build_uff_force_field(&mol);
let mut gradient = vec![0.0f64; n * 3];
let energy = ff.compute_system_energy_and_gradients(coords, &mut gradient);
Ok(energy)
}
pub fn compute_mmff94_energy(smiles: &str, coords: &[f64]) -> Result<f64, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let n = mol.graph.node_count();
if coords.len() != n * 3 {
return Err(format!("coords length {} != 3 * atoms {}", coords.len(), n));
}
let elements: Vec<u8> = (0..n)
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
.collect();
let bonds: Vec<(usize, usize, u8)> = mol
.graph
.edge_indices()
.map(|e| {
let (a, b) = mol.graph.edge_endpoints(e).unwrap();
let order = match mol.graph[e].order {
graph::BondOrder::Single => 1u8,
graph::BondOrder::Double => 2,
graph::BondOrder::Triple => 3,
graph::BondOrder::Aromatic => 2,
graph::BondOrder::Unknown => 1,
};
(a.index(), b.index(), order)
})
.collect();
let terms = forcefield::mmff94::Mmff94Builder::build(&elements, &bonds);
let (energy, _grad) = forcefield::mmff94::Mmff94Builder::total_energy(&terms, coords);
Ok(energy)
}
pub fn compute_pm3(elements: &[u8], positions: &[[f64; 3]]) -> Result<pm3::Pm3Result, String> {
pm3::solve_pm3(elements, positions)
}
pub fn compute_pm3_gradient(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<pm3::Pm3GradientResult, String> {
pm3::compute_pm3_gradient(elements, positions)
}
pub fn compute_xtb(elements: &[u8], positions: &[[f64; 3]]) -> Result<xtb::XtbResult, String> {
let gfn2 = xtb::gfn2::solve_gfn2(elements, positions)?;
Ok(xtb::XtbResult {
orbital_energies: gfn2.orbital_energies,
electronic_energy: gfn2.electronic_energy,
repulsive_energy: gfn2.repulsive_energy,
total_energy: gfn2.total_energy,
n_basis: gfn2.n_basis,
n_electrons: gfn2.n_electrons,
homo_energy: gfn2.homo_energy,
lumo_energy: gfn2.lumo_energy,
gap: gfn2.gap,
mulliken_charges: gfn2.mulliken_charges,
scc_iterations: gfn2.scc_iterations,
converged: gfn2.converged,
})
}
pub fn compute_xtb_gradient(
elements: &[u8],
positions: &[[f64; 3]],
) -> Result<xtb::XtbGradientResult, String> {
xtb::compute_xtb_gradient(elements, positions)
}
pub fn compute_stda_uvvis(
elements: &[u8],
positions: &[[f64; 3]],
sigma: f64,
e_min: f64,
e_max: f64,
n_points: usize,
broadening: reactivity::BroadeningType,
) -> Result<reactivity::StdaUvVisSpectrum, String> {
reactivity::compute_stda_uvvis_spectrum(
elements, positions, sigma, e_min, e_max, n_points, broadening,
)
}
pub fn compute_vibrational_analysis(
elements: &[u8],
positions: &[[f64; 3]],
method: &str,
step_size: Option<f64>,
) -> Result<ir::VibrationalAnalysis, String> {
let hessian_method =
match method {
"eht" => ir::HessianMethod::Eht,
"pm3" => ir::HessianMethod::Pm3,
"xtb" => ir::HessianMethod::Xtb,
"uff" => return Err(
"UFF vibrational analysis requires SMILES; use compute_vibrational_analysis_uff"
.to_string(),
),
_ => return Err(format!("Unknown method '{}', use eht/pm3/xtb/uff", method)),
};
ir::compute_vibrational_analysis(elements, positions, hessian_method, step_size)
}
pub fn compute_vibrational_analysis_uff(
smiles: &str,
elements: &[u8],
positions: &[[f64; 3]],
step_size: Option<f64>,
) -> Result<ir::VibrationalAnalysis, String> {
ir::vibrations::compute_vibrational_analysis_uff(smiles, elements, positions, step_size)
}
pub fn compute_ir_spectrum(
analysis: &ir::VibrationalAnalysis,
gamma: f64,
wn_min: f64,
wn_max: f64,
n_points: usize,
) -> ir::IrSpectrum {
ir::compute_ir_spectrum(analysis, gamma, wn_min, wn_max, n_points)
}
pub fn predict_nmr_shifts(smiles: &str) -> Result<nmr::NmrShiftResult, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
Ok(nmr::predict_chemical_shifts(&mol))
}
pub fn predict_nmr_shifts_for_nucleus(
smiles: &str,
nucleus: &str,
) -> Result<Vec<nmr::ChemicalShift>, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let nucleus = parse_nmr_nucleus(nucleus)?;
Ok(nmr::predict_chemical_shifts_for_nucleus(&mol, nucleus))
}
pub fn predict_nmr_couplings(
smiles: &str,
positions: &[[f64; 3]],
) -> Result<Vec<nmr::JCoupling>, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
Ok(nmr::predict_j_couplings(&mol, positions))
}
fn parse_nmr_nucleus(nucleus: &str) -> Result<nmr::NmrNucleus, String> {
nmr::NmrNucleus::parse(nucleus)
}
struct GiaoNmrPreflight {
nucleus: nmr::NmrNucleus,
system: scf::types::MolecularSystem,
basis: scf::basis::BasisSet,
fallback_elements: Vec<u8>,
}
const EXPLICIT_GIAO_STO3G_ELEMENTS: &[u8] =
&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 16, 17, 35, 53];
fn has_explicit_giao_sto3g_support(z: u8) -> bool {
EXPLICIT_GIAO_STO3G_ELEMENTS.contains(&z)
}
fn format_element_label(z: u8) -> String {
nmr::NmrNucleus::default_for_element(z)
.map(|nucleus| format!("{}({})", nucleus.element_symbol(), z))
.unwrap_or_else(|| format!("Z{}", z))
}
fn format_element_set(elements: &[u8]) -> String {
let mut unique = elements.to_vec();
unique.sort_unstable();
unique.dedup();
unique
.into_iter()
.map(format_element_label)
.collect::<Vec<_>>()
.join(", ")
}
fn preflight_giao_nmr(
elements: &[u8],
positions: &[[f64; 3]],
nucleus: &str,
config: &GiaoNmrConfig,
) -> Result<GiaoNmrPreflight, String> {
if elements.is_empty() {
return Err("GIAO NMR requires at least one atom.".to_string());
}
if elements.len() != positions.len() {
return Err(format!(
"GIAO NMR requires one 3D coordinate per atom; got {} elements and {} positions.",
elements.len(),
positions.len()
));
}
if config.max_scf_iterations == 0 {
return Err("GIAO NMR requires max_scf_iterations > 0.".to_string());
}
if config.multiplicity != 1 {
return Err(format!(
"The public GIAO NMR API currently supports singlet closed-shell systems only; got multiplicity {}.",
config.multiplicity
));
}
let parsed_nucleus = parse_nmr_nucleus(nucleus)?;
if !elements
.iter()
.any(|&z| z == parsed_nucleus.atomic_number())
{
return Err(format!(
"Requested nucleus {} is not present in the provided element list.",
parsed_nucleus.canonical()
));
}
let system = scf::types::MolecularSystem::from_angstrom(
elements,
positions,
config.charge,
config.multiplicity,
);
if !system.n_electrons().is_multiple_of(2) {
return Err(format!(
"The public GIAO NMR API currently supports even-electron closed-shell systems only; got {} electrons.",
system.n_electrons()
));
}
let fallback_elements: Vec<u8> = elements
.iter()
.copied()
.filter(|&z| !has_explicit_giao_sto3g_support(z))
.collect();
if !config.allow_basis_fallback && !fallback_elements.is_empty() {
return Err(format!(
"The public GIAO NMR SCF route only has explicit STO-3G basis data for {}. This system includes fallback-only elements: {}. Re-run with GiaoNmrConfig.allow_basis_fallback=true for a screening-only attempt.",
format_element_set(EXPLICIT_GIAO_STO3G_ELEMENTS),
format_element_set(&fallback_elements)
));
}
let basis = scf::basis::BasisSet::sto3g(&system.atomic_numbers, &system.positions_bohr);
let n_occupied = system.n_electrons() / 2;
if n_occupied > basis.n_basis {
return Err(format!(
"GIAO NMR preflight failed for {}: STO-3G supplies {} basis functions for {} occupied orbitals. This usually means one or more elements only have the hydrogen-like fallback basis in the current SCF path.",
parsed_nucleus.canonical(),
basis.n_basis,
n_occupied
));
}
Ok(GiaoNmrPreflight {
nucleus: parsed_nucleus,
system,
basis,
fallback_elements,
})
}
pub fn compute_giao_nmr(
elements: &[u8],
positions: &[[f64; 3]],
nucleus: &str,
) -> Result<GiaoNmrResult, String> {
compute_giao_nmr_configured(elements, positions, nucleus, &GiaoNmrConfig::default())
}
pub fn compute_giao_nmr_configured(
elements: &[u8],
positions: &[[f64; 3]],
nucleus: &str,
config: &GiaoNmrConfig,
) -> Result<GiaoNmrResult, String> {
let request = preflight_giao_nmr(elements, positions, nucleus, config)?;
let scf_config = scf::scf_loop::ScfConfig {
max_iterations: config.max_scf_iterations,
use_parallel_eri: config.use_parallel_eri,
parallel_threshold: if config.use_parallel_eri { 0 } else { 20 },
..scf::scf_loop::ScfConfig::default()
};
let scf = scf::scf_loop::run_scf(&request.system, &scf_config);
let shieldings = spectroscopy::compute_nmr_shieldings_for_nucleus(
request.nucleus,
&request.system.atomic_numbers,
&request.system.positions_bohr,
&spectroscopy::ScfInput::from(&scf),
&request.basis.function_to_atom,
);
if shieldings.is_empty() {
return Err(format!(
"Requested nucleus {} is not present in the provided element list.",
request.nucleus.canonical()
));
}
let entries: Vec<GiaoNmrEntry> = shieldings
.iter()
.map(|entry| GiaoNmrEntry {
atom_index: entry.atom_index,
element: entry.element,
tensor: entry.tensor,
isotropic: entry.isotropic,
anisotropy: entry.anisotropy,
chemical_shift: entry.chemical_shift,
})
.collect();
let mut notes = vec![format!(
"GIAO NMR for {} via the public SCF route (RHF/STO-3G).",
request.nucleus.unicode_label()
)];
if scf.converged {
notes.push(format!(
"SCF converged in {} iterations with total energy {:.6} Hartree.",
scf.scf_iterations, scf.total_energy
));
} else {
notes.push(format!(
"SCF did not reach the requested threshold after {} iterations; treat the returned shieldings as screening-level only.",
scf.scf_iterations
));
}
if request.fallback_elements.is_empty() {
notes.push(
"All elements in this system use explicit STO-3G basis data in the current SCF path."
.to_string(),
);
} else {
notes.push(format!(
"Fallback basis enabled for {}. Heavy-element shieldings are qualitative in this mode.",
format_element_set(&request.fallback_elements)
));
}
if request.nucleus.is_quadrupolar() {
notes.push(
"Quadrupolar nucleus selected: this API returns isotropic shieldings and chemical shifts only; relaxation-driven broadening still needs experimental lineshape modeling.".to_string(),
);
}
if !request.nucleus.is_primary_target() {
notes.push(
"Non-1H/13C nuclei remain sensitive to the reference shielding and basis choice; benchmark the target family before quantitative use.".to_string(),
);
}
Ok(GiaoNmrResult {
nucleus: request.nucleus.canonical().to_string(),
target_atomic_number: request.nucleus.atomic_number(),
method: "RHF/GIAO".to_string(),
basis_set: "STO-3G".to_string(),
charge: config.charge,
multiplicity: config.multiplicity,
scf_converged: scf.converged,
scf_iterations: scf.scf_iterations,
total_energy_hartree: scf.total_energy,
n_basis: scf.n_basis,
n_target_atoms: entries.len(),
chemical_shifts: entries.iter().map(|entry| entry.chemical_shift).collect(),
shieldings: entries,
fallback_elements: request.fallback_elements,
notes,
})
}
pub fn compute_nmr_spectrum(
smiles: &str,
nucleus: &str,
gamma: f64,
ppm_min: f64,
ppm_max: f64,
n_points: usize,
) -> Result<nmr::NmrSpectrum, String> {
compute_nmr_spectrum_with_coords(smiles, &[], nucleus, gamma, ppm_min, ppm_max, n_points)
}
pub fn compute_nmr_spectrum_with_coords(
smiles: &str,
positions: &[[f64; 3]],
nucleus: &str,
gamma: f64,
ppm_min: f64,
ppm_max: f64,
n_points: usize,
) -> Result<nmr::NmrSpectrum, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let couplings = nmr::predict_j_couplings(&mol, positions);
let nuc = parse_nmr_nucleus(nucleus)?;
let shifts = nmr::predict_chemical_shifts_for_nucleus(&mol, nuc);
Ok(nmr::spectrum::compute_nmr_spectrum_for_shifts(
&shifts, &couplings, nuc, gamma, ppm_min, ppm_max, n_points,
))
}
pub fn compute_hose_codes(smiles: &str, max_radius: usize) -> Result<Vec<nmr::HoseCode>, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
Ok(nmr::hose::generate_hose_codes(&mol, max_radius))
}
pub fn compute_orbital_mesh(
elements: &[u8],
positions: &[[f64; 3]],
method: &str,
mo_index: usize,
spacing: f64,
padding: f64,
isovalue: f32,
) -> Result<mesh::OrbitalMeshResult, String> {
let m = match method.to_lowercase().as_str() {
"eht" | "huckel" => mesh::MeshMethod::Eht,
"pm3" => mesh::MeshMethod::Pm3,
"xtb" | "gfn0" | "gfn-xtb" => mesh::MeshMethod::Xtb,
"hf3c" | "hf-3c" | "hf" => mesh::MeshMethod::Hf3c,
_ => {
return Err(format!(
"Unknown method '{}'. Supported: eht, pm3, xtb, hf3c",
method
))
}
};
mesh::compute_orbital_mesh(elements, positions, m, mo_index, spacing, padding, isovalue)
}
pub fn compute_ml_descriptors(
elements: &[u8],
bonds: &[(usize, usize, u8)],
charges: &[f64],
aromatic_atoms: &[bool],
) -> ml::MolecularDescriptors {
ml::compute_descriptors(elements, bonds, charges, aromatic_atoms)
}
pub fn predict_ml_properties(desc: &ml::MolecularDescriptors) -> ml::MlPropertyResult {
ml::predict_properties(desc)
}
pub fn predict_ensemble(
elements: &[u8],
bonds: &[(usize, usize, u8)],
charges: &[f64],
aromatic_atoms: &[bool],
) -> ml::EnsembleResult {
let desc = ml::compute_descriptors(elements, bonds, charges, aromatic_atoms);
ml::predict_ensemble(&desc, elements, bonds)
}
pub fn compute_tpsa(elements: &[u8], bonds: &[(usize, usize, u8)]) -> f64 {
ml::compute_tpsa(elements, bonds)
}
pub fn compute_md_trajectory(
smiles: &str,
coords: &[f64],
n_steps: usize,
dt_fs: f64,
seed: u64,
) -> Result<dynamics::MdTrajectory, String> {
dynamics::simulate_velocity_verlet_uff(smiles, coords, n_steps, dt_fs, seed, None)
}
pub fn compute_md_trajectory_nvt(
smiles: &str,
coords: &[f64],
n_steps: usize,
dt_fs: f64,
seed: u64,
target_temp_k: f64,
thermostat_tau_fs: f64,
) -> Result<dynamics::MdTrajectory, String> {
dynamics::simulate_velocity_verlet_uff(
smiles,
coords,
n_steps,
dt_fs,
seed,
Some((target_temp_k, thermostat_tau_fs)),
)
}
pub fn compute_simplified_neb_path(
smiles: &str,
start_coords: &[f64],
end_coords: &[f64],
n_images: usize,
n_iter: usize,
spring_k: f64,
step_size: f64,
) -> Result<dynamics::NebPathResult, String> {
dynamics::compute_simplified_neb_path(
smiles,
start_coords,
end_coords,
n_images,
n_iter,
spring_k,
step_size,
)
}
pub fn compute_simplified_neb_path_configurable(
smiles: &str,
start_coords: &[f64],
end_coords: &[f64],
n_images: usize,
n_iter: usize,
spring_k: f64,
step_size: f64,
method: &str,
) -> Result<dynamics::NebPathResult, String> {
dynamics::compute_simplified_neb_path_configurable(
smiles,
start_coords,
end_coords,
n_images,
n_iter,
spring_k,
step_size,
method,
)
}
pub fn neb_backend_energy_kcal(method: &str, smiles: &str, coords: &[f64]) -> Result<f64, String> {
dynamics::neb_backend_energy_kcal(method, smiles, coords)
}
pub fn neb_backend_energy_and_gradient(
method: &str,
smiles: &str,
coords: &[f64],
) -> Result<(f64, Vec<f64>), String> {
dynamics::neb_backend_energy_and_gradient(method, smiles, coords)
}
pub fn compute_reaction_dynamics(
reactant_smiles: &[&str],
product_smiles: &[&str],
method: &str,
config: &dynamics::ReactionDynamicsConfig,
) -> Result<dynamics::ReactionDynamicsResult, String> {
dynamics::compute_reaction_dynamics(reactant_smiles, product_smiles, method, config)
}
fn coords_flat_to_matrix_f32(coords: &[f64], n_atoms: usize) -> nalgebra::DMatrix<f32> {
let mut m = nalgebra::DMatrix::<f32>::zeros(n_atoms, 3);
for i in 0..n_atoms {
m[(i, 0)] = coords[3 * i] as f32;
m[(i, 1)] = coords[3 * i + 1] as f32;
m[(i, 2)] = coords[3 * i + 2] as f32;
}
m
}
fn coords_matrix_f32_to_flat(m: &nalgebra::DMatrix<f32>) -> Vec<f64> {
let mut out = Vec::with_capacity(m.nrows() * 3);
for i in 0..m.nrows() {
out.push(m[(i, 0)] as f64);
out.push(m[(i, 1)] as f64);
out.push(m[(i, 2)] as f64);
}
out
}
pub fn search_conformers_with_uff(
smiles: &str,
n_samples: usize,
seed: u64,
rmsd_threshold: f64,
) -> Result<ConformerSearchResult, String> {
if n_samples == 0 {
return Err("n_samples must be > 0".to_string());
}
if rmsd_threshold <= 0.0 {
return Err("rmsd_threshold must be > 0".to_string());
}
let mol = graph::Molecule::from_smiles(smiles)?;
let n_atoms = mol.graph.node_count();
let bounds = distgeom::smooth_bounds_matrix(distgeom::calculate_bounds_matrix(&mol));
let mut generated = Vec::new();
let mut notes = Vec::new();
let mut rotatable_bonds = 0usize;
for i in 0..n_samples {
let sample_seed = seed.wrapping_add(i as u64);
let conf = embed(smiles, sample_seed);
if conf.error.is_some() || conf.coords.len() != n_atoms * 3 {
continue;
}
let mut coords = coords_flat_to_matrix_f32(&conf.coords, n_atoms);
let rot_mc = forcefield::optimize_torsions_monte_carlo_bounds(
&mut coords,
&mol,
&bounds,
sample_seed ^ 0x9E37_79B9_7F4A_7C15,
64,
0.4,
);
let rot_greedy = forcefield::optimize_torsions_bounds(&mut coords, &mol, &bounds, 2);
let rot = rot_mc.max(rot_greedy);
rotatable_bonds = rot;
let coords_flat = coords_matrix_f32_to_flat(&coords);
match compute_uff_energy(smiles, &coords_flat) {
Ok(energy_kcal_mol) => generated.push(ConformerEnsembleMember {
seed: sample_seed,
cluster_id: None,
coords: coords_flat,
energy_kcal_mol,
}),
Err(_) => continue,
}
}
if generated.is_empty() {
return Err("failed to generate any valid conformers".to_string());
}
generated.sort_by(|a, b| {
a.energy_kcal_mol
.partial_cmp(&b.energy_kcal_mol)
.unwrap_or(std::cmp::Ordering::Equal)
});
let generated_count = generated.len();
let mut unique = Vec::new();
let mut cluster_members: Vec<Vec<u64>> = Vec::new();
for candidate in generated {
let existing_cluster = unique.iter().position(|u: &ConformerEnsembleMember| {
compute_rmsd(&candidate.coords, &u.coords) < rmsd_threshold
});
if let Some(cluster_id) = existing_cluster {
cluster_members[cluster_id].push(candidate.seed);
} else {
unique.push(candidate.clone());
cluster_members.push(vec![candidate.seed]);
}
}
for (cluster_id, member) in unique.iter_mut().enumerate() {
member.cluster_id = Some(cluster_id);
}
let clusters: Vec<ConformerClusterSummary> = unique
.iter()
.enumerate()
.map(|(cluster_id, representative)| ConformerClusterSummary {
cluster_id,
representative_seed: representative.seed,
size: cluster_members[cluster_id].len(),
member_seeds: cluster_members[cluster_id].clone(),
})
.collect();
notes.push(
"Conformers are preconditioned with Monte Carlo torsion sampling + greedy torsion refinement, ranked by UFF energy, deduplicated by Kabsch-aligned RMSD threshold, and summarized as explicit RMSD clusters."
.to_string(),
);
Ok(ConformerSearchResult {
generated: generated_count,
unique: unique.len(),
rotatable_bonds,
conformers: unique,
clusters,
notes,
})
}
pub fn compute_uff_energy_with_aromatic_heuristics(
smiles: &str,
coords: &[f64],
) -> Result<reactivity::UffHeuristicEnergy, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let n = mol.graph.node_count();
if coords.len() != n * 3 {
return Err(format!("coords length {} != 3 * atoms {}", coords.len(), n));
}
let ff = forcefield::builder::build_uff_force_field(&mol);
let mut gradient = vec![0.0f64; n * 3];
let raw = ff.compute_system_energy_and_gradients(coords, &mut gradient);
Ok(reactivity::apply_aromatic_uff_correction(&mol, raw))
}
pub fn compute_empirical_pka(smiles: &str) -> Result<reactivity::EmpiricalPkaResult, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let charges = compute_charges(smiles)?;
Ok(reactivity::estimate_empirical_pka(&mol, &charges.charges))
}
pub fn create_unit_cell(
a: f64,
b: f64,
c: f64,
alpha: f64,
beta: f64,
gamma: f64,
) -> materials::UnitCell {
materials::UnitCell::from_parameters(&materials::CellParameters {
a,
b,
c,
alpha,
beta,
gamma,
})
}
pub fn assemble_framework(
node: &materials::Sbu,
linker: &materials::Sbu,
topology: &materials::Topology,
cell: &materials::UnitCell,
) -> materials::CrystalStructure {
materials::assemble_framework(node, linker, topology, cell)
}
pub fn parse_cif(input: &str) -> Result<materials::CifStructure, String> {
materials::cif::parse_cif(input)
}
pub fn write_cif(structure: &materials::CifStructure) -> String {
materials::cif::write_cif(structure)
}
pub fn compute_uhf(
elements: &[u8],
positions: &[[f64; 3]],
charge: i32,
multiplicity: u32,
) -> Result<scf::uhf::UhfResult, String> {
let system =
scf::types::MolecularSystem::from_angstrom(elements, positions, charge, multiplicity);
Ok(scf::uhf::run_uhf_parallel(&system))
}
pub fn compute_rohf(
elements: &[u8],
positions: &[[f64; 3]],
charge: i32,
multiplicity: u32,
) -> Result<scf::uhf::UhfResult, String> {
let system =
scf::types::MolecularSystem::from_angstrom(elements, positions, charge, multiplicity);
Ok(scf::uhf::run_rohf(&system))
}
pub fn compute_uhf_configured(
elements: &[u8],
positions: &[[f64; 3]],
charge: i32,
multiplicity: u32,
config: &scf::uhf::UhfConfig,
) -> Result<scf::uhf::UhfResult, String> {
let system =
scf::types::MolecularSystem::from_angstrom(elements, positions, charge, multiplicity);
Ok(scf::uhf::run_uhf(&system, config))
}
pub fn compute_hf3c(
elements: &[u8],
positions: &[[f64; 3]],
config: &hf::HfConfig,
) -> Result<hf::Hf3cResult, String> {
hf::solve_hf3c(elements, positions, config)
}
pub fn compute_ani(elements: &[u8], positions: &[[f64; 3]]) -> Result<ani::AniResult, String> {
ani::api::compute_ani_test(elements, positions)
}
pub fn compute_ani_with_models(
elements: &[u8],
positions: &[[f64; 3]],
config: &ani::AniConfig,
models: &std::collections::HashMap<u8, ani::nn::FeedForwardNet>,
) -> Result<ani::AniResult, String> {
ani::compute_ani(elements, positions, config, models)
}
pub fn compute_esp_grid(
elements: &[u8],
positions: &[[f64; 3]],
spacing: f64,
padding: f64,
) -> Result<esp::EspGrid, String> {
compute_esp(elements, positions, spacing, padding)
}
pub fn to_canonical_smiles(smiles: &str) -> Result<String, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
Ok(canonical_smiles::to_canonical_smiles(&mol))
}
pub fn analyze_stereo(smiles: &str, coords: &[f64]) -> Result<stereo::StereoAnalysis, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let positions: Vec<[f64; 3]> = coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
Ok(stereo::analyze_stereo(&mol, &positions))
}
pub fn compute_nonpolar_solvation(
elements: &[u8],
positions: &[[f64; 3]],
probe_radius: Option<f64>,
) -> solvation::NonPolarSolvation {
solvation::compute_nonpolar_solvation(elements, positions, probe_radius)
}
pub fn compute_gb_solvation(
elements: &[u8],
positions: &[[f64; 3]],
charges: &[f64],
solvent_dielectric: Option<f64>,
solute_dielectric: Option<f64>,
probe_radius: Option<f64>,
) -> solvation::GbSolvation {
solvation::compute_gb_solvation(
elements,
positions,
charges,
solvent_dielectric,
solute_dielectric,
probe_radius,
)
}
pub fn butina_cluster(conformers: &[Vec<f64>], rmsd_cutoff: f64) -> clustering::ClusterResult {
clustering::butina_cluster(conformers, rmsd_cutoff)
}
pub fn compute_rmsd_matrix(conformers: &[Vec<f64>]) -> Vec<Vec<f64>> {
clustering::compute_rmsd_matrix(conformers)
}
pub fn compute_sssr(smiles: &str) -> Result<rings::sssr::SssrResult, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
Ok(rings::sssr::compute_sssr(&mol))
}
pub fn compute_ecfp(
smiles: &str,
radius: usize,
n_bits: usize,
) -> Result<rings::ecfp::ECFPFingerprint, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
Ok(rings::ecfp::compute_ecfp(&mol, radius, n_bits))
}
pub fn compute_ecfp_batch(
smiles_list: &[&str],
radius: usize,
n_bits: usize,
) -> Result<Vec<rings::ecfp::ECFPFingerprint>, String> {
let mols: Result<Vec<_>, _> = smiles_list
.iter()
.map(|s| graph::Molecule::from_smiles(s))
.collect();
let mols = mols?;
Ok(rings::ecfp::compute_ecfp_batch(&mols, radius, n_bits))
}
pub fn compute_tanimoto(
fp1: &rings::ecfp::ECFPFingerprint,
fp2: &rings::ecfp::ECFPFingerprint,
) -> f64 {
rings::ecfp::compute_tanimoto(fp1, fp2)
}
pub fn compute_charges_configured(
smiles: &str,
config: &charges::gasteiger::GasteigerConfig,
) -> Result<charges::gasteiger::ChargeResult, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let n = mol.graph.node_count();
let elements: Vec<u8> = (0..n)
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
.collect();
let bonds: Vec<(usize, usize)> = mol
.graph
.edge_indices()
.map(|e| {
let (a, b) = mol.graph.edge_endpoints(e).unwrap();
(a.index(), b.index())
})
.collect();
let formal_charges: Vec<i8> = (0..n)
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].formal_charge)
.collect();
charges::gasteiger::gasteiger_marsili_charges_configured(
&elements,
&bonds,
&formal_charges,
config,
)
}
pub fn compute_ensemble_j_couplings(
smiles: &str,
conformer_coords: &[Vec<f64>],
energies_kcal: &[f64],
temperature_k: f64,
) -> Result<Vec<nmr::JCoupling>, String> {
let mol = graph::Molecule::from_smiles(smiles)?;
let positions: Vec<Vec<[f64; 3]>> = conformer_coords
.iter()
.map(|c| c.chunks(3).map(|p| [p[0], p[1], p[2]]).collect())
.collect();
Ok(nmr::coupling::ensemble_averaged_j_couplings(
&mol,
&positions,
energies_kcal,
temperature_k,
))
}
pub fn compute_ir_spectrum_broadened(
analysis: &ir::VibrationalAnalysis,
gamma: f64,
wn_min: f64,
wn_max: f64,
n_points: usize,
broadening: &str,
) -> ir::IrSpectrum {
let bt = match broadening {
"gaussian" | "Gaussian" => ir::BroadeningType::Gaussian,
_ => ir::BroadeningType::Lorentzian,
};
ir::vibrations::compute_ir_spectrum_with_broadening(
analysis, gamma, wn_min, wn_max, n_points, bt,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cisplatin_style_support_metadata() {
let smiles = "[Pt](Cl)(Cl)([NH3])[NH3]";
let mol = parse(smiles).expect("Cisplatin-style example should parse");
let elements: Vec<u8> = (0..mol.graph.node_count())
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
.collect();
let caps = get_system_capabilities(&elements);
assert!(caps.eht.available);
assert!(matches!(
caps.eht.confidence,
eht::SupportLevel::Experimental
));
}
#[test]
fn test_pt_system_routes_to_uff_when_experimental_disabled() {
let smiles = "[Pt](Cl)(Cl)([NH3])[NH3]";
let mol = parse(smiles).expect("Pt example should parse");
let n = mol.graph.node_count();
let elements: Vec<u8> = (0..n)
.map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
.collect();
let positions = vec![[0.0, 0.0, 0.0]; n];
let result = compute_eht_or_uff_fallback(smiles, &elements, &positions, false)
.expect("Fallback workflow should return a result");
assert!(matches!(
result,
ElectronicWorkflowResult::UffFallback { .. }
));
}
#[test]
fn test_method_plan_prefers_supported_workflows_for_organic_systems() {
let plan = get_system_method_plan(&[6, 1, 1, 1, 1]);
assert_eq!(plan.geometry.recommended, Some(ScientificMethod::Embed));
assert_eq!(
plan.force_field_energy.recommended,
Some(ScientificMethod::Uff)
);
assert_eq!(plan.orbitals.recommended, Some(ScientificMethod::Eht));
assert_eq!(plan.population.recommended, Some(ScientificMethod::Eht));
assert_eq!(plan.orbital_grid.recommended, Some(ScientificMethod::Eht));
assert!(plan.orbitals.methods[0].confidence_score > 0.9);
}
#[test]
fn test_method_plan_marks_metal_orbital_workflow_experimental() {
let plan = get_system_method_plan(&[78, 17, 17, 7, 7]);
assert_eq!(plan.orbitals.recommended, Some(ScientificMethod::Eht));
assert_eq!(
plan.force_field_energy.recommended,
Some(ScientificMethod::Uff)
);
assert!(matches!(
plan.orbitals.methods[0].confidence,
eht::SupportLevel::Experimental
));
assert!(plan.orbitals.methods[0].confidence_score < 0.9);
assert!(!plan.orbitals.methods[0].warnings.is_empty());
}
#[test]
fn test_method_plan_reports_unavailable_workflows_for_unsupported_elements() {
let plan = get_system_method_plan(&[92]);
assert_eq!(plan.force_field_energy.recommended, None);
assert_eq!(plan.orbitals.recommended, None);
assert_eq!(plan.population.recommended, None);
assert_eq!(plan.orbital_grid.recommended, None);
assert!(!plan.orbitals.methods[0].limitations.is_empty());
}
#[test]
fn test_compare_methods_supported_system_returns_success_rows() {
let result = compare_methods("CC", &[6, 6], &[[0.0, 0.0, 0.0], [1.54, 0.0, 0.0]], true);
assert_eq!(result.comparisons.len(), 2);
assert!(result
.comparisons
.iter()
.any(|entry| matches!(entry.method, ScientificMethod::Uff) && entry.available));
assert!(result.comparisons.iter().any(|entry| matches!(
entry.method,
ScientificMethod::Eht
) && matches!(
entry.status,
MethodComparisonStatus::Success
)));
}
#[test]
fn test_compare_methods_blocks_experimental_eht_when_disabled() {
let result = compare_methods("[O]", &[78], &[[0.0, 0.0, 0.0]], false);
let eht_row = result
.comparisons
.iter()
.find(|entry| matches!(entry.method, ScientificMethod::Eht))
.expect("EHT row must exist");
assert!(matches!(
eht_row.status,
MethodComparisonStatus::Unavailable
));
}
#[test]
fn test_compare_methods_reports_unavailable_for_unsupported_elements() {
let result = compare_methods("[O]", &[92], &[[0.0, 0.0, 0.0]], true);
assert!(result
.comparisons
.iter()
.all(|entry| matches!(entry.status, MethodComparisonStatus::Unavailable)));
}
#[test]
fn test_compute_fukui_descriptors_returns_atomwise_output() {
let elements = [8u8, 1, 1];
let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
let result = compute_fukui_descriptors(&elements, &positions).unwrap();
assert_eq!(result.num_atoms, 3);
assert_eq!(result.condensed.len(), 3);
}
#[test]
fn test_compute_uv_vis_spectrum_returns_requested_grid_size() {
let elements = [6u8, 6, 1, 1, 1, 1];
let positions = [
[0.0, 0.0, 0.0],
[1.34, 0.0, 0.0],
[-0.6, 0.92, 0.0],
[-0.6, -0.92, 0.0],
[1.94, 0.92, 0.0],
[1.94, -0.92, 0.0],
];
let spectrum = compute_uv_vis_spectrum(&elements, &positions, 0.2, 0.5, 8.0, 256).unwrap();
assert_eq!(spectrum.energies_ev.len(), 256);
assert_eq!(spectrum.intensities.len(), 256);
}
#[test]
fn test_analyze_graph_features_reports_benzene_aromaticity() {
let analysis = analyze_graph_features("c1ccccc1").unwrap();
assert_eq!(analysis.aromaticity.aromatic_atoms.len(), 12);
assert_eq!(
analysis
.aromaticity
.aromatic_atoms
.iter()
.filter(|v| **v)
.count(),
6
);
assert_eq!(analysis.aromaticity.aromatic_bonds.len(), 6);
}
#[test]
fn test_compute_empirical_pka_finds_acidic_site_for_acetic_acid() {
let result = compute_empirical_pka("CC(=O)O").unwrap();
assert!(!result.acidic_sites.is_empty());
}
#[test]
fn test_compute_uff_energy_with_aromatic_heuristics_applies_correction() {
let conf = embed("c1ccccc1", 42);
assert!(conf.error.is_none());
let result = compute_uff_energy_with_aromatic_heuristics("c1ccccc1", &conf.coords).unwrap();
assert!(result.aromatic_bond_count >= 6);
assert!(result.corrected_energy_kcal_mol <= result.raw_energy_kcal_mol);
}
#[test]
fn test_search_conformers_with_uff_returns_ranked_unique_ensemble() {
let result = search_conformers_with_uff("CCCC", 10, 42, 0.2).unwrap();
assert!(result.generated >= 1);
assert!(result.unique >= 1);
assert_eq!(result.unique, result.conformers.len());
assert_eq!(result.unique, result.clusters.len());
assert!(result.rotatable_bonds >= 1);
let mut total_members = 0usize;
for (i, cluster) in result.clusters.iter().enumerate() {
assert_eq!(cluster.cluster_id, i);
assert!(cluster.size >= 1);
total_members += cluster.size;
assert_eq!(result.conformers[i].cluster_id, Some(i));
assert_eq!(result.conformers[i].seed, cluster.representative_seed);
}
assert_eq!(total_members, result.generated);
for i in 1..result.conformers.len() {
assert!(
result.conformers[i - 1].energy_kcal_mol <= result.conformers[i].energy_kcal_mol
);
}
}
#[test]
fn test_search_conformers_with_uff_large_rmsd_threshold_collapses_duplicates() {
let result = search_conformers_with_uff("CCCC", 8, 123, 10.0).unwrap();
assert_eq!(result.unique, 1);
assert_eq!(result.clusters.len(), 1);
assert_eq!(result.clusters[0].size, result.generated);
}
#[test]
fn test_compute_md_trajectory_velocity_verlet_runs() {
let conf = embed("CC", 42);
assert!(conf.error.is_none());
let trj = compute_md_trajectory("CC", &conf.coords, 10, 0.25, 7).unwrap();
assert_eq!(trj.frames.len(), 11);
assert!(trj
.frames
.iter()
.all(|f| f.coords.iter().all(|v| v.is_finite())));
}
#[test]
fn test_compute_md_trajectory_nvt_runs() {
let conf = embed("CCO", 42);
assert!(conf.error.is_none());
let trj =
compute_md_trajectory_nvt("CCO", &conf.coords, 12, 0.25, 17, 300.0, 10.0).unwrap();
assert_eq!(trj.frames.len(), 13);
assert!(trj.frames.iter().all(|f| f.temperature_k.is_finite()));
}
#[test]
fn test_compute_simplified_neb_path_runs() {
let c1 = embed("CC", 42);
let c2 = embed("CC", 43);
assert!(c1.error.is_none());
assert!(c2.error.is_none());
let path =
compute_simplified_neb_path("CC", &c1.coords, &c2.coords, 6, 20, 0.01, 1e-5).unwrap();
assert_eq!(path.images.len(), 6);
assert!(path
.images
.iter()
.all(|img| img.potential_energy_kcal_mol.is_finite()));
}
#[test]
fn test_compute_hf3c_water() {
let conf = embed("O", 42);
assert!(conf.error.is_none());
let pos: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let result = compute_hf3c(&conf.elements, &pos, &hf::HfConfig::default());
assert!(result.is_ok(), "HF-3c should succeed for water");
let r = result.unwrap();
assert!(r.energy.is_finite());
assert!(!r.orbital_energies.is_empty());
}
#[test]
fn test_compute_ani_water() {
let conf = embed("O", 42);
assert!(conf.error.is_none());
let pos: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let result = compute_ani(&conf.elements, &pos);
assert!(result.is_ok(), "ANI should succeed for water");
let r = result.unwrap();
assert!(r.energy.is_finite());
assert_eq!(r.forces.len(), 3); assert_eq!(r.species.len(), 3);
}
#[test]
fn test_compute_esp_grid_water() {
let conf = embed("O", 42);
assert!(conf.error.is_none());
let pos: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let result = compute_esp_grid(&conf.elements, &pos, 0.5, 3.0);
assert!(result.is_ok(), "ESP grid should succeed for water");
let g = result.unwrap();
assert!(!g.values.is_empty());
assert!(g.spacing > 0.0);
assert!(g.dims[0] > 0 && g.dims[1] > 0 && g.dims[2] > 0);
}
}