use std::f64::consts::PI;
pub struct WideAngleBpm1d {
pub n_x: usize,
pub dx: f64,
pub k0n0: f64,
pub field: Vec<[f64; 2]>, pub n_profile: Vec<f64>,
pub n_ref: f64,
}
impl WideAngleBpm1d {
pub fn new(n_x: usize, dx: f64, wavelength: f64, n_ref: f64) -> Self {
let k0 = 2.0 * PI / wavelength;
Self {
n_x,
dx,
k0n0: k0 * n_ref,
field: vec![[0.0, 0.0]; n_x],
n_profile: vec![n_ref; n_x],
n_ref,
}
}
pub fn set_gaussian(&mut self, a0: f64, x_center: f64, w0: f64) {
let x0 = self.n_x as f64 / 2.0 * self.dx + x_center;
for i in 0..self.n_x {
let x = i as f64 * self.dx;
let dx = x - x0;
let amp = a0 * (-dx * dx / (w0 * w0)).exp();
self.field[i] = [amp, 0.0];
}
}
pub fn set_index_profile(&mut self, n: Vec<f64>) {
assert_eq!(n.len(), self.n_x);
self.n_profile = n;
}
pub fn step(&mut self, dz: f64) {
let k0 = self.k0n0 / self.n_ref;
for i in 0..self.n_x {
let delta_n = self.n_profile[i] - self.n_ref;
let phase = k0 * delta_n * dz;
let (s, c) = phase.sin_cos();
let [re, im] = self.field[i];
self.field[i] = [re * c - im * s, re * s + im * c];
}
let beta = self.k0n0;
let alpha = 1.0 / (2.0 * beta * self.dx * self.dx); let rhs = self.apply_diffraction_rhs(dz, alpha);
self.field = self.tridiagonal_solve_cn(dz, alpha, &rhs);
}
fn apply_diffraction_rhs(&self, dz: f64, alpha: f64) -> Vec<[f64; 2]> {
let n = self.n_x;
let mut rhs = vec![[0.0f64; 2]; n];
let a_dz_half = alpha * dz * 0.5; #[allow(clippy::needless_range_loop)]
for i in 0..n {
let u = self.field[i];
let lu = if i == 0 || i == n - 1 {
[0.0, 0.0] } else {
let up = self.field[i + 1];
let um = self.field[i - 1];
[(up[0] - 2.0 * u[0] + um[0]), (up[1] - 2.0 * u[1] + um[1])]
};
rhs[i] = [u[0] + a_dz_half * (-lu[1]), u[1] + a_dz_half * (lu[0])];
}
rhs
}
fn tridiagonal_solve_cn(&self, dz: f64, alpha: f64, rhs: &[[f64; 2]]) -> Vec<[f64; 2]> {
let n = self.n_x;
let a_dz_half = alpha * dz * 0.5;
let off_re = 0.0; let off_im = -a_dz_half; let diag_re = 1.0;
let diag_im = 2.0 * a_dz_half; let mut d_re = vec![0.0; n];
let mut d_im = vec![0.0; n];
let mut w = vec![[0.0f64; 2]; n]; let mut g = vec![[0.0f64; 2]; n];
w[0] = [diag_re, diag_im];
g[0] = rhs[0];
for i in 1..n {
let wm = w[i - 1];
let denom = wm[0] * wm[0] + wm[1] * wm[1];
let off_over_wm_re = (off_re * wm[0] + off_im * wm[1]) / denom;
let off_over_wm_im = (off_im * wm[0] - off_re * wm[1]) / denom;
let factor_re = off_over_wm_re * off_re - off_over_wm_im * off_im;
let factor_im = off_over_wm_re * off_im + off_over_wm_im * off_re;
w[i] = [diag_re - factor_re, diag_im - factor_im];
let gm = g[i - 1];
let subtracted_re = off_over_wm_re * gm[0] - off_over_wm_im * gm[1];
let subtracted_im = off_over_wm_re * gm[1] + off_over_wm_im * gm[0];
g[i] = [rhs[i][0] - subtracted_re, rhs[i][1] - subtracted_im];
}
let wn = w[n - 1];
let denom = wn[0] * wn[0] + wn[1] * wn[1];
d_re[n - 1] = (g[n - 1][0] * wn[0] + g[n - 1][1] * wn[1]) / denom;
d_im[n - 1] = (g[n - 1][1] * wn[0] - g[n - 1][0] * wn[1]) / denom;
for i in (0..n - 1).rev() {
let wnext = [d_re[i + 1], d_im[i + 1]];
let off_d_re = off_re * wnext[0] - off_im * wnext[1];
let off_d_im = off_re * wnext[1] + off_im * wnext[0];
let num_re = g[i][0] - off_d_re;
let num_im = g[i][1] - off_d_im;
let wi = w[i];
let denom = wi[0] * wi[0] + wi[1] * wi[1];
d_re[i] = (num_re * wi[0] + num_im * wi[1]) / denom;
d_im[i] = (num_im * wi[0] - num_re * wi[1]) / denom;
}
(0..n).map(|i| [d_re[i], d_im[i]]).collect()
}
pub fn total_power(&self) -> f64 {
self.field
.iter()
.map(|&[re, im]| re * re + im * im)
.sum::<f64>()
* self.dx
}
pub fn rms_width(&self) -> f64 {
let x0 = self.n_x as f64 / 2.0 * self.dx;
let p = self.total_power();
if p < 1e-30 {
return 0.0;
}
let var: f64 = self
.field
.iter()
.enumerate()
.map(|(i, &[re, im])| {
let x = i as f64 * self.dx - x0;
(re * re + im * im) * x * x
})
.sum::<f64>()
* self.dx;
(var / p).sqrt()
}
pub fn propagate(&mut self, n_steps: usize, dz: f64) {
for _ in 0..n_steps {
self.step(dz);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wa_bpm_initializes_zero() {
let bpm = WideAngleBpm1d::new(128, 1e-6, 1550e-9, 1.0);
assert!(bpm.total_power() == 0.0);
}
#[test]
fn wa_bpm_gaussian_power() {
let mut bpm = WideAngleBpm1d::new(256, 0.5e-6, 1550e-9, 1.0);
bpm.set_gaussian(1.0, 0.0, 10e-6);
let p = bpm.total_power();
assert!(p > 0.0);
}
#[test]
fn wa_bpm_power_conserved_free_space() {
let mut bpm = WideAngleBpm1d::new(512, 0.25e-6, 1550e-9, 1.5);
bpm.set_gaussian(1.0, 0.0, 5e-6);
let p0 = bpm.total_power();
bpm.propagate(20, 5e-6);
let p1 = bpm.total_power();
let rel_err = (p1 - p0).abs() / p0;
assert!(rel_err < 0.05, "Power err={rel_err:.3}");
}
#[test]
fn wa_bpm_beam_spreads() {
let mut bpm = WideAngleBpm1d::new(512, 0.25e-6, 1550e-9, 1.5);
bpm.set_gaussian(1.0, 0.0, 5e-6);
let w0 = bpm.rms_width();
bpm.propagate(50, 10e-6);
let w1 = bpm.rms_width();
assert!(w1 > w0, "Beam should spread: w0={w0:.2e} w1={w1:.2e}");
}
}