use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, PartialEq)]
pub struct IntegratedIntensityCorrection {
pub values: Vec<f64>,
pub d_values_d_q_squared: Vec<f64>,
pub d_values_d_wavelength: Vec<f64>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum IntegratedIntensityCorrectionModel {
Neutral,
BraggBrentanoUnpolarizedLp {
wavelength_angstrom: f64,
},
BraggBrentanoPolarizedLp {
wavelength_angstrom: f64,
polarization: f64,
},
ConstantWavelengthNeutronLorentz {
wavelength_angstrom: f64,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IntegratedIntensityCorrectionError {
InvalidWavelength,
InvalidPolarization,
InvalidQSquared,
ReflectionOutsideAngularDomain,
}
impl Display for IntegratedIntensityCorrectionError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::InvalidWavelength => "correction wavelength must be positive and finite",
Self::InvalidPolarization => {
"Bragg-Brentano polarization must be finite and within [0, 1]"
}
Self::InvalidQSquared => "correction q_squared must be positive and finite",
Self::ReflectionOutsideAngularDomain => {
"Bragg-Brentano LP requires reflections strictly within 0 < 2theta < 180 degrees"
}
})
}
}
impl Error for IntegratedIntensityCorrectionError {}
impl IntegratedIntensityCorrectionModel {
#[must_use]
pub const fn with_wavelength(self, wavelength_angstrom: f64) -> Self {
match self {
Self::Neutral => Self::Neutral,
Self::BraggBrentanoUnpolarizedLp { .. } => Self::BraggBrentanoUnpolarizedLp {
wavelength_angstrom,
},
Self::BraggBrentanoPolarizedLp { polarization, .. } => Self::BraggBrentanoPolarizedLp {
wavelength_angstrom,
polarization,
},
Self::ConstantWavelengthNeutronLorentz { .. } => {
Self::ConstantWavelengthNeutronLorentz {
wavelength_angstrom,
}
}
}
}
pub fn evaluate(
self,
q_squared_inverse_angstrom2: &[f64],
) -> Result<IntegratedIntensityCorrection, IntegratedIntensityCorrectionError> {
if q_squared_inverse_angstrom2
.iter()
.any(|value| !value.is_finite() || *value <= 0.0)
{
return Err(IntegratedIntensityCorrectionError::InvalidQSquared);
}
match self {
Self::Neutral => Ok(IntegratedIntensityCorrection {
values: vec![1.0; q_squared_inverse_angstrom2.len()],
d_values_d_q_squared: vec![0.0; q_squared_inverse_angstrom2.len()],
d_values_d_wavelength: vec![0.0; q_squared_inverse_angstrom2.len()],
}),
Self::BraggBrentanoUnpolarizedLp {
wavelength_angstrom,
}
| Self::BraggBrentanoPolarizedLp {
wavelength_angstrom,
..
}
| Self::ConstantWavelengthNeutronLorentz {
wavelength_angstrom,
} => {
if !wavelength_angstrom.is_finite() || wavelength_angstrom <= 0.0 {
return Err(IntegratedIntensityCorrectionError::InvalidWavelength);
}
let (polarization, factor) = match self {
Self::BraggBrentanoUnpolarizedLp { .. } => (0.5, 1.0),
Self::BraggBrentanoPolarizedLp { polarization, .. } => (polarization, 1.0),
Self::ConstantWavelengthNeutronLorentz { .. } => (1.0, 0.5),
Self::Neutral => unreachable!(),
};
if !polarization.is_finite() || !(0.0..=1.0).contains(&polarization) {
return Err(IntegratedIntensityCorrectionError::InvalidPolarization);
}
let mut values = Vec::with_capacity(q_squared_inverse_angstrom2.len());
let mut derivatives = Vec::with_capacity(q_squared_inverse_angstrom2.len());
let mut wavelength_derivatives =
Vec::with_capacity(q_squared_inverse_angstrom2.len());
for &q_squared in q_squared_inverse_angstrom2 {
let (value, derivative, wavelength_derivative) =
bragg_brentano_lp(q_squared, wavelength_angstrom, polarization)?;
values.push(factor * value);
derivatives.push(factor * derivative);
wavelength_derivatives.push(factor * wavelength_derivative);
}
Ok(IntegratedIntensityCorrection {
values,
d_values_d_q_squared: derivatives,
d_values_d_wavelength: wavelength_derivatives,
})
}
}
}
}
fn bragg_brentano_lp(
q_squared: f64,
wavelength: f64,
polarization: f64,
) -> Result<(f64, f64, f64), IntegratedIntensityCorrectionError> {
let root_q = q_squared.sqrt();
let sin_theta = 0.5 * wavelength * root_q;
if !(0.0..1.0).contains(&sin_theta) {
return Err(IntegratedIntensityCorrectionError::ReflectionOutsideAngularDomain);
}
let theta = sin_theta.asin();
let cos_theta = theta.cos();
let two_theta = 2.0 * theta;
let (sin_two_theta, cos_two_theta) = two_theta.sin_cos();
let numerator = polarization + (1.0 - polarization) * cos_two_theta * cos_two_theta;
let value = numerator / (sin_theta * sin_theta * cos_theta);
let d_log_d_two_theta = -2.0 * (1.0 - polarization) * sin_two_theta * cos_two_theta / numerator
- 1.0 / theta.tan()
+ 0.5 * theta.tan();
let d_two_theta_d_q_squared = wavelength / (2.0 * root_q * cos_theta);
let d_two_theta_d_wavelength = root_q / cos_theta;
Ok((
value,
value * d_log_d_two_theta * d_two_theta_d_q_squared,
value * d_log_d_two_theta * d_two_theta_d_wavelength,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn neutral_model_is_exact_for_empty_and_nonempty_batches() {
let empty = IntegratedIntensityCorrectionModel::Neutral
.evaluate(&[])
.expect("empty neutral");
assert!(empty.values.is_empty());
let values = IntegratedIntensityCorrectionModel::Neutral
.evaluate(&[0.01, 0.1, 1.0])
.expect("neutral");
assert_eq!(values.values, vec![1.0; 3]);
assert_eq!(values.d_values_d_q_squared, vec![0.0; 3]);
assert_eq!(values.d_values_d_wavelength, vec![0.0; 3]);
}
#[test]
fn bragg_brentano_values_and_derivatives_match_closed_form_differences() {
let wavelength = 1.5406;
let model = IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
wavelength_angstrom: wavelength,
};
let q_squared = [0.02, 0.11, 0.37, 0.91];
let actual = model.evaluate(&q_squared).expect("LP values");
for (index, value) in q_squared.into_iter().enumerate() {
let theta = (0.5 * wavelength * value.sqrt()).asin();
let expected =
(1.0 + (2.0 * theta).cos().powi(2)) / (2.0 * theta.sin().powi(2) * theta.cos());
assert!((actual.values[index] - expected).abs() < 2.0e-14 * expected);
let step = 1.0e-6 * value;
let plus = model.evaluate(&[value + step]).expect("plus").values[0];
let minus = model.evaluate(&[value - step]).expect("minus").values[0];
let finite_difference = (plus - minus) / (2.0 * step);
assert!(
(actual.d_values_d_q_squared[index] - finite_difference).abs()
< 2.0e-8 * finite_difference.abs().max(1.0)
);
let wavelength_step = 1.0e-6 * wavelength;
let plus = IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
wavelength_angstrom: wavelength + wavelength_step,
}
.evaluate(&[value])
.expect("wavelength plus")
.values[0];
let minus = IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
wavelength_angstrom: wavelength - wavelength_step,
}
.evaluate(&[value])
.expect("wavelength minus")
.values[0];
let finite_difference = (plus - minus) / (2.0 * wavelength_step);
assert!(
(actual.d_values_d_wavelength[index] - finite_difference).abs()
< 2.0e-8 * finite_difference.abs().max(1.0)
);
}
}
#[test]
fn polarized_lp_matches_closed_form_derivatives_and_unpolarized_limit() {
let wavelength = 1.54051;
let polarization = 0.7;
let q_squared = [0.03, 0.19, 0.62];
let model = IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
wavelength_angstrom: wavelength,
polarization,
};
let actual = model.evaluate(&q_squared).expect("polarized LP");
for (index, value) in q_squared.into_iter().enumerate() {
let theta = (0.5 * wavelength * value.sqrt()).asin();
let expected = (polarization + (1.0 - polarization) * (2.0 * theta).cos().powi(2))
/ (theta.sin().powi(2) * theta.cos());
assert!((actual.values[index] - expected).abs() < 2.0e-14 * expected);
let q_step = value * 1.0e-6;
let q_finite = (model.evaluate(&[value + q_step]).expect("q plus").values[0]
- model.evaluate(&[value - q_step]).expect("q minus").values[0])
/ (2.0 * q_step);
assert!(
(actual.d_values_d_q_squared[index] - q_finite).abs()
< 2.0e-8 * q_finite.abs().max(1.0)
);
let wavelength_step = wavelength * 1.0e-6;
let evaluate_at = |selected| {
IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
wavelength_angstrom: selected,
polarization,
}
.evaluate(&[value])
.expect("wavelength finite difference")
.values[0]
};
let wavelength_finite = (evaluate_at(wavelength + wavelength_step)
- evaluate_at(wavelength - wavelength_step))
/ (2.0 * wavelength_step);
assert!(
(actual.d_values_d_wavelength[index] - wavelength_finite).abs()
< 2.0e-8 * wavelength_finite.abs().max(1.0)
);
}
let unpolarized = IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
wavelength_angstrom: wavelength,
}
.evaluate(&q_squared)
.expect("unpolarized");
let half = IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
wavelength_angstrom: wavelength,
polarization: 0.5,
}
.evaluate(&q_squared)
.expect("half polarized");
assert_eq!(half, unpolarized);
}
#[test]
fn neutron_lorentz_matches_constant_wavelength_powder_equation() {
let wavelength = 1.909;
let q_squared = [0.03, 0.19, 0.62];
let model = IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz {
wavelength_angstrom: wavelength,
};
let actual = model.evaluate(&q_squared).expect("neutron Lorentz values");
for (index, value) in q_squared.into_iter().enumerate() {
let theta = (0.5 * wavelength * value.sqrt()).asin();
let expected = 1.0 / (theta.sin() * (2.0 * theta).sin());
assert!((actual.values[index] - expected).abs() < 2.0e-14 * expected);
let step = 1.0e-6 * value;
let finite_difference = (model.evaluate(&[value + step]).unwrap().values[0]
- model.evaluate(&[value - step]).unwrap().values[0])
/ (2.0 * step);
assert!(
(actual.d_values_d_q_squared[index] - finite_difference).abs()
< 2.0e-8 * finite_difference.abs().max(1.0)
);
}
}
#[test]
fn invalid_domains_are_explicit() {
assert_eq!(
IntegratedIntensityCorrectionModel::Neutral.evaluate(&[0.0]),
Err(IntegratedIntensityCorrectionError::InvalidQSquared)
);
assert_eq!(
IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
wavelength_angstrom: 0.0,
}
.evaluate(&[1.0]),
Err(IntegratedIntensityCorrectionError::InvalidWavelength)
);
assert_eq!(
IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
wavelength_angstrom: 2.0,
}
.evaluate(&[1.0]),
Err(IntegratedIntensityCorrectionError::ReflectionOutsideAngularDomain)
);
assert_eq!(
IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
wavelength_angstrom: 1.0,
polarization: 1.1,
}
.evaluate(&[1.0]),
Err(IntegratedIntensityCorrectionError::InvalidPolarization)
);
}
}