use std::f64::consts::PI;
use oxifft::{fft, ifft, Complex};
pub struct FftBpm1d {
pub nx: usize,
pub dx: f64,
pub n_ref: f64,
pub wavelength: f64,
pub n_profile: Option<Vec<f64>>,
pub field: Vec<Complex<f64>>,
}
impl FftBpm1d {
pub fn new(nx: usize, dx: f64, n_ref: f64, wavelength: f64) -> Self {
Self {
nx,
dx,
n_ref,
wavelength,
n_profile: None,
field: vec![Complex::zero(); nx],
}
}
pub fn set_index_profile(&mut self, n_profile: Vec<f64>) {
assert_eq!(n_profile.len(), self.nx);
self.n_profile = Some(n_profile);
}
pub fn set_gaussian_input(&mut self, amplitude: f64, x_center: f64, w0: f64) {
for i in 0..self.nx {
let x = i as f64 * self.dx - x_center;
let envelope = (-x * x / (w0 * w0)).exp();
self.field[i] = Complex::new(amplitude * envelope, 0.0);
}
}
pub fn set_field(&mut self, field: Vec<Complex<f64>>) {
assert_eq!(field.len(), self.nx);
self.field = field;
}
pub fn step(&mut self, dz: f64) {
let k0 = 2.0 * PI / self.wavelength;
let kn = k0 * self.n_ref;
let kx: Vec<f64> = (0..self.nx)
.map(|i| {
let ki = if i < self.nx / 2 {
i as f64
} else {
i as f64 - self.nx as f64
};
2.0 * PI * ki / (self.nx as f64 * self.dx)
})
.collect();
let half_free_phase: Vec<Complex<f64>> = kx
.iter()
.map(|&kxi| {
let phase = -kxi * kxi * dz / (4.0 * kn);
Complex::new(0.0, phase).exp_im()
})
.collect();
let mut spec = fft(&self.field);
for (s, &h) in spec.iter_mut().zip(&half_free_phase) {
*s *= h;
}
let mut e = ifft(&spec);
if let Some(ref np) = self.n_profile {
for (ei, &ni) in e.iter_mut().zip(np) {
let dn = ni - self.n_ref;
let phase = k0 * dn * dz;
*ei *= Complex::new(0.0, phase).exp_im();
}
}
let mut spec2 = fft(&e);
for (s, &h) in spec2.iter_mut().zip(&half_free_phase) {
*s *= h;
}
self.field = ifft(&spec2);
}
pub fn propagate(&mut self, dz: f64, n_steps: usize) {
for _ in 0..n_steps {
self.step(dz);
}
}
pub fn intensity(&self) -> Vec<f64> {
self.field
.iter()
.map(|e| e.re * e.re + e.im * e.im)
.collect()
}
pub fn peak_intensity(&self) -> f64 {
self.intensity().iter().cloned().fold(0.0_f64, f64::max)
}
pub fn rms_width(&self) -> f64 {
let intensity = self.intensity();
let total: f64 = intensity.iter().sum();
if total == 0.0 {
return 0.0;
}
let mean = intensity
.iter()
.enumerate()
.map(|(i, &v)| i as f64 * self.dx * v)
.sum::<f64>()
/ total;
let var = intensity
.iter()
.enumerate()
.map(|(i, &v)| {
let x = i as f64 * self.dx - mean;
x * x * v
})
.sum::<f64>()
/ total;
var.sqrt()
}
}
trait ComplexExpIm {
fn exp_im(self) -> Self;
}
impl ComplexExpIm for Complex<f64> {
fn exp_im(self) -> Self {
let (s, c) = self.im.sin_cos();
Complex::new(c, s)
}
}
pub struct FftBpm2d {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub dy: f64,
pub n_ref: f64,
pub wavelength: f64,
pub n_profile: Option<Vec<f64>>,
pub field: Vec<Complex<f64>>,
}
impl FftBpm2d {
pub fn new(nx: usize, ny: usize, dx: f64, dy: f64, n_ref: f64, wavelength: f64) -> Self {
Self {
nx,
ny,
dx,
dy,
n_ref,
wavelength,
n_profile: None,
field: vec![Complex::zero(); nx * ny],
}
}
pub fn set_index_profile(&mut self, n_profile: Vec<f64>) {
assert_eq!(n_profile.len(), self.nx * self.ny);
self.n_profile = Some(n_profile);
}
pub fn set_gaussian_input(&mut self, amplitude: f64, xc: f64, yc: f64, w0: f64) {
for j in 0..self.ny {
for i in 0..self.nx {
let x = i as f64 * self.dx - xc;
let y = j as f64 * self.dy - yc;
let r2 = x * x + y * y;
let env = (-r2 / (w0 * w0)).exp();
self.field[j * self.nx + i] = Complex::new(amplitude * env, 0.0);
}
}
}
pub fn step(&mut self, dz: f64) {
let k0 = 2.0 * PI / self.wavelength;
let kn = k0 * self.n_ref;
let nxy = self.nx * self.ny;
let kx_vec: Vec<f64> = (0..self.nx)
.map(|i| {
let ki = if i < self.nx / 2 {
i as f64
} else {
i as f64 - self.nx as f64
};
2.0 * PI * ki / (self.nx as f64 * self.dx)
})
.collect();
let ky_vec: Vec<f64> = (0..self.ny)
.map(|j| {
let kj = if j < self.ny / 2 {
j as f64
} else {
j as f64 - self.ny as f64
};
2.0 * PI * kj / (self.ny as f64 * self.dy)
})
.collect();
let free_phase: Vec<Complex<f64>> = (0..nxy)
.map(|k| {
let i = k % self.nx;
let j = k / self.nx;
let phase = -(kx_vec[i] * kx_vec[i] + ky_vec[j] * ky_vec[j]) * dz / (4.0 * kn);
Complex::new(0.0, phase).exp_im()
})
.collect();
let mut spec = oxifft::fft2d(&self.field, self.ny, self.nx);
for (s, &h) in spec.iter_mut().zip(&free_phase) {
*s *= h;
}
let mut e = oxifft::ifft2d(&spec, self.ny, self.nx);
if let Some(ref np) = self.n_profile {
for (ei, &ni) in e.iter_mut().zip(np) {
let dn = ni - self.n_ref;
let phase = k0 * dn * dz;
*ei *= Complex::new(0.0, phase).exp_im();
}
}
let mut spec2 = oxifft::fft2d(&e, self.ny, self.nx);
for (s, &h) in spec2.iter_mut().zip(&free_phase) {
*s *= h;
}
self.field = oxifft::ifft2d(&spec2, self.ny, self.nx);
}
pub fn propagate(&mut self, dz: f64, n_steps: usize) {
for _ in 0..n_steps {
self.step(dz);
}
}
pub fn intensity(&self) -> Vec<f64> {
self.field
.iter()
.map(|e| e.re * e.re + e.im * e.im)
.collect()
}
pub fn peak_intensity(&self) -> f64 {
self.intensity().iter().cloned().fold(0.0_f64, f64::max)
}
pub fn total_power(&self) -> f64 {
self.intensity().iter().sum::<f64>() * self.dx * self.dy
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fft_bpm_free_space_power_conservation() {
let nx = 256;
let dx = 100e-9; let mut bpm = FftBpm1d::new(nx, dx, 1.0, 1550e-9);
let xc = nx as f64 * dx / 2.0;
bpm.set_gaussian_input(1.0, xc, 1e-6);
let p0: f64 = bpm.intensity().iter().sum::<f64>() * dx;
bpm.propagate(1e-7, 5); let p1: f64 = bpm.intensity().iter().sum::<f64>() * dx;
let rel_err = (p1 - p0).abs() / p0;
assert!(rel_err < 1e-3, "Power not conserved: rel_err={rel_err:.2e}");
}
#[test]
fn fft_bpm_gaussian_beam_spreads() {
let nx = 512;
let dx = 50e-9;
let mut bpm = FftBpm1d::new(nx, dx, 1.0, 1550e-9);
let xc = nx as f64 * dx / 2.0;
let w0 = 1e-6; bpm.set_gaussian_input(1.0, xc, w0);
let w_init = bpm.rms_width();
bpm.propagate(1e-6, 10); let w_final = bpm.rms_width();
assert!(
w_final > w_init,
"Beam should spread: w_init={w_init:.3e} w_final={w_final:.3e}"
);
}
#[test]
fn fft_bpm_gaussian_analytical_match() {
let nx = 512;
let dx = 30e-9; let wavelength = 1550e-9;
let n_ref = 1.0;
let w0 = 1.5e-6; let z_r = PI * w0 * w0 / wavelength;
let mut bpm = FftBpm1d::new(nx, dx, n_ref, wavelength);
let xc = nx as f64 * dx / 2.0;
bpm.set_gaussian_input(1.0, xc, w0);
let i0 = bpm.peak_intensity();
let dz = 0.02 * z_r;
let n_steps = 10;
let z_total = dz * n_steps as f64;
bpm.propagate(dz, n_steps);
let i_final = bpm.peak_intensity();
let expected_ratio = 1.0 / (1.0 + (z_total / z_r).powi(2)).sqrt();
let computed_ratio = i_final / i0;
let rel_err = (computed_ratio - expected_ratio).abs() / expected_ratio;
assert!(
rel_err < 0.001,
"Gaussian beam propagation error: rel_err={rel_err:.4e} \
(computed={computed_ratio:.6}, expected={expected_ratio:.6})"
);
}
}