use crate::qtty::{Nanometers, Quantity, Radians};
use alloc::vec::Vec;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use optica::grid::{GridError, OutOfRange};
use optica::phase::{PhaseTable, ScatteringFactor};
#[cfg(not(feature = "std"))]
use qtty::{Scalar, Transcendental};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NormalizedPhaseError {
#[error(transparent)]
Grid(#[from] GridError),
#[error("wavelengths must be positive and strictly ascending")]
WavelengthDomain,
#[error("angles must be strictly ascending from 0 to pi radians")]
AngularDomain,
#[error("phase value at row-major index {index} must be finite and nonnegative")]
InvalidValue {
index: usize,
},
#[error("phase integral at wavelength index {index} differs from unity by more than 1e-6")]
NotNormalized {
index: usize,
},
#[error("phase query coordinates must be finite")]
NonFiniteQuery,
}
#[derive(Debug, Clone)]
pub struct NormalizedPhaseTable {
grid: PhaseTable,
}
impl NormalizedPhaseTable {
pub fn new(
wavelengths: &[Nanometers],
angles: &[Radians],
values: &[Quantity<ScatteringFactor>],
) -> Result<Self, NormalizedPhaseError> {
let xs: Vec<_> = wavelengths.iter().map(|v| v.value()).collect();
let ys: Vec<_> = angles.iter().map(|v| v.value()).collect();
let data: Vec<_> = values.iter().map(|v| v.value()).collect();
let grid = PhaseTable::from_raw_row_major(&xs, &ys, &data, OutOfRange::Error)?;
if xs[0] <= 0.0 || xs[0] >= xs[xs.len() - 1] {
return Err(NormalizedPhaseError::WavelengthDomain);
}
if ys[0] != 0.0 || ys[ys.len() - 1] != PI {
return Err(NormalizedPhaseError::AngularDomain);
}
for (index, &value) in data.iter().enumerate() {
if !value.is_finite() || value < 0.0 {
return Err(NormalizedPhaseError::InvalidValue { index });
}
}
for ix in 0..xs.len() {
let mut sum = 0.0;
let mut correction = 0.0;
for (iy, pair) in ys.windows(2).enumerate() {
let integral = segment_integral(
pair[0],
pair[1],
data[iy * xs.len() + ix],
data[(iy + 1) * xs.len() + ix],
);
let adjusted = integral - correction;
let next = sum + adjusted;
correction = (next - sum) - adjusted;
sum = next;
}
if !sum.is_finite() || (TAU * sum - 1.0).abs() > 1e-6 {
return Err(NormalizedPhaseError::NotNormalized { index: ix });
}
}
Ok(Self { grid })
}
pub fn try_phase(
&self,
wavelength: Nanometers,
scattering_angle: Radians,
) -> Result<Quantity<ScatteringFactor>, NormalizedPhaseError> {
if !wavelength.value().is_finite() || !scattering_angle.value().is_finite() {
return Err(NormalizedPhaseError::NonFiniteQuery);
}
Ok(self.grid.try_interp_at(wavelength, scattering_angle)?)
}
}
fn segment_integral(a: f64, b: f64, p: f64, q: f64) -> f64 {
let h = (b - a) * 0.5;
let m = a + h;
let (sin_m, cos_m) = if m > FRAC_PI_2 {
let distance = (PI - b) + h;
(distance.sin(), -distance.cos())
} else {
(m.sin(), m.cos())
};
let (sinc, slope) = if h < 0.01 {
let h2 = h * h;
(
1.0 + h2 * (-1.0 / 6.0 + h2 * (1.0 / 120.0 - h2 / 5040.0)),
h * (1.0 / 3.0 + h2 * (-1.0 / 30.0 + h2 * (1.0 / 840.0 - h2 / 45360.0))),
)
} else {
let sinc = h.sin() / h;
(sinc, (sinc - h.cos()) / h)
};
(p * h) * (sin_m * sinc - cos_m * slope) + (q * h) * (sin_m * sinc + cos_m * slope)
}