use rexafs::rmc::*;
use rexafs::structure::{Edge, Lattice, Site, Structure};
use std::ops::ControlFlow;
#[derive(Default)]
struct Dimer {
calls: usize,
}
impl ExafsCalculator for Dimer {
fn name(&self) -> &str {
"analytic test dimer"
}
fn calculate(
&mut self,
c: &Configuration,
a: usize,
_: Edge,
k: &[f64],
) -> Result<Vec<f64>, RmcError> {
self.calls += 1;
let b = 1 - a;
let r = (0..3)
.map(|i| (c.atoms[a].position[i] - c.atoms[b].position[i]).powi(2))
.sum::<f64>()
.sqrt();
Ok(k.iter().map(|k| (2.0 * k * r).sin() / r.powi(2)).collect())
}
}
fn problem(r: f64) -> RmcProblem {
let k: Vec<_> = (0..81).map(|i| 3.0 + i as f64 * 0.1).collect();
let target: Vec<_> = k
.iter()
.map(|k| (2.0 * k * 2.5_f64).sin() / 2.5_f64.powi(2))
.collect();
RmcProblem {
configuration: Configuration {
cell: None,
atoms: vec![
Atom {
atomic_number: 29,
position: [0.0; 3],
},
Atom {
atomic_number: 29,
position: [r, 0.0, 0.0],
},
],
},
datasets: vec![ExafsDataset {
name: "toy".into(),
absorbers: vec![0],
edge: Edge::K,
k,
chi: target,
sigma: vec![0.01; 81],
weight: 1.0,
kweight: 0,
s02: 1.0,
delta_e0: 0.0,
}],
}
}
#[test]
fn recovers_distance_and_repeats_exactly_without_mutating_input() {
let p = problem(2.7);
let original = p.clone();
let settings = RmcSettings {
steps: 600,
temperature: 0.0,
seed: 42,
movable_atoms: vec![1],
..Default::default()
};
let result = refine(&p, &settings, &mut Dimer::default()).unwrap();
assert_eq!(p, original);
assert_eq!(
result,
refine(&p, &settings, &mut Dimer::default()).unwrap()
);
assert!(result.best.evaluation.score < result.initial.evaluation.score * 1e-5);
let pos = result.best.configuration.atoms[1].position;
let distance = pos.iter().map(|v| v * v).sum::<f64>().sqrt();
assert!((distance - 2.5).abs() < 0.001, "distance={distance}");
assert_eq!(result.best.configuration.atoms[0], p.configuration.atoms[0]);
assert!(result.history.iter().all(|s| s.best_score <= s.score));
assert_eq!(
result.best.evaluation,
evaluate(
&RmcProblem {
configuration: result.best.configuration.clone(),
datasets: p.datasets.clone(),
},
&mut Dimer::default()
)
.unwrap()
);
let serialized = serde_json::to_string(&result).unwrap();
assert_eq!(
result,
serde_json::from_str::<RmcResult>(&serialized).unwrap()
);
}
#[test]
fn rejected_moves_restore_coordinates_and_spectra() {
let p = problem(2.5);
let settings = RmcSettings {
steps: 40,
temperature: 0.0,
movable_atoms: vec![1],
..Default::default()
};
let result = refine(&p, &settings, &mut Dimer::default()).unwrap();
assert!(result
.history
.iter()
.all(|s| !s.accepted && !s.constraint_rejected));
assert_eq!(result.initial, result.final_state);
assert_eq!(result.initial, result.best);
}
#[test]
fn hard_constraints_skip_calculator_and_callback_can_stop() {
let mut calculator = Dimer::default();
let settings = RmcSettings {
steps: 50,
max_displacement: Some(1e-12),
..Default::default()
};
let result = refine_with_progress(&problem(2.7), &settings, &mut calculator, |step| {
if step.step == 7 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
})
.unwrap();
assert_eq!(calculator.calls, 1);
assert!(result.stopped);
assert_eq!(result.history.len(), 7);
assert!(result
.history
.iter()
.all(|s| s.constraint_rejected && s.trial_score.is_none()));
assert_eq!(result.initial, result.final_state);
}
#[test]
fn positive_tolerance_accepts_uphill_while_preserving_best() {
let settings = RmcSettings {
steps: 5,
temperature: 1e100,
movable_atoms: vec![1],
..Default::default()
};
let result = refine(&problem(2.5), &settings, &mut Dimer::default()).unwrap();
assert!(result
.history
.iter()
.any(|s| s.accepted && s.score > result.initial.evaluation.score));
assert_eq!(result.best, result.initial);
assert_ne!(result.final_state, result.best);
}
struct Grid;
impl ExafsCalculator for Grid {
fn name(&self) -> &str {
"grid and absorber test"
}
fn calculate(
&mut self,
_: &Configuration,
a: usize,
_: Edge,
k: &[f64],
) -> Result<Vec<f64>, RmcError> {
Ok(k.iter().map(|v| v + 2.0 * a as f64).collect())
}
}
#[test]
fn averages_before_residual_and_applies_energy_amplitude_and_joint_weights() {
let mut p = problem(2.5);
let d = &mut p.datasets[0];
d.k = vec![2.0, 3.0];
d.chi = vec![0.0, 0.0];
d.sigma = vec![2.0, 2.0];
d.absorbers = vec![0, 1];
d.s02 = 0.8;
d.kweight = 1;
d.weight = 3.0;
d.delta_e0 = 2.0;
let second = ExafsDataset {
name: "second edge".into(),
weight: 2.0,
edge: Edge::L3,
..d.clone()
};
p.datasets.push(second);
let result = evaluate(&p, &mut Grid).unwrap();
let etok = rexafs::xafs::xafsutils::constants::ETOK;
let expected: Vec<_> = [2.0_f64, 3.0]
.iter()
.map(|k| 0.8 * ((k * k - etok * 2.0).sqrt() + 1.0))
.collect();
assert_eq!(result.datasets[0].chi, expected);
let unweighted_score = expected
.iter()
.zip([2.0_f64, 3.0])
.map(|(c, k)| (c * k / 2.0).powi(2))
.sum::<f64>()
/ 2.0;
assert!((result.score - 5.0 * unweighted_score).abs() < 1e-12);
}
#[test]
fn invalid_inputs_fail_before_calculation() {
let settings = RmcSettings::default();
let mut variants = Vec::new();
let p = problem(2.5);
let mut bad = p.clone();
bad.datasets[0].sigma[0] = 0.0;
variants.push(bad);
let mut bad = p.clone();
bad.datasets[0].k[1] = bad.datasets[0].k[0];
variants.push(bad);
let mut bad = p.clone();
bad.datasets[0].chi[0] = f64::NAN;
variants.push(bad);
let mut bad = p.clone();
bad.datasets[0].absorbers = vec![0, 0];
variants.push(bad);
let mut bad = p.clone();
bad.datasets[0].absorbers = vec![2];
variants.push(bad);
let mut bad = p.clone();
bad.datasets[0].absorbers = vec![0, 1];
bad.configuration.atoms[1].atomic_number = 8;
variants.push(bad);
let mut bad = p.clone();
bad.datasets[0].delta_e0 = 10000.0;
variants.push(bad);
let mut bad = p.clone();
bad.configuration.atoms[1].position = [f64::INFINITY, 0.0, 0.0];
variants.push(bad);
let mut bad = p.clone();
bad.configuration.cell = Some([[0.0; 3]; 3]);
variants.push(bad);
let mut bad = p.clone();
bad.configuration.atoms[1].position = [0.0; 3];
variants.push(bad);
for bad in variants {
let mut calculator = Dimer::default();
assert!(refine(&bad, &settings, &mut calculator).is_err());
assert_eq!(calculator.calls, 0);
}
for bad in [
RmcSettings {
temperature: -1.0,
..settings.clone()
},
RmcSettings {
movable_atoms: vec![1, 1],
..settings.clone()
},
RmcSettings {
step_size: f64::NAN,
..settings
},
] {
assert!(refine(&p, &bad, &mut Dimer::default()).is_err());
}
let mut json = serde_json::to_value(RmcSettings::default()).unwrap();
json["temprature"] = 0.into();
assert!(serde_json::from_value::<RmcSettings>(json).is_err());
}
#[test]
fn periodic_constraints_detect_boundary_skew_and_self_image_contacts() {
let settings = RmcSettings {
steps: 0,
min_distance: 0.8,
..Default::default()
};
let mut p = problem(2.5);
p.configuration.cell = Some([[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]);
p.configuration.atoms[1].position = [9.8, 0.0, 0.0];
assert!(refine(&p, &settings, &mut Grid).is_err());
p.configuration.cell = Some([[10.0, 0.0, 0.0], [9.9, 1.0, 0.0], [0.0, 0.0, 10.0]]);
p.configuration.atoms[1].position = [9.751, 0.49, 0.0];
assert!(refine(&p, &settings, &mut Grid).is_err());
p.configuration.atoms.truncate(1);
p.configuration.cell = Some([[10.0, 0.0, 0.0], [9.9, 0.3, 0.0], [0.0, 0.0, 10.0]]);
assert!(refine(&p, &settings, &mut Grid).is_err());
p.configuration.cell = Some([[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]);
let result = refine(&p, &settings, &mut Grid).unwrap();
assert!(result.history.is_empty());
assert!(!result.stopped);
}
#[test]
fn expands_supercell_and_rejects_implicit_disorder() {
let mut structure = Structure::new(
"Cu",
Lattice::cubic(3.6).unwrap(),
vec![Site::new("Cu1", "Cu", [0.0; 3])],
);
let config = Configuration::from_structure(&structure, [2, 1, 1]).unwrap();
assert_eq!(config.atoms.len(), 2);
assert_eq!(config.atoms[1].position, [3.6, 0.0, 0.0]);
assert_eq!(config.cell.unwrap()[0], [7.2, 0.0, 0.0]);
structure.sites[0].species[0].occupancy = 0.5;
assert!(Configuration::from_structure(&structure, [1, 1, 1]).is_err());
}
struct InvalidCalculator;
impl ExafsCalculator for InvalidCalculator {
fn name(&self) -> &str {
"invalid"
}
fn calculate(
&mut self,
_: &Configuration,
_: usize,
_: Edge,
k: &[f64],
) -> Result<Vec<f64>, RmcError> {
Ok(vec![f64::NAN; k.len()])
}
}
#[test]
fn invalid_backend_spectrum_is_an_error() {
assert!(refine(
&problem(2.5),
&RmcSettings::default(),
&mut InvalidCalculator
)
.is_err());
}