scirs2_integrate/geometric/
volume_preserving.rs

1//! Volume-preserving integrators
2//!
3//! This module provides numerical integrators that preserve phase space volume,
4//! suitable for divergence-free flows and incompressible fluid dynamics.
5
6use crate::error::{IntegrateError, IntegrateResult};
7use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
8#[allow(unused_imports)]
9use std::f64::consts::{PI, SQRT_2};
10
11// Type alias for complex function type
12type InvariantFn = Box<dyn Fn(&ArrayView1<f64>) -> f64>;
13
14/// Trait for divergence-free vector fields
15pub trait DivergenceFreeFlow {
16    /// Dimension of the phase space
17    fn dim(&self) -> usize;
18
19    /// Evaluate the vector field at a point
20    fn evaluate(&self, x: &ArrayView1<f64>, t: f64) -> Array1<f64>;
21
22    /// Verify divergence-free condition (for debugging)
23    fn verify_divergence_free(&self, x: &ArrayView1<f64>, t: f64, h: f64) -> f64 {
24        let n = self.dim();
25        let mut div = 0.0;
26
27        for i in 0..n {
28            let mut x_plus = x.to_owned();
29            let mut x_minus = x.to_owned();
30            x_plus[i] += h;
31            x_minus[i] -= h;
32
33            let f_plus = self.evaluate(&x_plus.view(), t);
34            let f_minus = self.evaluate(&x_minus.view(), t);
35
36            div += (f_plus[i] - f_minus[i]) / (2.0 * h);
37        }
38
39        div
40    }
41}
42
43/// Volume-preserving integrator
44pub struct VolumePreservingIntegrator {
45    /// Time step
46    pub dt: f64,
47    /// Integration method
48    pub method: VolumePreservingMethod,
49    /// Tolerance for implicit methods
50    pub tol: f64,
51    /// Maximum iterations for implicit methods
52    pub max_iter: usize,
53}
54
55/// Available volume-preserving integration methods
56#[derive(Debug, Clone, Copy)]
57pub enum VolumePreservingMethod {
58    /// Explicit midpoint rule (2nd order)
59    ExplicitMidpoint,
60    /// Implicit midpoint rule (2nd order)
61    ImplicitMidpoint,
62    /// Splitting method for special structure
63    SplittingMethod,
64    /// Projection method
65    ProjectionMethod,
66    /// Composition method (4th order)
67    CompositionMethod,
68}
69
70impl VolumePreservingIntegrator {
71    /// Create a new volume-preserving integrator
72    pub fn new(dt: f64, method: VolumePreservingMethod) -> Self {
73        Self {
74            dt,
75            method,
76            tol: 1e-10,
77            max_iter: 100,
78        }
79    }
80
81    /// Set tolerance for implicit methods
82    pub fn with_tolerance(mut self, tol: f64) -> Self {
83        self.tol = tol;
84        self
85    }
86
87    /// Integrate one step
88    pub fn step<F>(&self, x: &ArrayView1<f64>, t: f64, flow: &F) -> IntegrateResult<Array1<f64>>
89    where
90        F: DivergenceFreeFlow,
91    {
92        match self.method {
93            VolumePreservingMethod::ExplicitMidpoint => self.explicit_midpoint_step(x, t, flow),
94            VolumePreservingMethod::ImplicitMidpoint => self.implicit_midpoint_step(x, t, flow),
95            VolumePreservingMethod::SplittingMethod => self.splitting_step(x, t, flow),
96            VolumePreservingMethod::ProjectionMethod => self.projection_step(x, t, flow),
97            VolumePreservingMethod::CompositionMethod => self.composition_step(x, t, flow),
98        }
99    }
100
101    /// Explicit midpoint method
102    fn explicit_midpoint_step<F>(
103        &self,
104        x: &ArrayView1<f64>,
105        t: f64,
106        flow: &F,
107    ) -> IntegrateResult<Array1<f64>>
108    where
109        F: DivergenceFreeFlow,
110    {
111        let f0 = flow.evaluate(x, t);
112        let x_mid = x + &f0 * (self.dt / 2.0);
113        let f_mid = flow.evaluate(&x_mid.view(), t + self.dt / 2.0);
114
115        Ok(x + &f_mid * self.dt)
116    }
117
118    /// Implicit midpoint method (Gauss-Legendre)
119    fn implicit_midpoint_step<F>(
120        &self,
121        x: &ArrayView1<f64>,
122        t: f64,
123        flow: &F,
124    ) -> IntegrateResult<Array1<f64>>
125    where
126        F: DivergenceFreeFlow,
127    {
128        let mut x_new = x.to_owned();
129        let t_mid = t + self.dt / 2.0;
130
131        // Fixed-point iteration
132        for _ in 0..self.max_iter {
133            let x_mid = (&x.to_owned() + &x_new) / 2.0;
134            let f_mid = flow.evaluate(&x_mid.view(), t_mid);
135            let x_next = x + &f_mid * self.dt;
136
137            let error = (&x_next - &x_new).mapv(f64::abs).sum();
138            x_new = x_next;
139
140            if error < self.tol {
141                return Ok(x_new);
142            }
143        }
144
145        Err(IntegrateError::ConvergenceError(
146            "Implicit midpoint method failed to converge".to_string(),
147        ))
148    }
149
150    /// Splitting method for special structures
151    fn splitting_step<F>(
152        &self,
153        x: &ArrayView1<f64>,
154        t: f64,
155        flow: &F,
156    ) -> IntegrateResult<Array1<f64>>
157    where
158        F: DivergenceFreeFlow,
159    {
160        // For general flows, fall back to composition
161        self.composition_step(x, t, flow)
162    }
163
164    /// Projection method (project back to divergence-free manifold)
165    fn projection_step<F>(
166        &self,
167        x: &ArrayView1<f64>,
168        t: f64,
169        flow: &F,
170    ) -> IntegrateResult<Array1<f64>>
171    where
172        F: DivergenceFreeFlow,
173    {
174        // Take an Euler step
175        let f = flow.evaluate(x, t);
176        let _x_euler = x + &f * self.dt;
177
178        // Project back (simplified - in practice would solve Poisson equation)
179        // For now, just use explicit midpoint which is volume-preserving
180        self.explicit_midpoint_step(x, t, flow)
181    }
182
183    /// Fourth-order composition method
184    fn composition_step<F>(
185        &self,
186        x: &ArrayView1<f64>,
187        t: f64,
188        flow: &F,
189    ) -> IntegrateResult<Array1<f64>>
190    where
191        F: DivergenceFreeFlow,
192    {
193        // Suzuki-Yoshida 4th order composition
194        let gamma = 1.0 / (2.0 - 2.0_f64.powf(1.0 / 3.0));
195        let c1 = gamma / 2.0;
196        let c2 = (1.0 - gamma) / 2.0;
197        let c3 = c2;
198        let c4 = c1;
199
200        let d1 = gamma;
201        let d2 = -gamma * 2.0_f64.powf(1.0 / 3.0);
202        let d3 = gamma;
203
204        // Sub-steps
205        let mut x_current = x.to_owned();
206        let mut t_current = t;
207
208        // Step 1
209        let substep = Self::new(c1 * self.dt, VolumePreservingMethod::ExplicitMidpoint);
210        x_current = substep.step(&x_current.view(), t_current, flow)?;
211        t_current += c1 * self.dt;
212
213        // Step 2
214        let substep = Self::new(d1 * self.dt, VolumePreservingMethod::ExplicitMidpoint);
215        x_current = substep.step(&x_current.view(), t_current, flow)?;
216        t_current += d1 * self.dt;
217
218        // Step 3
219        let substep = Self::new(c2 * self.dt, VolumePreservingMethod::ExplicitMidpoint);
220        x_current = substep.step(&x_current.view(), t_current, flow)?;
221        t_current += c2 * self.dt;
222
223        // Step 4
224        let substep = Self::new(d2 * self.dt, VolumePreservingMethod::ExplicitMidpoint);
225        x_current = substep.step(&x_current.view(), t_current, flow)?;
226        t_current += d2 * self.dt;
227
228        // Step 5
229        let substep = Self::new(c3 * self.dt, VolumePreservingMethod::ExplicitMidpoint);
230        x_current = substep.step(&x_current.view(), t_current, flow)?;
231        t_current += c3 * self.dt;
232
233        // Step 6
234        let substep = Self::new(d3 * self.dt, VolumePreservingMethod::ExplicitMidpoint);
235        x_current = substep.step(&x_current.view(), t_current, flow)?;
236        t_current += d3 * self.dt;
237
238        // Step 7
239        let substep = Self::new(c4 * self.dt, VolumePreservingMethod::ExplicitMidpoint);
240        x_current = substep.step(&x_current.view(), t_current, flow)?;
241
242        Ok(x_current)
243    }
244
245    /// Integrate for multiple steps
246    pub fn integrate<F>(
247        &self,
248        x0: &ArrayView1<f64>,
249        t0: f64,
250        t_final: f64,
251        flow: &F,
252    ) -> IntegrateResult<Vec<(f64, Array1<f64>)>>
253    where
254        F: DivergenceFreeFlow,
255    {
256        let n_steps = ((t_final - t0) / self.dt).ceil() as usize;
257        let mut trajectory = vec![(t0, x0.to_owned())];
258
259        let mut x_current = x0.to_owned();
260        let mut t_current = t0;
261
262        for _ in 0..n_steps {
263            let dt_actual = (t_final - t_current).min(self.dt);
264
265            if dt_actual != self.dt {
266                // Last step with adjusted time step
267                let integrator = Self::new(dt_actual, self.method);
268                x_current = integrator.step(&x_current.view(), t_current, flow)?;
269            } else {
270                x_current = self.step(&x_current.view(), t_current, flow)?;
271            }
272
273            t_current += dt_actual;
274            trajectory.push((t_current, x_current.clone()));
275
276            if t_current >= t_final - 1e-10 {
277                break;
278            }
279        }
280
281        Ok(trajectory)
282    }
283}
284
285/// Incompressible flow examples
286pub struct IncompressibleFlow;
287
288impl IncompressibleFlow {
289    /// 2D circular flow
290    pub fn circular_2d(&self) -> CircularFlow2D {
291        CircularFlow2D { omega: 1.0 }
292    }
293
294    /// ABC flow (Arnold-Beltrami-Childress)
295    pub fn abc_flow(a: f64, b: f64, c: f64) -> ABCFlow {
296        ABCFlow { a, b, c }
297    }
298
299    /// Double gyre flow
300    pub fn double_gyre(a: f64, epsilon: f64, omega: f64) -> DoubleGyre {
301        DoubleGyre { a, epsilon, omega }
302    }
303}
304
305/// 2D circular flow (simple incompressible flow)
306pub struct CircularFlow2D {
307    omega: f64,
308}
309
310impl DivergenceFreeFlow for CircularFlow2D {
311    fn dim(&self) -> usize {
312        2
313    }
314
315    fn evaluate(&self, x: &ArrayView1<f64>, t: f64) -> Array1<f64> {
316        Array1::from_vec(vec![-self.omega * x[1], self.omega * x[0]])
317    }
318}
319
320/// ABC flow (3D incompressible flow)
321pub struct ABCFlow {
322    a: f64,
323    b: f64,
324    c: f64,
325}
326
327impl DivergenceFreeFlow for ABCFlow {
328    fn dim(&self) -> usize {
329        3
330    }
331
332    fn evaluate(&self, x: &ArrayView1<f64>, t: f64) -> Array1<f64> {
333        Array1::from_vec(vec![
334            self.a * x[1].sin() + self.c * x[2].cos(),
335            self.b * x[2].sin() + self.a * x[0].cos(),
336            self.c * x[0].sin() + self.b * x[1].cos(),
337        ])
338    }
339}
340
341/// Double gyre flow (time-dependent 2D flow)
342pub struct DoubleGyre {
343    a: f64,
344    epsilon: f64,
345    omega: f64,
346}
347
348impl DivergenceFreeFlow for DoubleGyre {
349    fn dim(&self) -> usize {
350        2
351    }
352
353    fn evaluate(&self, x: &ArrayView1<f64>, t: f64) -> Array1<f64> {
354        let a_t = self.epsilon * (self.omega * t).sin();
355        let b_t = 1.0 - 2.0 * self.epsilon * (self.omega * t).sin();
356
357        let f = a_t * x[0].powi(2) + b_t * x[0];
358        let df_dx = 2.0 * a_t * x[0] + b_t;
359
360        Array1::from_vec(vec![
361            -PI * self.a * (PI * f).sin() * (PI * x[1]).cos(),
362            PI * self.a * (PI * f).cos() * df_dx * (PI * x[1]).sin(),
363        ])
364    }
365}
366
367/// Stream function based flow representation
368pub trait StreamFunction {
369    /// Evaluate stream function at a point
370    fn psi(&self, x: f64, y: f64, t: f64) -> f64;
371
372    /// Compute velocity field from stream function
373    fn velocity(&self, x: f64, y: f64, t: f64) -> (f64, f64) {
374        let h = 1e-8;
375
376        // u = ∂ψ/∂y
377        let u = (self.psi(x, y + h, t) - self.psi(x, y - h, t)) / (2.0 * h);
378
379        // v = -∂ψ/∂x
380        let v = -(self.psi(x + h, y, t) - self.psi(x - h, y, t)) / (2.0 * h);
381
382        (u, v)
383    }
384}
385
386/// Stuart vortex flow
387pub struct StuartVortex {
388    /// Amplitude parameter
389    pub alpha: f64,
390    /// Wavenumber
391    pub k: f64,
392}
393
394impl StreamFunction for StuartVortex {
395    fn psi(&self, x: f64, y: f64, t: f64) -> f64 {
396        -self.alpha.ln() * y.cos() + self.alpha * (self.k * x).cos() * y.sin()
397    }
398}
399
400impl DivergenceFreeFlow for StuartVortex {
401    fn dim(&self) -> usize {
402        2
403    }
404
405    fn evaluate(&self, x: &ArrayView1<f64>, t: f64) -> Array1<f64> {
406        let (u, v) = self.velocity(x[0], x[1], t);
407        Array1::from_vec(vec![u, v])
408    }
409}
410
411/// Taylor-Green vortex
412pub struct TaylorGreenVortex {
413    /// Viscosity parameter
414    pub nu: f64,
415}
416
417impl StreamFunction for TaylorGreenVortex {
418    fn psi(&self, x: f64, y: f64, t: f64) -> f64 {
419        let decay = (-2.0 * self.nu * t).exp();
420        decay * x.sin() * y.sin()
421    }
422}
423
424impl DivergenceFreeFlow for TaylorGreenVortex {
425    fn dim(&self) -> usize {
426        2
427    }
428
429    fn evaluate(&self, x: &ArrayView1<f64>, t: f64) -> Array1<f64> {
430        let (u, v) = self.velocity(x[0], x[1], t);
431        Array1::from_vec(vec![u, v])
432    }
433}
434
435/// Generalized Hamiltonian system with volume preservation
436pub struct HamiltonianFlow<H>
437where
438    H: Fn(&ArrayView1<f64>) -> f64,
439{
440    /// Hamiltonian function
441    pub hamiltonian: H,
442    /// System dimension (must be even)
443    pub dim: usize,
444}
445
446impl<H> DivergenceFreeFlow for HamiltonianFlow<H>
447where
448    H: Fn(&ArrayView1<f64>) -> f64,
449{
450    fn dim(&self) -> usize {
451        self.dim
452    }
453
454    fn evaluate(&self, x: &ArrayView1<f64>, t: f64) -> Array1<f64> {
455        let n = self.dim / 2;
456        let h = 1e-8;
457        let mut dx = Array1::zeros(self.dim);
458
459        // Compute gradients
460        let mut grad_h = Array1::zeros(self.dim);
461        for i in 0..self.dim {
462            let mut x_plus = x.to_owned();
463            let mut x_minus = x.to_owned();
464            x_plus[i] += h;
465            x_minus[i] -= h;
466
467            grad_h[i] = ((self.hamiltonian)(&x_plus.view()) - (self.hamiltonian)(&x_minus.view()))
468                / (2.0 * h);
469        }
470
471        // Hamilton's equations: dq/dt = ∂H/∂p, dp/dt = -∂H/∂q
472        for i in 0..n {
473            dx[i] = grad_h[n + i]; // dq/dt = ∂H/∂p
474            dx[n + i] = -grad_h[i]; // dp/dt = -∂H/∂q
475        }
476
477        dx
478    }
479}
480
481/// Modified midpoint method with volume error correction
482pub struct ModifiedMidpointIntegrator {
483    /// Base integrator
484    base: VolumePreservingIntegrator,
485    /// Volume correction strength
486    correction_factor: f64,
487}
488
489impl ModifiedMidpointIntegrator {
490    /// Create a new modified midpoint integrator
491    pub fn new(_dt: f64, correctionfactor: f64) -> Self {
492        Self {
493            base: VolumePreservingIntegrator::new(_dt, VolumePreservingMethod::ImplicitMidpoint),
494            correction_factor: correctionfactor,
495        }
496    }
497
498    /// Step with volume correction
499    pub fn step_with_correction<F>(
500        &self,
501        x: &ArrayView1<f64>,
502        t: f64,
503        flow: &F,
504    ) -> IntegrateResult<Array1<f64>>
505    where
506        F: DivergenceFreeFlow,
507    {
508        // Take base step
509        let x_new = self.base.step(x, t, flow)?;
510
511        // Compute divergence at midpoint
512        let x_mid = (&x.to_owned() + &x_new) / 2.0;
513        let div = flow.verify_divergence_free(&x_mid.view(), t + self.base.dt / 2.0, 1e-8);
514
515        // Apply correction if needed
516        if div.abs() > 1e-10 {
517            let correction = -self.correction_factor * div * self.base.dt;
518            let n = x.len();
519            let corrected = &x_new * (1.0 + correction / n as f64);
520            Ok(corrected)
521        } else {
522            Ok(x_new)
523        }
524    }
525}
526
527/// Variational integrator for volume-preserving systems
528pub struct VariationalIntegrator {
529    /// Time step
530    dt: f64,
531    /// Number of quadrature points
532    n_quad: usize,
533}
534
535impl VariationalIntegrator {
536    /// Create a new variational integrator
537    pub fn new(dt: f64, nquad: usize) -> Self {
538        Self { dt, n_quad: nquad }
539    }
540
541    /// Discrete Lagrangian for volume-preserving flow
542    pub fn discrete_lagrangian<F>(
543        &self,
544        x0: &ArrayView1<f64>,
545        x1: &ArrayView1<f64>,
546        t: f64,
547        flow: &F,
548    ) -> IntegrateResult<f64>
549    where
550        F: DivergenceFreeFlow,
551    {
552        // Gauss-Legendre quadrature points
553        let (weights, nodes) = self.gauss_legendre_quadrature()?;
554
555        let mut l_d = 0.0;
556
557        for i in 0..self.n_quad {
558            let tau = nodes[i];
559            let x_tau = x0 * (1.0 - tau) + x1 * tau;
560            let t_tau = t + self.dt * tau;
561
562            let f = flow.evaluate(&x_tau.view(), t_tau);
563            let v = (x1 - x0) / self.dt;
564
565            // Lagrangian density
566            let l = 0.5 * v.dot(&v) - v.dot(&f);
567            l_d += weights[i] * l;
568        }
569
570        Ok(l_d * self.dt)
571    }
572
573    /// Gauss-Legendre quadrature on [0,1]
574    fn gauss_legendre_quadrature(&self) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
575        match self.n_quad {
576            1 => Ok((vec![1.0], vec![0.5])),
577            2 => Ok((
578                vec![0.5, 0.5],
579                vec![0.5 - 0.5 / 3.0_f64.sqrt(), 0.5 + 0.5 / 3.0_f64.sqrt()],
580            )),
581            3 => Ok((
582                vec![5.0 / 18.0, 8.0 / 18.0, 5.0 / 18.0],
583                vec![
584                    0.5 - 0.5 * (0.6_f64).sqrt(),
585                    0.5,
586                    0.5 + 0.5 * (0.6_f64).sqrt(),
587                ],
588            )),
589            _ => Err(IntegrateError::ValueError(format!(
590                "Quadrature order {} not implemented",
591                self.n_quad
592            ))),
593        }
594    }
595}
596
597/// Discrete gradient method for preserving multiple invariants
598pub struct DiscreteGradientIntegrator {
599    /// Time step
600    #[allow(dead_code)]
601    dt: f64,
602    /// Invariant functions
603    #[allow(dead_code)]
604    invariants: Vec<InvariantFn>,
605}
606
607impl DiscreteGradientIntegrator {
608    /// Create a new discrete gradient integrator
609    pub fn new(dt: f64) -> Self {
610        Self {
611            dt,
612            invariants: Vec::new(),
613        }
614    }
615
616    /// Add an invariant function to preserve
617    pub fn add_invariant<I>(&mut self, invariant: I) -> &mut Self
618    where
619        I: Fn(&ArrayView1<f64>) -> f64 + 'static,
620    {
621        self.invariants.push(Box::new(invariant));
622        self
623    }
624
625    /// Compute discrete gradient
626    pub fn discrete_gradient(
627        &self,
628        x0: &ArrayView1<f64>,
629        x1: &ArrayView1<f64>,
630        invariantidx: usize,
631    ) -> Array1<f64> {
632        let h = &self.invariants[invariantidx];
633        let h0 = h(x0);
634        let h1 = h(x1);
635
636        if (x1 - x0).mapv(|x| x.abs()).sum() < 1e-14 {
637            // If x0 ≈ x1, use standard gradient
638            self.gradient(x0, invariantidx)
639        } else {
640            // Average vector field
641            let g0 = self.gradient(x0, invariantidx);
642            let g1 = self.gradient(x1, invariantidx);
643            let g_avg = (&g0 + &g1) / 2.0;
644
645            // Correction term
646            let dx = x1 - x0;
647            let correction = (h1 - h0 - g_avg.dot(&dx)) / dx.dot(&dx) * &dx;
648
649            g_avg + correction
650        }
651    }
652
653    /// Standard gradient computation
654    fn gradient(&self, x: &ArrayView1<f64>, invariantidx: usize) -> Array1<f64> {
655        let h = &self.invariants[invariantidx];
656        let eps = 1e-8;
657        let n = x.len();
658        let mut grad = Array1::zeros(n);
659
660        for i in 0..n {
661            let mut x_plus = x.to_owned();
662            let mut x_minus = x.to_owned();
663            x_plus[i] += eps;
664            x_minus[i] -= eps;
665
666            grad[i] = (h(&x_plus.view()) - h(&x_minus.view())) / (2.0 * eps);
667        }
668
669        grad
670    }
671}
672
673/// Volume computation utilities
674pub struct VolumeChecker;
675
676impl VolumeChecker {
677    /// Check volume preservation for a set of points
678    pub fn check_volume_preservation<F>(
679        points: &Array2<f64>,
680        integrator: &VolumePreservingIntegrator,
681        flow: &F,
682        t0: f64,
683        t_final: f64,
684    ) -> IntegrateResult<f64>
685    where
686        F: DivergenceFreeFlow,
687    {
688        let npoints = points.nrows();
689        let dim = points.ncols();
690
691        // Initial volume (using convex hull approximation for simplicity)
692        let initial_volume = Self::estimate_volume(points)?;
693
694        // Evolve all points
695        let mut evolvedpoints = Array2::zeros((npoints, dim));
696        for i in 0..npoints {
697            let x0 = points.row(i);
698            let trajectory = integrator.integrate(&x0, t0, t_final, flow)?;
699            let (_, x_final) = trajectory.last().unwrap();
700            evolvedpoints.row_mut(i).assign(x_final);
701        }
702
703        // Final volume
704        let final_volume = Self::estimate_volume(&evolvedpoints)?;
705
706        // Return relative volume change
707        Ok((final_volume - initial_volume).abs() / initial_volume)
708    }
709
710    /// Estimate volume using bounding box (simplified)
711    fn estimate_volume(points: &Array2<f64>) -> IntegrateResult<f64> {
712        if points.nrows() == 0 {
713            return Ok(0.0);
714        }
715
716        let dim = points.ncols();
717        let mut min_coords = points.row(0).to_owned();
718        let mut max_coords = points.row(0).to_owned();
719
720        for i in 1..points.nrows() {
721            for j in 0..dim {
722                min_coords[j] = min_coords[j].min(points[[i, j]]);
723                max_coords[j] = max_coords[j].max(points[[i, j]]);
724            }
725        }
726
727        let mut volume = 1.0;
728        for j in 0..dim {
729            volume *= max_coords[j] - min_coords[j];
730        }
731
732        Ok(volume)
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739    use approx::assert_relative_eq;
740
741    #[test]
742    fn test_circular_flow_volume_preservation() {
743        let flow = CircularFlow2D { omega: 1.0 };
744        let integrator =
745            VolumePreservingIntegrator::new(0.1, VolumePreservingMethod::ExplicitMidpoint);
746
747        // Create a square of points
748        let mut points = Array2::zeros((4, 2));
749        points[[0, 0]] = 1.0;
750        points[[0, 1]] = 0.0;
751        points[[1, 0]] = 0.0;
752        points[[1, 1]] = 1.0;
753        points[[2, 0]] = -1.0;
754        points[[2, 1]] = 0.0;
755        points[[3, 0]] = 0.0;
756        points[[3, 1]] = -1.0;
757
758        let volume_change =
759            VolumeChecker::check_volume_preservation(&points, &integrator, &flow, 0.0, 2.0 * PI)
760                .unwrap();
761
762        assert!(
763            volume_change < 0.01,
764            "Volume not preserved: {volume_change}"
765        );
766    }
767
768    #[test]
769    fn test_divergence_free_verification() {
770        let flow = ABCFlow {
771            a: 1.0,
772            b: SQRT_2,
773            c: PI / 2.0,
774        };
775        let x = Array1::from_vec(vec![0.5, 0.5, 0.5]);
776
777        let div = flow.verify_divergence_free(&x.view(), 0.0, 1e-6);
778        assert!(div.abs() < 1e-8, "Flow not divergence-free: {div}");
779    }
780
781    #[test]
782    fn test_implicit_midpoint_convergence() {
783        let flow = CircularFlow2D { omega: 1.0 };
784        let dt = 0.1;
785
786        let integrator =
787            VolumePreservingIntegrator::new(dt, VolumePreservingMethod::ImplicitMidpoint);
788        let x0 = Array1::from_vec(vec![1.0, 0.0]);
789
790        let x1 = integrator.step(&x0.view(), 0.0, &flow).unwrap();
791
792        // After one step, should approximately be at (cos(dt), sin(dt))
793        assert_relative_eq!(x1[0], dt.cos(), epsilon = 1e-3);
794        assert_relative_eq!(x1[1], dt.sin(), epsilon = 1e-3);
795    }
796}