1pub(crate) mod bindings;
18
19use rayon::prelude::*;
20
21const CROSSING_LOSS_DB: f64 = 0.08;
24#[allow(dead_code)] const MZI_INSERTION_LOSS_DB: f64 = 0.5;
26
27#[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
39pub 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
83pub 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, 0.0, 0.0, s, 0.0, s, c, 0.0]
96}
97
98pub 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]; for &phase in phases {
105 let m = mzi_transfer_matrix(phase);
106 result = complex_mat_mul(&result, &m);
107 }
108
109 result
110}
111
112fn complex_mat_mul(a: &[f64; 8], b: &[f64; 8]) -> [f64; 8] {
114 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 let c00 = cadd(cmul(a[0], a[1], b[0], b[1]), cmul(a[2], a[3], b[4], b[5]));
123 let c01 = cadd(cmul(a[0], a[1], b[2], b[3]), cmul(a[2], a[3], b[6], b[7]));
125 let c10 = cadd(cmul(a[4], a[5], b[0], b[1]), cmul(a[6], a[7], b[4], b[5]));
127 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#[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
145pub 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#[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
189pub fn analyze_power_budget(
191 waveguides: &[(usize, usize, f64)], mzi_ports: &[(Vec<usize>, usize, f64)], 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#[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 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 let dn_eff = 0.1 * (-gap_nm / l_decay_nm).exp();
277 let lambda_um = wavelength_nm * 1.0e-3;
279 let kappa = std::f64::consts::PI * dn_eff / lambda_um;
280 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
291pub 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
348pub 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
376pub 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#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn test_route_simple_2x2() {
400 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 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 assert!((result[0].loss_db - 0.2).abs() < 0.01);
423 }
424
425 #[test]
426 fn test_mzi_identity() {
427 let m = mzi_transfer_matrix(0.0);
429 assert!((m[0] - 1.0).abs() < 1e-10); assert!((m[6] - 1.0).abs() < 1e-10); assert!(m[3].abs() < 1e-10); }
433
434 #[test]
435 fn test_mzi_pi_barstate() {
436 let m = mzi_transfer_matrix(std::f64::consts::PI);
438 assert!(m[0].abs() < 1e-10); assert!((m[3] - 1.0).abs() < 1e-10); }
442
443 #[test]
444 fn test_cascade_identity() {
445 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 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 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)]; 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); 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)]; let result = analyze_power_budget(&wgs, &[], 0.0, -20.0);
495 assert!(!result[0].passed); }
497
498 #[test]
499 fn test_thermal_power() {
500 let p = thermal_power_for_phase(
501 std::f64::consts::PI, 1550.0, 100.0, 1.86e-4, 10.0, );
507 assert!(p > 0.0);
508 assert!(p < 100.0); }
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 assert!(wide.worst_isolation_db > narrow.worst_isolation_db);
517 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); assert_eq!(r.num_far_pairs, 3); 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 assert!(out[0].coupling_ratio >= out[1].coupling_ratio);
548 assert!(out[1].coupling_ratio >= out[2].coupling_ratio);
549 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}