use std::f64::consts::PI;
const C_LIGHT: f64 = 2.997_924_58e8; const H_PLANCK: f64 = 6.626_070_15e-34;
#[derive(Debug, Clone)]
pub struct OpaLidar {
pub opa: super::phased_array::OpticalPhasedArray1d,
pub pulse_energy_j: f64,
pub pulse_duration_s: f64,
pub receiver_aperture_m2: f64,
pub wavelength_m: f64,
pub n_bits_dac: u32,
}
impl OpaLidar {
pub fn angular_resolution_rad(&self) -> f64 {
self.opa.hpbw_rad()
}
pub fn angular_resolution_deg(&self) -> f64 {
self.angular_resolution_rad().to_degrees()
}
pub fn n_resolvable_points(&self, fov_rad: f64) -> usize {
let hpbw = self.angular_resolution_rad();
if hpbw < f64::MIN_POSITIVE {
return 0;
}
(fov_rad / hpbw).floor() as usize
}
pub fn peak_power_w(&self) -> f64 {
if self.pulse_duration_s < f64::MIN_POSITIVE {
return 0.0;
}
self.pulse_energy_j / self.pulse_duration_s
}
pub fn photon_energy_j(&self) -> f64 {
H_PLANCK * C_LIGHT / self.wavelength_m
}
pub fn beam_solid_angle_sr(&self) -> f64 {
let hpbw = self.angular_resolution_rad();
hpbw * hpbw
}
pub fn received_power_w(&self, range_m: f64, target_reflectivity: f64) -> f64 {
if range_m < 1.0e-6 {
return 0.0;
}
let p_peak = self.peak_power_w();
let omega = self.beam_solid_angle_sr();
p_peak * self.receiver_aperture_m2 * target_reflectivity * omega
/ (4.0 * PI * PI * range_m * range_m)
}
pub fn max_range_m(&self, target_reflectivity: f64, noise_bandwidth_hz: f64) -> f64 {
const N_MIN_PHOTONS: f64 = 10.0; let p_noise = N_MIN_PHOTONS * self.photon_energy_j() * noise_bandwidth_hz;
if p_noise < f64::MIN_POSITIVE {
return f64::MAX;
}
let p_peak = self.peak_power_w();
let omega = self.beam_solid_angle_sr();
let numerator = p_peak * self.receiver_aperture_m2 * target_reflectivity * omega;
let denominator = 4.0 * PI * PI * p_noise;
if denominator < f64::MIN_POSITIVE {
return 0.0;
}
(numerator / denominator).sqrt()
}
pub fn range_resolution_m(&self) -> f64 {
C_LIGHT * self.pulse_duration_s / 2.0
}
pub fn pointing_error_from_phase_noise_rad(&self, phase_noise_rms_rad: f64) -> f64 {
let d = self.opa.pitch_m;
phase_noise_rms_rad * self.wavelength_m / (2.0 * PI * d)
}
pub fn required_dac_bits(&self, required_pointing_accuracy_rad: f64) -> u32 {
if required_pointing_accuracy_rad < f64::MIN_POSITIVE {
return 32;
}
let d = self.opa.pitch_m;
let ratio = self.wavelength_m / (required_pointing_accuracy_rad * d);
if ratio <= 1.0 {
return 1;
}
ratio.log2().ceil() as u32
}
pub fn scan_rate_fps(&self, phase_update_rate_hz: f64, fov_rad: f64) -> f64 {
let n_pts = self.n_resolvable_points(fov_rad);
if n_pts == 0 {
return 0.0;
}
phase_update_rate_hz / n_pts as f64
}
pub fn receiver_bandwidth_hz(&self) -> f64 {
if self.pulse_duration_s < f64::MIN_POSITIVE {
return 0.0;
}
0.44 / self.pulse_duration_s
}
}
#[derive(Debug, Clone)]
pub struct SiliconOpa {
pub n_elements: usize,
pub aperture_width_m: f64,
pub wavelength_m: f64,
pub tuning_range_nm: f64,
pub phase_shifter_vpi: f64,
}
impl SiliconOpa {
pub fn element_pitch_m(&self) -> f64 {
if self.n_elements == 0 {
return 0.0;
}
self.aperture_width_m / self.n_elements as f64
}
pub fn hpbw_x_rad(&self) -> f64 {
if self.aperture_width_m < f64::MIN_POSITIVE {
return PI;
}
0.886 * self.wavelength_m / self.aperture_width_m
}
pub fn wavelength_steering_angle_rad(&self, delta_lambda_m: f64) -> f64 {
let n_group: f64 = 4.0; (n_group / self.wavelength_m) * delta_lambda_m
}
pub fn total_fov_2d_rad2(&self) -> (f64, f64) {
let d = self.element_pitch_m();
let fov_x = if d > f64::MIN_POSITIVE {
let arg = self.wavelength_m / (2.0 * d) - 1.0 / self.n_elements.max(1) as f64;
let half = if arg >= 1.0 {
PI / 2.0
} else if arg <= -1.0 {
0.0
} else {
arg.asin()
};
2.0 * half
} else {
0.0
};
let delta_lambda = self.tuning_range_nm * 1.0e-9 / 2.0; let fov_y = 2.0 * self.wavelength_steering_angle_rad(delta_lambda).abs();
(fov_x, fov_y)
}
pub fn power_per_element_w(&self, total_power_w: f64) -> f64 {
if self.n_elements == 0 {
return 0.0;
}
total_power_w / self.n_elements as f64
}
pub fn n_resolvable_spots_2d(&self) -> usize {
let (fov_x, fov_y) = self.total_fov_2d_rad2();
let hpbw = self.hpbw_x_rad();
if hpbw < f64::MIN_POSITIVE {
return 0;
}
let nx = (fov_x / hpbw).round() as usize;
let ny = (fov_y / hpbw).round() as usize;
nx.max(1) * ny.max(1)
}
pub fn drive_voltage_for_phase(&self, phase_rad: f64) -> f64 {
self.phase_shifter_vpi * phase_rad / PI
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::photonic_antenna::phased_array::OpticalPhasedArray1d;
fn default_lidar() -> OpaLidar {
OpaLidar {
opa: OpticalPhasedArray1d::new(64, 775.0e-9, 1550.0e-9),
pulse_energy_j: 1.0e-9, pulse_duration_s: 10.0e-9, receiver_aperture_m2: 1.0e-4, wavelength_m: 1550.0e-9,
n_bits_dac: 8,
}
}
#[test]
fn lidar_range_resolution_10ns() {
let lidar = default_lidar();
let dr = lidar.range_resolution_m();
assert!((dr - 1.499).abs() < 0.01, "Range resolution: {dr}m");
}
#[test]
fn lidar_angular_resolution_positive() {
let lidar = default_lidar();
let ar = lidar.angular_resolution_rad();
assert!(ar > 0.0, "Angular resolution must be positive: {ar}");
}
#[test]
fn lidar_max_range_positive_for_reflective_target() {
let lidar = default_lidar();
let r_max = lidar.max_range_m(0.1, 100.0e6);
assert!(r_max > 0.0, "Max range must be positive: {r_max}");
}
#[test]
fn lidar_received_power_decreases_with_range() {
let lidar = default_lidar();
let p1 = lidar.received_power_w(10.0, 0.1);
let p2 = lidar.received_power_w(100.0, 0.1);
assert!(
p1 > p2,
"Power must decrease with range: P(10m)={p1}, P(100m)={p2}"
);
}
#[test]
fn lidar_pointing_error_proportional_to_phase_noise() {
let lidar = default_lidar();
let err1 = lidar.pointing_error_from_phase_noise_rad(0.1);
let err2 = lidar.pointing_error_from_phase_noise_rad(0.2);
assert!(
(err2 / err1 - 2.0).abs() < 1.0e-10,
"Pointing error must scale linearly with phase noise"
);
}
#[test]
fn lidar_required_dac_bits_reasonable() {
let lidar = default_lidar();
let bits = lidar.required_dac_bits(1.0e-4);
assert!(
(4..=16).contains(&bits),
"DAC bits should be in [4, 16]: {bits}"
);
}
#[test]
fn silicon_opa_fov_positive() {
let opa = SiliconOpa {
n_elements: 512,
aperture_width_m: 400.0e-6, wavelength_m: 1550.0e-9,
tuning_range_nm: 100.0,
phase_shifter_vpi: 3.0,
};
let (fov_x, fov_y) = opa.total_fov_2d_rad2();
assert!(fov_x > 0.0, "FOV_x must be positive: {fov_x}");
assert!(fov_y > 0.0, "FOV_y must be positive: {fov_y}");
}
#[test]
fn silicon_opa_power_per_element_divides_correctly() {
let opa = SiliconOpa {
n_elements: 100,
aperture_width_m: 100.0e-6,
wavelength_m: 1550.0e-9,
tuning_range_nm: 50.0,
phase_shifter_vpi: 5.0,
};
let p = opa.power_per_element_w(10.0e-3); assert!(
(p - 0.1e-3).abs() < 1.0e-10,
"Power per element must be 0.1 mW: {p}"
);
}
}