#![allow(clippy::needless_range_loop)]
use std::sync::Arc;
use molpack::objective::{compute_f, compute_fg};
use molpack::{F, InsideBoxRestraint, PackContext};
fn setup_cells(sys: &mut PackContext, cell_n: usize, cell_len: F) {
sys.ncells = [cell_n, cell_n, cell_n];
sys.cell_length = [cell_len; 3];
sys.pbc_min = [0.0; 3];
sys.pbc_length = [cell_len * cell_n as F; 3];
sys.resize_cell_arrays();
}
fn mixed_system() -> (PackContext, Vec<F>) {
let mut sys = PackContext::new(3, 3, 1);
sys.ntype_with_fixed = 1;
sys.nmols = vec![3];
sys.natoms = vec![1];
sys.idfirst = vec![0];
sys.comptype = vec![true];
sys.coor = vec![[0.0, 0.0, 0.0]];
sys.radius = vec![1.0; 3];
sys.radius_ini = vec![1.0; 3];
sys.fscale = vec![1.0; 3];
sys.ibmol = vec![0, 1, 2];
sys.sync_atom_props();
sys.restraints = vec![Arc::new(InsideBoxRestraint::new(
[0.0, 0.0, 0.0],
[5.0, 5.0, 5.0],
[false; 3],
))];
sys.iratom_offsets = vec![0, 1, 2, 3];
sys.iratom_data = vec![0, 0, 0];
setup_cells(&mut sys, 1, 10.0);
let x = vec![
6.0, 2.0, 2.0, 3.0, 2.0, 2.0, 3.5, 2.5, 2.0, 0.1, 0.2, 0.3, 0.0, 0.0, 0.0, 0.2, 0.1, 0.4,
];
(sys, x)
}
fn force_cache_miss(sys: &mut PackContext) {
sys.work.cached_geometry_valid = false;
}
#[test]
fn compute_f_cached_path_matches_fresh() {
let (mut sys_a, x) = mixed_system();
let (mut sys_b, _) = mixed_system();
let f_a1 = compute_f(&x, &mut sys_a);
let fdist_a1 = sys_a.fdist;
let frest_a1 = sys_a.frest;
force_cache_miss(&mut sys_b);
let f_b1 = compute_f(&x, &mut sys_b);
let fdist_b1 = sys_b.fdist;
let frest_b1 = sys_b.frest;
assert_eq!(
f_a1.to_bits(),
f_b1.to_bits(),
"compute_f first call must agree bitwise"
);
assert_eq!(fdist_a1.to_bits(), fdist_b1.to_bits());
assert_eq!(frest_a1.to_bits(), frest_b1.to_bits());
let f_a2 = compute_f(&x, &mut sys_a);
force_cache_miss(&mut sys_b);
let f_b2 = compute_f(&x, &mut sys_b);
assert_eq!(f_a2.to_bits(), f_b2.to_bits());
assert_eq!(f_a2.to_bits(), f_a1.to_bits(), "cache must be pure");
assert_eq!(sys_a.fdist.to_bits(), sys_b.fdist.to_bits());
assert_eq!(sys_a.frest.to_bits(), sys_b.frest.to_bits());
}
#[test]
fn compute_fg_cached_path_matches_fresh() {
let (mut sys_a, x) = mixed_system();
let (mut sys_b, _) = mixed_system();
let mut g_a1 = vec![0.0; x.len()];
let mut g_b1 = vec![0.0; x.len()];
let f_a1 = compute_fg(&x, &mut sys_a, &mut g_a1);
force_cache_miss(&mut sys_b);
let f_b1 = compute_fg(&x, &mut sys_b, &mut g_b1);
assert_eq!(f_a1.to_bits(), f_b1.to_bits());
for i in 0..x.len() {
assert_eq!(
g_a1[i].to_bits(),
g_b1[i].to_bits(),
"compute_fg first call: g[{i}] mismatch {} vs {}",
g_a1[i],
g_b1[i]
);
}
let mut g_a2 = vec![0.0; x.len()];
let mut g_b2 = vec![0.0; x.len()];
let f_a2 = compute_fg(&x, &mut sys_a, &mut g_a2);
force_cache_miss(&mut sys_b);
let f_b2 = compute_fg(&x, &mut sys_b, &mut g_b2);
assert_eq!(f_a2.to_bits(), f_b2.to_bits());
assert_eq!(f_a2.to_bits(), f_a1.to_bits(), "cache must be pure");
for i in 0..x.len() {
assert_eq!(g_a2[i].to_bits(), g_b2[i].to_bits());
assert_eq!(g_a1[i].to_bits(), g_a2[i].to_bits());
}
}
#[test]
fn compute_f_reuses_compute_fg_geometry() {
let (mut sys_a, x) = mixed_system();
let (mut sys_b, _) = mixed_system();
let mut g_a = vec![0.0; x.len()];
let _ = compute_fg(&x, &mut sys_a, &mut g_a);
let f_a = compute_f(&x, &mut sys_a);
let mut g_b = vec![0.0; x.len()];
force_cache_miss(&mut sys_b);
let _ = compute_fg(&x, &mut sys_b, &mut g_b);
force_cache_miss(&mut sys_b);
let f_b = compute_f(&x, &mut sys_b);
assert_eq!(f_a.to_bits(), f_b.to_bits());
assert_eq!(sys_a.fdist.to_bits(), sys_b.fdist.to_bits());
assert_eq!(sys_a.frest.to_bits(), sys_b.frest.to_bits());
}
#[test]
fn radii_swap_between_fg_and_f_cached_matches_fresh() {
let (mut sys_a, x) = mixed_system();
let (mut sys_b, _) = mixed_system();
let scaled: Vec<F> = sys_a.radius_ini.iter().map(|r| r * 1.2).collect();
let unscaled = sys_a.radius_ini.clone();
sys_a.radius = scaled.clone();
sys_a.sync_atom_props();
let mut g_a = vec![0.0; x.len()];
let _ = compute_fg(&x, &mut sys_a, &mut g_a);
sys_a.radius = unscaled.clone();
sys_a.sync_atom_props();
let f_a = compute_f(&x, &mut sys_a);
sys_b.radius = scaled.clone();
sys_b.sync_atom_props();
let mut g_b = vec![0.0; x.len()];
force_cache_miss(&mut sys_b);
let _ = compute_fg(&x, &mut sys_b, &mut g_b);
sys_b.radius = unscaled.clone();
sys_b.sync_atom_props();
force_cache_miss(&mut sys_b);
let f_b = compute_f(&x, &mut sys_b);
assert_eq!(f_a.to_bits(), f_b.to_bits());
assert_eq!(sys_a.fdist.to_bits(), sys_b.fdist.to_bits());
assert_eq!(sys_a.frest.to_bits(), sys_b.frest.to_bits());
}
#[test]
fn move_flag_true_bypasses_cache() {
let (mut sys, x) = mixed_system();
let _ = compute_f(&x, &mut sys);
assert!(sys.work.cached_geometry_valid);
sys.move_flag = true;
sys.fdist_atom.iter_mut().for_each(|v| *v = 0.0);
sys.frest_atom.iter_mut().for_each(|v| *v = 0.0);
let _ = compute_f(&x, &mut sys);
let fdist_move_a = sys.fdist_atom.clone();
let frest_move_a = sys.frest_atom.clone();
let (mut sys2, _) = mixed_system();
force_cache_miss(&mut sys2);
let _ = compute_f(&x, &mut sys2);
sys2.move_flag = true;
sys2.fdist_atom.iter_mut().for_each(|v| *v = 0.0);
sys2.frest_atom.iter_mut().for_each(|v| *v = 0.0);
force_cache_miss(&mut sys2);
let _ = compute_f(&x, &mut sys2);
assert_eq!(
fdist_move_a.len(),
sys2.fdist_atom.len(),
"fdist_atom shape mismatch"
);
for (i, (&a, &b)) in fdist_move_a.iter().zip(sys2.fdist_atom.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"fdist_atom[{i}] mismatch under move_flag: {a} vs {b}"
);
}
for (i, (&a, &b)) in frest_move_a.iter().zip(sys2.frest_atom.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"frest_atom[{i}] mismatch under move_flag: {a} vs {b}"
);
}
}