Skip to main content

sc_neurocore_engine/
photonic.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Rust Photonic NoC Acceleration
8
9//! High-performance photonic network-on-chip primitives.
10//!
11//! Accelerates the hot paths in the Python `photonic_noc` bridge:
12//! - **Waveguide routing** — O(N²) Manhattan distance on mesh
13//! - **Crosstalk analysis** — O(N²) pairwise channel coupling
14//! - **MZI transfer matrix** — 2×2 unitary matrix cascade
15//! - **Power budget** — path loss accumulation
16
17use rayon::prelude::*;
18
19// ── Constants ────────────────────────────────────────────────────────
20
21const CROSSING_LOSS_DB: f64 = 0.08;
22#[allow(dead_code)] // reserved for future MZI loss accounting
23const MZI_INSERTION_LOSS_DB: f64 = 0.5;
24
25// ── Waveguide routing ────────────────────────────────────────────────
26
27/// A routed waveguide segment.
28#[derive(Clone, Debug)]
29pub struct WaveguideResult {
30    pub source: usize,
31    pub target: usize,
32    pub length_um: f64,
33    pub loss_db: f64,
34    pub n_crossings: usize,
35}
36
37/// Route waveguides on a mesh topology from an adjacency matrix.
38///
39/// Returns vector of (source, target, length_um, loss_db, n_crossings).
40pub fn route_waveguides(
41    adjacency: &[f64],
42    n: usize,
43    pitch_um: f64,
44    loss_db_per_cm: f64,
45) -> Vec<WaveguideResult> {
46    let grid_size = ((n as f64).sqrt().ceil() as usize).max(1);
47
48    let pairs: Vec<(usize, usize)> = (0..n)
49        .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
50        .collect();
51
52    pairs
53        .par_iter()
54        .filter_map(|&(i, j)| {
55            let w = adjacency[i * n + j].abs() + adjacency[j * n + i].abs();
56            if w < 1e-12 {
57                return None;
58            }
59
60            let (ri, ci) = (i / grid_size, i % grid_size);
61            let (rj, cj) = (j / grid_size, j % grid_size);
62            let manhattan = (ri as isize - rj as isize).unsigned_abs()
63                + (ci as isize - cj as isize).unsigned_abs();
64
65            let length_um = manhattan as f64 * pitch_um;
66            let mut loss = length_um * 1e-4 * loss_db_per_cm;
67            let n_crossings = if manhattan > 0 { manhattan - 1 } else { 0 };
68            loss += n_crossings as f64 * CROSSING_LOSS_DB;
69
70            Some(WaveguideResult {
71                source: i,
72                target: j,
73                length_um,
74                loss_db: loss,
75                n_crossings,
76            })
77        })
78        .collect()
79}
80
81// ── MZI transfer matrix ──────────────────────────────────────────────
82
83/// MZI 2×2 transfer matrix elements (complex).
84///
85/// M = [[cos(φ/2), i·sin(φ/2)], [i·sin(φ/2), cos(φ/2)]]
86///
87/// Returns (re_00, im_00, re_01, im_01, re_10, im_10, re_11, im_11)
88pub fn mzi_transfer_matrix(phase_rad: f64) -> [f64; 8] {
89    let half = phase_rad / 2.0;
90    let c = half.cos();
91    let s = half.sin();
92    // [[c, i·s], [i·s, c]]  →  stored as (re, im) pairs
93    [c, 0.0, 0.0, s, 0.0, s, c, 0.0]
94}
95
96/// Cascade N MZI stages by multiplying transfer matrices.
97///
98/// Returns the final 2×2 complex matrix as 8 f64s.
99pub fn cascade_mzi(phases: &[f64]) -> [f64; 8] {
100    let mut result = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0]; // identity
101
102    for &phase in phases {
103        let m = mzi_transfer_matrix(phase);
104        result = complex_mat_mul(&result, &m);
105    }
106
107    result
108}
109
110/// Multiply two 2×2 complex matrices (each stored as 8 f64s).
111fn complex_mat_mul(a: &[f64; 8], b: &[f64; 8]) -> [f64; 8] {
112    // a = [[a00, a01], [a10, a11]], b = [[b00, b01], [b10, b11]]
113    // Each element is (re, im) at indices (2k, 2k+1)
114    let cmul = |ar: f64, ai: f64, br: f64, bi: f64| -> (f64, f64) {
115        (ar * br - ai * bi, ar * bi + ai * br)
116    };
117    let cadd = |a: (f64, f64), b: (f64, f64)| -> (f64, f64) { (a.0 + b.0, a.1 + b.1) };
118
119    // c00 = a00*b00 + a01*b10
120    let c00 = cadd(cmul(a[0], a[1], b[0], b[1]), cmul(a[2], a[3], b[4], b[5]));
121    // c01 = a00*b01 + a01*b11
122    let c01 = cadd(cmul(a[0], a[1], b[2], b[3]), cmul(a[2], a[3], b[6], b[7]));
123    // c10 = a10*b00 + a11*b10
124    let c10 = cadd(cmul(a[4], a[5], b[0], b[1]), cmul(a[6], a[7], b[4], b[5]));
125    // c11 = a10*b01 + a11*b11
126    let c11 = cadd(cmul(a[4], a[5], b[2], b[3]), cmul(a[6], a[7], b[6], b[7]));
127
128    [c00.0, c00.1, c01.0, c01.1, c10.0, c10.1, c11.0, c11.1]
129}
130
131// ── Crosstalk analysis ───────────────────────────────────────────────
132
133/// Crosstalk result per channel.
134#[derive(Clone, Debug)]
135pub struct CrosstalkResult {
136    pub channel_id: usize,
137    pub wavelength_nm: f64,
138    pub n_adjacent: usize,
139    pub crosstalk_db: f64,
140    pub osnr_db: f64,
141}
142
143/// Analyze inter-channel crosstalk for WDM channels.
144///
145/// channels: list of (channel_id, wavelength_nm, bandwidth_nm, power_dbm)
146pub fn analyze_crosstalk(
147    channels: &[(usize, f64, f64, f64)],
148    adjacent_xt_db: f64,
149) -> Vec<CrosstalkResult> {
150    channels
151        .par_iter()
152        .map(|&(ch_id, wl, bw, power)| {
153            let n_adj = channels
154                .iter()
155                .filter(|&&(other_id, other_wl, _, _)| {
156                    other_id != ch_id && (wl - other_wl).abs() < bw * 3.0
157                })
158                .count();
159
160            let xt = adjacent_xt_db + 10.0 * (n_adj.max(1) as f64).log10();
161            let osnr = power - xt;
162
163            CrosstalkResult {
164                channel_id: ch_id,
165                wavelength_nm: wl,
166                n_adjacent: n_adj,
167                crosstalk_db: xt,
168                osnr_db: osnr,
169            }
170        })
171        .collect()
172}
173
174// ── Power budget ─────────────────────────────────────────────────────
175
176/// Path power budget result.
177#[derive(Clone, Debug)]
178pub struct PowerBudgetResult {
179    pub source: usize,
180    pub target: usize,
181    pub total_loss_db: f64,
182    pub received_power_dbm: f64,
183    pub margin_db: f64,
184    pub passed: bool,
185}
186
187/// Analyze power budget for all waveguide paths.
188pub fn analyze_power_budget(
189    waveguides: &[(usize, usize, f64)], // (source, target, wg_loss_db)
190    mzi_ports: &[(Vec<usize>, usize, f64)], // (input_ports, output_port, insertion_loss)
191    laser_power_dbm: f64,
192    detector_sensitivity_dbm: f64,
193) -> Vec<PowerBudgetResult> {
194    waveguides
195        .par_iter()
196        .map(|&(src, tgt, wg_loss)| {
197            let mzi_loss: f64 = mzi_ports
198                .iter()
199                .filter(|(inputs, output, _)| inputs.contains(&src) || *output == tgt)
200                .map(|(_, _, loss)| loss)
201                .sum();
202
203            let total_loss = wg_loss + mzi_loss;
204            let received = laser_power_dbm - total_loss;
205            let margin = received - detector_sensitivity_dbm;
206
207            PowerBudgetResult {
208                source: src,
209                target: tgt,
210                total_loss_db: total_loss,
211                received_power_dbm: received,
212                margin_db: margin,
213                passed: margin >= 0.0,
214            }
215        })
216        .collect()
217}
218
219// ── Geometric (evanescent) crosstalk for a waveguide bank ───────────
220//
221// Physical model: coupled-mode theory with a Marcatili-form transverse
222// decay for the coupling coefficient.
223//
224//   L_decay(λ, n_core, n_clad) = λ / (2π √(n_core² - n_clad²))      [nm]
225//   Δn_eff(g)                  = 0.1 · exp(-g / L_decay)             [—]
226//   κ(g)                       = π · Δn_eff(g) / (λ [µm])            [µm⁻¹]
227//   power coupling ratio       = sin²(κ·L)                           [—]
228//   pair isolation             = -10 log₁₀(ratio)                    [dB]
229//
230// References:
231// - Marcatili, Bell Syst. Tech. J. 48(7):2071-2102, 1969
232// - Okamoto, *Fundamentals of Optical Waveguides*, 2006, Ch. 4
233
234#[derive(Clone, Debug)]
235pub struct CrosstalkPairResult {
236    pub index_a: usize,
237    pub index_b: usize,
238    pub gap_nm: f64,
239    pub coupling_length_um: f64,
240    pub coupling_coefficient_per_um: f64,
241    pub coupling_ratio: f64,
242    pub isolation_db: f64,
243}
244
245#[derive(Clone, Debug)]
246pub struct CrosstalkBankResult {
247    pub num_waveguides: usize,
248    pub num_near_pairs: usize,
249    pub num_far_pairs: usize,
250    pub gap_nm: f64,
251    pub coupling_length_um: f64,
252    pub adjacent_coupling_ratio: f64,
253    pub adjacent_isolation_db: f64,
254    pub next_nearest_coupling_ratio: f64,
255    pub next_nearest_isolation_db: f64,
256    pub worst_isolation_db: f64,
257    pub mean_coupling_ratio: f64,
258    pub max_coupling_ratio: f64,
259    pub crosstalk_safe: bool,
260}
261
262#[inline]
263fn pair_coupling(
264    gap_nm: f64,
265    coupling_length_um: f64,
266    wavelength_nm: f64,
267    core_index: f64,
268    cladding_index: f64,
269) -> (f64, f64, f64) {
270    // Transverse evanescent decay length (Marcatili).
271    let n2 = (core_index * core_index - cladding_index * cladding_index).max(1e-6);
272    let l_decay_nm = wavelength_nm / (2.0 * std::f64::consts::PI * n2.sqrt());
273    // Effective-index split at the coupler.
274    let dn_eff = 0.1 * (-gap_nm / l_decay_nm).exp();
275    // κ in per-µm. λ converted µm.
276    let lambda_um = wavelength_nm * 1.0e-3;
277    let kappa = std::f64::consts::PI * dn_eff / lambda_um;
278    // Power coupling ratio for uniform parallel coupler of length L.
279    let kl = kappa * coupling_length_um;
280    let ratio = kl.sin().powi(2);
281    let iso_db = if ratio > 1.0e-30 {
282        -10.0 * ratio.log10()
283    } else {
284        300.0
285    };
286    (kappa, ratio, iso_db)
287}
288
289/// Analyse crosstalk in a uniform parallel-waveguide bank. Adjacent pairs
290/// (gap = g) are the dominant term; next-nearest (gap = 2g) are included
291/// as the largest secondary term — Marcatili 1969 predicts that all other
292/// pairs are at least `exp(-2·g/L_decay)` smaller still.
293pub fn analyze_crosstalk_bank(
294    num_waveguides: usize,
295    gap_nm: f64,
296    coupling_length_um: f64,
297    wavelength_nm: f64,
298    core_index: f64,
299    cladding_index: f64,
300) -> CrosstalkBankResult {
301    let (_, near_ratio, near_iso) = pair_coupling(
302        gap_nm,
303        coupling_length_um,
304        wavelength_nm,
305        core_index,
306        cladding_index,
307    );
308    let (_, far_ratio, far_iso) = pair_coupling(
309        2.0 * gap_nm,
310        coupling_length_um,
311        wavelength_nm,
312        core_index,
313        cladding_index,
314    );
315
316    let num_near = num_waveguides.saturating_sub(1);
317    let num_far = num_waveguides.saturating_sub(2);
318    let total_pairs = num_near + num_far;
319    let (worst_iso, mean_ratio, max_ratio) = if total_pairs == 0 {
320        (f64::INFINITY, 0.0, 0.0)
321    } else {
322        let worst = near_iso.min(far_iso);
323        let mean =
324            ((num_near as f64) * near_ratio + (num_far as f64) * far_ratio) / (total_pairs as f64);
325        let mx = near_ratio.max(far_ratio);
326        (worst, mean, mx)
327    };
328
329    CrosstalkBankResult {
330        num_waveguides,
331        num_near_pairs: num_near,
332        num_far_pairs: num_far,
333        gap_nm,
334        coupling_length_um,
335        adjacent_coupling_ratio: near_ratio,
336        adjacent_isolation_db: near_iso,
337        next_nearest_coupling_ratio: far_ratio,
338        next_nearest_isolation_db: far_iso,
339        worst_isolation_db: worst_iso,
340        mean_coupling_ratio: mean_ratio,
341        max_coupling_ratio: max_ratio,
342        crosstalk_safe: worst_iso > 20.0,
343    }
344}
345
346/// Per-pair crosstalk for arbitrary waveguide geometry.
347/// `pairs` carries `(idx_a, idx_b, gap_nm, coupling_length_um)` per pair.
348/// Evaluated in parallel via Rayon — this is the O(N²) path the
349/// commercial layout tools call after full pair enumeration.
350pub fn analyze_crosstalk_pairs(
351    pairs: &[(usize, usize, f64, f64)],
352    wavelength_nm: f64,
353    core_index: f64,
354    cladding_index: f64,
355) -> Vec<CrosstalkPairResult> {
356    pairs
357        .par_iter()
358        .map(|&(a, b, gap, len)| {
359            let (kappa, ratio, iso) =
360                pair_coupling(gap, len, wavelength_nm, core_index, cladding_index);
361            CrosstalkPairResult {
362                index_a: a,
363                index_b: b,
364                gap_nm: gap,
365                coupling_length_um: len,
366                coupling_coefficient_per_um: kappa,
367                coupling_ratio: ratio,
368                isolation_db: iso,
369            }
370        })
371        .collect()
372}
373
374// ── Thermal phase shifter ────────────────────────────────────────────
375
376/// Compute electrical power (mW) needed for a phase shift.
377pub fn thermal_power_for_phase(
378    phase_rad: f64,
379    wavelength_nm: f64,
380    heater_length_um: f64,
381    dn_dt: f64,
382    thermal_resistance_kw: f64,
383) -> f64 {
384    let wl_m = wavelength_nm * 1e-9;
385    let l_m = heater_length_um * 1e-6;
386    let delta_t = (phase_rad * wl_m) / (2.0 * std::f64::consts::PI * dn_dt * l_m);
387    delta_t.abs() / thermal_resistance_kw
388}
389
390// ── Tests ────────────────────────────────────────────────────────────
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn test_route_simple_2x2() {
398        // 2-node full adjacency
399        let adj = vec![0.0, 1.0, 1.0, 0.0];
400        let result = route_waveguides(&adj, 2, 250.0, 2.0);
401        assert_eq!(result.len(), 1);
402        assert_eq!(result[0].source, 0);
403        assert_eq!(result[0].target, 1);
404        assert!((result[0].length_um - 250.0).abs() < 1.0);
405    }
406
407    #[test]
408    fn test_route_sparse() {
409        // 3 nodes, only 0↔1 connected
410        let adj = vec![0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0];
411        let result = route_waveguides(&adj, 3, 250.0, 2.0);
412        assert_eq!(result.len(), 1);
413    }
414
415    #[test]
416    fn test_route_loss_model() {
417        let adj = vec![0.0, 1.0, 1.0, 0.0];
418        let result = route_waveguides(&adj, 2, 1000.0, 2.0);
419        // 1000 µm = 0.1 cm → 0.2 dB propagation loss
420        assert!((result[0].loss_db - 0.2).abs() < 0.01);
421    }
422
423    #[test]
424    fn test_mzi_identity() {
425        // phase=0 → identity
426        let m = mzi_transfer_matrix(0.0);
427        assert!((m[0] - 1.0).abs() < 1e-10); // M00 real
428        assert!((m[6] - 1.0).abs() < 1e-10); // M11 real
429        assert!(m[3].abs() < 1e-10); // M01 imag = 0
430    }
431
432    #[test]
433    fn test_mzi_pi_barstate() {
434        // phase=π → bar state (swap)
435        let m = mzi_transfer_matrix(std::f64::consts::PI);
436        // cos(π/2) ≈ 0, sin(π/2) ≈ 1
437        assert!(m[0].abs() < 1e-10); // M00 real ≈ 0
438        assert!((m[3] - 1.0).abs() < 1e-10); // M01 imag ≈ 1
439    }
440
441    #[test]
442    fn test_cascade_identity() {
443        // Cascade 0 phases → identity
444        let result = cascade_mzi(&[]);
445        assert!((result[0] - 1.0).abs() < 1e-10);
446        assert!((result[6] - 1.0).abs() < 1e-10);
447    }
448
449    #[test]
450    fn test_cascade_two_stages() {
451        let phases = vec![std::f64::consts::PI / 4.0, std::f64::consts::PI / 4.0];
452        let result = cascade_mzi(&phases);
453        // Should be equivalent to single π/2 cascade (non-trivial)
454        let mag = (result[0] * result[0] + result[1] * result[1]).sqrt();
455        assert!(mag <= 1.0 + 1e-10);
456    }
457
458    #[test]
459    fn test_crosstalk_single() {
460        let channels = vec![(0, 1550.0, 0.8, 0.0)];
461        let result = analyze_crosstalk(&channels, -25.0);
462        assert_eq!(result.len(), 1);
463    }
464
465    #[test]
466    fn test_crosstalk_adjacent() {
467        let channels = vec![
468            (0, 1550.0, 0.8, 0.0),
469            (1, 1550.8, 0.8, 0.0),
470            (2, 1551.6, 0.8, 0.0),
471        ];
472        let result = analyze_crosstalk(&channels, -25.0);
473        assert_eq!(result.len(), 3);
474        // Middle channel has 2 adjacent
475        let mid = &result[1];
476        assert_eq!(mid.n_adjacent, 2);
477    }
478
479    #[test]
480    fn test_power_budget_pass() {
481        let wgs = vec![(0, 1, 1.0)]; // 1 dB loss
482        let mzis: Vec<(Vec<usize>, usize, f64)> = vec![];
483        let result = analyze_power_budget(&wgs, &mzis, 0.0, -20.0);
484        assert_eq!(result.len(), 1);
485        assert!(result[0].passed); // 0 - 1 = -1 dBm > -20 dBm
486        assert!((result[0].margin_db - 19.0).abs() < 0.01);
487    }
488
489    #[test]
490    fn test_power_budget_fail() {
491        let wgs = vec![(0, 1, 25.0)]; // 25 dB loss
492        let result = analyze_power_budget(&wgs, &[], 0.0, -20.0);
493        assert!(!result[0].passed); // 0 - 25 = -25 dBm < -20 dBm
494    }
495
496    #[test]
497    fn test_thermal_power() {
498        let p = thermal_power_for_phase(
499            std::f64::consts::PI, // π phase shift
500            1550.0,               // wavelength
501            100.0,                // heater length
502            1.86e-4,              // dn/dT
503            10.0,                 // thermal R
504        );
505        assert!(p > 0.0);
506        assert!(p < 100.0); // reasonable range
507    }
508
509    #[test]
510    fn crosstalk_bank_isolation_grows_with_gap() {
511        let narrow = analyze_crosstalk_bank(4, 100.0, 10.0, 1550.0, 3.48, 1.45);
512        let wide = analyze_crosstalk_bank(4, 400.0, 10.0, 1550.0, 3.48, 1.45);
513        // Wider gap ⇒ less coupling ⇒ higher isolation (dB).
514        assert!(wide.worst_isolation_db > narrow.worst_isolation_db);
515        // Nearest-neighbour dominates over next-nearest (smaller gap couples more).
516        assert!(narrow.adjacent_coupling_ratio >= narrow.next_nearest_coupling_ratio);
517    }
518
519    #[test]
520    fn crosstalk_bank_counts_match_bank_size() {
521        let r = analyze_crosstalk_bank(5, 200.0, 10.0, 1550.0, 3.48, 1.45);
522        assert_eq!(r.num_near_pairs, 4); // N-1 adjacent
523        assert_eq!(r.num_far_pairs, 3); // N-2 next-nearest
524        assert!(r.crosstalk_safe || r.worst_isolation_db <= 20.0);
525    }
526
527    #[test]
528    fn crosstalk_bank_single_waveguide_has_no_pairs() {
529        let r = analyze_crosstalk_bank(1, 200.0, 10.0, 1550.0, 3.48, 1.45);
530        assert_eq!(r.num_near_pairs, 0);
531        assert_eq!(r.num_far_pairs, 0);
532        assert!(r.worst_isolation_db.is_infinite());
533    }
534
535    #[test]
536    fn crosstalk_pairs_parallelism_matches_serial_math() {
537        let pairs = vec![
538            (0, 1, 200.0, 10.0),
539            (1, 2, 400.0, 10.0),
540            (0, 2, 800.0, 10.0),
541        ];
542        let out = analyze_crosstalk_pairs(&pairs, 1550.0, 3.48, 1.45);
543        assert_eq!(out.len(), 3);
544        // Sanity: larger gap ⇒ smaller coupling ratio.
545        assert!(out[0].coupling_ratio >= out[1].coupling_ratio);
546        assert!(out[1].coupling_ratio >= out[2].coupling_ratio);
547        // Isolation monotonically increases (inverse of ratio).
548        assert!(out[0].isolation_db <= out[1].isolation_db);
549        assert!(out[1].isolation_db <= out[2].isolation_db);
550    }
551}