1use molrs::types::F;
2use std::collections::HashMap;
3use std::env;
4
5use std::sync::Arc;
6
7use crate::numerics::numeric_controls;
8use crate::restraint::AtomRestraint;
9use crate::target::Target;
10
11#[derive(Debug, Clone, Copy, Default)]
13pub struct ViolationMetrics {
14 pub max_distance_violation: F,
16 pub max_constraint_penalty: F,
18 pub violating_pairs: usize,
20 pub violating_atoms: usize,
22}
23
24#[derive(Debug, Clone)]
26pub struct ValidationReport {
27 pub expected_atoms: usize,
28 pub actual_atoms: usize,
29 pub expected_molecules: usize,
30 pub atom_count_ok: bool,
32 pub molecule_count_ok: bool,
34 pub distance_ok: bool,
36 pub constraints_ok: bool,
38 pub metrics: ViolationMetrics,
40}
41
42impl ValidationReport {
43 pub fn is_valid(&self) -> bool {
44 self.atom_count_ok && self.molecule_count_ok && self.distance_ok && self.constraints_ok
45 }
46}
47
48#[derive(Debug, Clone)]
49struct ExpandedMol<'a> {
50 target: &'a Target,
51 start: usize,
52 end: usize,
53 molecule_id: usize,
54}
55
56#[derive(Clone, Default)]
57struct AtomRestraints {
58 restraints: Vec<Arc<dyn AtomRestraint>>,
59}
60
61pub fn validate_from_targets(
63 targets: &[Target],
64 coordinates: &[[F; 3]],
65 tolerance: F,
66 precision: F,
67) -> ValidationReport {
68 let expanded = expand_targets(targets);
69 let expected_atoms = expanded.last().map_or(0usize, |m| m.end);
70 let expected_molecules = expanded.len();
71
72 let (constraint_penalty, constraint_violating_atoms) =
73 constraint_metrics(&expanded, coordinates, precision);
74 let (distance_violation, pair_violations, distance_violating_atoms) =
75 distance_metrics(&expanded, coordinates, tolerance, precision);
76
77 let violating_atoms = union_count(
78 coordinates.len(),
79 &constraint_violating_atoms,
80 &distance_violating_atoms,
81 );
82
83 ValidationReport {
84 expected_atoms,
85 actual_atoms: coordinates.len(),
86 expected_molecules,
87 atom_count_ok: coordinates.len() == expected_atoms,
88 molecule_count_ok: expected_molecules > 0,
89 distance_ok: distance_violation <= precision,
90 constraints_ok: constraint_penalty <= precision,
91 metrics: ViolationMetrics {
92 max_distance_violation: distance_violation,
93 max_constraint_penalty: constraint_penalty,
94 violating_pairs: pair_violations,
95 violating_atoms,
96 },
97 }
98}
99
100fn expand_targets(targets: &[Target]) -> Vec<ExpandedMol<'_>> {
101 let mut expanded = Vec::new();
108 let mut cursor = 0usize;
109 let mut mol_id = 0usize;
110
111 for target in targets.iter() {
112 let nmols = if target.fixed_at.is_some() {
113 1
114 } else {
115 target.count
116 };
117 for _ in 0..nmols {
118 let start = cursor;
119 let end = start + target.natoms();
120 expanded.push(ExpandedMol {
121 target,
122 start,
123 end,
124 molecule_id: mol_id,
125 });
126 cursor = end;
127 mol_id += 1;
128 }
129 }
130
131 expanded
132}
133
134fn atom_restraints(target: &Target) -> Vec<AtomRestraints> {
135 let mut per_atom = vec![
136 AtomRestraints {
137 restraints: target.molecule_restraints.clone(),
138 };
139 target.natoms()
140 ];
141
142 for (indices, restraint) in &target.atom_restraints {
143 for &idx in indices {
144 if let Some(slot) = per_atom.get_mut(idx) {
145 slot.restraints.push(Arc::clone(restraint));
146 }
147 }
148 }
149 per_atom
150}
151
152fn constraint_metrics(
153 expanded: &[ExpandedMol<'_>],
154 coordinates: &[[F; 3]],
155 precision: F,
156) -> (F, Vec<bool>) {
157 let mut max_penalty = 0.0 as F;
158 let mut violating_atoms = vec![false; coordinates.len()];
159 let penalty_eps = precision;
160
161 for mol in expanded {
162 let per_atom = atom_restraints(mol.target);
163 for (local_i, atom_i) in (mol.start..mol.end).enumerate() {
164 let pos = coordinates[atom_i];
165 let mut atom_penalty = 0.0 as F;
166 for r in &per_atom[local_i].restraints {
167 atom_penalty += r.f(&pos, 1.0, crate::numerics::DEFAULT_SCALE2);
168 }
169 if atom_penalty > max_penalty {
170 max_penalty = atom_penalty;
171 }
172 if atom_penalty > penalty_eps {
173 violating_atoms[atom_i] = true;
174 }
175 }
176 }
177
178 (max_penalty, violating_atoms)
179}
180
181fn distance_metrics(
182 expanded: &[ExpandedMol<'_>],
183 coordinates: &[[F; 3]],
184 tolerance: F,
185 precision: F,
186) -> (F, usize, Vec<bool>) {
187 if coordinates.is_empty() {
188 return (0.0, 0, Vec::new());
189 }
190
191 let mut molecule_of_atom = vec![0usize; coordinates.len()];
192 for mol in expanded {
193 for atom_mol in molecule_of_atom.iter_mut().take(mol.end).skip(mol.start) {
194 *atom_mol = mol.molecule_id;
195 }
196 }
197
198 let mut minc = [F::INFINITY; 3];
199 let mut maxc = [F::NEG_INFINITY; 3];
200 for p in coordinates {
201 for k in 0..3 {
202 minc[k] = minc[k].min(p[k]);
203 maxc[k] = maxc[k].max(p[k]);
204 }
205 }
206
207 let cell = tolerance.max(1.0e-6);
208 let inv = 1.0 / cell;
209 let mut buckets: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
210 let mut max_violation = 0.0 as F;
211 let mut violating_pairs = 0usize;
212 let mut violating_atoms = vec![false; coordinates.len()];
213 let eps = precision.max(numeric_controls().epsrel);
214 let debug = env::var("MOLPACK_DEBUG_VALIDATION").is_ok();
215 let mut debug_left = 5usize;
216
217 for (i, p) in coordinates.iter().enumerate() {
218 let cx = ((p[0] - minc[0]) * inv).floor() as i64;
219 let cy = ((p[1] - minc[1]) * inv).floor() as i64;
220 let cz = ((p[2] - minc[2]) * inv).floor() as i64;
221
222 for dx in -1..=1 {
223 for dy in -1..=1 {
224 for dz in -1..=1 {
225 let key = (cx + dx, cy + dy, cz + dz);
226 if let Some(list) = buckets.get(&key) {
227 for &j in list {
228 if molecule_of_atom[i] == molecule_of_atom[j] {
229 continue;
230 }
231 let q = coordinates[j];
232 let d2 = (p[0] - q[0]).powi(2)
233 + (p[1] - q[1]).powi(2)
234 + (p[2] - q[2]).powi(2);
235 let d = d2.sqrt();
236 if d < tolerance {
237 let v = tolerance - d;
238 if v > max_violation {
239 max_violation = v;
240 }
241 if v > eps {
242 violating_pairs += 1;
243 violating_atoms[i] = true;
244 violating_atoms[j] = true;
245 if debug && debug_left > 0 {
246 eprintln!(
247 "validation pair: i={} j={} d={:.6} tol={:.3} v={:.6} mi={} mj={}",
248 i,
249 j,
250 d,
251 tolerance,
252 v,
253 molecule_of_atom[i],
254 molecule_of_atom[j]
255 );
256 debug_left -= 1;
257 }
258 }
259 }
260 }
261 }
262 }
263 }
264 }
265
266 buckets.entry((cx, cy, cz)).or_default().push(i);
267 }
268
269 (max_violation, violating_pairs, violating_atoms)
270}
271
272fn union_count(n: usize, a: &[bool], b: &[bool]) -> usize {
273 if a.len() != n || b.len() != n {
274 return 0;
275 }
276 (0..n).filter(|&i| a[i] || b[i]).count()
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
291 fn fixed_target_declared_first_skips_its_internal_contacts() {
292 let a = Target::from_coords(
294 &[
295 [0.0, 0.0, 0.0],
296 [1.5, 0.0, 0.0],
297 [0.0, 1.5, 0.0],
298 [0.0, 0.0, 1.5],
299 ],
300 &[1.0; 4],
301 1,
302 )
303 .fixed_at([0.0, 0.0, 0.0]);
304 let b = Target::from_coords(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], &[1.0; 2], 2);
306
307 let coords = vec![
309 [0.0, 0.0, 0.0],
310 [1.5, 0.0, 0.0],
311 [0.0, 1.5, 0.0],
312 [0.0, 0.0, 1.5],
313 [50.0, 50.0, 50.0],
314 [51.0, 50.0, 50.0],
315 [60.0, 60.0, 60.0],
316 [61.0, 60.0, 60.0],
317 ];
318 let report = validate_from_targets(&[a, b], &coords, 2.0, 1e-2);
319 assert!(
320 report.is_valid(),
321 "fixed solute's internal contacts must not count as overlaps: {report:?}"
322 );
323 assert_eq!(report.metrics.violating_pairs, 0);
324 }
325}