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