Skip to main content

koopman_dmd/
maps.rs

1use std::f64::consts::PI;
2
3use faer::Mat;
4
5/// Trait for dynamical system maps.
6///
7/// A map takes a state vector and returns the next state.
8pub trait MapFn: Send + Sync {
9    /// Apply one iteration of the map.
10    fn step(&self, state: &[f64]) -> Vec<f64>;
11
12    /// State space dimension.
13    fn dim(&self) -> usize;
14
15    /// Map name.
16    fn name(&self) -> &str;
17}
18
19/// Chirikov standard map (2D, symplectic/area-preserving).
20///
21/// y' = (y + ε sin(2πx)) mod 1
22/// x' = (x + y') mod 1
23#[derive(Debug, Clone)]
24pub struct StandardMap {
25    pub epsilon: f64,
26}
27
28impl Default for StandardMap {
29    fn default() -> Self {
30        Self { epsilon: 0.12 }
31    }
32}
33
34impl MapFn for StandardMap {
35    fn step(&self, state: &[f64]) -> Vec<f64> {
36        let x = state[0];
37        let y = state[1];
38        let y_new = (y + self.epsilon * (2.0 * PI * x).sin()).rem_euclid(1.0);
39        let x_new = (x + y_new).rem_euclid(1.0);
40        vec![x_new, y_new]
41    }
42    fn dim(&self) -> usize {
43        2
44    }
45    fn name(&self) -> &str {
46        "standard_map"
47    }
48}
49
50/// Froeschlé map (4D, symplectic) — two coupled standard maps.
51///
52/// y1' = (y1 + ε1 sin(2πx1) + η sin(2π(x1+x2))) mod 1
53/// x1' = (x1 + y1') mod 1
54/// y2' = (y2 + ε2 sin(2πx2) + η sin(2π(x1+x2))) mod 1
55/// x2' = (x2 + y2') mod 1
56#[derive(Debug, Clone)]
57pub struct FroeschleMap {
58    pub epsilon1: f64,
59    pub epsilon2: f64,
60    pub eta: f64,
61}
62
63impl Default for FroeschleMap {
64    fn default() -> Self {
65        Self {
66            epsilon1: 0.02,
67            epsilon2: 0.02,
68            eta: 0.01,
69        }
70    }
71}
72
73impl MapFn for FroeschleMap {
74    fn step(&self, state: &[f64]) -> Vec<f64> {
75        let (x1, y1, x2, y2) = (state[0], state[1], state[2], state[3]);
76        let coupling = self.eta * (2.0 * PI * x1 + 2.0 * PI * x2).sin();
77
78        let y1_new = (y1 + self.epsilon1 * (2.0 * PI * x1).sin() + coupling).rem_euclid(1.0);
79        let x1_new = (x1 + y1_new).rem_euclid(1.0);
80        let y2_new = (y2 + self.epsilon2 * (2.0 * PI * x2).sin() + coupling).rem_euclid(1.0);
81        let x2_new = (x2 + y2_new).rem_euclid(1.0);
82
83        vec![x1_new, y1_new, x2_new, y2_new]
84    }
85    fn dim(&self) -> usize {
86        4
87    }
88    fn name(&self) -> &str {
89        "froeschle_map"
90    }
91}
92
93/// Extended standard map (3D, action-action-angle).
94///
95/// x' = (x + ε sin(2πz) + δ sin(2πy)) mod 1
96/// y' = (y + ε sin(2πz)) mod 1
97/// z' = (z + x') mod 1
98#[derive(Debug, Clone)]
99pub struct ExtendedStandardMap {
100    pub epsilon: f64,
101    pub delta: f64,
102}
103
104impl Default for ExtendedStandardMap {
105    fn default() -> Self {
106        Self {
107            epsilon: 0.01,
108            delta: 0.001,
109        }
110    }
111}
112
113impl MapFn for ExtendedStandardMap {
114    fn step(&self, state: &[f64]) -> Vec<f64> {
115        let (x, y, z) = (state[0], state[1], state[2]);
116        let x_new = (x + self.epsilon * (2.0 * PI * z).sin() + self.delta * (2.0 * PI * y).sin())
117            .rem_euclid(1.0);
118        let y_new = (y + self.epsilon * (2.0 * PI * z).sin()).rem_euclid(1.0);
119        let z_new = (z + x_new).rem_euclid(1.0);
120        vec![x_new, y_new, z_new]
121    }
122    fn dim(&self) -> usize {
123        3
124    }
125    fn name(&self) -> &str {
126        "extended_standard_map"
127    }
128}
129
130/// Hénon map (2D, dissipative).
131///
132/// x' = 1 - a·x² + y
133/// y' = b·x
134#[derive(Debug, Clone)]
135pub struct HenonMap {
136    pub a: f64,
137    pub b: f64,
138}
139
140impl Default for HenonMap {
141    fn default() -> Self {
142        Self { a: 1.4, b: 0.3 }
143    }
144}
145
146impl MapFn for HenonMap {
147    fn step(&self, state: &[f64]) -> Vec<f64> {
148        let x = state[0];
149        let y = state[1];
150        vec![1.0 - self.a * x * x + y, self.b * x]
151    }
152    fn dim(&self) -> usize {
153        2
154    }
155    fn name(&self) -> &str {
156        "henon_map"
157    }
158}
159
160/// Logistic map (1D, dissipative).
161///
162/// x' = r·x·(1-x)
163#[derive(Debug, Clone)]
164pub struct LogisticMap {
165    pub r: f64,
166}
167
168impl Default for LogisticMap {
169    fn default() -> Self {
170        Self { r: 3.9 }
171    }
172}
173
174impl MapFn for LogisticMap {
175    fn step(&self, state: &[f64]) -> Vec<f64> {
176        let x = state[0];
177        vec![self.r * x * (1.0 - x)]
178    }
179    fn dim(&self) -> usize {
180        1
181    }
182    fn name(&self) -> &str {
183        "logistic_map"
184    }
185}
186
187/// A wrapper that turns a closure into a MapFn.
188pub struct ClosureMap<F: Fn(&[f64]) -> Vec<f64> + Send + Sync> {
189    func: F,
190    dim: usize,
191    name: String,
192}
193
194impl<F: Fn(&[f64]) -> Vec<f64> + Send + Sync> ClosureMap<F> {
195    pub fn new(func: F, dim: usize, name: impl Into<String>) -> Self {
196        Self {
197            func,
198            dim,
199            name: name.into(),
200        }
201    }
202}
203
204impl<F: Fn(&[f64]) -> Vec<f64> + Send + Sync> MapFn for ClosureMap<F> {
205    fn step(&self, state: &[f64]) -> Vec<f64> {
206        (self.func)(state)
207    }
208    fn dim(&self) -> usize {
209        self.dim
210    }
211    fn name(&self) -> &str {
212        &self.name
213    }
214}
215
216/// Generate a trajectory by iterating a map from an initial condition.
217///
218/// Returns a matrix (n_dim × n_iter+1) where column 0 is the initial condition.
219pub fn generate_trajectory(initial_condition: &[f64], map: &dyn MapFn, n_iter: usize) -> Mat<f64> {
220    let n_dim = initial_condition.len();
221    let mut traj = Mat::<f64>::zeros(n_dim, n_iter + 1);
222
223    // Store initial condition
224    for i in 0..n_dim {
225        traj[(i, 0)] = initial_condition[i];
226    }
227
228    let mut state = initial_condition.to_vec();
229    for k in 1..=n_iter {
230        state = map.step(&state);
231        for i in 0..n_dim {
232            traj[(i, k)] = state[i];
233        }
234    }
235
236    traj
237}
238
239/// Generate a 2D grid of initial conditions.
240///
241/// Returns (x_coords, y_coords) vectors.
242pub fn generate_phase_grid(
243    x_range: (f64, f64),
244    y_range: (f64, f64),
245    resolution: usize,
246) -> (Vec<f64>, Vec<f64>) {
247    let x_coords: Vec<f64> = (0..resolution)
248        .map(|i| x_range.0 + (x_range.1 - x_range.0) * i as f64 / (resolution - 1) as f64)
249        .collect();
250    let y_coords: Vec<f64> = (0..resolution)
251        .map(|i| y_range.0 + (y_range.1 - y_range.0) * i as f64 / (resolution - 1) as f64)
252        .collect();
253    (x_coords, y_coords)
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    fn assert_near(a: f64, b: f64, eps: f64) {
261        assert!(
262            (a - b).abs() < eps,
263            "expected {a} ≈ {b} (diff = {})",
264            (a - b).abs()
265        );
266    }
267
268    #[test]
269    fn test_standard_map_integrable() {
270        // With epsilon=0, orbits lie on lines of constant y
271        let map = StandardMap { epsilon: 0.0 };
272        let state = vec![0.3, 0.2];
273        let next = map.step(&state);
274        assert_near(next[1], 0.2, 1e-12); // y unchanged
275        assert_near(next[0], 0.5, 1e-12); // x = 0.3 + 0.2 = 0.5
276    }
277
278    #[test]
279    fn test_standard_map_modular() {
280        let map = StandardMap { epsilon: 0.12 };
281        let state = vec![0.9, 0.9];
282        let next = map.step(&state);
283        assert!(next[0] >= 0.0 && next[0] < 1.0);
284        assert!(next[1] >= 0.0 && next[1] < 1.0);
285    }
286
287    #[test]
288    fn test_henon_map_fixed_point() {
289        // The Henon map with a=0, b=0 has fixed point at (1, 0)
290        let map = HenonMap { a: 0.0, b: 0.0 };
291        let state = vec![0.5, 0.0];
292        let next = map.step(&state);
293        assert_near(next[0], 1.0, 1e-12); // 1 - 0 + 0 = 1
294        assert_near(next[1], 0.0, 1e-12);
295    }
296
297    #[test]
298    fn test_logistic_map_fixed_point() {
299        // r=2 has fixed point at x=0.5
300        let map = LogisticMap { r: 2.0 };
301        let state = vec![0.5];
302        let next = map.step(&state);
303        assert_near(next[0], 0.5, 1e-12); // 2*0.5*0.5 = 0.5
304    }
305
306    #[test]
307    fn test_logistic_map_chaos() {
308        let map = LogisticMap { r: 4.0 };
309        let state = vec![0.1];
310        let next = map.step(&state);
311        assert_near(next[0], 0.36, 1e-12); // 4*0.1*0.9 = 0.36
312    }
313
314    #[test]
315    fn test_froeschle_map_uncoupled() {
316        // With eta=0, subsystems are independent
317        let map = FroeschleMap {
318            epsilon1: 0.0,
319            epsilon2: 0.0,
320            eta: 0.0,
321        };
322        let state = vec![0.3, 0.2, 0.5, 0.1];
323        let next = map.step(&state);
324        // Subsystem 1: x1'=0.3+0.2=0.5, y1'=0.2
325        assert_near(next[0], 0.5, 1e-12);
326        assert_near(next[1], 0.2, 1e-12);
327        // Subsystem 2: x2'=0.5+0.1=0.6, y2'=0.1
328        assert_near(next[2], 0.6, 1e-12);
329        assert_near(next[3], 0.1, 1e-12);
330    }
331
332    #[test]
333    fn test_extended_standard_map() {
334        let map = ExtendedStandardMap::default();
335        let state = vec![0.5, 0.3, 0.2];
336        let next = map.step(&state);
337        assert_eq!(next.len(), 3);
338        // All coordinates should be in [0, 1)
339        for &v in &next {
340            assert!(v >= 0.0 && v < 1.0);
341        }
342    }
343
344    #[test]
345    fn test_generate_trajectory() {
346        let map = StandardMap { epsilon: 0.12 };
347        let traj = generate_trajectory(&[0.5, 0.3], &map, 100);
348        assert_eq!(traj.nrows(), 2);
349        assert_eq!(traj.ncols(), 101); // initial + 100 steps
350        assert_near(traj[(0, 0)], 0.5, 1e-12);
351        assert_near(traj[(1, 0)], 0.3, 1e-12);
352    }
353
354    #[test]
355    fn test_generate_phase_grid() {
356        let (x, y) = generate_phase_grid((0.0, 1.0), (0.0, 1.0), 10);
357        assert_eq!(x.len(), 10);
358        assert_eq!(y.len(), 10);
359        assert_near(x[0], 0.0, 1e-12);
360        assert_near(x[9], 1.0, 1e-12);
361    }
362
363    #[test]
364    fn test_closure_map() {
365        let map = ClosureMap::new(|state: &[f64]| vec![state[0] * 2.0], 1, "doubling");
366        assert_eq!(map.dim(), 1);
367        assert_eq!(map.name(), "doubling");
368        let next = map.step(&[0.25]);
369        assert_near(next[0], 0.5, 1e-12);
370    }
371}