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