Skip to main content

acoustic_engine/
solver.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2
3use crate::grid::Grid2D;
4
5/// Forme d'onde d'une source ponctuelle.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum Waveform {
8    /// Ondelette de Ricker (dérivée seconde de gaussienne), centrée à `t = 1/freq`.
9    /// Sans composante continue : idéale pour la simulation.
10    Ricker { freq: f32 },
11    /// Sinusoïde continue.
12    Sine { freq: f32 },
13}
14
15impl Waveform {
16    /// Valeur du signal au temps `t` (secondes).
17    pub fn eval(&self, t: f64) -> f32 {
18        match *self {
19            Waveform::Ricker { freq } => {
20                let f = freq as f64;
21                let a = (std::f64::consts::PI * f * (t - 1.0 / f)).powi(2);
22                ((1.0 - 2.0 * a) * (-a).exp()) as f32
23            }
24            Waveform::Sine { freq } => {
25                (2.0 * std::f64::consts::PI * freq as f64 * t).sin() as f32
26            }
27        }
28    }
29}
30
31#[derive(Debug, Clone)]
32struct Source {
33    x: usize,
34    y: usize,
35    waveform: Waveform,
36    amplitude: f32,
37}
38
39/// Solveur acoustique 2D (FDTD, schéma saute-mouton sur grille décalée).
40///
41/// Disposition : `p[x,y]` au centre des cellules, `vx[x,y]` entre `p[x,y]` et
42/// `p[x+1,y]`, `vy[x,y]` entre `p[x,y]` et `p[x,y+1]`.
43/// Les frontières sont des murs rigides (vitesse normale nulle), sauf si
44/// [`set_absorbing_boundary`](Self::set_absorbing_boundary) est utilisé.
45pub struct AcousticSolver2D {
46    pub p: Grid2D,
47    pub vx: Grid2D,
48    pub vy: Grid2D,
49    rho: Grid2D,
50    inv_rho: Grid2D,
51    bulk_modulus: Grid2D,
52    /// Facteur multiplicatif appliqué à chaque pas (1.0 = pas d'amortissement).
53    damp: Grid2D,
54    zero_row: Vec<f32>,
55    sources: Vec<Source>,
56    receivers: Vec<(usize, usize)>,
57    traces: Vec<Vec<f32>>,
58    time: f64,
59}
60
61impl AcousticSolver2D {
62    pub fn new(nx: usize, ny: usize, dx: f32, dy: f32, default_rho: f32, default_c: f32) -> Self {
63        assert!(nx >= 2 && ny >= 2, "la grille doit faire au moins 2x2");
64        assert!(default_rho > 0.0 && default_c > 0.0, "rho et c doivent être > 0");
65
66        let mut rho = Grid2D::new(nx, ny, dx, dy);
67        rho.fill(default_rho);
68        let mut inv_rho = Grid2D::new(nx, ny, dx, dy);
69        inv_rho.fill(1.0 / default_rho);
70        let mut bulk_modulus = Grid2D::new(nx, ny, dx, dy);
71        bulk_modulus.fill(default_rho * default_c * default_c);
72        let mut damp = Grid2D::new(nx, ny, dx, dy);
73        damp.fill(1.0);
74
75        Self {
76            p: Grid2D::new(nx, ny, dx, dy),
77            vx: Grid2D::new(nx, ny, dx, dy),
78            vy: Grid2D::new(nx, ny, dx, dy),
79            rho,
80            inv_rho,
81            bulk_modulus,
82            damp,
83            zero_row: vec![0.0; nx],
84            sources: Vec::new(),
85            receivers: Vec::new(),
86            traces: Vec::new(),
87            time: 0.0,
88        }
89    }
90
91    // ---------------------------------------------------------------- milieu
92
93    /// Densité (lecture seule). Utiliser [`set_medium`](Self::set_medium) pour modifier.
94    pub fn rho(&self) -> &Grid2D {
95        &self.rho
96    }
97
98    /// Module de compressibilité K = ρc² (lecture seule).
99    pub fn bulk_modulus(&self) -> &Grid2D {
100        &self.bulk_modulus
101    }
102
103    /// Définit le milieu d'une cellule : densité `rho` et vitesse du son `c`.
104    pub fn set_medium(&mut self, x: usize, y: usize, rho: f32, c: f32) {
105        assert!(rho > 0.0 && c > 0.0, "rho et c doivent être > 0");
106        self.rho.set(x, y, rho);
107        self.inv_rho.set(x, y, 1.0 / rho);
108        self.bulk_modulus.set(x, y, rho * c * c);
109    }
110
111    /// Définit le milieu d'un rectangle `[x0, x1) × [y0, y1)`.
112    pub fn set_medium_rect(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, rho: f32, c: f32) {
113        for y in y0..y1.min(self.p.ny) {
114            for x in x0..x1.min(self.p.nx) {
115                self.set_medium(x, y, rho, c);
116            }
117        }
118    }
119
120    /// Vitesse du son maximale dans le domaine.
121    pub fn c_max(&self) -> f32 {
122        self.bulk_modulus
123            .as_slice()
124            .iter()
125            .zip(self.inv_rho.as_slice())
126            .map(|(k, ir)| (k * ir).sqrt())
127            .fold(0.0, f32::max)
128    }
129
130    /// Pas de temps maximal stable (condition CFL 2D) pour le milieu actuel.
131    pub fn max_stable_dt(&self) -> f32 {
132        let inv = (1.0 / (self.p.dx * self.p.dx) + 1.0 / (self.p.dy * self.p.dy)).sqrt();
133        1.0 / (self.c_max() * inv)
134    }
135
136    /// `safety × max_stable_dt()`, avec `0 < safety <= 1` (0.9 est un bon choix).
137    pub fn stable_dt(&self, safety: f32) -> f32 {
138        safety * self.max_stable_dt()
139    }
140
141    // ------------------------------------------------------ bords absorbants
142
143    /// Couche éponge de `width` cellules sur les 4 bords.
144    ///
145    /// `max_damping` (entre 0 et 1) est la fraction d'amplitude retirée **à chaque
146    /// pas** sur le bord extérieur ; le profil décroît de façon quadratique vers
147    /// l'intérieur. Valeurs typiques : `width` 15–30, `max_damping` 0.05–0.2.
148    /// `width = 0` désactive l'amortissement (murs rigides).
149    pub fn set_absorbing_boundary(&mut self, width: usize, max_damping: f32) {
150        assert!((0.0..1.0).contains(&max_damping), "max_damping doit être dans [0, 1)");
151        let (nx, ny) = (self.p.nx, self.p.ny);
152        for y in 0..ny {
153            for x in 0..nx {
154                let d = x.min(nx - 1 - x).min(y).min(ny - 1 - y);
155                let f = if width > 0 && d < width {
156                    let t = (width - d) as f32 / width as f32;
157                    1.0 - max_damping * t * t
158                } else {
159                    1.0
160                };
161                self.damp.set(x, y, f);
162            }
163        }
164    }
165
166    // -------------------------------------------------- sources, récepteurs
167
168    /// Ajoute une source ponctuelle pilotée par une forme d'onde.
169    pub fn add_source(&mut self, x: usize, y: usize, waveform: Waveform, amplitude: f32) {
170        assert!(x < self.p.nx && y < self.p.ny, "source hors grille");
171        self.sources.push(Source { x, y, waveform, amplitude });
172    }
173
174    /// Injecte instantanément une impulsion de pression.
175    pub fn inject_source(&mut self, x: usize, y: usize, value: f32) {
176        let current = self.p.get(x, y);
177        self.p.set(x, y, current + value);
178    }
179
180    /// Ajoute un récepteur ; retourne son identifiant.
181    pub fn add_receiver(&mut self, x: usize, y: usize) -> usize {
182        assert!(x < self.p.nx && y < self.p.ny, "récepteur hors grille");
183        self.receivers.push((x, y));
184        self.traces.push(Vec::new());
185        self.receivers.len() - 1
186    }
187
188    /// Signal de pression enregistré (un échantillon par pas de temps).
189    pub fn receiver(&self, id: usize) -> &[f32] {
190        &self.traces[id]
191    }
192
193    /// Temps simulé écoulé (secondes).
194    pub fn time(&self) -> f64 {
195        self.time
196    }
197
198    // ------------------------------------------------------------ simulation
199
200    /// Effectue un pas de simulation temporel `dt`.
201    pub fn step(&mut self, dt: f32) {
202        let nx = self.p.nx;
203        let ny = self.p.ny;
204        let cx = dt / self.p.dx;
205        let cy = dt / self.p.dy;
206        let inv_dx = 1.0 / self.p.dx;
207        let inv_dy = 1.0 / self.p.dy;
208
209        // 0. Sources
210        for s in &self.sources {
211            let idx = self.p.index(s.x, s.y);
212            self.p.as_mut_slice()[idx] += s.amplitude * s.waveform.eval(self.time);
213        }
214
215        // 1. Vitesses à partir du gradient de pression
216        {
217            let p = self.p.as_slice();
218            let inv_rho = self.inv_rho.as_slice();
219            let damp = self.damp.as_slice();
220
221            let vel = |y: usize, vxr: &mut [f32], vyr: &mut [f32]| {
222                let o = y * nx;
223                let prow = &p[o..o + nx];
224                let irow = &inv_rho[o..o + nx];
225                let drow = &damp[o..o + nx];
226                let vxr = &mut vxr[..nx];
227                for i in 0..nx - 1 {
228                    vxr[i] = (vxr[i] - cx * irow[i] * (prow[i + 1] - prow[i])) * drow[i];
229                }
230                if y + 1 < ny {
231                    let pn = &p[o + nx..o + 2 * nx];
232                    let vyr = &mut vyr[..nx];
233                    for i in 0..nx {
234                        vyr[i] = (vyr[i] - cy * irow[i] * (pn[i] - prow[i])) * drow[i];
235                    }
236                }
237            };
238            for_each_row2(self.vx.as_mut_slice(), self.vy.as_mut_slice(), nx, vel);
239        }
240
241        // 2. Pression à partir de la divergence des vitesses
242        {
243            let vx = self.vx.as_slice();
244            let vy = self.vy.as_slice();
245            let bulk = self.bulk_modulus.as_slice();
246            let damp = self.damp.as_slice();
247            let zeros = &self.zero_row[..];
248
249            let prs = |y: usize, prow: &mut [f32]| {
250                let o = y * nx;
251                let vxr = &vx[o..o + nx];
252                let vyr = &vy[o..o + nx];
253                let vyp: &[f32] = if y > 0 { &vy[o - nx..o] } else { zeros };
254                let kr = &bulk[o..o + nx];
255                let drow = &damp[o..o + nx];
256                let prow = &mut prow[..nx];
257
258                // i = 0 : vx[-1] = 0 (mur)
259                let div0 = vxr[0] * inv_dx + (vyr[0] - vyp[0]) * inv_dy;
260                prow[0] = (prow[0] - dt * kr[0] * div0) * drow[0];
261                for i in 1..nx {
262                    let div = (vxr[i] - vxr[i - 1]) * inv_dx + (vyr[i] - vyp[i]) * inv_dy;
263                    prow[i] = (prow[i] - dt * kr[i] * div) * drow[i];
264                }
265            };
266            for_each_row1(self.p.as_mut_slice(), nx, prs);
267        }
268
269        // 3. Enregistrement et avance du temps
270        for (&(x, y), trace) in self.receivers.iter().zip(self.traces.iter_mut()) {
271            trace.push(self.p.get(x, y));
272        }
273        self.time += dt as f64;
274    }
275}
276
277// ---- Parcours des lignes : parallèle avec la feature `rayon`, séquentiel sinon.
278
279#[cfg(feature = "rayon")]
280fn for_each_row1<F>(a: &mut [f32], nx: usize, f: F)
281where
282    F: Fn(usize, &mut [f32]) + Sync + Send,
283{
284    use rayon::prelude::*;
285    a.par_chunks_mut(nx).enumerate().for_each(|(y, r)| f(y, r));
286}
287
288#[cfg(not(feature = "rayon"))]
289fn for_each_row1<F>(a: &mut [f32], nx: usize, f: F)
290where
291    F: Fn(usize, &mut [f32]) + Sync + Send,
292{
293    for (y, r) in a.chunks_mut(nx).enumerate() {
294        f(y, r);
295    }
296}
297
298#[cfg(feature = "rayon")]
299fn for_each_row2<F>(a: &mut [f32], b: &mut [f32], nx: usize, f: F)
300where
301    F: Fn(usize, &mut [f32], &mut [f32]) + Sync + Send,
302{
303    use rayon::prelude::*;
304    a.par_chunks_mut(nx)
305        .zip(b.par_chunks_mut(nx))
306        .enumerate()
307        .for_each(|(y, (ra, rb))| f(y, ra, rb));
308}
309
310#[cfg(not(feature = "rayon"))]
311fn for_each_row2<F>(a: &mut [f32], b: &mut [f32], nx: usize, f: F)
312where
313    F: Fn(usize, &mut [f32], &mut [f32]) + Sync + Send,
314{
315    for (y, (ra, rb)) in a.chunks_mut(nx).zip(b.chunks_mut(nx)).enumerate() {
316        f(y, ra, rb);
317    }
318}