use hisab::num::Complex;
use hisab::num::fft;
#[cfg(test)]
use hisab::num::ifft;
use tracing::trace;
#[inline]
fn norm_sq(c: &Complex) -> f64 {
c.re * c.re + c.im * c.im
}
#[inline]
fn from_phase(theta: f64) -> Complex {
let (s, c) = theta.sin_cos();
Complex::new(c, s)
}
#[inline]
fn npot(n: usize) -> usize {
n.next_power_of_two()
}
#[derive(Debug, Clone, PartialEq)]
pub struct Pattern2D {
pub data: Vec<f64>,
pub width: usize,
pub height: usize,
}
impl Pattern2D {
#[must_use]
pub fn new(width: usize, height: usize) -> Self {
Self {
data: vec![0.0; width * height],
width,
height,
}
}
#[must_use]
#[inline]
pub fn get(&self, col: usize, row: usize) -> f64 {
self.data[row * self.width + col]
}
#[inline]
pub fn set(&mut self, col: usize, row: usize, value: f64) {
self.data[row * self.width + col] = value;
}
#[must_use]
pub fn max_intensity(&self) -> f64 {
self.data.iter().copied().fold(0.0_f64, f64::max)
}
#[must_use]
pub fn normalized(&self) -> Self {
let max = self.max_intensity();
if max < 1e-30 {
return self.clone();
}
let inv = 1.0 / max;
Self {
data: self.data.iter().map(|&v| v * inv).collect(),
width: self.width,
height: self.height,
}
}
}
#[must_use]
pub fn diffraction_pattern_2d(aperture: &[f64], width: usize, height: usize) -> Pattern2D {
trace!(width, height, "diffraction_pattern_2d");
let nw = npot(width);
let nh = npot(height);
let mut grid = vec![Complex::new(0.0, 0.0); nw * nh];
for row in 0..height {
for col in 0..width {
grid[row * nw + col] = Complex::new(aperture[row * width + col], 0.0);
}
}
for row in 0..nh {
let start = row * nw;
let _ = fft(&mut grid[start..start + nw]);
}
let mut col_buf = vec![Complex::new(0.0, 0.0); nh];
for col in 0..nw {
for row in 0..nh {
col_buf[row] = grid[row * nw + col];
}
let _ = fft(&mut col_buf);
for row in 0..nh {
grid[row * nw + col] = col_buf[row];
}
}
let mut pattern = Pattern2D::new(nw, nh);
for row in 0..nh {
for col in 0..nw {
let sr = (row + nh / 2) % nh;
let sc = (col + nw / 2) % nw;
pattern.set(col, row, norm_sq(&grid[sr * nw + sc]));
}
}
pattern
}
#[must_use]
pub fn diffraction_pattern_circular(grid_size: usize, radius: f64) -> Pattern2D {
trace!(grid_size, radius, "diffraction_pattern_circular");
let n = npot(grid_size);
let cx = n as f64 / 2.0;
let cy = n as f64 / 2.0;
let r2 = radius * radius;
let mut aperture = vec![0.0; n * n];
for row in 0..n {
for col in 0..n {
let dx = col as f64 - cx;
let dy = row as f64 - cy;
if dx * dx + dy * dy <= r2 {
aperture[row * n + col] = 1.0;
}
}
}
diffraction_pattern_2d(&aperture, n, n)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PointSource {
pub x: f64,
pub y: f64,
pub amplitude: f64,
pub phase: f64,
}
impl PointSource {
#[must_use]
#[inline]
pub const fn new(x: f64, y: f64, amplitude: f64, phase: f64) -> Self {
Self {
x,
y,
amplitude,
phase,
}
}
}
#[must_use]
pub fn interference_pattern(
sources: &[PointSource],
wavelength: f64,
distance: f64,
grid_x: (f64, f64),
grid_y: (f64, f64),
nx: usize,
ny: usize,
) -> Pattern2D {
trace!(num_sources = sources.len(), nx, ny, "interference_pattern");
let k = std::f64::consts::TAU / wavelength;
let dx = if nx > 1 {
(grid_x.1 - grid_x.0) / (nx as f64 - 1.0)
} else {
0.0
};
let dy = if ny > 1 {
(grid_y.1 - grid_y.0) / (ny as f64 - 1.0)
} else {
0.0
};
let mut pattern = Pattern2D::new(nx, ny);
for row in 0..ny {
let obs_y = grid_y.0 + row as f64 * dy;
for col in 0..nx {
let obs_x = grid_x.0 + col as f64 * dx;
let mut e_re = 0.0;
let mut e_im = 0.0;
for src in sources {
let sx = obs_x - src.x;
let sy = obs_y - src.y;
let r = (sx * sx + sy * sy + distance * distance).sqrt();
let phase = k * r + src.phase;
let (sin_p, cos_p) = phase.sin_cos();
let amp = src.amplitude / r; e_re += amp * cos_p;
e_im += amp * sin_p;
}
pattern.set(col, row, e_re * e_re + e_im * e_im);
}
}
pattern
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StripColor {
pub r: f64,
pub g: f64,
pub b: f64,
}
#[must_use]
pub fn spectrum_strip(num_samples: usize) -> Vec<StripColor> {
trace!(num_samples, "spectrum_strip");
let mut colors = Vec::with_capacity(num_samples);
for i in 0..num_samples {
let t = i as f64 / (num_samples as f64 - 1.0).max(1.0);
let wl = 380.0 + t * 400.0; let (r, g, b) = wavelength_to_rgb_approx(wl);
colors.push(StripColor { r, g, b });
}
colors
}
#[must_use]
pub fn spectrum_strip_range(start_nm: f64, end_nm: f64, num_samples: usize) -> Vec<StripColor> {
trace!(start_nm, end_nm, num_samples, "spectrum_strip_range");
let mut colors = Vec::with_capacity(num_samples);
for i in 0..num_samples {
let t = i as f64 / (num_samples as f64 - 1.0).max(1.0);
let wl = start_nm + t * (end_nm - start_nm);
let (r, g, b) = wavelength_to_rgb_approx(wl);
colors.push(StripColor { r, g, b });
}
colors
}
fn wavelength_to_rgb_approx(nm: f64) -> (f64, f64, f64) {
if !(380.0..=780.0).contains(&nm) {
return (0.0, 0.0, 0.0);
}
let (r, g, b) = if nm < 440.0 {
(-(nm - 440.0) / (440.0 - 380.0), 0.0, 1.0)
} else if nm < 490.0 {
(0.0, (nm - 440.0) / (490.0 - 440.0), 1.0)
} else if nm < 510.0 {
(0.0, 1.0, -(nm - 510.0) / (510.0 - 490.0))
} else if nm < 580.0 {
((nm - 510.0) / (580.0 - 510.0), 1.0, 0.0)
} else if nm < 645.0 {
(1.0, -(nm - 645.0) / (645.0 - 580.0), 0.0)
} else {
(1.0, 0.0, 0.0)
};
let factor = if nm < 420.0 {
0.3 + 0.7 * (nm - 380.0) / 40.0
} else if nm > 700.0 {
0.3 + 0.7 * (780.0 - nm) / 80.0
} else {
1.0
};
(r * factor, g * factor, b * factor)
}
#[must_use]
pub fn psf_from_wavefront(
pupil: &[f64],
wavefront_error: &[f64],
width: usize,
height: usize,
wavelength: f64,
) -> Pattern2D {
trace!(width, height, wavelength, "psf_from_wavefront");
let nw = npot(width);
let nh = npot(height);
let k = std::f64::consts::TAU / wavelength;
let mut grid = vec![Complex::new(0.0, 0.0); nw * nh];
for row in 0..height {
for col in 0..width {
let idx = row * width + col;
let p = pupil[idx];
if p > 0.0 {
let phase = k * wavefront_error[idx];
let e = from_phase(phase);
grid[row * nw + col] = Complex::new(p * e.re, p * e.im);
}
}
}
for row in 0..nh {
let start = row * nw;
let _ = fft(&mut grid[start..start + nw]);
}
let mut col_buf = vec![Complex::new(0.0, 0.0); nh];
for col in 0..nw {
for row in 0..nh {
col_buf[row] = grid[row * nw + col];
}
let _ = fft(&mut col_buf);
for row in 0..nh {
grid[row * nw + col] = col_buf[row];
}
}
let mut pattern = Pattern2D::new(nw, nh);
for row in 0..nh {
for col in 0..nw {
let sr = (row + nh / 2) % nh;
let sc = (col + nw / 2) % nw;
pattern.set(col, row, norm_sq(&grid[sr * nw + sc]));
}
}
pattern
}
#[must_use]
pub fn psf_diffraction_limited(grid_size: usize, radius: f64) -> Pattern2D {
trace!(grid_size, radius, "psf_diffraction_limited");
diffraction_pattern_circular(grid_size, radius)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-6;
#[test]
fn test_fft_single_element() {
let mut data = [Complex::new(5.0, 0.0)];
let _ = fft(&mut data);
assert!((data[0].re - 5.0).abs() < EPS);
}
#[test]
fn test_fft_two_elements() {
let mut data = [Complex::new(1.0, 0.0), Complex::new(1.0, 0.0)];
let _ = fft(&mut data);
assert!((data[0].re - 2.0).abs() < EPS); assert!(norm_sq(&data[1]) < EPS); }
#[test]
fn test_fft_roundtrip() {
let original = [
Complex::new(1.0, 0.0),
Complex::new(2.0, 0.0),
Complex::new(3.0, 0.0),
Complex::new(4.0, 0.0),
];
let mut data = original;
let _ = fft(&mut data);
let _ = ifft(&mut data);
for (a, b) in data.iter().zip(original.iter()) {
assert!(
(a.re - b.re).abs() < EPS && (a.im - b.im).abs() < EPS,
"FFT roundtrip failed"
);
}
}
#[test]
fn test_fft_parseval() {
let mut data = [
Complex::new(1.0, 0.0),
Complex::new(0.0, 1.0),
Complex::new(-1.0, 0.0),
Complex::new(0.0, -1.0),
];
let energy_time: f64 = data.iter().map(norm_sq).sum();
let _ = fft(&mut data);
let energy_freq: f64 = data.iter().map(norm_sq).sum();
assert!(
(energy_time * 4.0 - energy_freq).abs() < EPS,
"Parseval: {energy_time}*4 ≠ {energy_freq}"
);
}
#[test]
fn test_diffraction_2d_dimensions() {
let aperture = vec![1.0; 8 * 8];
let pattern = diffraction_pattern_2d(&aperture, 8, 8);
assert_eq!(pattern.width, 8);
assert_eq!(pattern.height, 8);
}
#[test]
fn test_diffraction_2d_non_power_of_2() {
let aperture = vec![1.0; 6 * 6];
let pattern = diffraction_pattern_2d(&aperture, 6, 6);
assert_eq!(pattern.width, 8); assert_eq!(pattern.height, 8);
}
#[test]
fn test_diffraction_2d_center_peak() {
let aperture = vec![1.0; 16 * 16];
let pattern = diffraction_pattern_2d(&aperture, 16, 16);
let center = pattern.get(8, 8);
let corner = pattern.get(0, 0);
assert!(center > corner, "Center should be brightest");
}
#[test]
fn test_diffraction_2d_all_non_negative() {
let aperture = vec![1.0; 16 * 16];
let pattern = diffraction_pattern_2d(&aperture, 16, 16);
for &v in &pattern.data {
assert!(v >= 0.0, "Intensity should be non-negative");
}
}
#[test]
fn test_diffraction_circular_symmetric() {
let pattern = diffraction_pattern_circular(32, 8.0);
let above = pattern.get(16, 12);
let below = pattern.get(16, 20);
assert!(
(above - below).abs() / (above + below + 1e-30) < 0.1,
"Circular pattern should be symmetric"
);
}
#[test]
fn test_diffraction_circular_center_brightest() {
let pattern = diffraction_pattern_circular(32, 8.0);
let max = pattern.max_intensity();
let center = pattern.get(16, 16);
assert!(
(center - max).abs() / max < 0.01,
"Center should be the maximum"
);
}
#[test]
fn test_interference_single_source() {
let sources = [PointSource::new(0.0, 0.0, 1.0, 0.0)];
let pattern =
interference_pattern(&sources, 0.5e-6, 1.0, (-1e-3, 1e-3), (-1e-3, 1e-3), 16, 16);
assert_eq!(pattern.width, 16);
assert_eq!(pattern.height, 16);
for &v in &pattern.data {
assert!(v >= 0.0);
}
}
#[test]
fn test_interference_two_sources_fringes() {
let sources = [
PointSource::new(-0.5e-3, 0.0, 1.0, 0.0),
PointSource::new(0.5e-3, 0.0, 1.0, 0.0),
];
let pattern = interference_pattern(&sources, 0.5e-6, 1.0, (-5e-3, 5e-3), (0.0, 0.0), 64, 1);
let min = pattern.data.iter().copied().fold(f64::INFINITY, f64::min);
let max = pattern.data.iter().copied().fold(0.0_f64, f64::max);
assert!(
max > min * 1.5,
"Two sources should produce fringes: min={min}, max={max}"
);
}
#[test]
fn test_interference_constructive() {
let sources = [
PointSource::new(0.0, 0.0, 1.0, 0.0),
PointSource::new(0.0, 0.0, 1.0, 0.0),
];
let one = [PointSource::new(0.0, 0.0, 1.0, 0.0)];
let p2 = interference_pattern(&sources, 0.5e-6, 1.0, (0.0, 0.0), (0.0, 0.0), 1, 1);
let p1 = interference_pattern(&one, 0.5e-6, 1.0, (0.0, 0.0), (0.0, 0.0), 1, 1);
assert!(
(p2.data[0] / p1.data[0] - 4.0).abs() < 0.01,
"Constructive: 2 sources → 4× intensity"
);
}
#[test]
fn test_interference_all_non_negative() {
let sources = [
PointSource::new(-1e-3, 0.0, 1.0, 0.0),
PointSource::new(1e-3, 0.0, 1.0, 0.0),
PointSource::new(0.0, 1e-3, 0.5, 0.5),
];
let pattern =
interference_pattern(&sources, 0.5e-6, 1.0, (-5e-3, 5e-3), (-5e-3, 5e-3), 16, 16);
for &v in &pattern.data {
assert!(v >= 0.0, "Intensity must be non-negative");
}
}
#[test]
fn test_spectrum_strip_count() {
let strip = spectrum_strip(100);
assert_eq!(strip.len(), 100);
}
#[test]
fn test_spectrum_strip_range() {
let strip = spectrum_strip(50);
for c in &strip {
assert!((0.0..=1.0).contains(&c.r), "R out of range");
assert!((0.0..=1.0).contains(&c.g), "G out of range");
assert!((0.0..=1.0).contains(&c.b), "B out of range");
}
}
#[test]
fn test_spectrum_strip_red_end() {
let strip = spectrum_strip(100);
let last = &strip[strip.len() - 1];
assert!(last.r > last.b, "Red end should be red-dominant");
}
#[test]
fn test_spectrum_strip_blue_start() {
let strip = spectrum_strip(100);
let blue_region = &strip[10]; assert!(
blue_region.b > blue_region.r,
"Blue region should be blue-dominant"
);
}
#[test]
fn test_spectrum_strip_custom_range() {
let strip = spectrum_strip_range(500.0, 600.0, 20);
assert_eq!(strip.len(), 20);
for c in &strip {
assert!(c.g > 0.3, "500-600nm should have green: g={}", c.g);
}
}
#[test]
fn test_psf_perfect_wavefront() {
let n = 32;
let radius = 8.0;
let cx = n as f64 / 2.0;
let cy = n as f64 / 2.0;
let r2 = radius * radius;
let mut pupil = vec![0.0; n * n];
let wavefront = vec![0.0; n * n];
for row in 0..n {
for col in 0..n {
let dx = col as f64 - cx;
let dy = row as f64 - cy;
if dx * dx + dy * dy <= r2 {
pupil[row * n + col] = 1.0;
}
}
}
let psf = psf_from_wavefront(&pupil, &wavefront, n, n, 0.55e-6);
assert!(psf.max_intensity() > 0.0);
let center = psf.get(n / 2, n / 2);
assert!(center > psf.get(0, 0), "PSF peak should be at center");
}
#[test]
fn test_psf_diffraction_limited() {
let psf = psf_diffraction_limited(32, 8.0);
assert_eq!(psf.width, 32);
assert!(psf.max_intensity() > 0.0);
}
#[test]
fn test_psf_aberrated_broader() {
let n = 32;
let radius = 8.0;
let cx = n as f64 / 2.0;
let cy = n as f64 / 2.0;
let r2 = radius * radius;
let mut pupil = vec![0.0; n * n];
let wfe_zero = vec![0.0; n * n];
let mut wfe_aberr = vec![0.0; n * n];
for row in 0..n {
for col in 0..n {
let dx = col as f64 - cx;
let dy = row as f64 - cy;
let rho2 = dx * dx + dy * dy;
if rho2 <= r2 {
pupil[row * n + col] = 1.0;
wfe_aberr[row * n + col] = 0.5e-6 * (rho2 / r2) * (rho2 / r2);
}
}
}
let psf_perfect = psf_from_wavefront(&pupil, &wfe_zero, n, n, 0.55e-6);
let psf_aberr = psf_from_wavefront(&pupil, &wfe_aberr, n, n, 0.55e-6);
assert!(
psf_aberr.max_intensity() < psf_perfect.max_intensity(),
"Aberrated PSF should have lower peak"
);
}
#[test]
fn test_pattern_normalized() {
let mut p = Pattern2D::new(4, 4);
p.set(0, 0, 10.0);
p.set(1, 1, 5.0);
let norm = p.normalized();
assert!((norm.get(0, 0) - 1.0).abs() < EPS);
assert!((norm.get(1, 1) - 0.5).abs() < EPS);
}
#[test]
fn test_pattern_normalized_zero() {
let p = Pattern2D::new(4, 4);
let norm = p.normalized();
assert!(norm.max_intensity().abs() < EPS);
}
#[test]
fn test_pattern_max_intensity() {
let mut p = Pattern2D::new(4, 4);
p.set(2, 3, 42.0);
assert!((p.max_intensity() - 42.0).abs() < EPS);
}
}