use super::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default, deny_unknown_fields)]
pub struct EnergyRefinement {
pub bounds: [f64; 2],
pub interval: usize,
pub initial_step: f64,
pub radius: f64,
pub step: f64,
}
impl Default for EnergyRefinement {
fn default() -> Self {
Self {
bounds: [-15., 15.],
interval: 250,
initial_step: 0.5,
radius: 1.,
step: 0.1,
}
}
}
impl EnergyRefinement {
pub fn validate(&self) -> Result<(), RmcError> {
require(
self.bounds.iter().all(|v| v.is_finite())
&& self.bounds[0] < self.bounds[1]
&& self.interval > 0
&& [self.initial_step, self.radius, self.step]
.iter()
.all(|v| v.is_finite() && *v > 0.)
&& (self.bounds[1] - self.bounds[0]) / self.initial_step <= 1000.
&& (2. * self.radius).min(self.bounds[1] - self.bounds[0]) / self.step <= 1000.,
"invalid ΔE₀ refinement bounds, interval or grid spacing",
)
}
pub(super) fn validate_problem(&self, problem: &EnsembleProblem) -> Result<(), RmcError> {
self.validate()?;
for dataset in &problem.datasets {
require(
self.bounds[0] <= dataset.exafs.delta_e0
&& dataset.exafs.delta_e0 <= self.bounds[1],
"initial ΔE₀ lies outside its refinement bounds",
)?;
for shift in self.bounds {
let mut data = dataset.exafs.clone();
data.delta_e0 = shift;
super::engine::shifted_grid(&data)?;
}
}
Ok(())
}
fn grid(&self, center: f64, initial: bool) -> Vec<f64> {
let [lo, hi] = if initial {
self.bounds
} else {
[
(center - self.radius).max(self.bounds[0]),
(center + self.radius).min(self.bounds[1]),
]
};
let step = if initial {
self.initial_step
} else {
self.step
};
let count = ((hi - lo) / step).ceil() as usize;
let mut grid: Vec<_> = (0..count).map(|i| (lo + i as f64 * step).min(hi)).collect();
grid.extend([center, hi]);
grid.sort_by(f64::total_cmp);
grid.dedup();
grid
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EnergyUpdate {
pub calculator: String,
pub before: Vec<f64>,
pub after: Vec<f64>,
pub score_before: f64,
pub score_after: f64,
pub candidates: Vec<usize>,
pub at_bound: Vec<bool>,
}
impl EnergyUpdate {
pub(super) fn validate(
&self,
policy: &EnergyRefinement,
datasets: usize,
) -> Result<(), RmcError> {
require(
!self.calculator.is_empty()
&& self.before.len() == datasets
&& self.after.len() == datasets
&& self.candidates.len() == datasets
&& self.at_bound.len() == datasets
&& self
.before
.iter()
.chain(&self.after)
.all(|v| v.is_finite() && *v >= policy.bounds[0] && *v <= policy.bounds[1])
&& self.candidates.iter().all(|v| *v > 0 && *v <= 2004)
&& self
.after
.iter()
.zip(&self.at_bound)
.all(|(v, bound)| policy.bounds.contains(v) == *bound)
&& self.score_before.is_finite()
&& self.score_after.is_finite()
&& self.score_after <= self.score_before,
"invalid saved ΔE₀ update",
)
}
}
pub(super) fn refine_energy<C: ExafsCalculator + ?Sized>(
problem: &EnsembleProblem,
settings: &SessionSettings,
prepared: &super::session::PreparedEnsemble,
start: &EnsembleState,
initial: bool,
calculator: &mut C,
) -> Result<(EnsembleState, EnergyUpdate), RmcError> {
let identity = calculator.identity();
let policy = settings
.energy_refinement
.as_ref()
.ok_or_else(|| RmcError::Invalid("ΔE₀ refinement is disabled".into()))?;
let before = start.energy_shifts(problem)?;
let mut shifts = before.clone();
let mut fixed = problem.clone();
fixed.structures = start.structures.clone();
let mut counts = Vec::new();
for (index, ¢er) in before.iter().enumerate() {
let amplitude = fixed.datasets[index].exafs.s02;
let mut best_shift = center;
let mut best_score = start.evaluation.datasets[index].score;
let mut count = 0;
for broad in [true, false].into_iter().filter(|broad| initial || !broad) {
let grid = policy.grid(best_shift, broad);
count += grid.len();
let result = calibrate_dataset(
&fixed,
index,
&CalibrationSettings {
delta_e0: grid,
s02_bounds: [amplitude, amplitude],
},
calculator,
)?;
if result.best.score < best_score {
best_score = result.best.score;
best_shift = result.best.delta_e0;
}
}
shifts[index] = best_shift;
counts.push(count);
}
let verified = super::session::evaluate_at_shifts(
problem,
settings,
prepared,
start.structures.clone(),
&shifts,
None,
calculator,
)?;
let best = if verified.evaluation.score < start.evaluation.score {
verified
} else {
start.clone()
};
let after = best.energy_shifts(problem)?;
let update = EnergyUpdate {
calculator: identity.clone(),
at_bound: after.iter().map(|v| policy.bounds.contains(v)).collect(),
before,
after,
score_before: start.evaluation.score,
score_after: best.evaluation.score,
candidates: counts,
};
require(
calculator.identity() == identity,
"calculator identity changed during ΔE₀ refinement",
)?;
Ok((best, update))
}