Skip to main content

sc_neurocore_engine/photonic/
bindings.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 — Photonic NoC PyO3 bindings
8
9//! Python bindings for the photonic routing, MZI, crosstalk, and power-budget core.
10
11use pyo3::prelude::*;
12use pyo3::types::PyDict;
13
14use crate::photonic;
15
16// ── Photonic NoC PyO3 Wrappers ───────────────────────────────────────
17
18/// Route waveguides on a mesh topology (Rust-accelerated).
19#[pyfunction]
20#[pyo3(signature = (adjacency_flat, n, pitch_um=250.0, loss_db_per_cm=2.0))]
21fn py_ph_route_waveguides<'py>(
22    py: Python<'py>,
23    adjacency_flat: Vec<f64>,
24    n: usize,
25    pitch_um: f64,
26    loss_db_per_cm: f64,
27) -> PyResult<Py<PyAny>> {
28    let result = photonic::route_waveguides(&adjacency_flat, n, pitch_um, loss_db_per_cm);
29
30    let dict = PyDict::new(py);
31    let sources: Vec<usize> = result.iter().map(|r| r.source).collect();
32    let targets: Vec<usize> = result.iter().map(|r| r.target).collect();
33    let lengths: Vec<f64> = result.iter().map(|r| r.length_um).collect();
34    let losses: Vec<f64> = result.iter().map(|r| r.loss_db).collect();
35    let crossings: Vec<usize> = result.iter().map(|r| r.n_crossings).collect();
36
37    dict.set_item("sources", sources)?;
38    dict.set_item("targets", targets)?;
39    dict.set_item("lengths_um", lengths)?;
40    dict.set_item("losses_db", losses)?;
41    dict.set_item("crossings", crossings)?;
42    dict.set_item("n_segments", result.len())?;
43    dict.set_item("backend", "rust")?;
44    Ok(dict.into_any().unbind())
45}
46
47/// Compute MZI 2×2 transfer matrix for a given phase.
48#[pyfunction]
49fn py_ph_mzi_transfer_matrix(_py: Python<'_>, phase_rad: f64) -> Vec<f64> {
50    photonic::mzi_transfer_matrix(phase_rad).to_vec()
51}
52
53/// Cascade multiple MZI stages via matrix multiplication.
54#[pyfunction]
55fn py_ph_cascade_mzi(_py: Python<'_>, phases: Vec<f64>) -> Vec<f64> {
56    photonic::cascade_mzi(&phases).to_vec()
57}
58
59/// Analyze WDM crosstalk (Rust-accelerated).
60#[pyfunction]
61#[pyo3(signature = (channel_ids, wavelengths, bandwidths, powers, adjacent_xt_db=-25.0))]
62fn py_ph_analyze_crosstalk<'py>(
63    py: Python<'py>,
64    channel_ids: Vec<usize>,
65    wavelengths: Vec<f64>,
66    bandwidths: Vec<f64>,
67    powers: Vec<f64>,
68    adjacent_xt_db: f64,
69) -> PyResult<Py<PyAny>> {
70    let channels: Vec<(usize, f64, f64, f64)> = channel_ids
71        .into_iter()
72        .zip(wavelengths)
73        .zip(bandwidths)
74        .zip(powers)
75        .map(|(((id, wl), bw), p)| (id, wl, bw, p))
76        .collect();
77
78    let result = photonic::analyze_crosstalk(&channels, adjacent_xt_db);
79
80    let dict = PyDict::new(py);
81    let ids: Vec<usize> = result.iter().map(|r| r.channel_id).collect();
82    let xts: Vec<f64> = result.iter().map(|r| r.crosstalk_db).collect();
83    let osnrs: Vec<f64> = result.iter().map(|r| r.osnr_db).collect();
84    let adjs: Vec<usize> = result.iter().map(|r| r.n_adjacent).collect();
85
86    dict.set_item("channel_ids", ids)?;
87    dict.set_item("crosstalk_db", xts)?;
88    dict.set_item("osnr_db", osnrs)?;
89    dict.set_item("n_adjacent", adjs)?;
90    dict.set_item("backend", "rust")?;
91    Ok(dict.into_any().unbind())
92}
93
94/// Analyze optical power budget (Rust-accelerated).
95#[pyfunction]
96#[pyo3(signature = (wg_sources, wg_targets, wg_losses, laser_power_dbm=0.0, detector_sensitivity_dbm=-20.0))]
97fn py_ph_analyze_power_budget<'py>(
98    py: Python<'py>,
99    wg_sources: Vec<usize>,
100    wg_targets: Vec<usize>,
101    wg_losses: Vec<f64>,
102    laser_power_dbm: f64,
103    detector_sensitivity_dbm: f64,
104) -> PyResult<Py<PyAny>> {
105    let wgs: Vec<(usize, usize, f64)> = wg_sources
106        .into_iter()
107        .zip(wg_targets)
108        .zip(wg_losses)
109        .map(|((s, t), l)| (s, t, l))
110        .collect();
111
112    let result =
113        photonic::analyze_power_budget(&wgs, &[], laser_power_dbm, detector_sensitivity_dbm);
114
115    let dict = PyDict::new(py);
116    let margins: Vec<f64> = result.iter().map(|r| r.margin_db).collect();
117    let passed: Vec<bool> = result.iter().map(|r| r.passed).collect();
118    let total_losses: Vec<f64> = result.iter().map(|r| r.total_loss_db).collect();
119
120    dict.set_item("margins_db", margins)?;
121    dict.set_item("passed", passed)?;
122    dict.set_item("total_losses_db", total_losses)?;
123    dict.set_item("n_paths", result.len())?;
124    dict.set_item("backend", "rust")?;
125    Ok(dict.into_any().unbind())
126}
127
128/// Geometric crosstalk analysis for a uniform bank of parallel waveguides.
129#[pyfunction]
130#[pyo3(signature = (num_waveguides, gap_nm, coupling_length_um, wavelength_nm=1550.0, core_index=3.48, cladding_index=1.45))]
131fn py_ph_analyze_crosstalk_bank<'py>(
132    py: Python<'py>,
133    num_waveguides: usize,
134    gap_nm: f64,
135    coupling_length_um: f64,
136    wavelength_nm: f64,
137    core_index: f64,
138    cladding_index: f64,
139) -> PyResult<Py<PyAny>> {
140    let r = photonic::analyze_crosstalk_bank(
141        num_waveguides,
142        gap_nm,
143        coupling_length_um,
144        wavelength_nm,
145        core_index,
146        cladding_index,
147    );
148    let dict = PyDict::new(py);
149    dict.set_item("num_waveguides", r.num_waveguides)?;
150    dict.set_item("num_pairs", r.num_near_pairs + r.num_far_pairs)?;
151    dict.set_item("num_near_pairs", r.num_near_pairs)?;
152    dict.set_item("num_far_pairs", r.num_far_pairs)?;
153    dict.set_item("gap_nm", r.gap_nm)?;
154    dict.set_item("coupling_length_um", r.coupling_length_um)?;
155    dict.set_item("adjacent_coupling_ratio", r.adjacent_coupling_ratio)?;
156    dict.set_item("adjacent_isolation_db", r.adjacent_isolation_db)?;
157    dict.set_item("next_nearest_coupling_ratio", r.next_nearest_coupling_ratio)?;
158    dict.set_item("next_nearest_isolation_db", r.next_nearest_isolation_db)?;
159    dict.set_item("worst_isolation_db", r.worst_isolation_db)?;
160    dict.set_item("mean_coupling_ratio", r.mean_coupling_ratio)?;
161    dict.set_item("max_coupling_ratio", r.max_coupling_ratio)?;
162    dict.set_item("crosstalk_safe", r.crosstalk_safe)?;
163    dict.set_item("backend", "rust")?;
164    Ok(dict.into_any().unbind())
165}
166
167/// Per-pair geometric crosstalk for arbitrary waveguide geometry.
168/// `pairs_a[i]`, `pairs_b[i]`, `gaps_nm[i]`, `lengths_um[i]` describe pair i.
169/// Evaluated in parallel via Rayon — the O(N²) analysis path.
170#[pyfunction]
171#[pyo3(signature = (pairs_a, pairs_b, gaps_nm, lengths_um, wavelength_nm=1550.0, core_index=3.48, cladding_index=1.45))]
172fn py_ph_analyze_crosstalk_pairs<'py>(
173    py: Python<'py>,
174    pairs_a: Vec<usize>,
175    pairs_b: Vec<usize>,
176    gaps_nm: Vec<f64>,
177    lengths_um: Vec<f64>,
178    wavelength_nm: f64,
179    core_index: f64,
180    cladding_index: f64,
181) -> PyResult<Py<PyAny>> {
182    let n = pairs_a.len();
183    if pairs_b.len() != n || gaps_nm.len() != n || lengths_um.len() != n {
184        return Err(pyo3::exceptions::PyValueError::new_err(
185            "pairs_a, pairs_b, gaps_nm, lengths_um must be equal length",
186        ));
187    }
188    let pairs: Vec<(usize, usize, f64, f64)> = pairs_a
189        .into_iter()
190        .zip(pairs_b)
191        .zip(gaps_nm)
192        .zip(lengths_um)
193        .map(|(((a, b), g), l)| (a, b, g, l))
194        .collect();
195    let results =
196        photonic::analyze_crosstalk_pairs(&pairs, wavelength_nm, core_index, cladding_index);
197
198    let dict = PyDict::new(py);
199    let idx_a: Vec<usize> = results.iter().map(|r| r.index_a).collect();
200    let idx_b: Vec<usize> = results.iter().map(|r| r.index_b).collect();
201    let gaps: Vec<f64> = results.iter().map(|r| r.gap_nm).collect();
202    let lens: Vec<f64> = results.iter().map(|r| r.coupling_length_um).collect();
203    let kappas: Vec<f64> = results
204        .iter()
205        .map(|r| r.coupling_coefficient_per_um)
206        .collect();
207    let ratios: Vec<f64> = results.iter().map(|r| r.coupling_ratio).collect();
208    let isos: Vec<f64> = results.iter().map(|r| r.isolation_db).collect();
209
210    dict.set_item("pair_a", idx_a)?;
211    dict.set_item("pair_b", idx_b)?;
212    dict.set_item("gap_nm", gaps)?;
213    dict.set_item("coupling_length_um", lens)?;
214    dict.set_item("coupling_coefficient_per_um", kappas)?;
215    dict.set_item("coupling_ratio", ratios)?;
216    dict.set_item("isolation_db", isos)?;
217    dict.set_item("num_pairs", n)?;
218    dict.set_item("backend", "rust")?;
219    Ok(dict.into_any().unbind())
220}
221
222pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
223    m.add_function(wrap_pyfunction!(py_ph_route_waveguides, m)?)?;
224    m.add_function(wrap_pyfunction!(py_ph_mzi_transfer_matrix, m)?)?;
225    m.add_function(wrap_pyfunction!(py_ph_cascade_mzi, m)?)?;
226    m.add_function(wrap_pyfunction!(py_ph_analyze_crosstalk, m)?)?;
227    m.add_function(wrap_pyfunction!(py_ph_analyze_power_budget, m)?)?;
228    m.add_function(wrap_pyfunction!(py_ph_analyze_crosstalk_bank, m)?)?;
229    m.add_function(wrap_pyfunction!(py_ph_analyze_crosstalk_pairs, m)?)?;
230    Ok(())
231}