sc_neurocore_engine/scpn/
kuramoto.rs1use rand::{RngExt, SeedableRng};
15use rand_chacha::ChaCha8Rng;
16use rayon::prelude::*;
17
18const TWO_PI: f64 = std::f64::consts::TAU;
19
20pub struct KuramotoSolver {
28 pub n: usize,
30 pub n_inv: f64,
32 pub omega: Vec<f64>,
34 pub coupling: Vec<f64>,
36 pub phases: Vec<f64>,
38 pub noise_amp: f64,
40 pub field_pressure: f64,
42 dtheta: Vec<f64>,
44 sin_diff: Vec<f64>,
46 noise: Vec<f64>,
48 cos_theta: Vec<f64>,
50 geo_coupling: Vec<f64>,
52 pgbo_coupling: Vec<f64>,
54}
55
56impl KuramotoSolver {
57 pub fn new(
59 omega: Vec<f64>,
60 coupling_flat: Vec<f64>,
61 initial_phases: Vec<f64>,
62 noise_amp: f64,
63 ) -> Self {
64 let n = omega.len();
65 assert!(n > 0, "omega must not be empty");
66 assert_eq!(
67 initial_phases.len(),
68 n,
69 "initial_phases length mismatch: got {}, expected {}",
70 initial_phases.len(),
71 n
72 );
73 assert_eq!(
74 coupling_flat.len(),
75 n * n,
76 "coupling length mismatch: got {}, expected {}",
77 coupling_flat.len(),
78 n * n
79 );
80 assert_all_finite("omega", &omega);
81 assert_all_finite("coupling", &coupling_flat);
82 assert_all_finite("initial_phases", &initial_phases);
83 assert!(
84 noise_amp.is_finite() && noise_amp >= 0.0,
85 "noise_amp must be finite and non-negative"
86 );
87
88 Self {
89 n,
90 n_inv: 1.0 / n as f64,
91 omega,
92 coupling: coupling_flat,
93 phases: initial_phases,
94 noise_amp,
95 field_pressure: 0.0,
96 dtheta: vec![0.0; n],
97 sin_diff: vec![0.0; n * n],
98 noise: vec![0.0; n],
99 cos_theta: vec![0.0; n],
100 geo_coupling: vec![0.0; n],
101 pgbo_coupling: vec![0.0; n],
102 }
103 }
104
105 pub fn set_field_pressure(&mut self, f: f64) {
107 assert!(f.is_finite(), "field_pressure must be finite");
108 self.field_pressure = f;
109 }
110
111 pub fn step(&mut self, dt: f64, seed: u64) -> f64 {
115 assert_dt(dt);
116 let n = self.n;
117 let phases = &self.phases;
118
119 self.sin_diff
120 .par_chunks_mut(n)
121 .enumerate()
122 .for_each(|(row_idx, row)| {
123 let theta_n = phases[row_idx];
124 for (col_idx, value) in row.iter_mut().enumerate() {
125 *value = (phases[col_idx] - theta_n).sin();
126 }
127 });
128
129 if seed == 0 || self.noise_amp == 0.0 {
130 self.noise.fill(0.0);
131 } else {
132 fill_standard_normals(&mut self.noise, seed);
133 }
134
135 self.dtheta
136 .par_iter_mut()
137 .enumerate()
138 .for_each(|(row_idx, dtheta_n)| {
139 let coupling_row = &self.coupling[row_idx * n..(row_idx + 1) * n];
140 let sin_row = &self.sin_diff[row_idx * n..(row_idx + 1) * n];
141 let coupling_sum = crate::simd::dot_f64_dispatch(coupling_row, sin_row);
142 *dtheta_n = self.omega[row_idx]
144 + coupling_sum * self.n_inv
145 + self.noise_amp * self.noise[row_idx];
146 });
147
148 for (phase, dtheta) in self.phases.iter_mut().zip(self.dtheta.iter()) {
149 *phase = (*phase + dtheta * dt).rem_euclid(TWO_PI);
150 }
151
152 self.order_parameter()
153 }
154
155 pub fn run(&mut self, n_steps: usize, dt: f64, seed: u64) -> Vec<f64> {
157 assert_dt(dt);
158 let mut order_values = Vec::with_capacity(n_steps);
159 for step_idx in 0..n_steps {
160 let step_seed = if seed == 0 {
161 0
162 } else {
163 seed.wrapping_add(step_idx as u64)
164 };
165 order_values.push(self.step(dt, step_seed));
166 }
167 order_values
168 }
169
170 #[allow(clippy::too_many_arguments)]
179 pub fn step_ssgf(
180 &mut self,
181 dt: f64,
182 seed: u64,
183 w_flat: &[f64],
184 sigma_g: f64,
185 h_flat: &[f64],
186 pgbo_weight: f64,
187 ) -> f64 {
188 assert_dt(dt);
189 assert!(sigma_g.is_finite(), "sigma_g must be finite");
190 assert!(pgbo_weight.is_finite(), "pgbo_weight must be finite");
191 let n = self.n;
192 let phases = &self.phases;
193
194 self.sin_diff
196 .par_chunks_mut(n)
197 .enumerate()
198 .for_each(|(row_idx, row)| {
199 let theta_n = phases[row_idx];
200 for (col_idx, value) in row.iter_mut().enumerate() {
201 *value = (phases[col_idx] - theta_n).sin();
202 }
203 });
204
205 if seed == 0 || self.noise_amp == 0.0 {
207 self.noise.fill(0.0);
208 } else {
209 fill_standard_normals(&mut self.noise, seed);
210 }
211
212 let has_geo = !w_flat.is_empty() && sigma_g != 0.0;
214 if has_geo {
215 assert_eq!(
216 w_flat.len(),
217 n * n,
218 "w_flat length mismatch: got {}, expected {}",
219 w_flat.len(),
220 n * n
221 );
222 assert_all_finite("w_flat", w_flat);
223 self.geo_coupling
224 .par_iter_mut()
225 .enumerate()
226 .for_each(|(row_idx, geo_n)| {
227 let w_row = &w_flat[row_idx * n..(row_idx + 1) * n];
228 let sin_row = &self.sin_diff[row_idx * n..(row_idx + 1) * n];
229 *geo_n = sigma_g
230 * w_row
231 .iter()
232 .zip(sin_row.iter())
233 .map(|(w, s)| w * s)
234 .sum::<f64>();
235 });
236 } else {
237 self.geo_coupling.fill(0.0);
238 }
239
240 let has_pgbo = !h_flat.is_empty() && pgbo_weight != 0.0;
242 if has_pgbo {
243 assert_eq!(
244 h_flat.len(),
245 n * n,
246 "h_flat length mismatch: got {}, expected {}",
247 h_flat.len(),
248 n * n
249 );
250 assert_all_finite("h_flat", h_flat);
251 self.pgbo_coupling
252 .par_iter_mut()
253 .enumerate()
254 .for_each(|(row_idx, pgbo_n)| {
255 let h_row = &h_flat[row_idx * n..(row_idx + 1) * n];
256 let sin_row = &self.sin_diff[row_idx * n..(row_idx + 1) * n];
257 *pgbo_n = pgbo_weight
258 * h_row
259 .iter()
260 .zip(sin_row.iter())
261 .map(|(h, s)| h * s)
262 .sum::<f64>();
263 });
264 } else {
265 self.pgbo_coupling.fill(0.0);
266 }
267
268 if self.field_pressure != 0.0 {
270 for (c, &theta) in self.cos_theta.iter_mut().zip(phases.iter()) {
271 *c = theta.cos();
272 }
273 } else {
274 self.cos_theta.fill(0.0);
275 }
276
277 self.dtheta
279 .par_iter_mut()
280 .enumerate()
281 .for_each(|(i, dtheta_n)| {
282 let coupling_row = &self.coupling[i * n..(i + 1) * n];
283 let sin_row = &self.sin_diff[i * n..(i + 1) * n];
284 let coupling_sum = crate::simd::dot_f64_dispatch(coupling_row, sin_row);
285
286 *dtheta_n = self.omega[i]
287 + coupling_sum * self.n_inv
288 + self.geo_coupling[i]
289 + self.pgbo_coupling[i]
290 + self.field_pressure * self.cos_theta[i]
291 + self.noise_amp * self.noise[i];
292 });
293
294 for (phase, dtheta) in self.phases.iter_mut().zip(self.dtheta.iter()) {
295 *phase = (*phase + dtheta * dt).rem_euclid(TWO_PI);
296 }
297
298 self.order_parameter()
299 }
300
301 #[allow(clippy::too_many_arguments)]
303 pub fn run_ssgf(
304 &mut self,
305 n_steps: usize,
306 dt: f64,
307 seed: u64,
308 w_flat: &[f64],
309 sigma_g: f64,
310 h_flat: &[f64],
311 pgbo_weight: f64,
312 ) -> Vec<f64> {
313 assert_dt(dt);
314 assert!(sigma_g.is_finite(), "sigma_g must be finite");
315 assert!(pgbo_weight.is_finite(), "pgbo_weight must be finite");
316 if !w_flat.is_empty() {
317 assert_eq!(
318 w_flat.len(),
319 self.n * self.n,
320 "w_flat length mismatch: got {}, expected {}",
321 w_flat.len(),
322 self.n * self.n
323 );
324 assert_all_finite("w_flat", w_flat);
325 }
326 if !h_flat.is_empty() {
327 assert_eq!(
328 h_flat.len(),
329 self.n * self.n,
330 "h_flat length mismatch: got {}, expected {}",
331 h_flat.len(),
332 self.n * self.n
333 );
334 assert_all_finite("h_flat", h_flat);
335 }
336 let mut order_values = Vec::with_capacity(n_steps);
337 for step_idx in 0..n_steps {
338 let step_seed = if seed == 0 {
339 0
340 } else {
341 seed.wrapping_add(step_idx as u64)
342 };
343 order_values.push(self.step_ssgf(dt, step_seed, w_flat, sigma_g, h_flat, pgbo_weight));
344 }
345 order_values
346 }
347
348 pub fn order_parameter(&self) -> f64 {
351 if self.phases.is_empty() {
352 return 0.0;
353 }
354
355 let n_inv = 1.0 / self.phases.len() as f64;
356 let mean_cos = self.phases.iter().map(|theta| theta.cos()).sum::<f64>() * n_inv;
357 let mean_sin = self.phases.iter().map(|theta| theta.sin()).sum::<f64>() * n_inv;
358 (mean_cos * mean_cos + mean_sin * mean_sin).sqrt()
359 }
360
361 pub fn get_phases(&self) -> &[f64] {
363 &self.phases
364 }
365
366 pub fn set_phases(&mut self, phases: Vec<f64>) {
368 assert_eq!(
369 phases.len(),
370 self.n,
371 "phases length mismatch: got {}, expected {}",
372 phases.len(),
373 self.n
374 );
375 assert_all_finite("phases", &phases);
376 self.phases = phases;
377 }
378
379 pub fn set_coupling(&mut self, coupling_flat: Vec<f64>) {
381 assert_eq!(
382 coupling_flat.len(),
383 self.n * self.n,
384 "coupling length mismatch: got {}, expected {}",
385 coupling_flat.len(),
386 self.n * self.n
387 );
388 assert_all_finite("coupling", &coupling_flat);
389 self.coupling = coupling_flat;
390 }
391}
392
393fn assert_dt(dt: f64) {
394 assert!(dt.is_finite() && dt > 0.0, "dt must be finite and positive");
395}
396
397fn assert_all_finite(name: &str, values: &[f64]) {
398 assert!(
399 values.iter().all(|value| value.is_finite()),
400 "{name} values must be finite"
401 );
402}
403
404fn fill_standard_normals(out: &mut [f64], seed: u64) {
405 let mut rng = ChaCha8Rng::seed_from_u64(seed);
406 let mut i = 0usize;
407
408 while i + 1 < out.len() {
409 let u1 = rng.random::<f64>().max(f64::MIN_POSITIVE);
410 let u2 = rng.random::<f64>();
411 let r = (-2.0 * u1.ln()).sqrt();
412 let theta = TWO_PI * u2;
413 out[i] = r * theta.cos();
414 out[i + 1] = r * theta.sin();
415 i += 2;
416 }
417
418 if i < out.len() {
419 let u1 = rng.random::<f64>().max(f64::MIN_POSITIVE);
420 let u2 = rng.random::<f64>();
421 let r = (-2.0 * u1.ln()).sqrt();
422 out[i] = r * (TWO_PI * u2).cos();
423 }
424}