use pantometry::prelude::*;
mod common;
use common::svg::{diverging, rgb, ticks, Plot};
use common::{check, check_between, heading};
const WIDTH: f64 = 4.4;
const HEIGHT: f64 = 3.1;
const DEPTH: f64 = 2.4;
const ACROSS: usize = 23;
const MODE: (u32, u32, u32) = (1, 1, 1);
fn main() {
let hall = Hall::of_air(
"hall",
Length::m(WIDTH),
Length::m(HEIGHT),
Length::m(DEPTH),
ACROSS,
);
let (lx, ly, lz) = (
hall.width().to_si(),
hall.height().to_si(),
hall.depth().to_si(),
);
let (nx, ny, nz) = hall.nodes();
heading("A room with a ceiling");
println!(
" {:<30} {lx:.3} x {ly:.3} x {lz:.3} m, quantised from {WIDTH} x {HEIGHT} x {DEPTH}",
"size"
);
println!(
" {:<30} {nx} x {ny} x {nz} = {} nodes at {:.1} mm",
"grid",
nx * ny * nz,
hall.spacing().to_si() * 1e3
);
heading("The closed form, and the family a floor plan loses");
let exact = |a: f64, b: f64, c: f64| {
343.0 / 2.0 * ((a / lx).powi(2) + (b / ly).powi(2) + (c / lz).powi(2)).sqrt()
};
for (mode, a, b, c, what) in [
((1, 0, 0), 1.0, 0.0, 0.0, "axial, along the width"),
((0, 1, 0), 0.0, 1.0, 0.0, "axial, across"),
(
(0, 0, 1),
0.0,
0.0,
1.0,
"axial, floor to ceiling — absent in 2D",
),
((1, 1, 0), 1.0, 1.0, 0.0, "tangential, in the floor plan"),
(
(1, 1, 1),
1.0,
1.0,
1.0,
"oblique — needs all three, absent in 2D",
),
] {
let got = hall.mode_frequency(mode).to_si();
println!(" {:<9} {:>8.2} Hz {what}", format!("{mode:?}"), got);
check(&format!("mode {mode:?}"), got, exact(a, b, c), 1e-12, "Hz");
}
let vertical = hall.mode_frequency((0, 0, 1)).to_si();
check(
"the vertical mode is c/2Lz",
vertical,
343.0 / (2.0 * lz),
1e-12,
"Hz",
);
check_between(
"and it is inside the audible bass",
vertical,
60.0,
85.0,
"Hz",
);
heading("How many modes there are, which is the real difference");
let pi = std::f64::consts::PI;
let (v, surface, edges) = (
lx * ly * lz,
2.0 * (lx * ly + ly * lz + lz * lx),
4.0 * (lx + ly + lz),
);
let weyl = |f: f64| {
let k = f / 343.0;
4.0 * pi / 3.0 * v * k.powi(3) + pi / 4.0 * surface * k.powi(2) + edges * k / 8.0
};
let mut counts = Vec::new();
for f in [100.0, 150.0, 200.0, 300.0] {
let n = hall.modes_below(Frequency::from_si(f)).len() as f64;
counts.push((f, n));
println!(
" below {f:>5.0} Hz {n:>5.0} modes Weyl {:>7.1} leading term alone {:>7.1}",
weyl(f),
4.0 * pi / 3.0 * v * (f / 343.0).powi(3)
);
}
let (f_hi, n_hi) = *counts.last().unwrap();
check("against Weyl at 300 Hz", n_hi, weyl(f_hi), 0.1, "modes");
check_between(
"the leading term alone is not enough",
n_hi / (4.0 * pi / 3.0 * v * (f_hi / 343.0).powi(3)),
1.2,
2.0,
"x",
);
let ratio = counts[3].1 / counts[1].1;
let weyl_ratio = weyl(counts[3].0) / weyl(counts[1].0);
println!(
" {:<30} {ratio:.2}x from 150 to 300 Hz",
"doubling the frequency"
);
println!(
" {:<30} {weyl_ratio:.2}x — the f^3 asymptote is 8, and a floor plan would give 4",
"Weyl predicts"
);
check(
"the count grows the way Weyl says",
ratio,
weyl_ratio,
0.08,
"x",
);
check_between("which is well clear of a plane's f^2", ratio, 5.0, 8.0, "x");
heading("And the integration rides the closed form");
let f = hall.mode_frequency(MODE).to_si();
let mut hall = hall.released_in_mode(MODE, Pressure::from_si(1.0));
let dt = Time::from_si(hall.max_stable_dt(Time::from_si(0.0)).to_si() * 0.9);
let mut bus = Exchange::new();
let (mut t, mut worst) = (0.0, 0.0f64);
let mut trace = Vec::new();
while t < 2.5 / f {
hall.step(Time::from_si(t), dt, &mut bus).expect("stable");
t += dt.to_si();
let peak = hall.peak_pressure().to_si();
let want = (2.0 * pi * f * t).cos().abs();
worst = worst.max((peak - want).abs());
trace.push((t, peak, want));
}
println!(
" {:<30} {f:.2} Hz, followed for {:.1} periods at {} nodes across",
"the (1,1,1) mode",
t * f,
nx
);
check_between(
"worst departure from |cos 2 pi f t|",
worst,
0.0,
0.03,
"Pa",
);
println!(
" {:<30} {:.3e} J, reported rather than hidden",
"startup adjustment",
hall.startup_adjustment().to_si().abs()
);
if let Some(path) = common::output_path() {
let mid = nz / 2;
let plane: Vec<f64> = (0..ny)
.flat_map(|j| (0..nx).map(move |i| (i, j)).collect::<Vec<_>>())
.map(|(i, j)| hall.pressure_at(i, j, mid).to_si())
.collect();
common::write(&path, &draw(&trace, &plane, nx, ny, f, &counts, &weyl));
}
}
fn draw(
trace: &[(f64, f64, f64)],
plane: &[f64],
nx: usize,
ny: usize,
f: f64,
counts: &[(f64, f64)],
weyl: &dyn Fn(f64) -> f64,
) -> String {
let (w, h) = (960.0, 640.0);
let t_end = trace.last().map_or(1.0, |(t, _, _)| *t);
let mut top =
Plot::new(w, h, (0.0, t_end * 1e3), (-0.05, 1.15)).viewport(74.0, 56.0, w - 130.0, 250.0);
top.title("A room with a ceiling: the oblique (1,1,1) mode");
top.polyline(
trace.iter().map(|(t, _, c)| (t * 1e3, *c)),
&rgb(150, 160, 175),
3.0,
);
top.polyline(
trace.iter().map(|(t, p, _)| (t * 1e3, *p)),
&rgb(255, 82, 33),
2.0,
);
top.axes(
&ticks(0.0, t_end * 1e3, 6),
&ticks(0.0, 1.0, 5),
|v| format!("{v:.0} ms"),
|v| format!("{v:.2}"),
);
top.caption(&format!(
"grey |cos(2 pi f t)| at {f:.2} Hz from the closed form, orange the integration"
));
let scale = plane.iter().fold(0.0f64, |m, v| m.max(v.abs())).max(1e-30);
let mut left =
Plot::new(w, h, (0.0, nx as f64), (0.0, ny as f64)).viewport(74.0, 372.0, 330.0, 210.0);
left.raster(nx, ny, (0.0, nx as f64), (0.0, ny as f64), |i, j| {
diverging(plane[j * nx + i] / scale)
});
left.caption("pressure at mid-height: one nodal line each way, and a third out of the page");
let f_hi = counts.last().map_or(300.0, |(f, _)| *f);
let n_hi = counts.last().map_or(1.0, |(_, n)| *n);
let mut right =
Plot::new(w, h, (0.0, f_hi), (0.0, n_hi * 1.1)).viewport(500.0, 372.0, w - 560.0, 210.0);
right.polyline(
(1..=60).map(|k| {
let f = f_hi * k as f64 / 60.0;
(f, weyl(f))
}),
&rgb(150, 160, 175),
3.0,
);
right.polyline(counts.iter().map(|(f, n)| (*f, *n)), &rgb(255, 82, 33), 2.0);
right.axes(
&ticks(0.0, f_hi, 4),
&ticks(0.0, n_hi * 1.1, 4),
|v| format!("{v:.0} Hz"),
|v| format!("{v:.0}"),
);
right.caption("modes below a frequency: counted, against Weyl with its surface term");
common::svg::document(w, h, [top.into_body(), left.into_body(), right.into_body()])
}