use super::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CalibrationSettings {
pub delta_e0: Vec<f64>,
pub s02_bounds: [f64; 2],
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CalibrationPoint {
pub delta_e0: f64,
pub s02: f64,
pub score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CalibrationResult {
pub calculator: String,
pub dataset: String,
pub candidates: Vec<CalibrationPoint>,
pub best: CalibrationPoint,
pub chi: Vec<f64>,
pub report: FitReport,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FitReport {
pub name: String,
pub points: usize,
pub objective_score: f64,
pub chi_rmse: f64,
pub normalized_k_residual: Option<f64>,
pub amplitude_at_bound: Option<bool>,
}
pub fn fit_report(
dataset: &ExafsDataset,
model: &[f64],
objective: &Objective,
) -> Result<FitReport, RmcError> {
let score = objective.score(dataset, model)?;
let mut numerator = 0.;
let mut denominator = 0.;
let mut squares = 0.;
for (((&q, &observed), &sigma), &calculated) in dataset
.k
.iter()
.zip(&dataset.chi)
.zip(&dataset.sigma)
.zip(model)
{
let difference = calculated - observed;
squares += difference * difference;
let weight = q.powi(i32::from(dataset.kweight)) / sigma;
numerator += (weight * difference).powi(2);
denominator += (weight * observed).powi(2);
}
require(
squares.is_finite() && numerator.is_finite() && denominator.is_finite(),
"fit report overflow",
)?;
Ok(FitReport {
name: dataset.name.clone(),
points: model.len(),
objective_score: score,
chi_rmse: (squares / model.len() as f64).sqrt(),
normalized_k_residual: if denominator > 0. {
Some(numerator / denominator)
} else {
None
},
amplitude_at_bound: None,
})
}
pub fn calibrate_dataset<C: ExafsCalculator + ?Sized>(
problem: &EnsembleProblem,
dataset: usize,
settings: &CalibrationSettings,
calculator: &mut C,
) -> Result<CalibrationResult, RmcError> {
require(
dataset < problem.datasets.len(),
"calibration dataset out of range",
)?;
require(
!settings.delta_e0.is_empty()
&& settings.delta_e0.len() <= 10_000
&& settings.delta_e0.iter().all(|v| v.is_finite())
&& settings.delta_e0.windows(2).all(|v| v[1] > v[0])
&& settings.s02_bounds.iter().all(|v| v.is_finite() && *v > 0.)
&& settings.s02_bounds[1] >= settings.s02_bounds[0],
"invalid calibration grid or amplitude bounds",
)?;
let mut fixed = problem.clone();
super::session::normalize(&mut fixed.structures)?;
fixed.datasets = vec![problem.datasets[dataset].clone()];
let session = SessionSettings {
moves: RmcSettings {
steps: 0,
..Default::default()
},
..Default::default()
};
super::session::prepare(&fixed, &session)?;
let data = &fixed.datasets[0];
let objective = data.objective.prepare(&data.exafs.k)?;
require(
settings.delta_e0.len().saturating_mul(data.exafs.k.len()) <= 1_000_000,
"calibration union grid exceeds one million points",
)?;
let mut shifted = Vec::new();
let mut grid = Vec::new();
for &energy in &settings.delta_e0 {
let mut candidate = data.exafs.clone();
candidate.delta_e0 = energy;
let q = super::engine::shifted_grid(&candidate)?;
grid.extend(&q);
shifted.push(q);
}
grid.sort_by(f64::total_cmp);
grid.dedup();
let mut requests = Vec::new();
let mut weights = Vec::new();
for (s, structure) in fixed.structures.iter().enumerate() {
let absorbers = if data.absorbers_by_structure.is_empty() {
&data.exafs.absorbers
} else {
&data.absorbers_by_structure[s]
};
for &absorber in absorbers {
requests.push(CalculationRequest {
structure: s,
configuration: &structure.configuration,
absorber,
edge: data.exafs.edge,
k: &grid,
options: data.refeff.as_ref(),
paths: false,
});
weights.push(structure.weight / absorbers.len() as f64);
}
}
let spectra = calculator.calculate_batch(&requests)?;
require(
spectra.len() == requests.len(),
"calibration backend returned wrong batch size",
)?;
let mut theory = vec![0.; grid.len()];
for (spectrum, weight) in spectra.into_iter().zip(weights) {
require(
spectrum.chi.len() == grid.len() && spectrum.chi.iter().all(|v| v.is_finite()),
"calibration backend returned invalid spectrum",
)?;
for (v, x) in theory.iter_mut().zip(spectrum.chi) {
*v += weight * x;
}
}
let zero = vec![0.; data.exafs.k.len()];
let f0 = objective.score(&data.exafs, &zero)?;
let mut zero_data = data.exafs.clone();
zero_data.chi = zero;
let mut candidates = Vec::new();
let mut best_model = Vec::new();
let mut best_score = f64::INFINITY;
let mut best_index = 0;
for (index, (&energy, q)) in settings.delta_e0.iter().zip(shifted).enumerate() {
let raw: Vec<_> = q
.iter()
.map(|v| {
theory[grid
.binary_search_by(|q| q.total_cmp(v))
.expect("union contains each requested k")]
})
.collect();
let quadratic = objective.score(&zero_data, &raw)?;
require(
quadratic.is_finite() && quadratic > 0.,
"calibration amplitude is unresolved: model has no objective-space norm",
)?;
let balance = if f0 > 0. {
quadratic.sqrt() / f0.sqrt()
} else {
1.
};
require(
balance.is_finite() && balance > 0.,
"calibration normalization is numerically unresolved",
)?;
let mut balanced = data.exafs.clone();
for v in &mut balanced.chi {
*v *= balance;
}
let fp = objective.score(&balanced, &raw)?;
let negative: Vec<_> = raw.iter().map(|v| -v).collect();
let fm = objective.score(&balanced, &negative)?;
let linear = (fp - fm) / 4.;
let s02 =
(-linear / quadratic / balance).clamp(settings.s02_bounds[0], settings.s02_bounds[1]);
let model: Vec<_> = raw.iter().map(|v| s02 * v).collect();
let score = objective.score(&data.exafs, &model)?;
candidates.push(CalibrationPoint {
delta_e0: energy,
s02,
score,
});
if score < best_score {
best_score = score;
best_index = index;
best_model = model;
}
}
let best = candidates[best_index].clone();
let mut report = fit_report(&data.exafs, &best_model, &data.objective)?;
report.amplitude_at_bound =
Some(best.s02 == settings.s02_bounds[0] || best.s02 == settings.s02_bounds[1]);
Ok(CalibrationResult {
calculator: calculator.identity(),
dataset: data.exafs.name.clone(),
candidates,
best,
chi: best_model,
report,
})
}