#![allow(dead_code)]
use rayon::prelude::*;
use serde::Deserialize;
use std::panic;
#[cfg(feature = "experimental-gpu")]
use std::sync::OnceLock;
use std::time::Instant;
#[derive(Deserialize)]
struct Fixture {
molecules: Vec<MolEntry>,
}
#[derive(Deserialize, Clone)]
struct MolEntry {
id: usize,
name: String,
formula: String,
smiles: String,
n_atoms: usize,
experimental: Experimental,
}
#[derive(Deserialize, Clone)]
struct Experimental {
heat_of_formation: f64,
ionization_potential: f64,
dipole_moment: f64,
}
#[derive(Default)]
struct MethodResult {
converged: bool,
total_energy_ev: f64,
homo_ev: f64,
lumo_ev: f64,
gap_ev: f64,
heat_of_formation: Option<f64>, time_ms: f64,
backend: String,
used_gpu: bool,
error: Option<String>,
}
struct MoleculeRow {
id: usize,
name: String,
formula: String,
smiles: String,
n_atoms: usize,
exp: Experimental,
embed_ok: bool,
pm3: MethodResult,
gfn0: MethodResult,
gfn1: MethodResult,
gfn2: MethodResult,
hf3c: MethodResult,
}
#[derive(Copy, Clone)]
enum MethodId {
Pm3,
Gfn0,
Gfn1,
Gfn2,
Hf3c,
}
fn method_result(row: &MoleculeRow, method: MethodId) -> &MethodResult {
match method {
MethodId::Pm3 => &row.pm3,
MethodId::Gfn0 => &row.gfn0,
MethodId::Gfn1 => &row.gfn1,
MethodId::Gfn2 => &row.gfn2,
MethodId::Hf3c => &row.hf3c,
}
}
fn method_label(method: MethodId) -> &'static str {
match method {
MethodId::Pm3 => "PM3",
MethodId::Gfn0 => "GFN0",
MethodId::Gfn1 => "GFN1",
MethodId::Gfn2 => "GFN2",
MethodId::Hf3c => "HF-3c",
}
}
fn method_cell_pm3(result: &MethodResult) -> String {
if result.error.is_some() {
"ERR".to_string()
} else if !result.converged {
"NC".to_string()
} else if let Some(value) = result.heat_of_formation {
format!("{value:>8.1}")
} else {
"NA".to_string()
}
}
fn method_cell_gap(result: &MethodResult) -> String {
if result.error.is_some() {
"ERR".to_string()
} else if !result.converged {
"NC".to_string()
} else {
format!("{:>8.2}", result.gap_ev)
}
}
fn row_issue_count(row: &MoleculeRow) -> usize {
let mut count = usize::from(!row.embed_ok);
for method in [
MethodId::Pm3,
MethodId::Gfn0,
MethodId::Gfn1,
MethodId::Gfn2,
MethodId::Hf3c,
] {
let result = method_result(row, method);
if result.error.is_some() || !result.converged {
count += 1;
}
}
count
}
fn embed_smiles(smiles: &str) -> Option<(Vec<u8>, Vec<[f64; 3]>)> {
let conf = sci_form::embed(smiles, 42);
if conf.error.is_some() {
return None;
}
let pos: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
Some((conf.elements, pos))
}
fn gpu_available() -> bool {
#[cfg(feature = "experimental-gpu")]
{
static GPU_AVAILABLE: OnceLock<bool> = OnceLock::new();
*GPU_AVAILABLE.get_or_init(|| sci_form::gpu::context::GpuContext::try_create().is_ok())
}
#[cfg(not(feature = "experimental-gpu"))]
{
false
}
}
fn pm3_backend(elements: &[u8]) -> (String, bool) {
let n_basis: usize = elements
.iter()
.map(|&z| sci_form::pm3::params::num_pm3_basis_functions(z))
.sum();
if gpu_available() && n_basis >= 16 {
("GPU(two-center)+CPU".to_string(), true)
} else {
("CPU".to_string(), false)
}
}
fn xtb_backend(elements: &[u8]) -> (String, bool) {
if gpu_available() && elements.len() >= 8 {
("GPU(gamma)+CPU".to_string(), true)
} else {
("CPU".to_string(), false)
}
}
fn hf3c_backend(elements: &[u8], positions: &[[f64; 3]]) -> (String, bool) {
let n_basis = sci_form::hf::basis::build_sto3g_basis(elements, positions).n_basis();
if gpu_available() && n_basis >= 4 {
("GPU(ERI+Fock)+CPU".to_string(), true)
} else {
("CPU".to_string(), false)
}
}
fn evaluate_molecule(mol: &MolEntry) -> MoleculeRow {
let embed_result = embed_smiles(&mol.smiles);
let embed_ok = embed_result.is_some();
let (pm3, gfn0, gfn1, gfn2, hf3c) = if let Some((ref elems, ref pos)) = embed_result {
(
run_pm3(elems, pos),
run_gfn0(elems, pos),
run_gfn1(elems, pos),
run_gfn2(elems, pos),
run_hf3c(elems, pos),
)
} else {
(
MethodResult {
error: Some("embed failed".into()),
..Default::default()
},
MethodResult {
error: Some("embed failed".into()),
..Default::default()
},
MethodResult {
error: Some("embed failed".into()),
..Default::default()
},
MethodResult {
error: Some("embed failed".into()),
..Default::default()
},
MethodResult {
error: Some("embed failed".into()),
..Default::default()
},
)
};
MoleculeRow {
id: mol.id,
name: mol.name.clone(),
formula: mol.formula.clone(),
smiles: mol.smiles.clone(),
n_atoms: mol.n_atoms,
exp: mol.experimental.clone(),
embed_ok,
pm3,
gfn0,
gfn1,
gfn2,
hf3c,
}
}
fn run_pm3(elements: &[u8], positions: &[[f64; 3]]) -> MethodResult {
let elems = elements.to_vec();
let pos = positions.to_vec();
let t0 = Instant::now();
let (backend, used_gpu) = pm3_backend(elements);
match panic::catch_unwind(|| sci_form::compute_pm3(&elems, &pos)) {
Ok(Ok(r)) => MethodResult {
converged: r.converged,
total_energy_ev: r.total_energy,
homo_ev: r.homo_energy,
lumo_ev: r.lumo_energy,
gap_ev: r.gap,
heat_of_formation: Some(r.heat_of_formation),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
error: None,
},
Ok(Err(e)) => MethodResult {
error: Some(e),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
Err(_) => MethodResult {
error: Some("panic".into()),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
}
}
fn run_gfn0(elements: &[u8], positions: &[[f64; 3]]) -> MethodResult {
let elems = elements.to_vec();
let pos = positions.to_vec();
let t0 = Instant::now();
let (backend, used_gpu) = xtb_backend(elements);
match panic::catch_unwind(|| sci_form::compute_xtb(&elems, &pos)) {
Ok(Ok(r)) => MethodResult {
converged: r.converged,
total_energy_ev: r.total_energy,
homo_ev: r.homo_energy,
lumo_ev: r.lumo_energy,
gap_ev: r.gap,
heat_of_formation: None,
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
error: None,
},
Ok(Err(e)) => MethodResult {
error: Some(e),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
Err(_) => MethodResult {
error: Some("panic".into()),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
}
}
fn run_gfn1(elements: &[u8], positions: &[[f64; 3]]) -> MethodResult {
let elems = elements.to_vec();
let pos = positions.to_vec();
let t0 = Instant::now();
let (backend, used_gpu) = xtb_backend(elements);
match panic::catch_unwind(|| sci_form::xtb::solve_gfn1(&elems, &pos)) {
Ok(Ok(r)) => MethodResult {
converged: r.converged,
total_energy_ev: r.total_energy,
homo_ev: r.homo_energy,
lumo_ev: r.lumo_energy,
gap_ev: r.gap,
heat_of_formation: None,
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
error: None,
},
Ok(Err(e)) => MethodResult {
error: Some(e),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
Err(_) => MethodResult {
error: Some("panic".into()),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
}
}
fn run_gfn2(elements: &[u8], positions: &[[f64; 3]]) -> MethodResult {
let elems = elements.to_vec();
let pos = positions.to_vec();
let t0 = Instant::now();
let (backend, used_gpu) = xtb_backend(elements);
match panic::catch_unwind(|| sci_form::xtb::solve_gfn2(&elems, &pos)) {
Ok(Ok(r)) => MethodResult {
converged: r.converged,
total_energy_ev: r.total_energy,
homo_ev: r.homo_energy,
lumo_ev: r.lumo_energy,
gap_ev: r.gap,
heat_of_formation: None,
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
error: None,
},
Ok(Err(e)) => MethodResult {
error: Some(e),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
Err(_) => MethodResult {
error: Some("panic".into()),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
}
}
fn run_hf3c(elements: &[u8], positions: &[[f64; 3]]) -> MethodResult {
let elems = elements.to_vec();
let pos = positions.to_vec();
let t0 = Instant::now();
let (backend, used_gpu) = hf3c_backend(elements, positions);
let config = sci_form::hf::HfConfig {
n_cis_states: 0,
..Default::default()
};
match panic::catch_unwind(move || sci_form::compute_hf3c(&elems, &pos, &config)) {
Ok(Ok(r)) => {
let hartree_to_ev = 27.211386;
let orb = &r.orbital_energies;
let n_occ = elements.iter().map(|&z| z as usize).sum::<usize>() / 2;
let homo = if n_occ > 0 && n_occ <= orb.len() {
orb[n_occ - 1] * hartree_to_ev
} else {
0.0
};
let lumo = if n_occ < orb.len() {
orb[n_occ] * hartree_to_ev
} else {
0.0
};
MethodResult {
converged: r.converged,
total_energy_ev: r.energy * hartree_to_ev,
homo_ev: homo,
lumo_ev: lumo,
gap_ev: (lumo - homo).abs(),
heat_of_formation: None,
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
error: None,
}
}
Ok(Err(e)) => MethodResult {
error: Some(e),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
Err(_) => MethodResult {
error: Some("panic".into()),
time_ms: t0.elapsed().as_secs_f64() * 1000.0,
backend,
used_gpu,
..Default::default()
},
}
}
struct Stats {
n: usize,
mae: f64,
rmse: f64,
max_err: f64,
r_squared: f64,
}
fn compute_stats(pairs: &[(f64, f64)]) -> Stats {
let n = pairs.len();
if n == 0 {
return Stats {
n: 0,
mae: 0.0,
rmse: 0.0,
max_err: 0.0,
r_squared: 0.0,
};
}
let mae = pairs
.iter()
.map(|(calc, exp)| (calc - exp).abs())
.sum::<f64>()
/ n as f64;
let mse = pairs
.iter()
.map(|(calc, exp)| (calc - exp).powi(2))
.sum::<f64>()
/ n as f64;
let rmse = mse.sqrt();
let max_err = pairs
.iter()
.map(|(calc, exp)| (calc - exp).abs())
.fold(0.0_f64, f64::max);
let mean_exp = pairs.iter().map(|(_, e)| e).sum::<f64>() / n as f64;
let ss_res = pairs.iter().map(|(c, e)| (e - c).powi(2)).sum::<f64>();
let ss_tot = pairs
.iter()
.map(|(_, e)| (e - mean_exp).powi(2))
.sum::<f64>();
let r_squared = if ss_tot > 1e-12 {
1.0 - ss_res / ss_tot
} else {
1.0
};
Stats {
n,
mae,
rmse,
max_err,
r_squared,
}
}
#[test]
fn benchmark_100_molecules_all_methods() {
let json_str = include_str!("../fixtures/benchmark_100_molecules.json");
let fixture: Fixture = serde_json::from_str(json_str).expect("parse fixture JSON");
let molecules = &fixture.molecules;
assert!(
molecules.len() >= 100,
"Need at least 100 molecules, got {}",
molecules.len()
);
println!(
"\n╔══════════════════════════════════════════════════════════════════════════════════╗"
);
println!(
"║ BENCHMARK: {} molecules × 5 methods (PM3, GFN0, GFN1, GFN2, HF-3c) ║",
molecules.len()
);
println!(
"╚══════════════════════════════════════════════════════════════════════════════════╝\n"
);
let t_total = Instant::now();
let rows: Vec<MoleculeRow> = molecules.par_iter().map(evaluate_molecule).collect();
let elapsed_total = t_total.elapsed();
let embed_failures: Vec<String> = rows
.iter()
.filter(|r| !r.embed_ok)
.map(|r| format!("#{} {} ({})", r.id, r.name, r.smiles))
.collect();
if !embed_failures.is_empty() {
println!(
"⚠ Embed failures ({}/{}):",
embed_failures.len(),
molecules.len()
);
for f in &embed_failures {
println!(" {f}");
}
println!();
}
println!("Leyenda: PM3 muestra ΔHf (kcal/mol); GFN0/GFN1/GFN2/HF-3c muestran gap HOMO-LUMO (eV). ERR=error, NC=no convergió.");
println!("┌─────┬────────────────────────────┬──────────┬───────────┬───────────┬───────────┬───────────┬───────────┐");
println!("│ # │ Name │ Exp ΔHf │ PM3 ΔHf │ GFN0 gap │ GFN1 gap │ GFN2 gap │ HF3c gap │");
println!("├─────┼────────────────────────────┼──────────┼───────────┼───────────┼───────────┼───────────┼───────────┤");
for row in &rows {
let pm3_hof = method_cell_pm3(&row.pm3);
let fmt_gap = |mr: &MethodResult| -> String { method_cell_gap(mr) };
let name_trunc: String = if row.name.len() > 26 {
format!("{}…", &row.name[..25])
} else {
row.name.clone()
};
println!(
"│ {:>3} │ {:<26} │ {:>8.1} │ {} │ {} │ {} │ {} │ {} │",
row.id,
name_trunc,
row.exp.heat_of_formation,
pm3_hof,
fmt_gap(&row.gfn0),
fmt_gap(&row.gfn1),
fmt_gap(&row.gfn2),
fmt_gap(&row.hf3c),
);
}
println!("└─────┴────────────────────────────┴──────────┴───────────┴───────────┴───────────┴───────────┴───────────┘");
println!();
let mut issue_rows: Vec<&MoleculeRow> = rows
.iter()
.filter(|r| !r.embed_ok || row_issue_count(r) > 0)
.collect();
issue_rows.sort_by_key(|r| (std::cmp::Reverse(row_issue_count(r)), r.id));
if !issue_rows.is_empty() {
println!("┌─────┬────────────────────────────┬──────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────┐");
println!("│ # │ Molecule │ Emb │ PM3 ΔHf │ GFN0 gap │ GFN1 gap │ GFN2 gap │ HF3c gap │ Issues│");
println!("├─────┼────────────────────────────┼──────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────┤");
for row in issue_rows.iter().take(40) {
let name_trunc: String = if row.name.len() > 26 {
format!("{}…", &row.name[..25])
} else {
row.name.clone()
};
println!(
"│ {:>3} │ {:<26} │ {:^4} │ {:>8} │ {:>8} │ {:>8} │ {:>8} │ {:>8} │ {:>5} │",
row.id,
name_trunc,
if row.embed_ok { "OK" } else { "ERR" },
method_cell_pm3(&row.pm3),
method_cell_gap(&row.gfn0),
method_cell_gap(&row.gfn1),
method_cell_gap(&row.gfn2),
method_cell_gap(&row.hf3c),
row_issue_count(row),
);
}
if issue_rows.len() > 40 {
println!("│ ... │ {:<26} │ │ │ │ │ │ │ │", format!("{} more rows", issue_rows.len() - 40));
}
println!("└─────┴────────────────────────────┴──────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────┘");
println!();
}
let n_total = rows.len();
let n_embedded = rows.iter().filter(|r| r.embed_ok).count();
let count = |f: &dyn Fn(&MoleculeRow) -> &MethodResult| -> (usize, usize) {
let converged = rows.iter().filter(|r| r.embed_ok && f(r).converged).count();
let errors = rows
.iter()
.filter(|r| r.embed_ok && f(r).error.is_some())
.count();
(converged, errors)
};
let (pm3_conv, pm3_err) = count(&|r| &r.pm3);
let (gfn0_conv, gfn0_err) = count(&|r| &r.gfn0);
let (gfn1_conv, gfn1_err) = count(&|r| &r.gfn1);
let (gfn2_conv, gfn2_err) = count(&|r| &r.gfn2);
let (hf3c_conv, hf3c_err) = count(&|r| &r.hf3c);
println!("┌──────────────────────────────────────────────────────────────┐");
println!("│ CONVERGENCE SUMMARY │");
println!("├─────────┬──────────┬───────────┬────────────┬───────────────┤");
println!(
"│ Method │ Converged│ Errors │ Conv rate │ Embed OK: {}/{} │",
n_embedded, n_total
);
println!("├─────────┼──────────┼───────────┼────────────┼───────────────┤");
println!(
"│ PM3 │ {:>5}/{:<3} │ {:>5} │ {:>5.1}% │ │",
pm3_conv,
n_embedded,
pm3_err,
pm3_conv as f64 / n_embedded as f64 * 100.0
);
println!(
"│ GFN0 │ {:>5}/{:<3} │ {:>5} │ {:>5.1}% │ │",
gfn0_conv,
n_embedded,
gfn0_err,
gfn0_conv as f64 / n_embedded as f64 * 100.0
);
println!(
"│ GFN1 │ {:>5}/{:<3} │ {:>5} │ {:>5.1}% │ │",
gfn1_conv,
n_embedded,
gfn1_err,
gfn1_conv as f64 / n_embedded as f64 * 100.0
);
println!(
"│ GFN2 │ {:>5}/{:<3} │ {:>5} │ {:>5.1}% │ │",
gfn2_conv,
n_embedded,
gfn2_err,
gfn2_conv as f64 / n_embedded as f64 * 100.0
);
println!(
"│ HF-3c │ {:>5}/{:<3} │ {:>5} │ {:>5.1}% │ │",
hf3c_conv,
n_embedded,
hf3c_err,
hf3c_conv as f64 / n_embedded as f64 * 100.0
);
println!("└─────────┴──────────┴───────────┴────────────┴───────────────┘");
println!();
let pm3_hof_pairs: Vec<(f64, f64)> = rows
.iter()
.filter(|r| r.pm3.converged && r.pm3.heat_of_formation.is_some())
.map(|r| (r.pm3.heat_of_formation.unwrap(), r.exp.heat_of_formation))
.collect();
if !pm3_hof_pairs.is_empty() {
let s = compute_stats(&pm3_hof_pairs);
println!("┌──────────────────────────────────────────────────────────────┐");
println!("│ PM3 HEAT OF FORMATION vs EXPERIMENTAL (kcal/mol) │");
println!("├─────────────────┬────────────────────────────────────────────┤");
println!(
"│ N molecules │ {:>5} │",
s.n
);
println!(
"│ MAE │ {:>8.2} kcal/mol │",
s.mae
);
println!(
"│ RMSE │ {:>8.2} kcal/mol │",
s.rmse
);
println!(
"│ Max error │ {:>8.2} kcal/mol │",
s.max_err
);
println!(
"│ R² │ {:>8.4} │",
s.r_squared
);
println!("└─────────────────┴────────────────────────────────────────────┘");
let mut worst: Vec<_> = rows
.iter()
.filter(|r| r.pm3.converged && r.pm3.heat_of_formation.is_some())
.map(|r| {
let err = (r.pm3.heat_of_formation.unwrap() - r.exp.heat_of_formation).abs();
(
r.name.as_str(),
r.pm3.heat_of_formation.unwrap(),
r.exp.heat_of_formation,
err,
)
})
.collect();
worst.sort_by(|a, b| b.3.partial_cmp(&a.3).unwrap());
println!("\n Top-10 worst PM3 ΔHf predictions:");
for (i, (name, calc, exp, err)) in worst.iter().take(10).enumerate() {
println!(
" {:>2}. {:<24} calc={:>8.1} exp={:>8.1} err={:>7.1}",
i + 1,
name,
calc,
exp,
err
);
}
println!();
}
println!("┌──────────────────────────────────────────────────────────────┐");
println!("│ HOMO-LUMO GAP COMPARISON (eV) — all converged molecules │");
println!("├─────────┬───────┬──────────┬──────────┬──────────┬──────────┤");
println!("│ Method │ N │ Mean │ Std Dev │ Min │ Max │");
println!("├─────────┼───────┼──────────┼──────────┼──────────┼──────────┤");
let gap_stats = |name: &str, f: &dyn Fn(&MoleculeRow) -> &MethodResult| {
let gaps: Vec<f64> = rows
.iter()
.filter(|r| f(r).converged && f(r).error.is_none())
.map(|r| f(r).gap_ev)
.collect();
if gaps.is_empty() {
return;
}
let n = gaps.len();
let mean = gaps.iter().sum::<f64>() / n as f64;
let var = gaps.iter().map(|g| (g - mean).powi(2)).sum::<f64>() / n as f64;
let min = gaps.iter().cloned().fold(f64::MAX, f64::min);
let max = gaps.iter().cloned().fold(f64::MIN, f64::max);
println!(
"│ {:<7} │ {:>5} │ {:>8.3} │ {:>8.3} │ {:>8.3} │ {:>8.3} │",
name,
n,
mean,
var.sqrt(),
min,
max
);
};
gap_stats("PM3", &|r| &r.pm3);
gap_stats("GFN0", &|r| &r.gfn0);
gap_stats("GFN1", &|r| &r.gfn1);
gap_stats("GFN2", &|r| &r.gfn2);
gap_stats("HF-3c", &|r| &r.hf3c);
println!("└─────────┴───────┴──────────┴──────────┴──────────┴──────────┘");
println!();
let avg_time = |f: &dyn Fn(&MoleculeRow) -> &MethodResult| -> f64 {
let times: Vec<f64> = rows
.iter()
.filter(|r| r.embed_ok && f(r).error.is_none())
.map(|r| f(r).time_ms)
.collect();
if times.is_empty() {
0.0
} else {
times.iter().sum::<f64>() / times.len() as f64
}
};
println!("┌──────────────────────────────────────────────────────────────┐");
println!("│ TIMING (avg ms/molecule) │");
println!("├─────────┬──────────────────────────────────────────────────┤");
println!(
"│ PM3 │ {:>10.2} ms │",
avg_time(&|r| &r.pm3)
);
println!(
"│ GFN0 │ {:>10.2} ms │",
avg_time(&|r| &r.gfn0)
);
println!(
"│ GFN1 │ {:>10.2} ms │",
avg_time(&|r| &r.gfn1)
);
println!(
"│ GFN2 │ {:>10.2} ms │",
avg_time(&|r| &r.gfn2)
);
println!(
"│ HF-3c │ {:>10.2} ms │",
avg_time(&|r| &r.hf3c)
);
println!("├─────────┼──────────────────────────────────────────────────┤");
println!(
"│ TOTAL │ {:>10.2} s for {} molecules │",
elapsed_total.as_secs_f64(),
n_total
);
println!("└─────────┴──────────────────────────────────────────────────┘");
println!();
let mean_pm3_hof = if pm3_hof_pairs.is_empty() {
0.0
} else {
pm3_hof_pairs.iter().map(|(calc, _)| *calc).sum::<f64>() / pm3_hof_pairs.len() as f64
};
let mean_gap = |f: &dyn Fn(&MoleculeRow) -> &MethodResult| -> f64 {
let values: Vec<f64> = rows
.iter()
.filter(|r| f(r).converged && f(r).error.is_none())
.map(|r| f(r).gap_ev)
.collect();
if values.is_empty() {
0.0
} else {
values.iter().sum::<f64>() / values.len() as f64
}
};
println!("┌─────────┬────────────┬─────────┬───────────┬─────────────┬─────────────┐");
println!("│ Method │ Converged │ Errors │ Issues │ Mean value │ Avg time ms │");
println!("├─────────┼────────────┼─────────┼───────────┼─────────────┼─────────────┤");
for (method, converged, errors, mean_value, avg_ms) in [
(
MethodId::Pm3,
pm3_conv,
pm3_err,
mean_pm3_hof,
avg_time(&|r| &r.pm3),
),
(
MethodId::Gfn0,
gfn0_conv,
gfn0_err,
mean_gap(&|r| &r.gfn0),
avg_time(&|r| &r.gfn0),
),
(
MethodId::Gfn1,
gfn1_conv,
gfn1_err,
mean_gap(&|r| &r.gfn1),
avg_time(&|r| &r.gfn1),
),
(
MethodId::Gfn2,
gfn2_conv,
gfn2_err,
mean_gap(&|r| &r.gfn2),
avg_time(&|r| &r.gfn2),
),
(
MethodId::Hf3c,
hf3c_conv,
hf3c_err,
mean_gap(&|r| &r.hf3c),
avg_time(&|r| &r.hf3c),
),
] {
let issues = rows
.iter()
.filter(|r| {
let result = method_result(r, method);
result.error.is_some() || !result.converged
})
.count();
println!(
"│ {:<7} │ {:>4}/{:<4} │ {:>5} │ {:>5} │ {:>9.2} │ {:>9.2} │",
method_label(method),
converged,
n_embedded,
errors,
issues,
mean_value,
avg_ms,
);
}
println!("└─────────┴────────────┴─────────┴───────────┴─────────────┴─────────────┘");
println!();
println!("┌─────────┬───────────┬───────────┬──────────────────────┐");
println!("│ Method │ GPU rows │ CPU rows │ Backend label │");
println!("├─────────┼───────────┼───────────┼──────────────────────┤");
for (method, sample) in [
(
MethodId::Pm3,
rows.iter().find(|r| r.embed_ok).map(|r| &r.pm3),
),
(
MethodId::Gfn0,
rows.iter().find(|r| r.embed_ok).map(|r| &r.gfn0),
),
(
MethodId::Gfn1,
rows.iter().find(|r| r.embed_ok).map(|r| &r.gfn1),
),
(
MethodId::Gfn2,
rows.iter().find(|r| r.embed_ok).map(|r| &r.gfn2),
),
(
MethodId::Hf3c,
rows.iter().find(|r| r.embed_ok).map(|r| &r.hf3c),
),
] {
let gpu_rows = rows
.iter()
.filter(|r| method_result(r, method).used_gpu)
.count();
let cpu_rows = rows
.iter()
.filter(|r| !method_result(r, method).used_gpu)
.count();
let label = sample.map(|s| s.backend.as_str()).unwrap_or("N/A");
println!(
"│ {:<7} │ {:>5} │ {:>5} │ {:<20} │",
method_label(method),
gpu_rows,
cpu_rows,
label,
);
}
println!("└─────────┴───────────┴───────────┴──────────────────────┘");
println!();
let print_failures = |name: &str, f: &dyn Fn(&MoleculeRow) -> &MethodResult| {
let failures: Vec<_> = rows
.iter()
.filter(|r| r.embed_ok && f(r).error.is_some())
.map(|r| {
format!(
" #{} {} ({}) [{}]: {}",
r.id,
r.name,
r.smiles,
f(r).backend,
f(r).error.as_deref().unwrap_or("?"),
)
})
.collect();
if !failures.is_empty() {
println!("{} failures ({}):", name, failures.len());
for f in &failures {
println!(" {f}");
}
println!();
}
};
print_failures("PM3", &|r| &r.pm3);
print_failures("GFN0", &|r| &r.gfn0);
print_failures("GFN1", &|r| &r.gfn1);
print_failures("GFN2", &|r| &r.gfn2);
print_failures("HF-3c", &|r| &r.hf3c);
let embed_rate = n_embedded as f64 / n_total as f64;
assert!(
embed_rate >= 0.90,
"Embed success rate too low: {:.1}% ({}/{})",
embed_rate * 100.0,
n_embedded,
n_total,
);
let gfn1_rate = gfn1_conv as f64 / n_embedded.max(1) as f64;
assert!(
gfn1_rate >= 0.01,
"GFN1 convergence rate unexpectedly zero: {:.1}% ({}/{})",
gfn1_rate * 100.0,
gfn1_conv,
n_embedded,
);
let gfn2_rate = gfn2_conv as f64 / n_embedded.max(1) as f64;
assert!(
gfn2_rate >= 0.90,
"GFN2 convergence rate too low: {:.1}% ({}/{})",
gfn2_rate * 100.0,
gfn2_conv,
n_embedded,
);
let gfn0_rate = gfn0_conv as f64 / n_embedded.max(1) as f64;
assert!(
gfn0_rate >= 0.70,
"GFN0 convergence rate too low: {:.1}% ({}/{})",
gfn0_rate * 100.0,
gfn0_conv,
n_embedded,
);
let pm3_rate = pm3_conv as f64 / n_embedded.max(1) as f64;
println!(
"PM3 convergence: {:.1}% ({}/{})",
pm3_rate * 100.0,
pm3_conv,
n_embedded
);
let hf3c_rate = hf3c_conv as f64 / n_embedded.max(1) as f64;
println!(
"HF-3c convergence: {:.1}% ({}/{})",
hf3c_rate * 100.0,
hf3c_conv,
n_embedded
);
if !pm3_hof_pairs.is_empty() {
let s = compute_stats(&pm3_hof_pairs);
println!(
"PM3 ΔHf final stats: MAE={:.2}, RMSE={:.2}, R²={:.4}",
s.mae, s.rmse, s.r_squared
);
if s.mae > 50.0 {
println!("⚠ WARNING: PM3 MAE={:.1} kcal/mol is extremely high (expected <25 for ETKDG geometries)", s.mae);
}
}
println!(
"\n✓ Benchmark completed: {} molecules processed in {:.2}s",
n_total,
elapsed_total.as_secs_f64()
);
}