use dualis::prelude::*;
use dualis_acoustic::Room;
mod common;
use common::svg::{diverging, document, rgb, ticks, Plot};
use common::{check, check_between, check_zero, heading};
const WIDTH: f64 = 4.4;
const HEIGHT: f64 = 3.1;
const CELLS: usize = 89;
fn room() -> Room {
Room::of_air(
"room",
Length::from_si(WIDTH),
Length::from_si(HEIGHT),
CELLS,
)
}
fn main() {
let quiet = room();
let (nx, ny) = quiet.cells();
let speed = 343.0;
heading("A 4.4 by 3.1 metre room in air");
println!(
" {nx} by {ny} nodes, cells {:.1} mm across",
quiet.width().to_si() / (nx - 1) as f64 * 1e3
);
for (a, b) in [(1u32, 0u32), (0, 1), (1, 1)] {
let f = quiet.mode_frequency(a, b).to_si();
let expected = speed / 2.0
* ((a as f64 / WIDTH).powi(2) + (b as f64 / quiet.height().to_si()).powi(2)).sqrt();
check(&format!("mode ({a},{b})"), f, expected, 1e-12, "Hz");
}
let ratio = quiet.mode_frequency(1, 1).to_si() / quiet.mode_frequency(1, 0).to_si();
check(
"f(1,1) / f(1,0) -- a pipe would give exactly 2",
ratio,
(1.0 + (WIDTH / quiet.height().to_si()).powi(2)).sqrt(),
1e-12,
"x",
);
println!(" irrational, so the two lowest resonances are not in tune with each other");
let below = |hz: f64| quiet.modes_below(Frequency::from_si(hz)).len() as f64;
let (low, high) = (below(100.0), below(200.0));
println!(" {low:.0} modes below 100 Hz, {high:.0} below 200");
check_between(
" quadrupling with the frequency",
high / low,
2.5,
6.0,
"x",
);
heading("The integration converges on it, and the rate is the interesting part");
let measure = |cells: usize| -> (f64, f64, Vec<(f64, f64)>) {
let reference = Room::of_air(
"room",
Length::from_si(WIDTH),
Length::from_si(HEIGHT),
cells,
);
let exact = reference.mode_frequency(1, 1).to_si();
let mut sound = Room::of_air(
"room",
Length::from_si(WIDTH),
Length::from_si(HEIGHT),
cells,
)
.released_in_mode(1, 1, Pressure::from_si(1.0));
let dt = Time::from_si(sound.max_stable_dt(Time::ZERO).to_si() * 0.9);
let corner = LengthVec::ZERO;
let mut previous = sound.at(corner, Time::ZERO);
let mut elapsed = 0.0;
let mut bus = Exchange::new();
let mut crossings: Vec<f64> = Vec::new();
let mut trace: Vec<(f64, f64)> = vec![(0.0, previous)];
while elapsed < 0.25 {
sound
.step(Time::ZERO, dt, &mut bus)
.expect("inside the CFL limit");
let now = sound.at(corner, Time::ZERO);
elapsed += dt.to_si();
if previous.signum() != now.signum() {
let fraction = previous / (previous - now);
crossings.push(elapsed - dt.to_si() * (1.0 - fraction));
}
previous = now;
if trace.len() < 4000 {
trace.push((elapsed, now));
}
}
let (first, last) = (crossings[0], crossings[crossings.len() - 1]);
let f = (crossings.len() - 1) as f64 / (2.0 * (last - first));
(f, exact, trace)
};
let (coarse, exact_coarse, _) = measure(45);
let (medium, exact_medium, trace) = measure(89);
let (fine, exact_fine, _) = measure(177);
let error = |m: f64, e: f64| (1.0 - m / e).abs();
for (cells, m, e) in [
(45, coarse, exact_coarse),
(89, medium, exact_medium),
(177, fine, exact_fine),
] {
println!(
" {cells:>4} cells: {m:9.5} Hz against {e:9.5} Hz {:+.4}%",
(m / e - 1.0) * 100.0
);
}
assert!(coarse < exact_coarse && medium < exact_medium && fine < exact_fine);
check(
"error ratio, 45 to 89 cells (4 = second order)",
error(coarse, exact_coarse) / error(medium, exact_medium),
4.0,
0.02,
"x",
);
check(
"error ratio, 89 to 177 cells",
error(medium, exact_medium) / error(fine, exact_fine),
4.0,
0.02,
"x",
);
check_between(
" so 89 cells is off by",
error(medium, exact_medium) * 100.0,
0.0,
0.01,
"%",
);
heading("And the energy stays put");
let mut settled = room().released_in_mode(2, 1, Pressure::from_si(1.0));
let dt = Time::from_si(settled.max_stable_dt(Time::ZERO).to_si() * 0.9);
let mut bus = Exchange::new();
let started = settled.energy().to_si();
for _ in 0..4000 {
settled.step(Time::ZERO, dt, &mut bus).unwrap();
}
check_zero(
"energy drift over 4000 steps",
settled.energy().to_si() - started,
started,
1e-9,
"J",
);
heading("What the field reports, read through ScalarField");
let shape = room().released_in_mode(1, 1, Pressure::from_si(1.0));
let h = Length::from_si(WIDTH / (nx - 1) as f64);
let peak_dx = shape
.gradient(LengthVec::m(WIDTH / 2.0, 1.0, 0.0), Time::ZERO, h)
.x
.abs();
let peak_dy = shape
.gradient(LengthVec::m(1.0, HEIGHT / 2.0, 0.0), Time::ZERO, h)
.y
.abs();
check_zero(
"dp/dx on the left wall",
shape.gradient(LengthVec::m(0.0, 1.0, 0.0), Time::ZERO, h).x,
peak_dx,
1e-12,
"Pa/m",
);
check_zero(
"dp/dy on the floor",
shape.gradient(LengthVec::m(1.0, 0.0, 0.0), Time::ZERO, h).y,
peak_dy,
1e-12,
"Pa/m",
);
let dx = h.to_si();
let probe = LengthVec::from_si(glam::DVec3::new(13.0 * dx, 19.0 * dx, 0.0));
let omega =
-speed * speed * shape.laplacian(probe, Time::ZERO, h) / shape.at(probe, Time::ZERO);
check(
"mode (1,1) from the field's curvature",
omega.sqrt() / std::f64::consts::TAU,
quiet.mode_frequency(1, 1).to_si(),
1e-3,
"Hz",
);
println!(" the curvature is a spatial statement, so the wall defect does not touch it");
let Some(path) = common::output_path() else {
println!("\npass a path to write an SVG, e.g. `cargo run --example room_modes out.svg`");
return;
};
common::write(&path, &draw(&trace, quiet.mode_frequency(1, 1).to_si()));
}
fn draw(trace: &[(f64, f64)], f11: f64) -> String {
let (w, h) = (900.0, 470.0);
let mut parts = Vec::new();
let modes = [(1u32, 0u32), (0, 1), (1, 1), (2, 1)];
for (k, (a, b)) in modes.into_iter().enumerate() {
let shape = room().released_in_mode(a, b, Pressure::from_si(1.0));
let f = shape.mode_frequency(a, b).to_si();
let x = 56.0 + k as f64 * 214.0;
let (pw, ph) = (186.0, 186.0 * HEIGHT / WIDTH);
let mut panel = Plot::new(w, h, (0.0, WIDTH), (0.0, HEIGHT)).viewport(x, 56.0, pw, ph);
let n = 52;
panel.raster(n, n, (0.0, WIDTH), (0.0, HEIGHT), |i, j| {
let p = LengthVec::m(
WIDTH * (i as f64 + 0.5) / n as f64,
HEIGHT * (j as f64 + 0.5) / n as f64,
0.0,
);
diverging(shape.at(p, Time::ZERO))
});
panel.label(
x + pw / 2.0,
56.0 + ph + 22.0,
&format!("({a},{b}) {f:.1} Hz"),
12.5,
"#3a3a3a",
"middle",
);
parts.push(panel.into_body());
}
let span = trace.last().map(|(t, _)| *t).unwrap_or(1.0).min(0.06);
let mut wave =
Plot::new(w, h, (0.0, span * 1e3), (-1.15, 1.15)).viewport(56.0, 320.0, 788.0, 104.0);
wave.axes(
&ticks(0.0, span * 1e3, 8),
&[-1.0, 0.0, 1.0],
|v| format!("{v:.0}"),
|v| format!("{v:.0}"),
);
let period_ms = 1000.0 / f11;
let mut marker = period_ms;
while marker < span * 1e3 {
wave.polyline([(marker, -1.15), (marker, 1.15)], "#00000028", 1.0);
marker += period_ms;
}
wave.polyline(
trace
.iter()
.filter(|(t, _)| *t <= span)
.map(|(t, p)| (t * 1e3, *p)),
&rgb(28, 76, 168),
1.8,
);
wave.label(
56.0,
24.0,
"Rigid-wall modes of a 4.4 x 3.1 m room, and the corner pressure of mode (1,1)",
15.0,
"#1b1b1b",
"start",
);
wave.label(
844.0,
24.0,
&format!("grid lines every 1/f(1,1) = {period_ms:.1} ms"),
12.0,
"#6a6a6a",
"end",
);
wave.label(
450.0,
452.0,
"milliseconds -- the integration crosses the grid lines it was never told about",
11.5,
"#6a6a6a",
"middle",
);
parts.push(wave.into_body());
document(w, h, parts)
}