use num_complex::Complex64;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct FundamentalSoliton {
pub peak_power: f64,
pub pulse_width: f64,
pub beta2: f64,
pub gamma: f64,
pub wavelength: f64,
}
impl FundamentalSoliton {
pub fn new(t0: f64, beta2: f64, gamma: f64, wavelength: f64) -> Self {
assert!(gamma > 0.0, "FundamentalSoliton: gamma must be positive");
assert!(
t0 > 0.0,
"FundamentalSoliton: pulse_width (T0) must be positive"
);
let peak_power = beta2.abs() / (gamma * t0 * t0);
Self {
peak_power,
pulse_width: t0,
beta2,
gamma,
wavelength,
}
}
#[inline]
pub fn soliton_power(&self) -> f64 {
self.beta2.abs() / (self.gamma * self.pulse_width * self.pulse_width)
}
#[inline]
pub fn soliton_period(&self) -> f64 {
PI * self.pulse_width * self.pulse_width / (2.0 * self.beta2.abs())
}
pub fn field(&self, z: f64, t: f64) -> Complex64 {
let z0 = self.soliton_period();
let amplitude = self.peak_power.sqrt();
let sech = 1.0 / (t / self.pulse_width).cosh();
let phase = z / (2.0 * z0);
amplitude * sech * Complex64::new(0.0, phase).exp()
}
pub fn soliton_number(&self) -> f64 {
let n_sq =
self.gamma * self.peak_power * self.pulse_width * self.pulse_width / self.beta2.abs();
n_sq.sqrt()
}
pub fn raman_frequency_shift_rate(&self, raman_time: f64) -> f64 {
-8.0 * raman_time * self.beta2.abs() / (15.0 * self.pulse_width.powi(4))
}
pub fn dispersive_wave_frequency(&self, beta3: f64) -> f64 {
if beta3.abs() < 1e-60 {
return 0.0;
}
let delta_omega_linear = -3.0 * self.beta2 / beta3;
let correction = if delta_omega_linear.abs() > 1e-10 {
3.0 * self.gamma * self.peak_power / (beta3 * delta_omega_linear * delta_omega_linear)
} else {
0.0
};
delta_omega_linear - correction
}
}
#[derive(Debug, Clone)]
pub struct HigherOrderSoliton {
pub soliton_number: u32,
pub t0: f64,
pub beta2: f64,
pub gamma: f64,
pub wavelength_m: f64,
}
impl HigherOrderSoliton {
pub fn new(n: u32, t0: f64, beta2: f64, gamma: f64, wavelength_m: f64) -> Self {
assert!(n > 0, "HigherOrderSoliton: soliton_number must be ≥ 1");
assert!(gamma > 0.0, "HigherOrderSoliton: gamma must be positive");
assert!(t0 > 0.0, "HigherOrderSoliton: t0 must be positive");
assert!(
wavelength_m > 0.0,
"HigherOrderSoliton: wavelength_m must be positive"
);
Self {
soliton_number: n,
t0,
beta2,
gamma,
wavelength_m,
}
}
pub fn peak_power(&self) -> f64 {
let n = self.soliton_number as f64;
n * n * self.beta2.abs() / (self.gamma * self.t0 * self.t0)
}
pub fn soliton_period(&self) -> f64 {
PI * self.t0 * self.t0 / (2.0 * self.beta2.abs())
}
pub fn fission_distance(&self) -> f64 {
self.soliton_period() / self.soliton_number as f64
}
pub fn fission_products(&self) -> Vec<FundamentalSoliton> {
let n = self.soliton_number;
(1..=n)
.map(|k| {
let width_factor = 2 * n - 2 * k + 1;
let tk = self.t0 / width_factor as f64;
FundamentalSoliton::new(tk, self.beta2, self.gamma, self.wavelength_m)
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SolitonTrap {
pub soliton1: FundamentalSoliton,
pub soliton2: FundamentalSoliton,
pub temporal_separation: f64,
}
impl SolitonTrap {
pub fn new(s1: FundamentalSoliton, s2: FundamentalSoliton, dt: f64) -> Self {
Self {
soliton1: s1,
soliton2: s2,
temporal_separation: dt,
}
}
pub fn xpm_attraction_force(&self) -> f64 {
let s1 = &self.soliton1;
let s2 = &self.soliton2;
let gamma_eff = 0.5 * (s1.gamma + s2.gamma);
let t0_eff = 0.5 * (s1.pulse_width + s2.pulse_width);
let xi = self.temporal_separation / t0_eff;
let sech2 = 1.0 / xi.cosh().powi(2);
let tanh_val = xi.tanh();
2.0 * gamma_eff * s1.peak_power * sech2 * tanh_val.abs() / t0_eff
}
pub fn trapping_condition(&self) -> bool {
let n1 = self.soliton1.soliton_number();
let n2 = self.soliton2.soliton_number();
let anomalous1 = self.soliton1.beta2 < 0.0;
let anomalous2 = self.soliton2.beta2 < 0.0;
let t0_avg = 0.5 * (self.soliton1.pulse_width + self.soliton2.pulse_width);
let overlap = self.temporal_separation.abs() < 5.0 * t0_avg;
(n1 - 1.0).abs() < 0.2 && (n2 - 1.0).abs() < 0.2 && anomalous1 && anomalous2 && overlap
}
}
#[derive(Debug, Clone)]
pub struct PeregineSoliton {
pub background_amplitude: f64,
pub beta2: f64,
pub gamma: f64,
}
impl PeregineSoliton {
pub fn new(a0: f64, beta2: f64, gamma: f64) -> Self {
assert!(
a0 > 0.0,
"PeregineSoliton: background_amplitude must be positive"
);
assert!(gamma > 0.0, "PeregineSoliton: gamma must be positive");
Self {
background_amplitude: a0,
beta2,
gamma,
}
}
pub fn field(&self, z: f64, t: f64) -> Complex64 {
let a0 = self.background_amplitude;
let zeta = self.gamma * a0 * a0 * z;
let tau_scale = (2.0 * self.gamma * a0 * a0 / self.beta2.abs()).sqrt();
let tau = t * tau_scale;
let denom = 1.0 + 4.0 * tau * tau + 4.0 * zeta * zeta;
let numerator = Complex64::new(4.0, -8.0 * zeta); let rational_part = Complex64::new(1.0, 0.0) - Complex64::new(4.0, 8.0 * zeta) / denom;
let _ = numerator; let background_phase = Complex64::new(0.0, self.gamma * a0 * a0 * z).exp();
a0 * rational_part * background_phase
}
pub fn peak_amplitude(&self) -> f64 {
3.0 * self.background_amplitude
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn fundamental_soliton_power_matches_condition() {
let t0 = 1e-12;
let beta2 = -20e-27; let gamma = 1.3e-3;
let sol = FundamentalSoliton::new(t0, beta2, gamma, 1550e-9);
let expected_power = beta2.abs() / (gamma * t0 * t0);
assert_abs_diff_eq!(sol.soliton_power(), expected_power, epsilon = 1e-10);
}
#[test]
fn fundamental_soliton_number_is_one() {
let sol = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
assert_abs_diff_eq!(sol.soliton_number(), 1.0, epsilon = 1e-10);
}
#[test]
fn fundamental_soliton_period_positive() {
let sol = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
assert!(sol.soliton_period() > 0.0);
}
#[test]
fn fundamental_soliton_field_at_z0_t0() {
let sol = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
let f = sol.field(0.0, 0.0);
assert_abs_diff_eq!(f.norm(), sol.peak_power.sqrt(), epsilon = 1e-14);
}
#[test]
fn fundamental_soliton_raman_shift_is_negative() {
let sol = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
let rate = sol.raman_frequency_shift_rate(3e-15); assert!(
rate < 0.0,
"SSFS rate must be negative (red shift), got {rate}"
);
}
#[test]
fn fundamental_soliton_dispersive_wave_zero_beta3() {
let sol = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
assert_abs_diff_eq!(sol.dispersive_wave_frequency(0.0), 0.0, epsilon = 1e-30);
}
#[test]
fn fundamental_soliton_dispersive_wave_nonzero_beta3() {
let sol = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
let beta3 = 0.1e-39; let dw = sol.dispersive_wave_frequency(beta3);
assert!(dw.is_finite(), "dispersive_wave_frequency must be finite");
}
#[test]
fn higher_order_soliton_peak_power() {
let n = 3u32;
let t0 = 1e-12;
let beta2 = -20e-27;
let gamma = 1.3e-3;
let hos = HigherOrderSoliton::new(n, t0, beta2, gamma, 1550e-9);
let expected = (n as f64).powi(2) * beta2.abs() / (gamma * t0 * t0);
assert_abs_diff_eq!(hos.peak_power(), expected, epsilon = 1e-10);
}
#[test]
fn higher_order_soliton_fission_distance_less_than_period() {
let hos = HigherOrderSoliton::new(3, 1e-12, -20e-27, 1.3e-3, 1550e-9);
let z0 = hos.soliton_period();
let lfiss = hos.fission_distance();
assert!(
lfiss < z0,
"fission distance {lfiss:.3e} must be < soliton period {z0:.3e}"
);
}
#[test]
fn higher_order_soliton_fission_products_count() {
let n = 4u32;
let hos = HigherOrderSoliton::new(n, 1e-12, -20e-27, 1.3e-3, 1550e-9);
let products = hos.fission_products();
assert_eq!(
products.len(),
n as usize,
"should produce N fundamental solitons"
);
}
#[test]
fn higher_order_soliton_products_ordered_by_width() {
let hos = HigherOrderSoliton::new(3, 1e-12, -20e-27, 1.3e-3, 1550e-9);
let products = hos.fission_products();
assert!(products[0].pulse_width < products[1].pulse_width);
assert!(products[1].pulse_width < products[2].pulse_width);
}
#[test]
fn fission_products_inherit_wavelength() {
let soliton = HigherOrderSoliton::new(3, 100e-15, -20e-27, 1.3e-3, 1310e-9);
let products = soliton.fission_products();
assert_eq!(products.len(), 3);
for p in &products {
assert!(
(p.wavelength - 1310e-9).abs() < 1e-15,
"product wavelength should be 1310 nm, got {}",
p.wavelength
);
}
}
#[test]
fn soliton_trap_trapping_condition_fundamental_pair() {
let s1 = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
let s2 = FundamentalSoliton::new(1.2e-12, -18e-27, 1.3e-3, 1530e-9);
let trap = SolitonTrap::new(s1, s2, 2e-12); assert!(trap.trapping_condition());
}
#[test]
fn soliton_trap_no_trap_large_separation() {
let s1 = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
let s2 = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1530e-9);
let trap = SolitonTrap::new(s1, s2, 100e-12); assert!(!trap.trapping_condition(), "Large separation: no trapping");
}
#[test]
fn soliton_trap_xpm_force_positive() {
let s1 = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1550e-9);
let s2 = FundamentalSoliton::new(1e-12, -20e-27, 1.3e-3, 1530e-9);
let trap = SolitonTrap::new(s1, s2, 2e-12);
assert!(
trap.xpm_attraction_force() >= 0.0,
"XPM force must be non-negative"
);
}
#[test]
fn peregine_soliton_peak_amplitude_triple_background() {
let sol = PeregineSoliton::new(1.0, -20e-27, 1.3e-3);
assert_abs_diff_eq!(sol.peak_amplitude(), 3.0, epsilon = 1e-12);
}
#[test]
fn peregine_soliton_field_at_origin() {
let a0 = 2.0;
let sol = PeregineSoliton::new(a0, -20e-27, 1.3e-3);
let f = sol.field(0.0, 0.0);
assert_abs_diff_eq!(f.norm(), 3.0 * a0, epsilon = 1e-10);
}
#[test]
fn peregine_soliton_background_at_large_t() {
let a0 = 1.5;
let sol = PeregineSoliton::new(a0, -20e-27, 1.3e-3);
let t_scale = (sol.beta2.abs() / (2.0 * sol.gamma * a0 * a0)).sqrt();
let f = sol.field(0.0, 100.0 * t_scale); assert!(
(f.norm() / a0 - 1.0).abs() < 0.005,
"|ψ(t→∞)| = {:.4} should ≈ a₀ = {a0}",
f.norm()
);
}
}