1use pyo3::exceptions::PyValueError;
12use pyo3::prelude::*;
13
14use crate::{matrix_inputs_binding::extract_matrix_f64, scpn};
15
16pub(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}