Skip to main content

sc_neurocore_engine/bindings/
kuramoto.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 — Kuramoto solver PyO3 binding
8
9//! Python binding and validation contracts for the Kuramoto solver.
10
11use pyo3::exceptions::PyValueError;
12use pyo3::prelude::*;
13
14use crate::{matrix_inputs_binding::extract_matrix_f64, scpn};
15
16/// Register the Kuramoto solver with the extension module.
17pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
18    module.add_class::<PyKuramotoSolver>()?;
19    Ok(())
20}
21
22#[pyclass(
23    name = "KuramotoSolver",
24    module = "sc_neurocore_engine.sc_neurocore_engine"
25)]
26pub struct PyKuramotoSolver {
27    inner: scpn::KuramotoSolver,
28}
29
30fn validate_kuramoto_finite(name: &str, values: &[f64]) -> PyResult<()> {
31    if values.iter().all(|value| value.is_finite()) {
32        Ok(())
33    } else {
34        Err(PyValueError::new_err(format!(
35            "{name} values must be finite"
36        )))
37    }
38}
39
40fn validate_kuramoto_dt(dt: f64) -> PyResult<()> {
41    if dt.is_finite() && dt > 0.0 {
42        Ok(())
43    } else {
44        Err(PyValueError::new_err("dt must be finite and positive"))
45    }
46}
47
48fn validate_kuramoto_matrix_shape(
49    name: &str,
50    values_len: usize,
51    rows: usize,
52    cols: usize,
53    n: usize,
54) -> PyResult<()> {
55    let is_absent = rows == 0 && cols == 0 && values_len == 0;
56    let is_flat = rows == 1 && values_len == n * n;
57    let is_square = rows == n && cols == n;
58    if is_absent || is_flat || is_square {
59        Ok(())
60    } else {
61        Err(PyValueError::new_err(format!(
62            "{name} must be shape ({n}, {n}) or flat length {}",
63            n * n
64        )))
65    }
66}
67
68#[pymethods]
69impl PyKuramotoSolver {
70    #[getter]
71    fn phases(&self) -> Vec<f64> {
72        self.inner.phases.clone()
73    }
74    #[new]
75    #[pyo3(signature = (omega, coupling, phases, noise_amp=0.1))]
76    fn new(
77        omega: Vec<f64>,
78        coupling: &Bound<'_, PyAny>,
79        phases: Vec<f64>,
80        noise_amp: f64,
81    ) -> PyResult<Self> {
82        let n = omega.len();
83        if n == 0 {
84            return Err(PyValueError::new_err("omega must not be empty."));
85        }
86        if phases.len() != n {
87            return Err(PyValueError::new_err(format!(
88                "phases length mismatch: got {}, expected {}.",
89                phases.len(),
90                n
91            )));
92        }
93        validate_kuramoto_finite("omega", &omega)?;
94        validate_kuramoto_finite("initial_phases", &phases)?;
95        if !(noise_amp.is_finite() && noise_amp >= 0.0) {
96            return Err(PyValueError::new_err(
97                "noise_amp must be finite and non-negative",
98            ));
99        }
100
101        let (coupling_flat, rows, cols) = extract_matrix_f64(coupling, "coupling")?;
102        if rows == 1 {
103            if coupling_flat.len() != n * n {
104                return Err(PyValueError::new_err(format!(
105                    "Flat coupling length mismatch: got {}, expected {}.",
106                    coupling_flat.len(),
107                    n * n
108                )));
109            }
110        } else if rows != n || cols != n {
111            return Err(PyValueError::new_err(format!(
112                "coupling must be shape ({}, {}) or flat length {}, got ({}, {}).",
113                n,
114                n,
115                n * n,
116                rows,
117                cols
118            )));
119        }
120        validate_kuramoto_finite("coupling", &coupling_flat)?;
121
122        Ok(Self {
123            inner: scpn::KuramotoSolver::new(omega, coupling_flat, phases, noise_amp),
124        })
125    }
126
127    #[pyo3(signature = (dt, seed=0))]
128    fn step(&mut self, dt: f64, seed: u64) -> PyResult<f64> {
129        validate_kuramoto_dt(dt)?;
130        Ok(self.inner.step(dt, seed))
131    }
132
133    #[pyo3(signature = (n_steps, dt, seed=0))]
134    fn run(&mut self, n_steps: usize, dt: f64, seed: u64) -> PyResult<Vec<f64>> {
135        validate_kuramoto_dt(dt)?;
136        Ok(self.inner.run(n_steps, dt, seed))
137    }
138
139    fn set_field_pressure(&mut self, f: f64) -> PyResult<()> {
140        if !f.is_finite() {
141            return Err(PyValueError::new_err("field_pressure must be finite"));
142        }
143        self.inner.set_field_pressure(f);
144        Ok(())
145    }
146
147    #[pyo3(signature = (
148        dt,
149        seed=0,
150        W=None,
151        sigma_g=0.0,
152        h_munu=None,
153        pgbo_weight=0.0,
154    ))]
155    #[allow(non_snake_case)]
156    fn step_ssgf(
157        &mut self,
158        dt: f64,
159        seed: u64,
160        W: Option<&Bound<'_, PyAny>>,
161        sigma_g: f64,
162        h_munu: Option<&Bound<'_, PyAny>>,
163        pgbo_weight: f64,
164    ) -> PyResult<f64> {
165        validate_kuramoto_dt(dt)?;
166        if !sigma_g.is_finite() {
167            return Err(PyValueError::new_err("sigma_g must be finite"));
168        }
169        if !pgbo_weight.is_finite() {
170            return Err(PyValueError::new_err("pgbo_weight must be finite"));
171        }
172        let (w_flat, w_rows, w_cols) = match W {
173            Some(w) => extract_matrix_f64(w, "W")?,
174            None => (vec![], 0, 0),
175        };
176        let (h_flat, h_rows, h_cols) = match h_munu {
177            Some(h) => extract_matrix_f64(h, "h_munu")?,
178            None => (vec![], 0, 0),
179        };
180        validate_kuramoto_matrix_shape("W", w_flat.len(), w_rows, w_cols, self.inner.n)?;
181        validate_kuramoto_matrix_shape("h_munu", h_flat.len(), h_rows, h_cols, self.inner.n)?;
182        validate_kuramoto_finite("w_flat", &w_flat)?;
183        validate_kuramoto_finite("h_flat", &h_flat)?;
184        Ok(self
185            .inner
186            .step_ssgf(dt, seed, &w_flat, sigma_g, &h_flat, pgbo_weight))
187    }
188
189    #[pyo3(signature = (
190        n_steps,
191        dt,
192        seed=0,
193        W=None,
194        sigma_g=0.0,
195        h_munu=None,
196        pgbo_weight=0.0,
197    ))]
198    #[allow(clippy::too_many_arguments, non_snake_case)]
199    fn run_ssgf(
200        &mut self,
201        n_steps: usize,
202        dt: f64,
203        seed: u64,
204        W: Option<&Bound<'_, PyAny>>,
205        sigma_g: f64,
206        h_munu: Option<&Bound<'_, PyAny>>,
207        pgbo_weight: f64,
208    ) -> PyResult<Vec<f64>> {
209        validate_kuramoto_dt(dt)?;
210        if !sigma_g.is_finite() {
211            return Err(PyValueError::new_err("sigma_g must be finite"));
212        }
213        if !pgbo_weight.is_finite() {
214            return Err(PyValueError::new_err("pgbo_weight must be finite"));
215        }
216        let (w_flat, w_rows, w_cols) = match W {
217            Some(w) => extract_matrix_f64(w, "W")?,
218            None => (vec![], 0, 0),
219        };
220        let (h_flat, h_rows, h_cols) = match h_munu {
221            Some(h) => extract_matrix_f64(h, "h_munu")?,
222            None => (vec![], 0, 0),
223        };
224        validate_kuramoto_matrix_shape("W", w_flat.len(), w_rows, w_cols, self.inner.n)?;
225        validate_kuramoto_matrix_shape("h_munu", h_flat.len(), h_rows, h_cols, self.inner.n)?;
226        validate_kuramoto_finite("w_flat", &w_flat)?;
227        validate_kuramoto_finite("h_flat", &h_flat)?;
228        Ok(self
229            .inner
230            .run_ssgf(n_steps, dt, seed, &w_flat, sigma_g, &h_flat, pgbo_weight))
231    }
232
233    fn order_parameter(&self) -> f64 {
234        self.inner.order_parameter()
235    }
236
237    fn apply_phases(&mut self, phases: Vec<f64>) -> PyResult<()> {
238        if phases.len() != self.inner.n {
239            return Err(PyValueError::new_err(format!(
240                "phases length mismatch: got {}, expected {}.",
241                phases.len(),
242                self.inner.n
243            )));
244        }
245        validate_kuramoto_finite("phases", &phases)?;
246        self.inner.set_phases(phases);
247        Ok(())
248    }
249
250    fn set_phases(&mut self, phases: Vec<f64>) -> PyResult<()> {
251        self.apply_phases(phases)
252    }
253
254    #[setter(phases)]
255    fn set_phases_attr(&mut self, phases: Vec<f64>) -> PyResult<()> {
256        self.apply_phases(phases)
257    }
258
259    fn set_coupling(&mut self, coupling: &Bound<'_, PyAny>) -> PyResult<()> {
260        let n = self.inner.n;
261        let (coupling_flat, rows, cols) = extract_matrix_f64(coupling, "coupling")?;
262        if rows == 1 {
263            if coupling_flat.len() != n * n {
264                return Err(PyValueError::new_err(format!(
265                    "Flat coupling length mismatch: got {}, expected {}.",
266                    coupling_flat.len(),
267                    n * n
268                )));
269            }
270        } else if rows != n || cols != n {
271            return Err(PyValueError::new_err(format!(
272                "coupling must be shape ({}, {}) or flat length {}, got ({}, {}).",
273                n,
274                n,
275                n * n,
276                rows,
277                cols
278            )));
279        }
280        validate_kuramoto_finite("coupling", &coupling_flat)?;
281        self.inner.set_coupling(coupling_flat);
282        Ok(())
283    }
284}