sc_neurocore_engine/scpn/
kuramoto.rs1use rand::{RngExt, SeedableRng};
14use rand_chacha::ChaCha8Rng;
15use rayon::prelude::*;
16
17const TWO_PI: f64 = std::f64::consts::TAU;
18
19pub struct KuramotoSolver {
27 pub n: usize,
29 pub omega: Vec<f64>,
31 pub coupling: Vec<f64>,
33 pub phases: Vec<f64>,
35 pub noise_amp: f64,
37 pub field_pressure: f64,
39 dtheta: Vec<f64>,
41 sin_diff: Vec<f64>,
43 noise: Vec<f64>,
45 cos_theta: Vec<f64>,
47 geo_coupling: Vec<f64>,
49 pgbo_coupling: Vec<f64>,
51}
52
53impl KuramotoSolver {
54 pub fn new(
56 omega: Vec<f64>,
57 coupling_flat: Vec<f64>,
58 initial_phases: Vec<f64>,
59 noise_amp: f64,
60 ) -> Self {
61 let n = omega.len();
62 assert!(n > 0, "omega must not be empty");
63 assert_eq!(
64 initial_phases.len(),
65 n,
66 "initial_phases length mismatch: got {}, expected {}",
67 initial_phases.len(),
68 n
69 );
70 assert_eq!(
71 coupling_flat.len(),
72 n * n,
73 "coupling length mismatch: got {}, expected {}",
74 coupling_flat.len(),
75 n * n
76 );
77
78 Self {
79 n,
80 omega,
81 coupling: coupling_flat,
82 phases: initial_phases,
83 noise_amp,
84 field_pressure: 0.0,
85 dtheta: vec![0.0; n],
86 sin_diff: vec![0.0; n * n],
87 noise: vec![0.0; n],
88 cos_theta: vec![0.0; n],
89 geo_coupling: vec![0.0; n],
90 pgbo_coupling: vec![0.0; n],
91 }
92 }
93
94 pub fn set_field_pressure(&mut self, f: f64) {
96 self.field_pressure = f;
97 }
98
99 pub fn step(&mut self, dt: f64, seed: u64) -> f64 {
103 let n = self.n;
104 let phases = &self.phases;
105
106 self.sin_diff
107 .par_chunks_mut(n)
108 .enumerate()
109 .for_each(|(row_idx, row)| {
110 let theta_n = phases[row_idx];
111 for (col_idx, value) in row.iter_mut().enumerate() {
112 *value = (phases[col_idx] - theta_n).sin();
113 }
114 });
115
116 if seed == 0 || self.noise_amp == 0.0 {
117 self.noise.fill(0.0);
118 } else {
119 fill_standard_normals(&mut self.noise, seed);
120 }
121
122 self.dtheta
123 .par_iter_mut()
124 .enumerate()
125 .for_each(|(row_idx, dtheta_n)| {
126 let coupling_row = &self.coupling[row_idx * n..(row_idx + 1) * n];
127 let sin_row = &self.sin_diff[row_idx * n..(row_idx + 1) * n];
128 let coupling_sum: f64 = coupling_row
129 .iter()
130 .zip(sin_row.iter())
131 .map(|(k_nm, sin_diff)| k_nm * sin_diff)
132 .sum();
133 *dtheta_n = self.omega[row_idx]
135 + coupling_sum / (n as f64)
136 + self.noise_amp * self.noise[row_idx];
137 });
138
139 for (phase, dtheta) in self.phases.iter_mut().zip(self.dtheta.iter()) {
140 *phase = (*phase + dtheta * dt).rem_euclid(TWO_PI);
141 }
142
143 self.order_parameter()
144 }
145
146 pub fn run(&mut self, n_steps: usize, dt: f64, seed: u64) -> Vec<f64> {
148 let mut order_values = Vec::with_capacity(n_steps);
149 for step_idx in 0..n_steps {
150 let step_seed = if seed == 0 {
151 0
152 } else {
153 seed.wrapping_add(step_idx as u64)
154 };
155 order_values.push(self.step(dt, step_seed));
156 }
157 order_values
158 }
159
160 #[allow(clippy::too_many_arguments)]
169 pub fn step_ssgf(
170 &mut self,
171 dt: f64,
172 seed: u64,
173 w_flat: &[f64],
174 sigma_g: f64,
175 h_flat: &[f64],
176 pgbo_weight: f64,
177 ) -> f64 {
178 let n = self.n;
179 let phases = &self.phases;
180
181 self.sin_diff
183 .par_chunks_mut(n)
184 .enumerate()
185 .for_each(|(row_idx, row)| {
186 let theta_n = phases[row_idx];
187 for (col_idx, value) in row.iter_mut().enumerate() {
188 *value = (phases[col_idx] - theta_n).sin();
189 }
190 });
191
192 if seed == 0 || self.noise_amp == 0.0 {
194 self.noise.fill(0.0);
195 } else {
196 fill_standard_normals(&mut self.noise, seed);
197 }
198
199 let has_geo = !w_flat.is_empty() && sigma_g != 0.0;
201 if has_geo {
202 assert_eq!(
203 w_flat.len(),
204 n * n,
205 "w_flat length mismatch: got {}, expected {}",
206 w_flat.len(),
207 n * n
208 );
209 self.geo_coupling
210 .par_iter_mut()
211 .enumerate()
212 .for_each(|(row_idx, geo_n)| {
213 let w_row = &w_flat[row_idx * n..(row_idx + 1) * n];
214 let sin_row = &self.sin_diff[row_idx * n..(row_idx + 1) * n];
215 *geo_n = sigma_g
216 * w_row
217 .iter()
218 .zip(sin_row.iter())
219 .map(|(w, s)| w * s)
220 .sum::<f64>();
221 });
222 } else {
223 self.geo_coupling.fill(0.0);
224 }
225
226 let has_pgbo = !h_flat.is_empty() && pgbo_weight != 0.0;
228 if has_pgbo {
229 assert_eq!(
230 h_flat.len(),
231 n * n,
232 "h_flat length mismatch: got {}, expected {}",
233 h_flat.len(),
234 n * n
235 );
236 self.pgbo_coupling
237 .par_iter_mut()
238 .enumerate()
239 .for_each(|(row_idx, pgbo_n)| {
240 let h_row = &h_flat[row_idx * n..(row_idx + 1) * n];
241 let sin_row = &self.sin_diff[row_idx * n..(row_idx + 1) * n];
242 *pgbo_n = pgbo_weight
243 * h_row
244 .iter()
245 .zip(sin_row.iter())
246 .map(|(h, s)| h * s)
247 .sum::<f64>();
248 });
249 } else {
250 self.pgbo_coupling.fill(0.0);
251 }
252
253 if self.field_pressure != 0.0 {
255 for (c, &theta) in self.cos_theta.iter_mut().zip(phases.iter()) {
256 *c = theta.cos();
257 }
258 } else {
259 self.cos_theta.fill(0.0);
260 }
261
262 self.dtheta
264 .par_iter_mut()
265 .enumerate()
266 .for_each(|(i, dtheta_n)| {
267 let coupling_row = &self.coupling[i * n..(i + 1) * n];
268 let sin_row = &self.sin_diff[i * n..(i + 1) * n];
269 let coupling_sum: f64 = coupling_row
270 .iter()
271 .zip(sin_row.iter())
272 .map(|(k, s)| k * s)
273 .sum();
274
275 *dtheta_n = self.omega[i]
276 + coupling_sum / (n as f64)
277 + self.geo_coupling[i]
278 + self.pgbo_coupling[i]
279 + self.field_pressure * self.cos_theta[i]
280 + self.noise_amp * self.noise[i];
281 });
282
283 for (phase, dtheta) in self.phases.iter_mut().zip(self.dtheta.iter()) {
284 *phase = (*phase + dtheta * dt).rem_euclid(TWO_PI);
285 }
286
287 self.order_parameter()
288 }
289
290 #[allow(clippy::too_many_arguments)]
292 pub fn run_ssgf(
293 &mut self,
294 n_steps: usize,
295 dt: f64,
296 seed: u64,
297 w_flat: &[f64],
298 sigma_g: f64,
299 h_flat: &[f64],
300 pgbo_weight: f64,
301 ) -> Vec<f64> {
302 let mut order_values = Vec::with_capacity(n_steps);
303 for step_idx in 0..n_steps {
304 let step_seed = if seed == 0 {
305 0
306 } else {
307 seed.wrapping_add(step_idx as u64)
308 };
309 order_values.push(self.step_ssgf(dt, step_seed, w_flat, sigma_g, h_flat, pgbo_weight));
310 }
311 order_values
312 }
313
314 pub fn order_parameter(&self) -> f64 {
317 if self.phases.is_empty() {
318 return 0.0;
319 }
320
321 let n_inv = 1.0 / self.phases.len() as f64;
322 let mean_cos = self.phases.iter().map(|theta| theta.cos()).sum::<f64>() * n_inv;
323 let mean_sin = self.phases.iter().map(|theta| theta.sin()).sum::<f64>() * n_inv;
324 (mean_cos * mean_cos + mean_sin * mean_sin).sqrt()
325 }
326
327 pub fn get_phases(&self) -> &[f64] {
329 &self.phases
330 }
331
332 pub fn set_phases(&mut self, phases: Vec<f64>) {
334 assert_eq!(
335 phases.len(),
336 self.n,
337 "phases length mismatch: got {}, expected {}",
338 phases.len(),
339 self.n
340 );
341 self.phases = phases;
342 }
343
344 pub fn set_coupling(&mut self, coupling_flat: Vec<f64>) {
346 assert_eq!(
347 coupling_flat.len(),
348 self.n * self.n,
349 "coupling length mismatch: got {}, expected {}",
350 coupling_flat.len(),
351 self.n * self.n
352 );
353 self.coupling = coupling_flat;
354 }
355}
356
357fn fill_standard_normals(out: &mut [f64], seed: u64) {
358 let mut rng = ChaCha8Rng::seed_from_u64(seed);
359 let mut i = 0usize;
360
361 while i + 1 < out.len() {
362 let u1 = rng.random::<f64>().max(f64::MIN_POSITIVE);
363 let u2 = rng.random::<f64>();
364 let r = (-2.0 * u1.ln()).sqrt();
365 let theta = TWO_PI * u2;
366 out[i] = r * theta.cos();
367 out[i + 1] = r * theta.sin();
368 i += 2;
369 }
370
371 if i < out.len() {
372 let u1 = rng.random::<f64>().max(f64::MIN_POSITIVE);
373 let u2 = rng.random::<f64>();
374 let r = (-2.0 * u1.ln()).sqrt();
375 out[i] = r * (TWO_PI * u2).cos();
376 }
377}