Skip to main content

terminals_core/phase/
kuramoto.rs

1use std::f32::consts::PI;
2
3/// Kuramoto order parameter R·e^{iψ} = (1/N) Σ e^{iθ_j}
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct OrderParameter {
6    /// Synchronization magnitude in [0, 1]
7    pub r: f32,
8    /// Collective phase in (-π, π]
9    pub psi: f32,
10}
11
12/// Natural frequencies for oscillators: uniform or per-node.
13pub enum Omega {
14    Uniform(f32),
15    PerNode(Vec<f32>),
16}
17
18/// Wrap θ into [0, 2π).
19#[inline]
20pub fn wrap_to_2pi(theta: f32) -> f32 {
21    let two_pi = 2.0 * PI;
22    let mut t = theta % two_pi;
23    if t < 0.0 {
24        t += two_pi;
25    }
26    t
27}
28
29/// Wrap θ into (-π, π].
30#[inline]
31pub fn wrap_to_pi(theta: f32) -> f32 {
32    let two_pi = 2.0 * PI;
33    let mut t = (theta + PI) % two_pi;
34    if t < 0.0 {
35        t += two_pi;
36    }
37    t - PI
38}
39
40/// Compute the Kuramoto order parameter from a slice of phases.
41///
42/// R e^{iψ} = (1/N) Σ e^{iθ_j}
43pub fn order_parameter(phases: &[f32]) -> OrderParameter {
44    let n = phases.len();
45    if n == 0 {
46        return OrderParameter { r: 0.0, psi: 0.0 };
47    }
48    let mut c = 0.0f32;
49    let mut s = 0.0f32;
50    for &theta in phases {
51        c += theta.cos();
52        s += theta.sin();
53    }
54    let inv_n = 1.0 / n as f32;
55    c *= inv_n;
56    s *= inv_n;
57    OrderParameter {
58        r: c.hypot(s),
59        psi: s.atan2(c),
60    }
61}
62
63/// Euler step of the Kuramoto model.
64///
65/// dθ_i/dt = ω_i + (K/degree_i) Σ_j A_ij sin(θ_j - θ_i)
66///
67/// - `phases`: current oscillator phases (radians)
68/// - `omega`: natural frequencies (uniform scalar or per-node)
69/// - `k`: global coupling strength
70/// - `adjacency`: optional weighted adjacency; `None` = fully connected (weight 1)
71/// - `dt`: time step
72/// - `wrap`: whether to wrap result into (−π, π]
73pub fn kuramoto_step(
74    phases: &[f32],
75    omega: &Omega,
76    k: f32,
77    adjacency: Option<&[&[f32]]>,
78    dt: f32,
79    wrap: bool,
80) -> Vec<f32> {
81    let n = phases.len();
82    let mut next = Vec::with_capacity(n);
83
84    for i in 0..n {
85        let theta_i = phases[i];
86        let w_i = match omega {
87            Omega::Uniform(w) => *w,
88            Omega::PerNode(ref ws) => ws[i],
89        };
90
91        let (coupling_sum, degree) = if let Some(adj) = adjacency {
92            let row = adj[i];
93            let mut cs = 0.0f32;
94            let mut deg = 0.0f32;
95            for j in 0..n {
96                if j == i {
97                    continue;
98                }
99                let aij = if j < row.len() { row[j] } else { 0.0 };
100                if aij == 0.0 {
101                    continue;
102                }
103                cs += aij * (phases[j] - theta_i).sin();
104                deg += aij.abs();
105            }
106            (cs, deg)
107        } else {
108            let mut cs = 0.0f32;
109            for (j, &phase_j) in phases.iter().enumerate().take(n) {
110                if j == i {
111                    continue;
112                }
113                cs += (phase_j - theta_i).sin();
114            }
115            let deg = (n as f32 - 1.0).max(1.0);
116            (cs, deg)
117        };
118
119        let coupling = if degree > 0.0 {
120            (k / degree) * coupling_sum
121        } else {
122            0.0
123        };
124
125        let mut val = theta_i + (w_i + coupling) * dt;
126        if wrap {
127            val = wrap_to_pi(val);
128        }
129        next.push(val);
130    }
131
132    next
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_order_parameter_synchronized() {
141        let phases = vec![0.0f32; 10];
142        let op = order_parameter(&phases);
143        assert!((op.r - 1.0).abs() < 1e-6, "R = {}", op.r);
144    }
145
146    #[test]
147    fn test_order_parameter_uniform_spread() {
148        let n = 100usize;
149        let phases: Vec<f32> = (0..n).map(|i| 2.0 * PI * i as f32 / n as f32).collect();
150        let op = order_parameter(&phases);
151        assert!(op.r < 0.1, "R = {} (expected < 0.1)", op.r);
152    }
153
154    #[test]
155    fn test_order_parameter_empty() {
156        let op = order_parameter(&[]);
157        assert_eq!(op.r, 0.0);
158        assert_eq!(op.psi, 0.0);
159    }
160
161    #[test]
162    fn test_wrap_to_pi_zero() {
163        assert!((wrap_to_pi(0.0) - 0.0).abs() < 1e-6);
164    }
165
166    #[test]
167    fn test_wrap_to_pi_4pi() {
168        assert!(
169            (wrap_to_pi(4.0 * PI) - 0.0).abs() < 1e-4,
170            "got {}",
171            wrap_to_pi(4.0 * PI)
172        );
173    }
174
175    #[test]
176    fn test_wrap_to_pi_3pi() {
177        let w = wrap_to_pi(3.0 * PI);
178        // 3π ≡ π (mod 2π), so wrap_to_pi should give ±π
179        assert!(
180            (w - (-PI)).abs() < 1e-4 || (w - PI).abs() < 1e-4,
181            "wrap_to_pi(3π) = {}",
182            w
183        );
184    }
185
186    #[test]
187    fn test_wrap_to_2pi_negative() {
188        let w = wrap_to_2pi(-0.5);
189        assert!(w >= 0.0);
190        assert!(w < 2.0 * PI);
191    }
192
193    #[test]
194    fn test_kuramoto_step_converges() {
195        let phases = vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0];
196        let r_before = order_parameter(&phases).r;
197        let next = kuramoto_step(&phases, &Omega::Uniform(0.0), 5.0, None, 0.01, true);
198        let r_after = order_parameter(&next).r;
199        assert!(
200            r_after >= r_before - 0.01,
201            "r_before={} r_after={}",
202            r_before,
203            r_after
204        );
205    }
206
207    #[test]
208    fn test_kuramoto_step_preserves_length() {
209        let phases = vec![0.1f32, 0.2, 0.3];
210        let next = kuramoto_step(&phases, &Omega::Uniform(1.0), 1.0, None, 0.01, true);
211        assert_eq!(next.len(), 3);
212    }
213
214    #[test]
215    fn test_kuramoto_step_per_node_omega() {
216        let phases = vec![0.0f32, 0.0, 0.0];
217        let omega = Omega::PerNode(vec![1.0, 2.0, 3.0]);
218        let next = kuramoto_step(&phases, &omega, 0.0, None, 0.1, false);
219        assert!((next[0] - 0.1).abs() < 1e-6);
220        assert!((next[1] - 0.2).abs() < 1e-6);
221        assert!((next[2] - 0.3).abs() < 1e-6);
222    }
223
224    #[test]
225    fn test_kuramoto_step_strong_coupling_increases_r() {
226        // After many steps with strong coupling and zero natural frequencies,
227        // R should approach 1.
228        let mut phases = vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0];
229        for _ in 0..1000 {
230            phases = kuramoto_step(&phases, &Omega::Uniform(0.0), 5.0, None, 0.01, true);
231        }
232        let r = order_parameter(&phases).r;
233        assert!(r > 0.99, "R = {} after convergence", r);
234    }
235
236    #[test]
237    fn test_kuramoto_step_no_wrap() {
238        let phases = vec![3.0f32];
239        // Single oscillator, no coupling, omega = 1, dt = 1 → next = 4.0 (unwrapped)
240        let next = kuramoto_step(&phases, &Omega::Uniform(1.0), 0.0, None, 1.0, false);
241        assert!((next[0] - 4.0).abs() < 1e-6);
242    }
243}