Skip to main content

sva_samples/physics/
chaigne_doutaut.rs

1// Concern: one hammer/free-free-bar call site's finite-difference state | Non-concern: argument evaluation, builtin dispatch | IO: (params, sr) -> a site; () -> a sample
2
3//! Chaigne & Doutaut, JASA 101(1) 539-557 (1997): `u_tt = -kappa^2 u_xxxx`, free-free BCs,
4//! uniform cross-section, driven by `hammer.rs`.
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 ChaigneDoutautParams {
14    pub f0: f64,
15    pub strike_pos: f64,
16    pub vel: f64,
17    pub hammer_mass: f64,
18    pub hammer_k: f64,
19    pub hammer_p: f64,
20    pub damp_dc: f64,
21    pub damp_freq: f64,
22}
23
24impl ChaigneDoutautParams {
25    /// `chaigne_askenfelt`'s hammer defaults, at the fundamental asked for.
26    pub fn at(f0: f64) -> ChaigneDoutautParams {
27        ChaigneDoutautParams {
28            f0,
29            strike_pos: 0.15,
30            vel: 3.2,
31            hammer_mass: 2.9e-3,
32            hammer_k: 2.6646e8,
33            hammer_p: 2.5,
34            damp_dc: 0.6,
35            damp_freq: 1.6e-4,
36        }
37    }
38}
39
40impl ChaigneDoutautParams {
41    pub fn valid(&self) -> bool {
42        all(&[
43            (self.f0, Positive),
44            (self.strike_pos, OpenUnit),
45            (self.vel, Positive),
46            (self.hammer_mass, Positive),
47            (self.hammer_k, Positive),
48            (self.hammer_p, Positive),
49            (self.damp_dc, NonNegative),
50            (self.damp_freq, NonNegative),
51        ])
52    }
53}
54
55/// Aluminum, tuned to land a C4 bar near ~45cm.
56const BAR_YOUNGS_MODULUS_PA: f64 = 7.0e10;
57const BAR_DENSITY_KG_M3: f64 = 2700.0;
58const BAR_THICKNESS_M: f64 = 0.01;
59/// Cancels out of `kappa`; sets the contact-injection mass scale.
60const BAR_WIDTH_M: f64 = 1.0;
61/// Free-free root of `cos(beta L)cosh(beta L) = 1`.
62const BETA1_L: f64 = 4.730040744862704;
63/// A free-bar anti-node for nearly every low partial, off the strike.
64const PICKUP_POS: f64 = 0.93;
65
66pub(crate) struct BarGrid {
67    pub(crate) u_now: Vec<f64>,
68    pub(crate) u_prev: Vec<f64>,
69    pub(crate) u_next: Vec<f64>,
70    pub(crate) n: usize,
71    pub(crate) dx: f64,
72    pub(crate) rho: f64,
73    #[allow(dead_code)]
74    pub(crate) kappa: f64,
75    pub(crate) stiff_sq: f64,
76    pub(crate) damp_a: f64,
77    pub(crate) damp_b: f64,
78    pub(crate) bar_substeps: usize,
79}
80
81/// Free-end ghosts: `u_xx=0` gives `u_{-1}=2u_0-u_1`, then `u_xxx=0` gives
82/// `u_{-2}=4u_0-4u_1+u_2`.
83pub(crate) fn free_ghost(u: &[f64], n: usize, idx: isize) -> f64 {
84    if idx >= 0 && idx as usize <= n {
85        return u[idx as usize];
86    }
87    if idx < 0 {
88        match idx {
89            -1 => 2.0 * u[0] - u[1],
90            -2 => 4.0 * u[0] - 4.0 * u[1] + u[2],
91            _ => unreachable!(),
92        }
93    } else {
94        match idx as usize - n {
95            1 => 2.0 * u[n] - u[n - 1],
96            2 => 4.0 * u[n] - 4.0 * u[n - 1] + u[n - 2],
97            _ => unreachable!(),
98        }
99    }
100}
101
102/// von Neumann: `mu = kappa*dt/dx^2 <= 0.5`, at a 90% margin.
103pub(crate) fn bar_grid(
104    rho: f64,
105    kappa: f64,
106    length: f64,
107    damp_dc: f64,
108    damp_freq: f64,
109    sr: f64,
110) -> BarGrid {
111    let dt = 1.0 / sr;
112    let mu_max = 0.5;
113    let mu_target = 0.9 * mu_max;
114    let dx_target = (kappa * dt / mu_target).sqrt();
115    let n = ((length / dx_target).round() as usize).max(4);
116    let dx = length / n as f64;
117    // The n>=4 floor can outrun `dx_target` for a short (high-`f0`) bar; sub-step then.
118    let mu_at_dt = kappa * dt / (dx * dx);
119    let bar_substeps = (mu_at_dt / mu_target).ceil().max(1.0) as usize;
120    let dt_sub = dt / bar_substeps as f64;
121    let stiff_sq = kappa * kappa * dt_sub * dt_sub / dx.powi(4);
122
123    BarGrid {
124        u_now: vec![0.0; n + 1],
125        u_prev: vec![0.0; n + 1],
126        u_next: vec![0.0; n + 1],
127        n,
128        dx,
129        rho,
130        kappa,
131        stiff_sq,
132        damp_a: 2.0 * damp_dc * dt_sub,
133        damp_b: 2.0 * damp_freq * dt_sub / (dx * dx),
134        bar_substeps,
135    }
136}
137
138/// `f0 = kappa*(beta1 L)^2/(2 pi L^2)`, solved for `L`.
139fn build_grid(params: &ChaigneDoutautParams, sr: f64) -> BarGrid {
140    let kappa =
141        (BAR_YOUNGS_MODULUS_PA / BAR_DENSITY_KG_M3).sqrt() * BAR_THICKNESS_M / 12.0f64.sqrt();
142    let length = (kappa * BETA1_L * BETA1_L / (2.0 * std::f64::consts::PI * params.f0)).sqrt();
143    let rho = BAR_DENSITY_KG_M3 * BAR_WIDTH_M * BAR_THICKNESS_M;
144    bar_grid(rho, kappa, length, params.damp_dc, params.damp_freq, sr)
145}
146
147pub struct ChaigneDoutautSite {
148    bar: BarGrid,
149    hammer: Hammer,
150    detached: bool,
151    contact_index: usize,
152    pickup_index: usize,
153    dt: f64,
154}
155
156impl ChaigneDoutautSite {
157    pub fn new(params: &ChaigneDoutautParams, sr: f64) -> ChaigneDoutautSite {
158        let bar = build_grid(params, sr);
159        let contact_index = (params.strike_pos * bar.n as f64)
160            .round()
161            .clamp(1.0, (bar.n - 1) as f64) as usize;
162        let pickup_index = (PICKUP_POS * bar.n as f64)
163            .round()
164            .clamp(1.0, (bar.n - 1) as f64) as usize;
165        let dt = 1.0 / sr;
166        ChaigneDoutautSite {
167            bar,
168            hammer: Hammer::new(
169                params.hammer_mass,
170                params.hammer_k,
171                params.hammer_p,
172                params.vel,
173                dt,
174            ),
175            detached: false,
176            contact_index,
177            pickup_index,
178            dt,
179        }
180    }
181}
182
183impl Solver for ChaigneDoutautSite {
184    fn step(&mut self) -> f64 {
185        let bar = &mut self.bar;
186        let n = bar.n;
187        let u_h = bar.u_now[self.contact_index];
188
189        let mut forces = [0.0f64; 1];
190        self.hammer.substeps(
191            self.dt,
192            &[u_h],
193            std::slice::from_mut(&mut self.detached),
194            &mut forces,
195        );
196        let force = forces[0];
197
198        let sample = bar.u_now[self.pickup_index];
199
200        let dt_sub = self.dt / bar.bar_substeps as f64;
201        let injection = (dt_sub * dt_sub / (bar.rho * bar.dx)) * force;
202        for _ in 0..bar.bar_substeps {
203            for i in 0..=n {
204                let ii = i as isize;
205                let lap_now = free_ghost(&bar.u_now, n, ii + 1) - 2.0 * bar.u_now[i]
206                    + free_ghost(&bar.u_now, n, ii - 1);
207                let biharm = free_ghost(&bar.u_now, n, ii + 2)
208                    - 4.0 * free_ghost(&bar.u_now, n, ii + 1)
209                    + 6.0 * bar.u_now[i]
210                    - 4.0 * free_ghost(&bar.u_now, n, ii - 1)
211                    + free_ghost(&bar.u_now, n, ii - 2);
212                let lap_prev = free_ghost(&bar.u_prev, n, ii + 1) - 2.0 * bar.u_prev[i]
213                    + free_ghost(&bar.u_prev, n, ii - 1);
214                let mut next = 2.0 * bar.u_now[i]
215                    - bar.u_prev[i]
216                    - bar.stiff_sq * biharm
217                    - bar.damp_a * (bar.u_now[i] - bar.u_prev[i])
218                    + bar.damp_b * (lap_now - lap_prev);
219                if i == self.contact_index {
220                    next += injection;
221                }
222                bar.u_next[i] = next;
223            }
224            std::mem::swap(&mut bar.u_prev, &mut bar.u_now);
225            std::mem::swap(&mut bar.u_now, &mut bar.u_next);
226        }
227
228        sample
229    }
230}