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 } => (2.0 * std::f64::consts::PI * freq as f64 * t).sin() as f32,
25        }
26    }
27}
28
29#[derive(Debug, Clone)]
30struct Source {
31    x: usize,
32    y: usize,
33    waveform: Waveform,
34    amplitude: f32,
35}
36
37/// Solveur acoustique 2D (FDTD, schéma saute-mouton sur grille décalée).
38///
39/// Disposition : `p[x,y]` au centre des cellules, `vx[x,y]` entre `p[x,y]` et
40/// `p[x+1,y]`, `vy[x,y]` entre `p[x,y]` et `p[x,y+1]`.
41/// Les frontières sont des murs rigides (vitesse normale nulle), sauf si
42/// [`set_absorbing_boundary`](Self::set_absorbing_boundary) est utilisé.
43pub struct AcousticSolver2D {
44    pub p: Grid2D,
45    pub vx: Grid2D,
46    pub vy: Grid2D,
47    rho: Grid2D,
48    inv_rho: Grid2D,
49    bulk_modulus: Grid2D,
50    /// Facteur multiplicatif appliqué à chaque pas (1.0 = pas d'amortissement).
51    damp: Grid2D,
52    // Nouveaux champs pour les PML
53    sigma_x: Grid2D,
54    sigma_y: Grid2D,
55
56    zero_row: Vec<f32>,
57    sources: Vec<Source>,
58    receivers: Vec<(usize, usize)>,
59    traces: Vec<Vec<f32>>,
60    time: f64,
61}
62
63impl AcousticSolver2D {
64    pub fn new(nx: usize, ny: usize, dx: f32, dy: f32, default_rho: f32, default_c: f32) -> Self {
65        assert!(nx >= 2 && ny >= 2, "la grille doit faire au moins 2x2");
66        assert!(
67            default_rho > 0.0 && default_c > 0.0,
68            "rho et c doivent être > 0"
69        );
70
71        let mut rho = Grid2D::new(nx, ny, dx, dy);
72        rho.fill(default_rho);
73        let mut inv_rho = Grid2D::new(nx, ny, dx, dy);
74        inv_rho.fill(1.0 / default_rho);
75        let mut bulk_modulus = Grid2D::new(nx, ny, dx, dy);
76        bulk_modulus.fill(default_rho * default_c * default_c);
77        let mut damp = Grid2D::new(nx, ny, dx, dy);
78        damp.fill(1.0);
79
80        // Initialisation des grilles PML à zéro par défaut
81        let sigma_x = Grid2D::new(nx, ny, dx, dy);
82        let sigma_y = Grid2D::new(nx, ny, dx, dy);
83
84        Self {
85            p: Grid2D::new(nx, ny, dx, dy),
86            vx: Grid2D::new(nx, ny, dx, dy),
87            vy: Grid2D::new(nx, ny, dx, dy),
88            rho,
89            inv_rho,
90            bulk_modulus,
91            damp,
92            sigma_x, // Ajouté ici
93            sigma_y, // Ajouté ici
94            zero_row: vec![0.0; nx],
95            sources: Vec::new(),
96            receivers: Vec::new(),
97            traces: Vec::new(),
98            time: 0.0,
99        }
100    }
101
102    // ---------------------------------------------------------------- milieu
103
104    /// Densité (lecture seule). Utiliser [`set_medium`](Self::set_medium) pour modifier.
105    pub fn rho(&self) -> &Grid2D {
106        &self.rho
107    }
108
109    /// Configure des couches absorbantes PML sur les bords.
110    /// `width` : épaisseur en nombre de cellules (ex: 20 à 30).
111    /// `max_sigma` : intensité maximale de l'amortissement.
112    pub fn set_pml_boundary(&mut self, width: usize, max_sigma: f32) {
113        let (nx, ny) = (self.p.nx, self.p.ny);
114
115        for y in 0..ny {
116            for x in 0..nx {
117                // Distance par rapport aux bords pour l'axe X
118                let dist_x_left = if x < width { width - x } else { 0 };
119                let dist_x_right = if x >= nx - width {
120                    x - (nx - 1 - width)
121                } else {
122                    0
123                };
124                let dist_x = dist_x_left.max(dist_x_right) as f32;
125
126                if width > 0 && dist_x > 0.0 {
127                    let t = dist_x / width as f32;
128                    let val = max_sigma * t * t; // Profil quadratique
129                    self.sigma_x.set(x, y, val);
130                } else {
131                    self.sigma_x.set(x, y, 0.0);
132                }
133
134                // Distance par rapport aux bords pour l'axe Y
135                let dist_y_top = if y < width { width - y } else { 0 };
136                let dist_y_bottom = if y >= ny - width {
137                    y - (ny - 1 - width)
138                } else {
139                    0
140                };
141                let dist_y = dist_y_top.max(dist_y_bottom) as f32;
142
143                if width > 0 && dist_y > 0.0 {
144                    let t = dist_y / width as f32;
145                    let val = max_sigma * t * t;
146                    self.sigma_y.set(x, y, val);
147                } else {
148                    self.sigma_y.set(x, y, 0.0);
149                }
150            }
151        }
152    }
153
154    /// Module de compressibilité K = ρc² (lecture seule).
155    pub fn bulk_modulus(&self) -> &Grid2D {
156        &self.bulk_modulus
157    }
158
159    /// Définit le milieu d'une cellule : densité `rho` et vitesse du son `c`.
160    pub fn set_medium(&mut self, x: usize, y: usize, rho: f32, c: f32) {
161        assert!(rho > 0.0 && c > 0.0, "rho et c doivent être > 0");
162        self.rho.set(x, y, rho);
163        self.inv_rho.set(x, y, 1.0 / rho);
164        self.bulk_modulus.set(x, y, rho * c * c);
165    }
166
167    /// Définit le milieu d'un rectangle `[x0, x1) × [y0, y1)`.
168    pub fn set_medium_rect(
169        &mut self,
170        x0: usize,
171        y0: usize,
172        x1: usize,
173        y1: usize,
174        rho: f32,
175        c: f32,
176    ) {
177        for y in y0..y1.min(self.p.ny) {
178            for x in x0..x1.min(self.p.nx) {
179                self.set_medium(x, y, rho, c);
180            }
181        }
182    }
183
184    /// Vitesse du son maximale dans le domaine.
185    pub fn c_max(&self) -> f32 {
186        self.bulk_modulus
187            .as_slice()
188            .iter()
189            .zip(self.inv_rho.as_slice())
190            .map(|(k, ir)| (k * ir).sqrt())
191            .fold(0.0, f32::max)
192    }
193
194    /// Pas de temps maximal stable (condition CFL 2D) pour le milieu actuel.
195    pub fn max_stable_dt(&self) -> f32 {
196        let inv = (1.0 / (self.p.dx * self.p.dx) + 1.0 / (self.p.dy * self.p.dy)).sqrt();
197        1.0 / (self.c_max() * inv)
198    }
199
200    /// `safety × max_stable_dt()`, avec `0 < safety <= 1` (0.9 est un bon choix).
201    pub fn stable_dt(&self, safety: f32) -> f32 {
202        safety * self.max_stable_dt()
203    }
204
205    // ------------------------------------------------------ bords absorbants
206
207    /// Couche éponge de `width` cellules sur les 4 bords.
208    ///
209    /// `max_damping` (entre 0 et 1) est la fraction d'amplitude retirée **à chaque
210    /// pas** sur le bord extérieur ; le profil décroît de façon quadratique vers
211    /// l'intérieur. Valeurs typiques : `width` 15–30, `max_damping` 0.05–0.2.
212    /// `width = 0` désactive l'amortissement (murs rigides).
213    pub fn set_absorbing_boundary(&mut self, width: usize, max_damping: f32) {
214        assert!(
215            (0.0..1.0).contains(&max_damping),
216            "max_damping doit être dans [0, 1)"
217        );
218        let (nx, ny) = (self.p.nx, self.p.ny);
219        for y in 0..ny {
220            for x in 0..nx {
221                let d = x.min(nx - 1 - x).min(y).min(ny - 1 - y);
222                let f = if width > 0 && d < width {
223                    let t = (width - d) as f32 / width as f32;
224                    1.0 - max_damping * t * t
225                } else {
226                    1.0
227                };
228                self.damp.set(x, y, f);
229            }
230        }
231    }
232
233    // -------------------------------------------------- sources, récepteurs
234
235    /// Ajoute une source ponctuelle pilotée par une forme d'onde.
236    pub fn add_source(&mut self, x: usize, y: usize, waveform: Waveform, amplitude: f32) {
237        assert!(x < self.p.nx && y < self.p.ny, "source hors grille");
238        self.sources.push(Source {
239            x,
240            y,
241            waveform,
242            amplitude,
243        });
244    }
245
246    /// Injecte instantanément une impulsion de pression.
247    pub fn inject_source(&mut self, x: usize, y: usize, value: f32) {
248        let current = self.p.get(x, y);
249        self.p.set(x, y, current + value);
250    }
251
252    /// Ajoute un récepteur ; retourne son identifiant.
253    pub fn add_receiver(&mut self, x: usize, y: usize) -> usize {
254        assert!(x < self.p.nx && y < self.p.ny, "récepteur hors grille");
255        self.receivers.push((x, y));
256        self.traces.push(Vec::new());
257        self.receivers.len() - 1
258    }
259
260    /// Signal de pression enregistré (un échantillon par pas de temps).
261    pub fn receiver(&self, id: usize) -> &[f32] {
262        &self.traces[id]
263    }
264
265    /// Temps simulé écoulé (secondes).
266    pub fn time(&self) -> f64 {
267        self.time
268    }
269
270    // ------------------------------------------------------------ simulation
271
272    /// Effectue un pas de simulation temporel `dt`.
273    pub fn step(&mut self, dt: f32) {
274        let nx = self.p.nx;
275        let ny = self.p.ny;
276        let inv_dx = 1.0 / self.p.dx;
277        let inv_dy = 1.0 / self.p.dy;
278
279        // 0. Sources
280        for s in &self.sources {
281            let idx = self.p.index(s.x, s.y);
282            self.p.as_mut_slice()[idx] += s.amplitude * s.waveform.eval(self.time);
283        }
284
285        // 1. Vitesses à partir du gradient de pression (avec PML)
286        {
287            let p = self.p.as_slice();
288            let inv_rho = self.inv_rho.as_slice();
289            let sig_x = self.sigma_x.as_slice();
290            let sig_y = self.sigma_y.as_slice();
291
292            let vel = |y: usize, vxr: &mut [f32], vyr: &mut [f32]| {
293                let o = y * nx;
294                let prow = &p[o..o + nx];
295                let irow = &inv_rho[o..o + nx];
296                let sx_row = &sig_x[o..o + nx];
297                let vxr = &mut vxr[..nx];
298
299                for i in 0..nx - 1 {
300                    let damping_x = 1.0 / (1.0 + 0.5 * dt * sx_row[i]);
301                    let dp_dx = (prow[i + 1] - prow[i]) * inv_dx;
302                    vxr[i] = (vxr[i] - dt * irow[i] * dp_dx) * damping_x;
303                }
304
305                if y + 1 < ny {
306                    let pn = &p[o + nx..o + 2 * nx];
307                    let sy_next = &sig_y[o + nx..o + 2 * nx];
308                    let vyr = &mut vyr[..nx];
309
310                    for i in 0..nx {
311                        let damping_y = 1.0 / (1.0 + 0.5 * dt * sy_next[i]);
312                        let dp_dy = (pn[i] - prow[i]) * inv_dy;
313                        vyr[i] = (vyr[i] - dt * irow[i] * dp_dy) * damping_y;
314                    }
315                }
316            };
317            for_each_row2(self.vx.as_mut_slice(), self.vy.as_mut_slice(), nx, vel);
318        }
319
320        // 2. Pression à partir de la divergence des vitesses (avec PML)
321        {
322            let vx = self.vx.as_slice();
323            let vy = self.vy.as_slice();
324            let bulk = self.bulk_modulus.as_slice();
325            let sig_x = self.sigma_x.as_slice();
326            let sig_y = self.sigma_y.as_slice();
327            let zeros = &self.zero_row[..];
328
329            let prs = |y: usize, prow: &mut [f32]| {
330                let o = y * nx;
331                let vxr = &vx[o..o + nx];
332                let vyr = &vy[o..o + nx];
333                let vyp: &[f32] = if y > 0 { &vy[o - nx..o] } else { zeros };
334                let kr = &bulk[o..o + nx];
335                let sx_row = &sig_x[o..o + nx];
336                let sy_row = &sig_y[o..o + nx];
337                let prow = &mut prow[..nx];
338
339                // i = 0 : vx[-1] = 0 (mur)
340                let div0 = vxr[0] * inv_dx + (vyr[0] - vyp[0]) * inv_dy;
341                let damping_0 = 1.0 / (1.0 + 0.5 * dt * (sx_row[0] + sy_row[0]));
342                prow[0] = (prow[0] - dt * kr[0] * div0) * damping_0;
343
344                for i in 1..nx {
345                    let div = (vxr[i] - vxr[i - 1]) * inv_dx + (vyr[i] - vyp[i]) * inv_dy;
346                    let damping = 1.0 / (1.0 + 0.5 * dt * (sx_row[i] + sy_row[i]));
347                    prow[i] = (prow[i] - dt * kr[i] * div) * damping;
348                }
349            };
350            for_each_row1(self.p.as_mut_slice(), nx, prs);
351        }
352
353        // 3. Enregistrement et avance du temps
354        for (&(x, y), trace) in self.receivers.iter().zip(self.traces.iter_mut()) {
355            trace.push(self.p.get(x, y));
356        }
357        self.time += dt as f64;
358    }
359}
360
361// ---- Parcours des lignes : parallèle avec la feature `rayon`, séquentiel sinon.
362
363#[cfg(feature = "rayon")]
364fn for_each_row1<F>(a: &mut [f32], nx: usize, f: F)
365where
366    F: Fn(usize, &mut [f32]) + Sync + Send,
367{
368    use rayon::prelude::*;
369    a.par_chunks_mut(nx).enumerate().for_each(|(y, r)| f(y, r));
370}
371
372#[cfg(not(feature = "rayon"))]
373fn for_each_row1<F>(a: &mut [f32], nx: usize, f: F)
374where
375    F: Fn(usize, &mut [f32]) + Sync + Send,
376{
377    for (y, r) in a.chunks_mut(nx).enumerate() {
378        f(y, r);
379    }
380}
381
382#[cfg(feature = "rayon")]
383fn for_each_row2<F>(a: &mut [f32], b: &mut [f32], nx: usize, f: F)
384where
385    F: Fn(usize, &mut [f32], &mut [f32]) + Sync + Send,
386{
387    use rayon::prelude::*;
388    a.par_chunks_mut(nx)
389        .zip(b.par_chunks_mut(nx))
390        .enumerate()
391        .for_each(|(y, (ra, rb))| f(y, ra, rb));
392}
393
394#[cfg(not(feature = "rayon"))]
395fn for_each_row2<F>(a: &mut [f32], b: &mut [f32], nx: usize, f: F)
396where
397    F: Fn(usize, &mut [f32], &mut [f32]) + Sync + Send,
398{
399    for (y, (ra, rb)) in a.chunks_mut(nx).zip(b.chunks_mut(nx)).enumerate() {
400        f(y, ra, rb);
401    }
402}