use std::f64::consts::PI;
use num_complex::Complex64;
use crate::error::OxiPhotonError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EomType {
Phase,
Intensity,
IQ,
}
#[derive(Debug, Clone)]
pub struct PockelsEom {
pub v_pi: f64,
pub insertion_loss_db: f64,
pub bandwidth_ghz: f64,
pub center_wavelength_nm: f64,
pub modulator_type: EomType,
}
impl PockelsEom {
pub fn new(v_pi: f64, loss_db: f64, bw_ghz: f64, lambda_nm: f64, eom_type: EomType) -> Self {
Self {
v_pi,
insertion_loss_db: loss_db,
bandwidth_ghz: bw_ghz,
center_wavelength_nm: lambda_nm,
modulator_type: eom_type,
}
}
pub fn linbo3_phase(bw_ghz: f64) -> Self {
Self::new(3.0, 3.0, bw_ghz, 1550.0, EomType::Phase)
}
pub fn linbo3_intensity(bw_ghz: f64) -> Self {
Self::new(3.5, 4.0, bw_ghz, 1550.0, EomType::Intensity)
}
pub fn phase_shift_rad(&self, voltage_v: f64) -> f64 {
if self.v_pi.abs() < 1.0e-15 {
return 0.0;
}
PI * voltage_v / self.v_pi
}
pub fn phase_transmission(&self, voltage_v: f64) -> Complex64 {
let eta = 10.0_f64.powf(-self.insertion_loss_db / 10.0);
let phi = self.phase_shift_rad(voltage_v);
Complex64::new(0.0, phi).exp() * eta.sqrt()
}
pub fn intensity_transmission(&self, voltage_v: f64) -> f64 {
let eta = 10.0_f64.powf(-self.insertion_loss_db / 10.0);
if self.v_pi.abs() < 1.0e-15 {
return eta;
}
let arg = PI * voltage_v / (2.0 * self.v_pi) + PI / 4.0;
eta * arg.cos() * arg.cos()
}
pub fn extinction_ratio_db(&self) -> f64 {
40.0 }
pub fn chirp_parameter(&self) -> f64 {
match self.modulator_type {
EomType::Intensity => 0.0, EomType::Phase => 1.0, EomType::IQ => 0.0,
}
}
pub fn frequency_response(&self, freq_ghz: f64) -> f64 {
if self.bandwidth_ghz <= 0.0 {
return 0.0;
}
let x = freq_ghz / self.bandwidth_ghz;
1.0 / (1.0 + x * x).sqrt()
}
pub fn vpi_at_frequency(&self, freq_ghz: f64) -> f64 {
let h = self.frequency_response(freq_ghz);
if h < 1.0e-15 {
return f64::INFINITY;
}
self.v_pi / h
}
pub fn modulation_efficiency(&self) -> f64 {
if self.v_pi.abs() < 1.0e-15 {
return 0.0;
}
PI / self.v_pi
}
pub fn modulate_phase(
&self,
amplitude: &[Complex64],
voltage_waveform: &[f64],
) -> Result<Vec<Complex64>, OxiPhotonError> {
if amplitude.len() != voltage_waveform.len() {
return Err(OxiPhotonError::NumericalError(format!(
"amplitude length {} != voltage waveform length {}",
amplitude.len(),
voltage_waveform.len()
)));
}
let eta = 10.0_f64.powf(-self.insertion_loss_db / 10.0);
let amp_factor = eta.sqrt();
let result = amplitude
.iter()
.zip(voltage_waveform.iter())
.map(|(&a, &v)| {
let phi = self.phase_shift_rad(v);
a * Complex64::new(0.0, phi).exp() * amp_factor
})
.collect();
Ok(result)
}
pub fn modulate_intensity(
&self,
amplitude: &[Complex64],
voltage_waveform: &[f64],
) -> Result<Vec<Complex64>, OxiPhotonError> {
if amplitude.len() != voltage_waveform.len() {
return Err(OxiPhotonError::NumericalError(format!(
"amplitude length {} != voltage waveform length {}",
amplitude.len(),
voltage_waveform.len()
)));
}
let result = amplitude
.iter()
.zip(voltage_waveform.iter())
.map(|(&a, &v)| {
let t = self.intensity_transmission(v).max(0.0);
a * t.sqrt()
})
.collect();
Ok(result)
}
}
#[derive(Debug, Clone)]
pub struct IqModulator {
pub eom_i: PockelsEom,
pub eom_q: PockelsEom,
pub hybrid_phase: f64,
}
impl IqModulator {
pub fn new(v_pi: f64, loss_db: f64, bw_ghz: f64, lambda_nm: f64) -> Self {
Self {
eom_i: PockelsEom::new(v_pi, loss_db, bw_ghz, lambda_nm, EomType::Intensity),
eom_q: PockelsEom::new(v_pi, loss_db, bw_ghz, lambda_nm, EomType::Intensity),
hybrid_phase: PI / 2.0,
}
}
pub fn generate_field(&self, vi: f64, vq: f64) -> Complex64 {
let eta_i = 10.0_f64.powf(-self.eom_i.insertion_loss_db / 10.0);
let eta_q = 10.0_f64.powf(-self.eom_q.insertion_loss_db / 10.0);
let i_field = if self.eom_i.v_pi.abs() > 1.0e-15 {
(PI * vi / (2.0 * self.eom_i.v_pi)).cos() * eta_i.sqrt()
} else {
0.0
};
let q_field = if self.eom_q.v_pi.abs() > 1.0e-15 {
(PI * vq / (2.0 * self.eom_q.v_pi)).cos() * eta_q.sqrt()
} else {
0.0
};
let phase_q = Complex64::new(0.0, self.hybrid_phase).exp();
(Complex64::new(i_field, 0.0) + phase_q * q_field) / 2.0
}
pub fn qpsk_symbols() -> Vec<(f64, f64)> {
let v = 0.5_f64;
vec![(v, v), (-v, v), (-v, -v), (v, -v)]
}
pub fn qam16_symbols() -> Vec<(f64, f64)> {
let levels = [-0.75_f64, -0.25, 0.25, 0.75];
let mut symbols = Vec::with_capacity(16);
for &i in &levels {
for &q in &levels {
symbols.push((i, q));
}
}
symbols
}
pub fn modulate_symbols(&self, symbols: &[(f64, f64)]) -> Vec<Complex64> {
symbols
.iter()
.map(|&(vi, vq)| self.generate_field(vi, vq))
.collect()
}
pub fn evm_ideal_qpsk(&self) -> f64 {
let symbols = Self::qpsk_symbols();
let fields: Vec<Complex64> = self.modulate_symbols(&symbols);
let ref_amp = fields.iter().map(|f| f.norm()).sum::<f64>() / fields.len() as f64;
if ref_amp < 1.0e-20 {
return 1.0;
}
let ideal_amp = ref_amp;
let evm_sq = fields
.iter()
.map(|f| {
let err = f.norm() - ideal_amp;
err * err
})
.sum::<f64>()
/ fields.len() as f64;
evm_sq.sqrt() / ref_amp
}
}
#[derive(Debug, Clone)]
pub struct AcoustomOpticModulator {
pub center_frequency_mhz: f64,
pub bandwidth_mhz: f64,
pub diffraction_efficiency: f64,
pub rise_time_ns: f64,
pub wavelength_nm: f64,
}
impl AcoustomOpticModulator {
pub fn new(
center_mhz: f64,
bw_mhz: f64,
efficiency: f64,
rise_ns: f64,
lambda_nm: f64,
) -> Self {
Self {
center_frequency_mhz: center_mhz,
bandwidth_mhz: bw_mhz,
diffraction_efficiency: efficiency.clamp(0.0, 1.0),
rise_time_ns: rise_ns,
wavelength_nm: lambda_nm,
}
}
pub fn frequency_shift_hz(&self, order: i32) -> f64 {
order as f64 * self.center_frequency_mhz * 1.0e6
}
pub fn bragg_angle_rad(&self, sound_velocity_m_per_s: f64) -> f64 {
let lambda_m = self.wavelength_nm * 1.0e-9;
let f_hz = self.center_frequency_mhz * 1.0e6;
if sound_velocity_m_per_s < 1.0e-10 {
return 0.0;
}
let arg = (lambda_m * f_hz / (2.0 * sound_velocity_m_per_s)).clamp(-1.0, 1.0);
arg.asin()
}
pub fn diffracted_power_w(&self, input_power_w: f64) -> f64 {
self.diffraction_efficiency * input_power_w
}
pub fn frequency_response(&self, freq_mhz: f64) -> f64 {
let bw_term = if self.bandwidth_mhz > 0.0 {
1.0 / (1.0 + (freq_mhz / self.bandwidth_mhz).powi(2)).sqrt()
} else {
0.0
};
let tau_us = self.rise_time_ns * 1.0e-3; let ft = freq_mhz * tau_us; let sinc_term = if ft.abs() < 1.0e-10 {
1.0
} else {
(PI * ft).sin() / (PI * ft)
};
(bw_term * sinc_term.abs()).clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_pockels_vpi_phase() {
let eom = PockelsEom::linbo3_phase(10.0);
let phi = eom.phase_shift_rad(eom.v_pi);
assert_relative_eq!(phi, PI, max_relative = 1.0e-12);
}
#[test]
fn test_phase_modulator_intensity_constant() {
let eom = PockelsEom::linbo3_phase(10.0);
let eta = 10.0_f64.powf(-eom.insertion_loss_db / 10.0);
for v in [-5.0, -2.0, 0.0, 1.5, 3.0, 6.0] {
let t = eom.phase_transmission(v);
let power = t.norm_sqr();
assert!(
(power - eta).abs() < 1.0e-10,
"Phase mod power at V={v}: expected η={eta:.6}, got {power:.6}"
);
}
}
#[test]
fn test_intensity_mod_at_zero() {
let eom = PockelsEom::linbo3_intensity(10.0);
let t = eom.intensity_transmission(0.0);
let eta = 10.0_f64.powf(-eom.insertion_loss_db / 10.0);
let expected = eta * (PI / 4.0).cos().powi(2); assert!(
(t - expected).abs() < 1.0e-10,
"Intensity at V=0: expected {expected:.6}, got {t:.6}"
);
}
#[test]
fn test_intensity_mod_at_vpi() {
let eom = PockelsEom::linbo3_intensity(10.0);
let t_off = eom.intensity_transmission(eom.v_pi / 2.0);
assert!(
t_off < 1.0e-10,
"Off-state transmission should be ≈0, got {t_off:.6e}"
);
let t_on = eom.intensity_transmission(-eom.v_pi / 2.0);
let eta = 10.0_f64.powf(-eom.insertion_loss_db / 10.0);
assert!(
(t_on - eta).abs() < 1.0e-10,
"On-state transmission: expected η={eta:.4}, got {t_on:.4}"
);
}
#[test]
fn test_frequency_response_at_dc() {
let eom = PockelsEom::linbo3_phase(10.0);
let h0 = eom.frequency_response(0.0);
assert_relative_eq!(h0, 1.0, max_relative = 1.0e-12);
}
#[test]
fn test_frequency_response_at_3db() {
let bw = 20.0;
let eom = PockelsEom::linbo3_phase(bw);
let h_at_bw = eom.frequency_response(bw);
let expected = 1.0 / 2.0_f64.sqrt();
assert!(
(h_at_bw - expected).abs() < 1.0e-10,
"H(BW) should be 1/√2 ≈ {expected:.6}, got {h_at_bw:.6}"
);
}
#[test]
fn test_iq_qpsk_symbols() {
let syms = IqModulator::qpsk_symbols();
assert_eq!(syms.len(), 4, "QPSK must have 4 symbols");
let mag0 = (syms[0].0.powi(2) + syms[0].1.powi(2)).sqrt();
for &(vi, vq) in &syms {
let mag = (vi.powi(2) + vq.powi(2)).sqrt();
assert!(
(mag - mag0).abs() < 1.0e-10,
"QPSK symbols should have equal magnitude: {mag:.6} vs {mag0:.6}"
);
}
}
#[test]
fn test_iq_16qam_symbols() {
let syms = IqModulator::qam16_symbols();
assert_eq!(syms.len(), 16, "16-QAM must have 16 symbols");
}
#[test]
fn test_iq_modulate_symbols_output_length() {
let iq = IqModulator::new(3.5, 4.0, 20.0, 1550.0);
let syms = IqModulator::qpsk_symbols();
let fields = iq.modulate_symbols(&syms);
assert_eq!(
fields.len(),
syms.len(),
"Output length must match input length"
);
}
#[test]
fn test_aom_frequency_shift() {
let center_mhz = 80.0;
let aom = AcoustomOpticModulator::new(center_mhz, 10.0, 0.85, 50.0, 532.0);
let shift_p1 = aom.frequency_shift_hz(1);
let shift_m1 = aom.frequency_shift_hz(-1);
assert_relative_eq!(shift_p1, center_mhz * 1.0e6, max_relative = 1.0e-12);
assert_relative_eq!(shift_m1, -center_mhz * 1.0e6, max_relative = 1.0e-12);
}
#[test]
fn test_aom_bragg_angle_positive() {
let aom = AcoustomOpticModulator::new(80.0, 10.0, 0.85, 50.0, 780.0);
let theta = aom.bragg_angle_rad(4200.0);
assert!(theta > 0.0, "Bragg angle must be positive, got {theta:.4e}");
assert!(
theta < PI / 2.0,
"Bragg angle must be < π/2, got {theta:.4e}"
);
}
#[test]
fn test_aom_diffracted_power_scales_linearly() {
let aom = AcoustomOpticModulator::new(80.0, 10.0, 0.85, 50.0, 1064.0);
let p1 = aom.diffracted_power_w(1.0);
let p2 = aom.diffracted_power_w(2.0);
assert_relative_eq!(p2 / p1, 2.0, max_relative = 1.0e-12);
}
#[test]
fn test_aom_frequency_response_at_dc() {
let aom = AcoustomOpticModulator::new(80.0, 10.0, 0.85, 50.0, 1064.0);
let h = aom.frequency_response(0.0);
assert_relative_eq!(h, 1.0, max_relative = 1.0e-10);
}
#[test]
fn test_phase_modulate_preserves_length() {
let eom = PockelsEom::linbo3_phase(10.0);
let n = 128;
let amplitude: Vec<Complex64> = (0..n)
.map(|i| Complex64::new((i as f64 * 0.1).cos(), 0.0))
.collect();
let voltages: Vec<f64> = (0..n).map(|i| i as f64 * 0.05).collect();
let out = eom
.modulate_phase(&litude, &voltages)
.expect("modulate_phase");
assert_eq!(out.len(), n, "Output length must match input length");
}
#[test]
fn test_intensity_modulate_preserves_length() {
let eom = PockelsEom::linbo3_intensity(10.0);
let n = 64;
let amplitude: Vec<Complex64> = vec![Complex64::new(1.0, 0.0); n];
let voltages: Vec<f64> = vec![0.0; n];
let out = eom
.modulate_intensity(&litude, &voltages)
.expect("modulate_intensity");
assert_eq!(out.len(), n);
}
}