use super::*;
use crate::fitting::{
feffit_joint_with_options, FeffFitDataset, FeffFitOptions, FeffFitResult, FitSpace,
FitVariable, FitVariables, PathParamSpec,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default, deny_unknown_fields)]
pub struct FirstShellSettings {
pub initial: [f64; 4],
pub bounds: [[f64; 2]; 4],
pub solver: FeffFitOptions,
}
impl Default for FirstShellSettings {
fn default() -> Self {
Self {
initial: [0.9, 0., 1., 0.003],
bounds: [[0.1, 1.5], [-20., 20.], [0.8, 1.2], [0., 0.04]],
solver: FeffFitOptions::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FirstShellCalibration {
pub source: String,
pub dataset: FeffFitDataset,
pub settings: FirstShellSettings,
pub fit: FeffFitResult,
}
pub fn calibrate_first_shell(
dataset: &FeffFitDataset,
settings: &FirstShellSettings,
source: impl Into<String>,
) -> Result<FirstShellCalibration, RmcError> {
let source = source.into();
require(
!source.trim().is_empty(),
"first-shell calibration needs source provenance",
)?;
require(
dataset.transform.fitspace == FitSpace::R,
"first-shell calibration requires the native complex-R objective",
)?;
for (&initial, bounds) in settings.initial.iter().zip(&settings.bounds) {
require(
initial.is_finite()
&& bounds.iter().all(|v| v.is_finite())
&& bounds[0] < bounds[1]
&& initial >= bounds[0]
&& initial <= bounds[1],
"invalid first-shell calibration starts or bounds",
)?;
}
require(
settings.bounds[0][0] > 0. && settings.bounds[2][0] > 0. && settings.bounds[3][0] >= 0.,
"amplitude/distance must be positive and disorder nonnegative",
)?;
let mut dataset = dataset.clone();
require(
dataset.paths.iter().any(|p| p.use_path),
"first-shell calibration needs enabled paths",
)?;
for path in dataset.paths.iter_mut().filter(|p| p.use_path) {
require(
path.feff.nleg == 2
&& path.feff.reff.is_finite()
&& path.feff.reff > 0.
&& matches!(path.degen, PathParamSpec::Value(v) if v.is_finite() && v > 0.),
"first-shell calibration needs two-leg paths and fixed positive degeneracy",
)?;
path.s02 = "rmc_s02".into();
path.e0 = "rmc_delta_e0".into();
path.deltar = format!("{:.17} * (rmc_distance_scale - 1)", path.feff.reff).into();
path.sigma2 = "rmc_sigma2".into();
path.ei = 0.0.into();
path.third = 0.0.into();
path.fourth = 0.0.into();
}
let mut variables = FitVariables::new();
for (i, name) in [
"rmc_s02",
"rmc_delta_e0",
"rmc_distance_scale",
"rmc_sigma2",
]
.iter()
.enumerate()
{
variables.insert(
*name,
FitVariable::new(settings.initial[i], true)
.with_bounds(Some(settings.bounds[i][0]), Some(settings.bounds[i][1])),
);
}
let fit =
feffit_joint_with_options(std::slice::from_ref(&dataset), &variables, &settings.solver)
.map_err(|e| RmcError::Invalid(format!("first-shell fit: {e}")))?;
Ok(FirstShellCalibration {
source,
dataset,
settings: settings.clone(),
fit,
})
}
impl FirstShellCalibration {
pub fn apply_amplitude_energy(
&self,
problem: &EnsembleProblem,
dataset: usize,
) -> Result<EnsembleProblem, RmcError> {
require(
dataset < problem.datasets.len(),
"calibration target dataset out of range",
)?;
require(
self.fit.solver_report.as_ref().is_some_and(|r| r.converged),
"first-shell solver has not converged; inspect the retained fit",
)?;
let parameter = |name| {
self.fit
.variables
.get(name)
.map(|p| p.value)
.filter(|v| v.is_finite())
.ok_or_else(|| RmcError::Invalid(format!("missing finite fitted {name}")))
};
let s02 = parameter("rmc_s02")?;
let delta_e0 = parameter("rmc_delta_e0")?;
require(s02 > 0., "fitted amplitude must be positive")?;
let mut result = problem.clone();
result.datasets[dataset].exafs.s02 = s02;
result.datasets[dataset].exafs.delta_e0 = delta_e0;
super::engine::shifted_grid(&result.datasets[dataset].exafs)?;
Ok(result)
}
}