use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct OpticalPhasedArray1d {
pub n_elements: usize,
pub pitch_m: f64,
pub wavelength_m: f64,
pub phases: Vec<f64>,
}
impl OpticalPhasedArray1d {
pub fn new(n_elements: usize, pitch_m: f64, wavelength_m: f64) -> Self {
Self {
n_elements,
pitch_m,
wavelength_m,
phases: vec![0.0; n_elements],
}
}
pub fn wave_number(&self) -> f64 {
2.0 * PI / self.wavelength_m
}
pub fn steer_to_angle(&mut self, theta_rad: f64) {
let k = self.wave_number();
let d = self.pitch_m;
for (n, phi) in self.phases.iter_mut().enumerate() {
*phi = -(n as f64) * k * d * theta_rad.sin();
}
}
pub fn array_factor(&self, theta_rad: f64) -> f64 {
if self.n_elements == 0 {
return 0.0;
}
let k = self.wave_number();
let d = self.pitch_m;
let (mut re_sum, mut im_sum) = (0.0_f64, 0.0_f64);
for (n, &phi_n) in self.phases.iter().enumerate() {
let psi = phi_n + (n as f64) * k * d * theta_rad.sin();
re_sum += psi.cos();
im_sum += psi.sin();
}
let n = self.n_elements as f64;
(re_sum * re_sum + im_sum * im_sum).sqrt() / n
}
pub fn beam_pattern(&self, theta_rad: f64) -> f64 {
self.array_factor(theta_rad).powi(2)
}
pub fn has_grating_lobes(&self) -> bool {
self.pitch_m > self.wavelength_m / 2.0
}
pub fn hpbw_rad(&self) -> f64 {
0.886 * self.wavelength_m / (self.n_elements as f64 * self.pitch_m)
}
pub fn hpbw_deg(&self) -> f64 {
self.hpbw_rad().to_degrees()
}
pub fn scan_range_rad(&self) -> f64 {
let arg = self.wavelength_m / (2.0 * self.pitch_m) - 1.0 / self.n_elements as f64;
if arg >= 1.0 {
PI / 2.0
} else if arg <= -1.0 {
0.0
} else {
arg.asin()
}
}
pub fn scan_range_deg(&self) -> f64 {
self.scan_range_rad().to_degrees()
}
pub fn peak_gain(&self, element_gain: f64) -> f64 {
(self.n_elements as f64).powi(2) * element_gain
}
pub fn side_lobe_level_db(&self) -> f64 {
-13.3
}
pub fn phase_quantization_error_rad(&self, bits: u32) -> f64 {
if bits >= 63 {
return f64::MIN_POSITIVE;
}
2.0 * PI / (1u64 << bits) as f64
}
pub fn current_steering_angle_rad(&self) -> f64 {
if self.phases.len() < 2 {
return 0.0;
}
let dphi_dn = self.phases[1] - self.phases[0]; let arg = (self.wavelength_m / (2.0 * PI * self.pitch_m) * dphi_dn).clamp(-1.0, 1.0);
arg.asin()
}
pub fn scan_pattern(&self, n_points: usize) -> Vec<(f64, f64)> {
if n_points == 0 {
return Vec::new();
}
(0..n_points)
.map(|i| {
let theta = -PI / 2.0 + PI * i as f64 / (n_points - 1).max(1) as f64;
(theta, self.beam_pattern(theta))
})
.collect()
}
pub fn directivity_approx(&self) -> f64 {
2.0 * self.n_elements as f64 * self.pitch_m / self.wavelength_m
}
}
#[derive(Debug, Clone)]
pub struct OpticalPhasedArray2d {
pub nx: usize,
pub ny: usize,
pub pitch_x_m: f64,
pub pitch_y_m: f64,
pub wavelength_m: f64,
pub phases: Vec<Vec<f64>>,
}
impl OpticalPhasedArray2d {
pub fn new(nx: usize, ny: usize, pitch_x_m: f64, pitch_y_m: f64, wavelength_m: f64) -> Self {
let phases = vec![vec![0.0_f64; nx]; ny];
Self {
nx,
ny,
pitch_x_m,
pitch_y_m,
wavelength_m,
phases,
}
}
pub fn wave_number(&self) -> f64 {
2.0 * PI / self.wavelength_m
}
pub fn steer_to_angle(&mut self, theta_x_rad: f64, theta_y_rad: f64) {
let k = self.wave_number();
let dx = self.pitch_x_m;
let dy = self.pitch_y_m;
for iy in 0..self.ny {
for ix in 0..self.nx {
self.phases[iy][ix] = -(ix as f64) * k * dx * theta_x_rad.sin()
- (iy as f64) * k * dy * theta_y_rad.sin();
}
}
}
pub fn array_factor(&self, theta_x: f64, theta_y: f64) -> f64 {
if self.nx == 0 || self.ny == 0 {
return 0.0;
}
let k = self.wave_number();
let dx = self.pitch_x_m;
let dy = self.pitch_y_m;
let (mut re_sum, mut im_sum) = (0.0_f64, 0.0_f64);
for (iy, row) in self.phases.iter().enumerate() {
for (ix, &phi) in row.iter().enumerate() {
let psi = phi
+ (ix as f64) * k * dx * theta_x.sin()
+ (iy as f64) * k * dy * theta_y.sin();
re_sum += psi.cos();
im_sum += psi.sin();
}
}
let n_total = (self.nx * self.ny) as f64;
(re_sum * re_sum + im_sum * im_sum).sqrt() / n_total
}
pub fn total_elements(&self) -> usize {
self.nx * self.ny
}
pub fn hpbw_x_rad(&self) -> f64 {
0.886 * self.wavelength_m / (self.nx as f64 * self.pitch_x_m)
}
pub fn hpbw_y_rad(&self) -> f64 {
0.886 * self.wavelength_m / (self.ny as f64 * self.pitch_y_m)
}
pub fn scan_range_x_rad(&self) -> f64 {
let arg = self.wavelength_m / (2.0 * self.pitch_x_m) - 1.0 / self.nx as f64;
if arg >= 1.0 {
PI / 2.0
} else if arg <= -1.0 {
0.0
} else {
arg.asin()
}
}
pub fn scan_range_y_rad(&self) -> f64 {
let arg = self.wavelength_m / (2.0 * self.pitch_y_m) - 1.0 / self.ny as f64;
if arg >= 1.0 {
PI / 2.0
} else if arg <= -1.0 {
0.0
} else {
arg.asin()
}
}
pub fn n_resolvable_spots(&self) -> usize {
let nx = (2.0 * self.scan_range_x_rad() / self.hpbw_x_rad()).round() as usize;
let ny = (2.0 * self.scan_range_y_rad() / self.hpbw_y_rad()).round() as usize;
nx.max(1) * ny.max(1)
}
pub fn has_grating_lobes_x(&self) -> bool {
self.pitch_x_m > self.wavelength_m / 2.0
}
pub fn has_grating_lobes_y(&self) -> bool {
self.pitch_y_m > self.wavelength_m / 2.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opa_steer_to_10_degrees() {
let mut opa = OpticalPhasedArray1d::new(32, 775.0e-9, 1550.0e-9); let theta = 10_f64.to_radians();
opa.steer_to_angle(theta);
let af_at_theta = opa.array_factor(theta);
let af_at_zero = opa.array_factor(0.0);
assert!(
af_at_theta > af_at_zero * 0.9,
"Beam not at θ=10°: AF(θ)={:.4}, AF(0)={:.4}",
af_at_theta,
af_at_zero
);
}
#[test]
fn opa_broadside_array_factor_unity() {
let opa = OpticalPhasedArray1d::new(16, 775.0e-9, 1550.0e-9);
let af = opa.array_factor(0.0);
assert!((af - 1.0).abs() < 1.0e-10, "Broadside AF must be 1.0: {af}");
}
#[test]
fn opa_no_grating_lobes_at_half_wavelength_pitch() {
let opa = OpticalPhasedArray1d::new(32, 775.0e-9, 1550.0e-9);
assert!(
!opa.has_grating_lobes(),
"d=λ/2 should not produce grating lobes"
);
}
#[test]
fn opa_grating_lobes_at_full_wavelength_pitch() {
let opa = OpticalPhasedArray1d::new(32, 1550.0e-9, 1550.0e-9);
assert!(opa.has_grating_lobes(), "d=λ should produce grating lobes");
}
#[test]
fn opa_hpbw_decreases_with_more_elements() {
let opa8 = OpticalPhasedArray1d::new(8, 775.0e-9, 1550.0e-9);
let opa32 = OpticalPhasedArray1d::new(32, 775.0e-9, 1550.0e-9);
assert!(
opa32.hpbw_rad() < opa8.hpbw_rad(),
"Larger array must have narrower beam"
);
}
#[test]
fn opa_peak_gain_quadratic_in_n() {
let n = 16_usize;
let opa = OpticalPhasedArray1d::new(n, 775.0e-9, 1550.0e-9);
let g = opa.peak_gain(1.0);
let expected = (n * n) as f64;
assert!(
(g - expected).abs() < 1.0e-10,
"Peak gain = N²: {g} vs {expected}"
);
}
#[test]
fn opa2d_total_elements() {
let opa = OpticalPhasedArray2d::new(8, 8, 775.0e-9, 775.0e-9, 1550.0e-9);
assert_eq!(opa.total_elements(), 64);
}
#[test]
fn opa2d_broadside_af_unity() {
let opa = OpticalPhasedArray2d::new(8, 8, 775.0e-9, 775.0e-9, 1550.0e-9);
let af = opa.array_factor(0.0, 0.0);
assert!(
(af - 1.0).abs() < 1.0e-10,
"2D broadside AF must be 1.0: {af}"
);
}
#[test]
fn opa2d_steer_increases_af_at_target() {
let mut opa = OpticalPhasedArray2d::new(16, 16, 775.0e-9, 775.0e-9, 1550.0e-9);
let tx = 5_f64.to_radians();
let ty = 5_f64.to_radians();
opa.steer_to_angle(tx, ty);
let af_target = opa.array_factor(tx, ty);
let af_boresight = opa.array_factor(0.0, 0.0);
assert!(
af_target > af_boresight * 0.85,
"2D beam not at target: AF(tgt)={:.4} AF(0,0)={:.4}",
af_target,
af_boresight
);
}
}