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
81 Self {
82 n,
83 n_inv: 1.0 / n as f64,
84 omega,
85 coupling: coupling_flat,
86 phases: initial_phases,
87 noise_amp,
88 field_pressure: 0.0,
89 dtheta: vec![0.0; n],
90 sin_diff: vec![0.0; n * n],
91 noise: vec![0.0; n],
92 cos_theta: vec![0.0; n],
93 geo_coupling: vec![0.0; n],
94 pgbo_coupling: vec![0.0; n],
95 }
96 }
97
98 pub fn set_field_pressure(&mut self, f: f64) {
100 self.field_pressure = f;
101 }
102
103 pub fn step(&mut self, dt: f64, seed: u64) -> f64 {
107 let n = self.n;
108 let phases = &self.phases;
109
110 self.sin_diff
111 .par_chunks_mut(n)
112 .enumerate()
113 .for_each(|(row_idx, row)| {
114 let theta_n = phases[row_idx];
115 for (col_idx, value) in row.iter_mut().enumerate() {
116 *value = (phases[col_idx] - theta_n).sin();
117 }
118 });
119
120 if seed == 0 || self.noise_amp == 0.0 {
121 self.noise.fill(0.0);
122 } else {
123 fill_standard_normals(&mut self.noise, seed);
124 }
125
126 self.dtheta
127 .par_iter_mut()
128 .enumerate()
129 .for_each(|(row_idx, dtheta_n)| {
130 let coupling_row = &self.coupling[row_idx * n..(row_idx + 1) * n];
131 let sin_row = &self.sin_diff[row_idx * n..(row_idx + 1) * n];
132 let coupling_sum = crate::simd::dot_f64_dispatch(coupling_row, sin_row);
133 *dtheta_n = self.omega[row_idx]
135 + coupling_sum * self.n_inv
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 = crate::simd::dot_f64_dispatch(coupling_row, sin_row);
270
271 *dtheta_n = self.omega[i]
272 + coupling_sum * self.n_inv
273 + self.geo_coupling[i]
274 + self.pgbo_coupling[i]
275 + self.field_pressure * self.cos_theta[i]
276 + self.noise_amp * self.noise[i];
277 });
278
279 for (phase, dtheta) in self.phases.iter_mut().zip(self.dtheta.iter()) {
280 *phase = (*phase + dtheta * dt).rem_euclid(TWO_PI);
281 }
282
283 self.order_parameter()
284 }
285
286 #[allow(clippy::too_many_arguments)]
288 pub fn run_ssgf(
289 &mut self,
290 n_steps: usize,
291 dt: f64,
292 seed: u64,
293 w_flat: &[f64],
294 sigma_g: f64,
295 h_flat: &[f64],
296 pgbo_weight: f64,
297 ) -> Vec<f64> {
298 let mut order_values = Vec::with_capacity(n_steps);
299 for step_idx in 0..n_steps {
300 let step_seed = if seed == 0 {
301 0
302 } else {
303 seed.wrapping_add(step_idx as u64)
304 };
305 order_values.push(self.step_ssgf(dt, step_seed, w_flat, sigma_g, h_flat, pgbo_weight));
306 }
307 order_values
308 }
309
310 pub fn order_parameter(&self) -> f64 {
313 if self.phases.is_empty() {
314 return 0.0;
315 }
316
317 let n_inv = 1.0 / self.phases.len() as f64;
318 let mean_cos = self.phases.iter().map(|theta| theta.cos()).sum::<f64>() * n_inv;
319 let mean_sin = self.phases.iter().map(|theta| theta.sin()).sum::<f64>() * n_inv;
320 (mean_cos * mean_cos + mean_sin * mean_sin).sqrt()
321 }
322
323 pub fn get_phases(&self) -> &[f64] {
325 &self.phases
326 }
327
328 pub fn set_phases(&mut self, phases: Vec<f64>) {
330 assert_eq!(
331 phases.len(),
332 self.n,
333 "phases length mismatch: got {}, expected {}",
334 phases.len(),
335 self.n
336 );
337 self.phases = phases;
338 }
339
340 pub fn set_coupling(&mut self, coupling_flat: Vec<f64>) {
342 assert_eq!(
343 coupling_flat.len(),
344 self.n * self.n,
345 "coupling length mismatch: got {}, expected {}",
346 coupling_flat.len(),
347 self.n * self.n
348 );
349 self.coupling = coupling_flat;
350 }
351}
352
353fn fill_standard_normals(out: &mut [f64], seed: u64) {
354 let mut rng = ChaCha8Rng::seed_from_u64(seed);
355 let mut i = 0usize;
356
357 while i + 1 < out.len() {
358 let u1 = rng.random::<f64>().max(f64::MIN_POSITIVE);
359 let u2 = rng.random::<f64>();
360 let r = (-2.0 * u1.ln()).sqrt();
361 let theta = TWO_PI * u2;
362 out[i] = r * theta.cos();
363 out[i + 1] = r * theta.sin();
364 i += 2;
365 }
366
367 if i < out.len() {
368 let u1 = rng.random::<f64>().max(f64::MIN_POSITIVE);
369 let u2 = rng.random::<f64>();
370 let r = (-2.0 * u1.ln()).sqrt();
371 out[i] = r * (TWO_PI * u2).cos();
372 }
373}