1use 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 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
61const MEMBRANE_TENSION_N_M: f64 = 3000.0;
63const MEMBRANE_AREAL_DENSITY_KG_M2: f64 = 0.3;
65struct 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 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
85const MALLET_WINDOW_QUARTIC_COEFF_M4: f64 = 1.0e7;
87const MALLET_WINDOW_WEIGHT_FLOOR: f64 = 1e-8;
89
90struct 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
132const LAMBDA_TARGET: f64 = 0.9 * std::f64::consts::FRAC_1_SQRT_2;
134
135pub const NODE_COUNT_CEILING: usize = 4_000_000;
137
138pub 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
183fn 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 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}