1use rayon::prelude::*;
18
19const CROSSING_LOSS_DB: f64 = 0.08;
22#[allow(dead_code)] const MZI_INSERTION_LOSS_DB: f64 = 0.5;
24
25#[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
37pub 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
81pub 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, 0.0, 0.0, s, 0.0, s, c, 0.0]
94}
95
96pub 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]; for &phase in phases {
103 let m = mzi_transfer_matrix(phase);
104 result = complex_mat_mul(&result, &m);
105 }
106
107 result
108}
109
110fn complex_mat_mul(a: &[f64; 8], b: &[f64; 8]) -> [f64; 8] {
112 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 let c00 = cadd(cmul(a[0], a[1], b[0], b[1]), cmul(a[2], a[3], b[4], b[5]));
121 let c01 = cadd(cmul(a[0], a[1], b[2], b[3]), cmul(a[2], a[3], b[6], b[7]));
123 let c10 = cadd(cmul(a[4], a[5], b[0], b[1]), cmul(a[6], a[7], b[4], b[5]));
125 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#[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
143pub 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#[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
187pub fn analyze_power_budget(
189 waveguides: &[(usize, usize, f64)], mzi_ports: &[(Vec<usize>, usize, f64)], 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#[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 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 let dn_eff = 0.1 * (-gap_nm / l_decay_nm).exp();
275 let lambda_um = wavelength_nm * 1.0e-3;
277 let kappa = std::f64::consts::PI * dn_eff / lambda_um;
278 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
289pub 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
346pub 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
374pub 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#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_route_simple_2x2() {
398 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 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 assert!((result[0].loss_db - 0.2).abs() < 0.01);
421 }
422
423 #[test]
424 fn test_mzi_identity() {
425 let m = mzi_transfer_matrix(0.0);
427 assert!((m[0] - 1.0).abs() < 1e-10); assert!((m[6] - 1.0).abs() < 1e-10); assert!(m[3].abs() < 1e-10); }
431
432 #[test]
433 fn test_mzi_pi_barstate() {
434 let m = mzi_transfer_matrix(std::f64::consts::PI);
436 assert!(m[0].abs() < 1e-10); assert!((m[3] - 1.0).abs() < 1e-10); }
440
441 #[test]
442 fn test_cascade_identity() {
443 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 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 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)]; 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); 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)]; let result = analyze_power_budget(&wgs, &[], 0.0, -20.0);
493 assert!(!result[0].passed); }
495
496 #[test]
497 fn test_thermal_power() {
498 let p = thermal_power_for_phase(
499 std::f64::consts::PI, 1550.0, 100.0, 1.86e-4, 10.0, );
505 assert!(p > 0.0);
506 assert!(p < 100.0); }
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 assert!(wide.worst_isolation_db > narrow.worst_isolation_db);
515 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); assert_eq!(r.num_far_pairs, 3); 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 assert!(out[0].coupling_ratio >= out[1].coupling_ratio);
546 assert!(out[1].coupling_ratio >= out[2].coupling_ratio);
547 assert!(out[0].isolation_db <= out[1].isolation_db);
549 assert!(out[1].isolation_db <= out[2].isolation_db);
550 }
551}