1use crate::error::SampleError;
7use crate::physics::Solver;
8use crate::physics::tonehole::{RadCoeffs, ToneholeBranch, build_tonehole, radiation_coeffs};
9
10use crate::physics::bound::Bound::*;
11use crate::physics::bound::all;
12
13pub(super) const RHO0_KG_M3: f64 = 1.1769;
14pub(super) const C0_M_S: f64 = 347.23;
15const ETA0_PA_S: f64 = 1.846e-5;
16const SQRT_PRANDTL: f64 = 0.8410;
17const GAMMA0: f64 = 1.4017;
18const MIN_SEGMENTS: usize = 1;
19pub const MAX_HOLES: usize = 6;
20
21pub fn nyquist_wavenumber(sr: f64) -> f64 {
23 std::f64::consts::PI * sr / C0_M_S
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct ToneholeSpec {
28 pub pos: f64,
29 pub open: bool,
30 pub radius: f64,
31 pub height: f64,
33}
34
35#[derive(Clone, Debug, PartialEq)]
36pub struct BoreParams {
37 pub length: f64,
38 pub radius_in: f64,
39 pub radius_out: f64,
40 pub excite_pos: f64,
41 pub pulse_amp: f64,
42 pub pulse_width: f64,
43 pub damp_dc: f64,
45 pub damp_freq: f64,
46 pub holes: [Option<ToneholeSpec>; MAX_HOLES],
47}
48
49impl BoreParams {
50 pub fn at(length: f64) -> BoreParams {
52 BoreParams {
53 length,
54 radius_in: 0.01,
55 radius_out: 0.01,
56 excite_pos: 0.0,
57 pulse_amp: 1.0,
58 pulse_width: 0.001,
59 damp_dc: 1.0,
60 damp_freq: 1.0,
61 holes: [None; MAX_HOLES],
62 }
63 }
64}
65
66impl BoreParams {
67 pub fn valid(&self) -> bool {
68 all(&[
69 (self.length, Positive),
70 (self.radius_in, Positive),
71 (self.radius_out, Positive),
72 (self.excite_pos, HalfOpenUnit),
73 (self.pulse_amp, Positive),
74 (self.pulse_width, Positive),
75 (self.damp_dc, NonNegative),
76 (self.damp_freq, NonNegative),
77 ]) && self.holes.iter().flatten().all(|h| self.hole_valid(h))
78 }
79
80 fn hole_valid(&self, hole: &ToneholeSpec) -> bool {
81 let local_radius = self.radius_in + (self.radius_out - self.radius_in) * hole.pos;
82 all(&[
83 (hole.pos, HalfOpenUnit),
84 (hole.radius, Positive),
85 (hole.height, NonNegative),
86 ]) && hole.radius < local_radius
87 }
88}
89
90const LOSS_BRANCHES: usize = 6;
92
93const GHAT: [f64; LOSS_BRANCHES] = [
96 3.030565307333e-02,
97 3.623164588771e-02,
98 7.517234787811e-02,
99 1.661722612526e-01,
100 4.503547222868e-01,
101 8.057882518470e+01,
102];
103const AHAT: [f64; LOSS_BRANCHES] = [
104 7.367064671349e-04,
105 5.320144731723e-03,
106 2.447848326023e-02,
107 1.144572621260e-01,
108 6.460771511330e-01,
109 3.000000000000e+02,
110];
111
112#[derive(Clone, Copy)]
115struct LossNode {
116 gain: f64,
117 branch: [(f64, f64, f64); LOSS_BRANCHES],
119}
120
121impl LossNode {
122 fn new(radius: f64, sr: f64, damp: f64, residue_scale: f64, r0: f64) -> LossNode {
123 let unit = damp * residue_scale * 2.0 * (ETA0_PA_S / (RHO0_KG_M3 * sr)).sqrt() / radius;
124 let mut branch = [(0.0, 0.0, 0.0); LOSS_BRANCHES];
125 let mut sum_c = 0.0;
126 for (slot, (&g, &a)) in branch.iter_mut().zip(GHAT.iter().zip(&AHAT)) {
127 let inv_1pa = 1.0 / (1.0 + a);
128 let c = unit * g * inv_1pa;
129 sum_c += c;
130 *slot = (c, a, inv_1pa);
131 }
132 LossNode {
133 gain: 1.0 / (1.0 + damp * r0 / (RHO0_KG_M3 * sr) + sum_c),
134 branch,
135 }
136 }
137
138 fn viscous(radius: f64, sr: f64, damp: f64) -> LossNode {
139 LossNode::new(radius, sr, damp, 1.0, 3.0 * ETA0_PA_S / (radius * radius))
140 }
141
142 fn thermal(radius: f64, sr: f64, damp: f64) -> LossNode {
146 LossNode::new(radius, sr, damp, (GAMMA0 - 1.0) / SQRT_PRANDTL, 0.0)
147 }
148
149 #[inline]
151 fn close_out(
152 &self,
153 rhs: f64,
154 state: &mut [f64; LOSS_BRANCHES],
155 extra_y: f64,
156 extra_hist: f64,
157 ) -> f64 {
158 let mut acc = rhs;
159 for (&(c, _, _), s) in self.branch.iter().zip(state.iter()) {
160 acc += c * s;
161 }
162 let next = if extra_y == 0.0 {
163 self.gain * acc
164 } else {
165 (acc - extra_hist) / (1.0 / self.gain + extra_y)
166 };
167 for (&(_, a, inv_1pa), s) in self.branch.iter().zip(state.iter_mut()) {
168 *s = (*s + a * next) * inv_1pa;
169 }
170 next
171 }
172}
173
174#[derive(Clone)]
175pub(crate) struct DuctGrid {
176 psi: Vec<f64>,
177 v: Vec<f64>,
178 n: usize,
179 dz: f64,
180 #[allow(dead_code)]
181 courant: f64,
182 s_half: Vec<f64>,
183 bar_s: Vec<f64>,
184 loss_v: Vec<LossNode>,
185 loss_v_state: Vec<[f64; LOSS_BRANCHES]>,
186 loss_t: Vec<LossNode>,
187 loss_t_state: Vec<[f64; LOSS_BRANCHES]>,
188 rad: RadCoeffs,
189 rad_state: (f64, f64),
191 toneholes: Vec<ToneholeBranch>,
192}
193
194pub(crate) fn grid_segments(length: f64, sr: f64) -> usize {
196 let dt = 1.0 / sr;
197 let dz_bound = C0_M_S * dt;
198 ((length / dz_bound).floor() as usize).max(MIN_SEGMENTS)
199}
200
201fn build_grid(params: &BoreParams, sr: f64) -> DuctGrid {
202 let dt = 1.0 / sr;
203 let n = grid_segments(params.length, sr);
204 let dz = params.length / n as f64;
205 let courant = C0_M_S * dt / dz;
206
207 let radius_at = |z: f64| -> f64 {
208 params.radius_in + (params.radius_out - params.radius_in) * (z / params.length)
209 };
210 let s_half: Vec<f64> = (0..n)
211 .map(|l| {
212 let r = radius_at((l as f64 + 0.5) * dz);
213 std::f64::consts::PI * r * r
214 })
215 .collect();
216 let bar_s: Vec<f64> = (0..=n)
217 .map(|l| match (l.checked_sub(1), s_half.get(l)) {
218 (Some(left), Some(&right)) => 0.5 * (s_half[left] + right),
219 (Some(left), None) => s_half[left],
220 (None, Some(&right)) => right,
221 (None, None) => unreachable!("n >= MIN_SEGMENTS guarantees an s_half entry"),
222 })
223 .collect();
224
225 let radius_of = |s: f64| (s / std::f64::consts::PI).sqrt();
226 let loss_v: Vec<LossNode> = s_half
227 .iter()
228 .map(|&s| LossNode::viscous(radius_of(s), sr, params.damp_dc))
229 .collect();
230 let loss_v_state = vec![[0.0; LOSS_BRANCHES]; n];
231
232 let loss_t: Vec<LossNode> = bar_s[1..n]
234 .iter()
235 .map(|&s| LossNode::thermal(radius_of(s), sr, params.damp_freq))
236 .collect();
237 let loss_t_state = vec![[0.0; LOSS_BRANCHES]; n.saturating_sub(1)];
238
239 let toneholes: Vec<ToneholeBranch> = params
240 .holes
241 .iter()
242 .flatten()
243 .map(|spec| {
244 let node = (spec.pos * n as f64).round().clamp(1.0, (n - 1) as f64) as usize;
245 build_tonehole(spec, radius_at(spec.pos * params.length), node, dt, dz)
246 })
247 .collect();
248
249 DuctGrid {
250 psi: vec![0.0; n + 1],
251 v: vec![0.0; n],
252 n,
253 dz,
254 courant,
255 s_half,
256 bar_s,
257 loss_v,
258 loss_v_state,
259 loss_t,
260 loss_t_state,
261 rad: radiation_coeffs(params.radius_out, dt, dz),
262 rad_state: (0.0, 0.0),
263 toneholes,
264 }
265}
266
267#[derive(Clone)]
268pub struct BoreSite {
269 duct: DuctGrid,
270 excite_index: usize,
271 pulse_amp: f64,
272 pulse_width: f64,
273 sr: f64,
274 sample_index: u64,
275}
276
277impl BoreSite {
278 pub fn new(params: &BoreParams, sr: f64) -> BoreSite {
279 let duct = build_grid(params, sr);
280 let excite_index =
281 ((params.excite_pos * duct.n as f64).round() as usize).min(duct.n.saturating_sub(1));
282 BoreSite {
283 duct,
284 excite_index,
285 pulse_amp: params.pulse_amp,
286 pulse_width: params.pulse_width,
287 sr,
288 sample_index: 0,
289 }
290 }
291}
292
293impl BoreSite {
294 fn advance(&mut self) -> f64 {
295 let (t, dt) = (self.sample_index as f64 / self.sr, 1.0 / self.sr);
296 let source = crate::physics::raised_cosine_pulse(t, self.pulse_amp, self.pulse_width);
297
298 let duct = &mut self.duct;
299 let n = duct.n;
300 let rho_c2_dt = dt * RHO0_KG_M3 * C0_M_S * C0_M_S;
301
302 for l in 0..n {
303 let dpsi_dz = (duct.psi[l + 1] - duct.psi[l]) / duct.dz;
304 let rhs = duct.v[l] - dt * dpsi_dz / RHO0_KG_M3;
305 duct.v[l] = duct.loss_v[l].close_out(rhs, &mut duct.loss_v_state[l], 0.0, 0.0);
306 }
307
308 let hole_prep: Vec<(usize, f64, f64)> = duct
309 .toneholes
310 .iter()
311 .map(|h| {
312 let (y_eff, hist) = h.prepare();
313 (h.node(), y_eff, hist)
314 })
315 .collect();
316
317 for l in 1..n {
318 let flux = duct.s_half[l] * duct.v[l] - duct.s_half[l - 1] * duct.v[l - 1];
319 let coeff = rho_c2_dt / (duct.bar_s[l] * duct.dz);
320 let mut rhs = duct.psi[l] - coeff * flux;
321 if l == self.excite_index {
322 rhs += coeff * source;
323 }
324 let mut extra_y = 0.0;
325 let mut extra_hist = 0.0;
326 for &(node, y_eff, hist) in &hole_prep {
327 if node == l {
328 extra_y += coeff * y_eff;
329 extra_hist += coeff * hist;
330 }
331 }
332 duct.psi[l] = duct.loss_t[l - 1].close_out(
333 rhs,
334 &mut duct.loss_t_state[l - 1],
335 extra_y,
336 extra_hist,
337 );
338 }
339
340 for (hole, &(node, _, hist)) in duct.toneholes.iter_mut().zip(&hole_prep) {
341 hole.commit(duct.psi[node], hist, dt);
342 }
343
344 {
345 let flux = duct.s_half[0] * duct.v[0];
346 let coeff = rho_c2_dt / (duct.bar_s[0] * duct.dz);
347 let mut next = duct.psi[0] - coeff * flux;
348 if self.excite_index == 0 {
349 next += coeff * source;
350 }
351 duct.psi[0] = next;
352 }
353
354 {
355 let RadCoeffs {
356 a_p,
357 b_p,
358 bv,
359 cg,
360 r1,
361 k,
362 } = duct.rad;
363 let (v1, p1) = duct.rad_state;
364 let v_last = duct.v[n - 1];
365 let base = v1 - (a_p / r1) * p1;
366 let next = (duct.psi[n] + k * (v_last - base)) / (1.0 + k * cg);
367 let p1_next = a_p * p1 + b_p * next;
368 let v1_next = v1 + bv * next;
369 duct.psi[n] = next;
370 duct.rad_state = (v1_next, p1_next);
371 }
372
373 let sample = duct.psi[n];
374 self.sample_index += 1;
375 sample
376 }
377}
378
379impl Solver for BoreSite {
380 fn bytes(&self) -> usize {
381 let duct = &self.duct;
382 size_of::<Self>()
383 + super::floats(&duct.psi)
384 + super::floats(&duct.v)
385 + super::floats(&duct.s_half)
386 + super::floats(&duct.bar_s)
387 + std::mem::size_of_val(duct.loss_v.as_slice())
388 + std::mem::size_of_val(duct.loss_v_state.as_slice())
389 + std::mem::size_of_val(duct.loss_t.as_slice())
390 + std::mem::size_of_val(duct.loss_t_state.as_slice())
391 + std::mem::size_of_val(duct.toneholes.as_slice())
392 }
393
394 fn step(&mut self, _args: &[f64]) -> Result<f64, SampleError> {
395 Ok(self.advance())
396 }
397}