use std::f64::consts::PI;
#[derive(Debug, Clone, Copy)]
pub struct MziModulator {
pub v_pi: f64,
pub insertion_loss: f64,
pub extinction_ratio_db: f64,
pub bias_phase: f64,
}
impl MziModulator {
pub fn new(v_pi: f64) -> Self {
Self {
v_pi,
insertion_loss: 0.0,
bias_phase: 0.0,
extinction_ratio_db: f64::INFINITY,
}
}
pub fn with_params(v_pi: f64, insertion_loss_db: f64, extinction_ratio_db: f64) -> Self {
let insertion_loss = 1.0 - 10.0_f64.powf(-insertion_loss_db / 10.0);
Self {
v_pi,
insertion_loss,
bias_phase: 0.0,
extinction_ratio_db,
}
}
pub fn phase_shift(&self, voltage: f64) -> f64 {
PI * voltage / self.v_pi
}
pub fn transmission(&self, voltage: f64) -> f64 {
let dphi = self.phase_shift(voltage) + self.bias_phase;
let ideal_t = (dphi / 2.0).cos().powi(2);
(1.0 - self.insertion_loss) * ideal_t
}
pub fn transmission_db(&self, voltage: f64) -> f64 {
let t = self.transmission(voltage);
if t < 1e-30 {
-300.0
} else {
10.0 * t.log10()
}
}
pub fn v_3db(&self) -> f64 {
self.v_pi / 2.0
}
pub fn v_quadrature(&self) -> f64 {
self.v_pi / 2.0
}
pub fn small_signal_response(&self, v_rf: f64) -> f64 {
let slope = PI / (2.0 * self.v_pi);
slope * v_rf
}
pub fn transfer_curve(&self, v_min: f64, v_max: f64, n_pts: usize) -> Vec<(f64, f64)> {
(0..n_pts)
.map(|i| {
let v = v_min + i as f64 / (n_pts - 1) as f64 * (v_max - v_min);
(v, self.transmission(v))
})
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub struct PockelsModulator {
pub n: f64,
pub r33: f64,
pub length: f64,
pub gap: f64,
pub wavelength: f64,
}
impl PockelsModulator {
pub fn linbo3(length: f64, gap: f64, wavelength: f64) -> Self {
Self {
n: 2.14,
r33: 30.8e-12, length,
gap,
wavelength,
}
}
pub fn v_pi(&self) -> f64 {
self.wavelength * self.gap / (self.n.powi(3) * self.r33 * self.length)
}
pub fn phase_shift(&self, voltage: f64) -> f64 {
PI * voltage / self.v_pi()
}
pub fn as_mzi(&self) -> MziModulator {
MziModulator::new(self.v_pi())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mzi_zero_voltage_max_transmission() {
let m = MziModulator::new(5.0);
assert!((m.transmission(0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn mzi_v_pi_gives_zero_transmission() {
let m = MziModulator::new(5.0);
let t = m.transmission(5.0);
assert!(t < 1e-10, "T at V_π should be 0, got {t:.2e}");
}
#[test]
fn mzi_half_v_pi_is_half_transmission() {
let m = MziModulator::new(5.0);
let t = m.transmission(2.5); assert!(
(t - 0.5).abs() < 1e-6,
"T at V_π/2 should be 0.5, got {t:.4}"
);
}
#[test]
fn mzi_transfer_curve_length() {
let m = MziModulator::new(5.0);
let curve = m.transfer_curve(0.0, 10.0, 100);
assert_eq!(curve.len(), 100);
}
#[test]
fn mzi_transmission_in_unit_range() {
let m = MziModulator::new(5.0);
for v in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0] {
let t = m.transmission(v);
assert!((0.0..=1.0).contains(&t), "T={t:.4} at V={v:.1}");
}
}
#[test]
fn mzi_v3db_is_half_v_pi() {
let m = MziModulator::new(10.0);
assert!((m.v_3db() - 5.0).abs() < 1e-12);
}
#[test]
fn pockels_linbo3_v_pi_physical() {
let mod_ = PockelsModulator::linbo3(1e-2, 15e-6, 1550e-9);
let vpi = mod_.v_pi();
assert!(
vpi > 1.0 && vpi < 100.0,
"V_π={vpi:.2}V out of expected range"
);
}
#[test]
fn pockels_phase_shift_proportional_to_voltage() {
let m = PockelsModulator::linbo3(1e-2, 15e-6, 1550e-9);
let phi1 = m.phase_shift(1.0);
let phi2 = m.phase_shift(2.0);
assert!((phi2 / phi1 - 2.0).abs() < 1e-10);
}
}