use num_complex::Complex64;
use std::f64::consts::PI;
const SPEED_OF_LIGHT: f64 = 2.997_924_58e8;
#[derive(Debug, Clone)]
pub struct PhotonicBeamformer {
pub n_elements: usize,
pub element_spacing: f64,
pub wavelength_rf: f64,
pub wavelength_optical: f64,
pub delays: Vec<f64>,
}
impl PhotonicBeamformer {
pub fn new(n: usize, spacing: f64, lambda_rf: f64, lambda_opt: f64) -> Self {
PhotonicBeamformer {
n_elements: n,
element_spacing: spacing,
wavelength_rf: lambda_rf,
wavelength_optical: lambda_opt,
delays: vec![0.0; n],
}
}
pub fn new_steered(
n: usize,
spacing: f64,
lambda_rf: f64,
lambda_opt: f64,
theta_deg: f64,
) -> Self {
let mut bf = Self::new(n, spacing, lambda_rf, lambda_opt);
bf.set_steering_angle(theta_deg);
bf
}
pub fn set_steering_angle(&mut self, theta_deg: f64) {
let theta_rad = theta_deg.to_radians();
for k in 0..self.n_elements {
self.delays[k] = self.required_delay(k, theta_deg);
let _ = (theta_rad, k); }
}
pub fn required_delay(&self, element: usize, theta_deg: f64) -> f64 {
let theta_rad = theta_deg.to_radians();
element as f64 * self.element_spacing * theta_rad.sin() / SPEED_OF_LIGHT
}
pub fn array_factor(&self, theta_deg: f64) -> Complex64 {
let theta_rad = theta_deg.to_radians();
let k_rf = 2.0 * PI / self.wavelength_rf;
self.delays
.iter()
.enumerate()
.fold(Complex64::new(0.0, 0.0), |acc, (k, &tau_k)| {
let steering_phase = 2.0 * PI * SPEED_OF_LIGHT * tau_k / self.wavelength_rf;
let spatial_phase = k_rf * k as f64 * self.element_spacing * theta_rad.sin();
let total_phase = spatial_phase - steering_phase;
acc + Complex64::new(total_phase.cos(), total_phase.sin())
})
}
pub fn beam_pattern(&self, n_angles: usize) -> Vec<(f64, f64)> {
if n_angles == 0 {
return Vec::new();
}
let mut pattern: Vec<(f64, f64)> = (0..n_angles)
.map(|i| {
let theta = 180.0 * i as f64 / (n_angles as f64 - 1.0).max(1.0);
let power = self.array_factor(theta).norm_sqr();
(theta, power)
})
.collect();
let peak = pattern.iter().map(|(_, p)| *p).fold(0.0_f64, f64::max);
if peak > 0.0 {
for (_, p) in &mut pattern {
let normalized = *p / peak;
*p = if normalized > 1e-12 {
10.0 * normalized.log10()
} else {
-120.0
};
}
}
pattern
}
pub fn hpbw_deg(&self) -> f64 {
let hpbw_rad = 0.886 * self.wavelength_rf / (self.n_elements as f64 * self.element_spacing);
hpbw_rad.to_degrees()
}
pub fn beam_squint_deg_per_ghz(&self) -> f64 {
let theta_0 = self.current_steering_angle_deg();
let f_rf_ghz = SPEED_OF_LIGHT / self.wavelength_rf * 1e-9;
-theta_0 / f_rf_ghz
}
pub fn first_sidelobe_db(&self) -> f64 {
-13.26 }
fn current_steering_angle_deg(&self) -> f64 {
if self.n_elements < 2 || self.element_spacing <= 0.0 {
return 0.0;
}
let delta_tau = self.delays.last().copied().unwrap_or(0.0)
- self.delays.first().copied().unwrap_or(0.0);
let sin_theta =
delta_tau * SPEED_OF_LIGHT / ((self.n_elements as f64 - 1.0) * self.element_spacing);
sin_theta.clamp(-1.0, 1.0).asin().to_degrees()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BfnArchitecture {
Butler,
Blass,
Nolen,
}
#[derive(Debug, Clone)]
pub struct OpticalBfn {
pub n_ports: usize,
pub architecture: BfnArchitecture,
}
impl OpticalBfn {
pub fn new(n: usize, arch: BfnArchitecture) -> Self {
OpticalBfn {
n_ports: n,
architecture: arch,
}
}
pub fn n_beams(&self) -> usize {
match self.architecture {
BfnArchitecture::Butler => {
self.n_ports
}
BfnArchitecture::Blass => {
self.n_ports
}
BfnArchitecture::Nolen => self.n_ports,
}
}
pub fn insertion_loss_db(&self) -> f64 {
match self.architecture {
BfnArchitecture::Butler => {
let stages = (self.n_ports as f64).log2().ceil();
stages * (2.0 + 0.5)
}
BfnArchitecture::Blass => {
let stages = self.n_ports as f64;
stages * 1.5
}
BfnArchitecture::Nolen => {
let stages = (self.n_ports as f64).log2().ceil();
stages * 2.2
}
}
}
pub fn butler_beam_directions(&self) -> Vec<f64> {
let n = self.n_ports;
let mut directions = Vec::with_capacity(n);
for m in 1..=(n / 2) {
let sin_theta = (2 * m - 1) as f64 / n as f64;
if sin_theta.abs() <= 1.0 {
directions.push(-sin_theta.asin().to_degrees());
directions.push(sin_theta.asin().to_degrees());
}
}
directions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
directions
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn default_beamformer() -> PhotonicBeamformer {
let lambda_rf = SPEED_OF_LIGHT / 10.0e9;
PhotonicBeamformer::new(8, lambda_rf / 2.0, lambda_rf, 1550e-9)
}
#[test]
fn test_array_factor_broadside_is_n() {
let bf = default_beamformer();
let af = bf.array_factor(90.0); let af0 = bf.array_factor(0.0);
assert_abs_diff_eq!(af0.norm(), bf.n_elements as f64, epsilon = 1e-6);
let _ = af;
}
#[test]
fn test_required_delay_element_zero() {
let bf = default_beamformer();
assert_abs_diff_eq!(bf.required_delay(0, 30.0), 0.0, epsilon = 1e-20);
}
#[test]
fn test_required_delay_positive_angle() {
let bf = default_beamformer();
let lambda_rf = SPEED_OF_LIGHT / 10.0e9;
let d = lambda_rf / 2.0;
let tau = bf.required_delay(3, 30.0);
let expected = 3.0 * d * (30.0_f64.to_radians().sin()) / SPEED_OF_LIGHT;
assert_abs_diff_eq!(tau, expected, epsilon = 1e-15);
}
#[test]
fn test_hpbw_decreases_with_more_elements() {
let lambda_rf = SPEED_OF_LIGHT / 10.0e9;
let d = lambda_rf / 2.0;
let bf4 = PhotonicBeamformer::new(4, d, lambda_rf, 1550e-9);
let bf16 = PhotonicBeamformer::new(16, d, lambda_rf, 1550e-9);
assert!(
bf16.hpbw_deg() < bf4.hpbw_deg(),
"HPBW should decrease with more elements"
);
}
#[test]
fn test_hpbw_formula() {
let lambda_rf = SPEED_OF_LIGHT / 10.0e9;
let d = lambda_rf / 2.0;
let n = 8;
let bf = PhotonicBeamformer::new(n, d, lambda_rf, 1550e-9);
let expected_rad = 0.886 * lambda_rf / (n as f64 * d);
assert_abs_diff_eq!(bf.hpbw_deg(), expected_rad.to_degrees(), epsilon = 1e-6);
}
#[test]
fn test_beam_pattern_normalized_to_zero_db() {
let bf = default_beamformer();
let pattern = bf.beam_pattern(181);
let peak = pattern
.iter()
.map(|(_, p)| *p)
.fold(f64::NEG_INFINITY, f64::max);
assert_abs_diff_eq!(peak, 0.0, epsilon = 1e-6);
}
#[test]
fn test_beam_pattern_length() {
let bf = default_beamformer();
let pattern = bf.beam_pattern(91);
assert_eq!(pattern.len(), 91);
}
#[test]
fn test_set_steering_angle_updates_delays() {
let mut bf = default_beamformer();
bf.set_steering_angle(30.0);
assert!(bf.delays.iter().all(|&d| d >= 0.0));
for i in 1..bf.n_elements {
assert!(bf.delays[i] >= bf.delays[i - 1]);
}
}
#[test]
fn test_first_sidelobe_level() {
let bf = default_beamformer();
assert_abs_diff_eq!(bf.first_sidelobe_db(), -13.26, epsilon = 0.01);
}
#[test]
fn test_butler_n_beams() {
let bfn = OpticalBfn::new(8, BfnArchitecture::Butler);
assert_eq!(bfn.n_beams(), 8);
}
#[test]
fn test_butler_insertion_loss_increases_with_n() {
let bfn4 = OpticalBfn::new(4, BfnArchitecture::Butler);
let bfn16 = OpticalBfn::new(16, BfnArchitecture::Butler);
assert!(bfn16.insertion_loss_db() > bfn4.insertion_loss_db());
}
#[test]
fn test_butler_beam_directions_count() {
let bfn = OpticalBfn::new(8, BfnArchitecture::Butler);
let dirs = bfn.butler_beam_directions();
assert_eq!(dirs.len(), 8, "8-port Butler: expect 8 beam directions");
}
#[test]
fn test_blass_insertion_loss_positive() {
let bfn = OpticalBfn::new(4, BfnArchitecture::Blass);
assert!(bfn.insertion_loss_db() > 0.0);
}
}