Skip to main content

sva_samples/physics/
rhaouti_chaigne_joly.rs

1// Concern: one hammer/membrane call site's finite-difference state | Non-concern: argument evaluation, builtin dispatch | IO: (params, sr) -> a site; () -> a sample
2
3//! Rhaouti, Chaigne & Joly, JASA 105(6) (1999) 3545-3562, doi 10.1121/1.424679. eq. 3
4//! `F = K[(delta - u + W)+]^alpha` is `hammer.rs`; eq. 4-6's `W` windows a contact patch.
5
6use crate::physics::Solver;
7
8use crate::physics::bound::Bound::*;
9use crate::physics::bound::all;
10use crate::physics::hammer::Hammer;
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct RhaoutiChaigneJolyParams {
14    pub f0: f64,
15    pub aspect_ratio: f64,
16    pub strike_x: f64,
17    pub strike_y: f64,
18    pub vel: f64,
19    pub hammer_mass: f64,
20    pub hammer_k: f64,
21    pub hammer_p: f64,
22    pub damp_dc: f64,
23    pub damp_freq: f64,
24}
25
26impl RhaoutiChaigneJolyParams {
27    /// The strike sits off-centre so no low-order mode is silenced.
28    pub fn at(f0: f64) -> RhaoutiChaigneJolyParams {
29        RhaoutiChaigneJolyParams {
30            f0,
31            aspect_ratio: 1.0,
32            strike_x: 0.3,
33            strike_y: 0.4,
34            vel: 3.2,
35            hammer_mass: 2.9e-3,
36            hammer_k: 2.6646e8,
37            hammer_p: 2.5,
38            damp_dc: 0.6,
39            damp_freq: 1.6e-4,
40        }
41    }
42}
43
44impl RhaoutiChaigneJolyParams {
45    pub fn valid(&self) -> bool {
46        all(&[
47            (self.f0, Positive),
48            (self.aspect_ratio, Positive),
49            (self.strike_x, OpenUnit),
50            (self.strike_y, OpenUnit),
51            (self.vel, Positive),
52            (self.hammer_mass, Positive),
53            (self.hammer_k, Positive),
54            (self.hammer_p, Positive),
55            (self.damp_dc, NonNegative),
56            (self.damp_freq, NonNegative),
57        ])
58    }
59}
60
61/// A small tense drumhead's tension, tuned alongside [`MEMBRANE_AREAL_DENSITY_KG_M2`].
62const MEMBRANE_TENSION_N_M: f64 = 3000.0;
63/// Gives `c = sqrt(T/sigma) = 100 m/s`; order of magnitude only.
64const MEMBRANE_AREAL_DENSITY_KG_M2: f64 = 0.3;
65/// Row-major `iy * (nx + 1) + ix`; the edges are the clamped boundary.
66struct MembraneGrid {
67    u_now: Vec<f64>,
68    u_prev: Vec<f64>,
69    u_next: Vec<f64>,
70    nx: usize,
71    ny: usize,
72    h: f64,
73    sigma: f64,
74    /// `lambda = c*dt/h`; Bilbao eq. 11.13 bounds 2D at `1/sqrt(2)`, not 1D's `1`.
75    courant_sq: f64,
76    damp_a: f64,
77    damp_b: f64,
78}
79
80#[inline]
81fn node_index(nx: usize, ix: usize, iy: usize) -> usize {
82    iy * (nx + 1) + ix
83}
84
85/// RCJ Table I's window, `g(x,y) = exp[-C((x-x0)^4+(y-y0)^4)]`, `C` in `1/m^4`.
86const MALLET_WINDOW_QUARTIC_COEFF_M4: f64 = 1.0e7;
87/// Below this fraction of the window's peak, a node leaves the patch.
88const MALLET_WINDOW_WEIGHT_FLOOR: f64 = 1e-8;
89
90/// Weights (sum to 1, RCJ eq. 4) shared by the read average and force spread.
91struct ContactPatch {
92    nodes: Vec<usize>,
93    weights: Vec<f64>,
94}
95
96fn build_contact_patch(
97    nx: usize,
98    ny: usize,
99    contact_ix: usize,
100    contact_iy: usize,
101    h: f64,
102) -> ContactPatch {
103    let cutoff_m =
104        (MALLET_WINDOW_WEIGHT_FLOOR.ln().abs() / MALLET_WINDOW_QUARTIC_COEFF_M4).powf(0.25);
105    let radius_cells = (cutoff_m / h).ceil() as isize;
106
107    let mut nodes = Vec::new();
108    let mut weights = Vec::new();
109    let mut total = 0.0;
110    for dj in -radius_cells..=radius_cells {
111        for di in -radius_cells..=radius_cells {
112            let (ix, iy) = (contact_ix as isize + di, contact_iy as isize + dj);
113            if ix < 1 || ix > nx as isize - 1 || iy < 1 || iy > ny as isize - 1 {
114                continue;
115            }
116            let (dx, dy) = (di as f64 * h, dj as f64 * h);
117            let w = (-MALLET_WINDOW_QUARTIC_COEFF_M4 * (dx.powi(4) + dy.powi(4))).exp();
118            if w < MALLET_WINDOW_WEIGHT_FLOOR {
119                continue;
120            }
121            nodes.push(node_index(nx, ix as usize, iy as usize));
122            weights.push(w);
123            total += w;
124        }
125    }
126    for w in &mut weights {
127        *w /= total;
128    }
129    ContactPatch { nodes, weights }
130}
131
132/// The CFL bound at a 90% margin.
133const LAMBDA_TARGET: f64 = 0.9 * std::f64::consts::FRAC_1_SQRT_2;
134
135/// Nodes, not hertz: the rate decides as much as `f0`. Three `f64` planes, so ~100 MB.
136pub const NODE_COUNT_CEILING: usize = 4_000_000;
137
138/// Grows as `1/f0^2`.
139pub fn node_count(params: &RhaoutiChaigneJolyParams, sr: f64) -> f64 {
140    let (lx, ly) = sides(params);
141    let h = step(MEMBRANE_TENSION_N_M, MEMBRANE_AREAL_DENSITY_KG_M2, sr);
142    (axis_nodes(lx, h) + 1.0) * (axis_nodes(ly, h) + 1.0)
143}
144
145fn step(tension: f64, sigma: f64, sr: f64) -> f64 {
146    (tension / sigma).sqrt() * (1.0 / sr) / LAMBDA_TARGET
147}
148
149fn axis_nodes(span: f64, h: f64) -> f64 {
150    (span / h).round().max(4.0)
151}
152
153fn membrane_grid(
154    sigma: f64,
155    tension: f64,
156    lx: f64,
157    ly: f64,
158    damp_dc: f64,
159    damp_freq: f64,
160    sr: f64,
161) -> MembraneGrid {
162    let dt = 1.0 / sr;
163    let h = step(tension, sigma, sr);
164    let nx = axis_nodes(lx, h) as usize;
165    let ny = axis_nodes(ly, h) as usize;
166    let courant_sq = LAMBDA_TARGET * LAMBDA_TARGET;
167
168    let count = (nx + 1) * (ny + 1);
169    MembraneGrid {
170        u_now: vec![0.0; count],
171        u_prev: vec![0.0; count],
172        u_next: vec![0.0; count],
173        nx,
174        ny,
175        h,
176        sigma,
177        courant_sq,
178        damp_a: 2.0 * damp_dc * dt,
179        damp_b: 2.0 * damp_freq * dt / (h * h),
180    }
181}
182
183/// Bilbao eq. 11.8: `L0 = c/(f0*sqrt(2))`, split as `L0*sqrt(ar)` by `L0/sqrt(ar)` so the
184/// area holds at any `ar` (his Sec. 10.1 convention).
185fn sides(params: &RhaoutiChaigneJolyParams) -> (f64, f64) {
186    let c = (MEMBRANE_TENSION_N_M / MEMBRANE_AREAL_DENSITY_KG_M2).sqrt();
187    let l0 = c / (params.f0 * std::f64::consts::SQRT_2);
188    (
189        l0 * params.aspect_ratio.sqrt(),
190        l0 / params.aspect_ratio.sqrt(),
191    )
192}
193
194fn build_grid(params: &RhaoutiChaigneJolyParams, sr: f64) -> MembraneGrid {
195    let (lx, ly) = sides(params);
196    membrane_grid(
197        MEMBRANE_AREAL_DENSITY_KG_M2,
198        MEMBRANE_TENSION_N_M,
199        lx,
200        ly,
201        params.damp_dc,
202        params.damp_freq,
203        sr,
204    )
205}
206
207pub struct RhaoutiChaigneJolySite {
208    grid: MembraneGrid,
209    hammer: Hammer,
210    detached: bool,
211    contact_patch: ContactPatch,
212    pickup_ix: usize,
213    pickup_iy: usize,
214    dt: f64,
215}
216
217impl RhaoutiChaigneJolySite {
218    pub fn new(params: &RhaoutiChaigneJolyParams, sr: f64) -> RhaoutiChaigneJolySite {
219        let grid = build_grid(params, sr);
220        let contact_ix = (params.strike_x * grid.nx as f64)
221            .round()
222            .clamp(1.0, (grid.nx - 1) as f64) as usize;
223        let contact_iy = (params.strike_y * grid.ny as f64)
224            .round()
225            .clamp(1.0, (grid.ny - 1) as f64) as usize;
226        // (0.8, 0.2): off the even-(p,q)-silencing x=Lx/2, y=Ly/2 nodal lines.
227        let pickup_ix = (0.8 * grid.nx as f64)
228            .round()
229            .clamp(1.0, (grid.nx - 1) as f64) as usize;
230        let pickup_iy = (0.2 * grid.ny as f64)
231            .round()
232            .clamp(1.0, (grid.ny - 1) as f64) as usize;
233        let contact_patch = build_contact_patch(grid.nx, grid.ny, contact_ix, contact_iy, grid.h);
234        let dt = 1.0 / sr;
235        RhaoutiChaigneJolySite {
236            grid,
237            hammer: Hammer::new(
238                params.hammer_mass,
239                params.hammer_k,
240                params.hammer_p,
241                params.vel,
242                dt,
243            ),
244            detached: false,
245            contact_patch,
246            pickup_ix,
247            pickup_iy,
248            dt,
249        }
250    }
251}
252
253impl Solver for RhaoutiChaigneJolySite {
254    fn step(&mut self) -> f64 {
255        let grid = &mut self.grid;
256        let (nx, ny) = (grid.nx, grid.ny);
257        let u_h = if self.detached {
258            0.0
259        } else {
260            self.contact_patch
261                .nodes
262                .iter()
263                .zip(&self.contact_patch.weights)
264                .map(|(&n, &w)| w * grid.u_now[n])
265                .sum::<f64>()
266        };
267
268        let mut forces = [0.0f64; 1];
269        self.hammer.substeps(
270            self.dt,
271            &[u_h],
272            std::slice::from_mut(&mut self.detached),
273            &mut forces,
274        );
275        let force = forces[0];
276
277        for iy in 1..ny {
278            for ix in 1..nx {
279                let c = node_index(nx, ix, iy);
280                let lap_now = grid.u_now[node_index(nx, ix + 1, iy)]
281                    + grid.u_now[node_index(nx, ix - 1, iy)]
282                    + grid.u_now[node_index(nx, ix, iy + 1)]
283                    + grid.u_now[node_index(nx, ix, iy - 1)]
284                    - 4.0 * grid.u_now[c];
285                let lap_prev = grid.u_prev[node_index(nx, ix + 1, iy)]
286                    + grid.u_prev[node_index(nx, ix - 1, iy)]
287                    + grid.u_prev[node_index(nx, ix, iy + 1)]
288                    + grid.u_prev[node_index(nx, ix, iy - 1)]
289                    - 4.0 * grid.u_prev[c];
290                grid.u_next[c] = 2.0 * grid.u_now[c] - grid.u_prev[c] + grid.courant_sq * lap_now
291                    - grid.damp_a * (grid.u_now[c] - grid.u_prev[c])
292                    + grid.damp_b * (lap_now - lap_prev);
293            }
294        }
295        if force != 0.0 {
296            let injection = (self.dt * self.dt / (grid.sigma * grid.h * grid.h)) * force;
297            for (&node, &w) in self
298                .contact_patch
299                .nodes
300                .iter()
301                .zip(&self.contact_patch.weights)
302            {
303                grid.u_next[node] += injection * w;
304            }
305        }
306        for ix in 0..=nx {
307            grid.u_next[node_index(nx, ix, 0)] = 0.0;
308            grid.u_next[node_index(nx, ix, ny)] = 0.0;
309        }
310        for iy in 0..=ny {
311            grid.u_next[node_index(nx, 0, iy)] = 0.0;
312            grid.u_next[node_index(nx, nx, iy)] = 0.0;
313        }
314
315        let sample = grid.u_now[node_index(nx, self.pickup_ix, self.pickup_iy)];
316
317        std::mem::swap(&mut grid.u_prev, &mut grid.u_now);
318        std::mem::swap(&mut grid.u_now, &mut grid.u_next);
319
320        sample
321    }
322}