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