use molrs::types::F;
use std::collections::HashMap;
use std::env;
use std::sync::Arc;
use crate::numerics::numeric_controls;
use crate::restraint::AtomRestraint;
use crate::target::Target;
#[derive(Debug, Clone, Copy, Default)]
pub struct ViolationMetrics {
pub max_distance_violation: F,
pub max_constraint_penalty: F,
pub violating_pairs: usize,
pub violating_atoms: usize,
}
#[derive(Debug, Clone)]
pub struct ValidationReport {
pub expected_atoms: usize,
pub actual_atoms: usize,
pub expected_molecules: usize,
pub atom_count_ok: bool,
pub molecule_count_ok: bool,
pub distance_ok: bool,
pub constraints_ok: bool,
pub metrics: ViolationMetrics,
}
impl ValidationReport {
pub fn is_valid(&self) -> bool {
self.atom_count_ok && self.molecule_count_ok && self.distance_ok && self.constraints_ok
}
}
#[derive(Debug, Clone)]
struct ExpandedMol<'a> {
target: &'a Target,
start: usize,
end: usize,
molecule_id: usize,
}
#[derive(Clone, Default)]
struct AtomRestraints {
restraints: Vec<Arc<dyn AtomRestraint>>,
}
pub fn validate_from_targets(
targets: &[Target],
coordinates: &[[F; 3]],
tolerance: F,
precision: F,
) -> ValidationReport {
let expanded = expand_targets(targets);
let expected_atoms = expanded.last().map_or(0usize, |m| m.end);
let expected_molecules = expanded.len();
let (constraint_penalty, constraint_violating_atoms) =
constraint_metrics(&expanded, coordinates, precision);
let (distance_violation, pair_violations, distance_violating_atoms) =
distance_metrics(&expanded, coordinates, tolerance, precision);
let violating_atoms = union_count(
coordinates.len(),
&constraint_violating_atoms,
&distance_violating_atoms,
);
ValidationReport {
expected_atoms,
actual_atoms: coordinates.len(),
expected_molecules,
atom_count_ok: coordinates.len() == expected_atoms,
molecule_count_ok: expected_molecules > 0,
distance_ok: distance_violation <= precision,
constraints_ok: constraint_penalty <= precision,
metrics: ViolationMetrics {
max_distance_violation: distance_violation,
max_constraint_penalty: constraint_penalty,
violating_pairs: pair_violations,
violating_atoms,
},
}
}
fn expand_targets(targets: &[Target]) -> Vec<ExpandedMol<'_>> {
let mut expanded = Vec::new();
let mut cursor = 0usize;
let mut mol_id = 0usize;
for target in targets.iter() {
let nmols = if target.fixed_at.is_some() {
1
} else {
target.count
};
for _ in 0..nmols {
let start = cursor;
let end = start + target.natoms();
expanded.push(ExpandedMol {
target,
start,
end,
molecule_id: mol_id,
});
cursor = end;
mol_id += 1;
}
}
expanded
}
fn atom_restraints(target: &Target) -> Vec<AtomRestraints> {
let mut per_atom = vec![
AtomRestraints {
restraints: target.molecule_restraints.clone(),
};
target.natoms()
];
for (indices, restraint) in &target.atom_restraints {
for &idx in indices {
if let Some(slot) = per_atom.get_mut(idx) {
slot.restraints.push(Arc::clone(restraint));
}
}
}
per_atom
}
fn constraint_metrics(
expanded: &[ExpandedMol<'_>],
coordinates: &[[F; 3]],
precision: F,
) -> (F, Vec<bool>) {
let mut max_penalty = 0.0 as F;
let mut violating_atoms = vec![false; coordinates.len()];
let penalty_eps = precision;
for mol in expanded {
let per_atom = atom_restraints(mol.target);
for (local_i, atom_i) in (mol.start..mol.end).enumerate() {
let pos = coordinates[atom_i];
let mut atom_penalty = 0.0 as F;
for r in &per_atom[local_i].restraints {
atom_penalty += r.f(&pos, 1.0, crate::numerics::DEFAULT_SCALE2);
}
if atom_penalty > max_penalty {
max_penalty = atom_penalty;
}
if atom_penalty > penalty_eps {
violating_atoms[atom_i] = true;
}
}
}
(max_penalty, violating_atoms)
}
fn distance_metrics(
expanded: &[ExpandedMol<'_>],
coordinates: &[[F; 3]],
tolerance: F,
precision: F,
) -> (F, usize, Vec<bool>) {
if coordinates.is_empty() {
return (0.0, 0, Vec::new());
}
let mut molecule_of_atom = vec![0usize; coordinates.len()];
for mol in expanded {
for atom_mol in molecule_of_atom.iter_mut().take(mol.end).skip(mol.start) {
*atom_mol = mol.molecule_id;
}
}
let mut minc = [F::INFINITY; 3];
let mut maxc = [F::NEG_INFINITY; 3];
for p in coordinates {
for k in 0..3 {
minc[k] = minc[k].min(p[k]);
maxc[k] = maxc[k].max(p[k]);
}
}
let cell = tolerance.max(1.0e-6);
let inv = 1.0 / cell;
let mut buckets: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
let mut max_violation = 0.0 as F;
let mut violating_pairs = 0usize;
let mut violating_atoms = vec![false; coordinates.len()];
let eps = precision.max(numeric_controls().epsrel);
let debug = env::var("MOLPACK_DEBUG_VALIDATION").is_ok();
let mut debug_left = 5usize;
for (i, p) in coordinates.iter().enumerate() {
let cx = ((p[0] - minc[0]) * inv).floor() as i64;
let cy = ((p[1] - minc[1]) * inv).floor() as i64;
let cz = ((p[2] - minc[2]) * inv).floor() as i64;
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
let key = (cx + dx, cy + dy, cz + dz);
if let Some(list) = buckets.get(&key) {
for &j in list {
if molecule_of_atom[i] == molecule_of_atom[j] {
continue;
}
let q = coordinates[j];
let d2 = (p[0] - q[0]).powi(2)
+ (p[1] - q[1]).powi(2)
+ (p[2] - q[2]).powi(2);
let d = d2.sqrt();
if d < tolerance {
let v = tolerance - d;
if v > max_violation {
max_violation = v;
}
if v > eps {
violating_pairs += 1;
violating_atoms[i] = true;
violating_atoms[j] = true;
if debug && debug_left > 0 {
eprintln!(
"validation pair: i={} j={} d={:.6} tol={:.3} v={:.6} mi={} mj={}",
i,
j,
d,
tolerance,
v,
molecule_of_atom[i],
molecule_of_atom[j]
);
debug_left -= 1;
}
}
}
}
}
}
}
}
buckets.entry((cx, cy, cz)).or_default().push(i);
}
(max_violation, violating_pairs, violating_atoms)
}
fn union_count(n: usize, a: &[bool], b: &[bool]) -> usize {
if a.len() != n || b.len() != n {
return 0;
}
(0..n).filter(|&i| a[i] || b[i]).count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_target_declared_first_skips_its_internal_contacts() {
let a = Target::from_coords(
&[
[0.0, 0.0, 0.0],
[1.5, 0.0, 0.0],
[0.0, 1.5, 0.0],
[0.0, 0.0, 1.5],
],
&[1.0; 4],
1,
)
.fixed_at([0.0, 0.0, 0.0]);
let b = Target::from_coords(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], &[1.0; 2], 2);
let coords = vec![
[0.0, 0.0, 0.0],
[1.5, 0.0, 0.0],
[0.0, 1.5, 0.0],
[0.0, 0.0, 1.5],
[50.0, 50.0, 50.0],
[51.0, 50.0, 50.0],
[60.0, 60.0, 60.0],
[61.0, 60.0, 60.0],
];
let report = validate_from_targets(&[a, b], &coords, 2.0, 1e-2);
assert!(
report.is_valid(),
"fixed solute's internal contacts must not count as overlaps: {report:?}"
);
assert_eq!(report.metrics.violating_pairs, 0);
}
}