1use 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 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
62const MEMBRANE_TENSION_N_M: f64 = 3000.0;
64const MEMBRANE_AREAL_DENSITY_KG_M2: f64 = 0.3;
66#[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 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
87const MALLET_WINDOW_QUARTIC_COEFF_M4: f64 = 1.0e7;
89const MALLET_WINDOW_WEIGHT_FLOOR: f64 = 1e-8;
91
92#[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 =
107 (MALLET_WINDOW_WEIGHT_FLOOR.ln().abs() / MALLET_WINDOW_QUARTIC_COEFF_M4).powf(0.25);
108 let radius_cells = (cutoff_m / h).ceil() as isize;
109
110 let mut nodes = Vec::new();
111 let mut weights = Vec::new();
112 let mut total = 0.0;
113 for dj in -radius_cells..=radius_cells {
114 for di in -radius_cells..=radius_cells {
115 let (ix, iy) = (contact_ix as isize + di, contact_iy as isize + dj);
116 if ix < 1 || ix > nx as isize - 1 || iy < 1 || iy > ny as isize - 1 {
117 continue;
118 }
119 let (dx, dy) = (di as f64 * h, dj as f64 * h);
120 let w = (-MALLET_WINDOW_QUARTIC_COEFF_M4 * (dx.powi(4) + dy.powi(4))).exp();
121 if w < MALLET_WINDOW_WEIGHT_FLOOR {
122 continue;
123 }
124 nodes.push(node_index(nx, ix as usize, iy as usize));
125 weights.push(w);
126 total += w;
127 }
128 }
129 for w in &mut weights {
130 *w /= total;
131 }
132 ContactPatch { nodes, weights }
133}
134
135const LAMBDA_TARGET: f64 = 0.9 * std::f64::consts::FRAC_1_SQRT_2;
137
138pub const NODE_COUNT_CEILING: usize = 4_000_000;
140
141pub fn node_count(params: &RhaoutiChaigneJolyParams, sr: f64) -> f64 {
143 let (lx, ly) = sides(params);
144 let h = step(MEMBRANE_TENSION_N_M, MEMBRANE_AREAL_DENSITY_KG_M2, sr);
145 (axis_nodes(lx, h) + 1.0) * (axis_nodes(ly, h) + 1.0)
146}
147
148fn step(tension: f64, sigma: f64, sr: f64) -> f64 {
149 (tension / sigma).sqrt() * (1.0 / sr) / LAMBDA_TARGET
150}
151
152fn axis_nodes(span: f64, h: f64) -> f64 {
153 (span / h).round().max(4.0)
154}
155
156fn membrane_grid(
157 sigma: f64,
158 tension: f64,
159 lx: f64,
160 ly: f64,
161 damp_dc: f64,
162 damp_freq: f64,
163 sr: f64,
164) -> MembraneGrid {
165 let dt = 1.0 / sr;
166 let h = step(tension, sigma, sr);
167 let nx = axis_nodes(lx, h) as usize;
168 let ny = axis_nodes(ly, h) as usize;
169 let courant_sq = LAMBDA_TARGET * LAMBDA_TARGET;
170
171 let count = (nx + 1) * (ny + 1);
172 MembraneGrid {
173 u_now: vec![0.0; count],
174 u_prev: vec![0.0; count],
175 u_next: vec![0.0; count],
176 nx,
177 ny,
178 h,
179 sigma,
180 courant_sq,
181 damp_a: 2.0 * damp_dc * dt,
182 damp_b: 2.0 * damp_freq * dt / (h * h),
183 }
184}
185
186fn sides(params: &RhaoutiChaigneJolyParams) -> (f64, f64) {
189 let c = (MEMBRANE_TENSION_N_M / MEMBRANE_AREAL_DENSITY_KG_M2).sqrt();
190 let l0 = c / (params.f0 * std::f64::consts::SQRT_2);
191 (
192 l0 * params.aspect_ratio.sqrt(),
193 l0 / params.aspect_ratio.sqrt(),
194 )
195}
196
197fn build_grid(params: &RhaoutiChaigneJolyParams, sr: f64) -> MembraneGrid {
198 let (lx, ly) = sides(params);
199 membrane_grid(
200 MEMBRANE_AREAL_DENSITY_KG_M2,
201 MEMBRANE_TENSION_N_M,
202 lx,
203 ly,
204 params.damp_dc,
205 params.damp_freq,
206 sr,
207 )
208}
209
210#[derive(Clone)]
211pub struct RhaoutiChaigneJolySite {
212 grid: MembraneGrid,
213 hammer: Hammer,
214 detached: bool,
215 contact_patch: ContactPatch,
216 pickup_ix: usize,
217 pickup_iy: usize,
218 dt: f64,
219}
220
221impl RhaoutiChaigneJolySite {
222 pub fn new(params: &RhaoutiChaigneJolyParams, sr: f64) -> RhaoutiChaigneJolySite {
223 let grid = build_grid(params, sr);
224 let contact_ix = (params.strike_x * grid.nx as f64)
225 .round()
226 .clamp(1.0, (grid.nx - 1) as f64) as usize;
227 let contact_iy = (params.strike_y * grid.ny as f64)
228 .round()
229 .clamp(1.0, (grid.ny - 1) as f64) as usize;
230 let pickup_ix = (0.8 * grid.nx as f64)
232 .round()
233 .clamp(1.0, (grid.nx - 1) as f64) as usize;
234 let pickup_iy = (0.2 * grid.ny as f64)
235 .round()
236 .clamp(1.0, (grid.ny - 1) as f64) as usize;
237 let contact_patch = build_contact_patch(grid.nx, grid.ny, contact_ix, contact_iy, grid.h);
238 let dt = 1.0 / sr;
239 RhaoutiChaigneJolySite {
240 grid,
241 hammer: Hammer::new(
242 params.hammer_mass,
243 params.hammer_k,
244 params.hammer_p,
245 params.vel,
246 dt,
247 ),
248 detached: false,
249 contact_patch,
250 pickup_ix,
251 pickup_iy,
252 dt,
253 }
254 }
255}
256
257impl RhaoutiChaigneJolySite {
258 fn advance(&mut self) -> f64 {
259 let grid = &mut self.grid;
260 let (nx, ny) = (grid.nx, grid.ny);
261 let u_h = if self.detached {
262 0.0
263 } else {
264 self.contact_patch
265 .nodes
266 .iter()
267 .zip(&self.contact_patch.weights)
268 .map(|(&n, &w)| w * grid.u_now[n])
269 .sum::<f64>()
270 };
271
272 let mut forces = [0.0f64; 1];
273 self.hammer.substeps(
274 self.dt,
275 &[u_h],
276 std::slice::from_mut(&mut self.detached),
277 &mut forces,
278 );
279 let force = forces[0];
280
281 for iy in 1..ny {
282 for ix in 1..nx {
283 let c = node_index(nx, ix, iy);
284 let lap_now = grid.u_now[node_index(nx, ix + 1, iy)]
285 + grid.u_now[node_index(nx, ix - 1, iy)]
286 + grid.u_now[node_index(nx, ix, iy + 1)]
287 + grid.u_now[node_index(nx, ix, iy - 1)]
288 - 4.0 * grid.u_now[c];
289 let lap_prev = grid.u_prev[node_index(nx, ix + 1, iy)]
290 + grid.u_prev[node_index(nx, ix - 1, iy)]
291 + grid.u_prev[node_index(nx, ix, iy + 1)]
292 + grid.u_prev[node_index(nx, ix, iy - 1)]
293 - 4.0 * grid.u_prev[c];
294 grid.u_next[c] = 2.0 * grid.u_now[c] - grid.u_prev[c] + grid.courant_sq * lap_now
295 - grid.damp_a * (grid.u_now[c] - grid.u_prev[c])
296 + grid.damp_b * (lap_now - lap_prev);
297 }
298 }
299 if force != 0.0 {
300 let injection = (self.dt * self.dt / (grid.sigma * grid.h * grid.h)) * force;
301 for (&node, &w) in self
302 .contact_patch
303 .nodes
304 .iter()
305 .zip(&self.contact_patch.weights)
306 {
307 grid.u_next[node] += injection * w;
308 }
309 }
310 for ix in 0..=nx {
311 grid.u_next[node_index(nx, ix, 0)] = 0.0;
312 grid.u_next[node_index(nx, ix, ny)] = 0.0;
313 }
314 for iy in 0..=ny {
315 grid.u_next[node_index(nx, 0, iy)] = 0.0;
316 grid.u_next[node_index(nx, nx, iy)] = 0.0;
317 }
318
319 let sample = grid.u_now[node_index(nx, self.pickup_ix, self.pickup_iy)];
320
321 std::mem::swap(&mut grid.u_prev, &mut grid.u_now);
322 std::mem::swap(&mut grid.u_now, &mut grid.u_next);
323
324 sample
325 }
326}
327
328impl Solver for RhaoutiChaigneJolySite {
329 fn bytes(&self) -> usize {
330 let (grid, patch) = (&self.grid, &self.contact_patch);
331 size_of::<Self>()
332 + super::floats(&grid.u_now)
333 + super::floats(&grid.u_prev)
334 + super::floats(&grid.u_next)
335 + std::mem::size_of_val(patch.nodes.as_slice())
336 + super::floats(&patch.weights)
337 }
338
339 fn step(&mut self, _args: &[f64]) -> Result<f64, SampleError> {
340 Ok(self.advance())
341 }
342}