Modal2D · Guide
Practical recipes, keyboard reference, troubleshooting, and the mental models you need to read the code.
01 Recipes
Concrete walkthroughs. Each one assumes you've started the viewer and are in Play mode. Step counts in the headers are rough — your hands move faster than reading does.
- Toolbar: switch from Play to Create.
- Pick a shape from the Component combo. Start with
Box-2s— well-behaved, gives clean fundamentals. - Drag Oct: to pick the octave you want (e.g.
C4for middle C). - Click the C note button — that's the first target.
- Click Auto to make the tuner auto-strike between iterations.
- Click Fine-W. The strike button glows red — strike the shape once. The tuner then iterates on its own.
- When Fine-W converges, it auto-advances to D, then E, F, G, A, B, C′.
- When all 8 notes are green, click Save, then switch to Test to play your instrument.
Auto automates strike + advance so you can do all
8 notes hands-free. HF acceleration (on by default) typically converges
each note in 2–3 solves.
- Switch to Discover. The chromatic grid appears in the right panel.
- Set Lo / Hi octave drag-ints to span the range you care about (default C0–C9).
- Drag W / H / Shell sliders. Strike the shape after each change.
- If the detected freq lands within threshold cents of a chromatic note, the cell auto-fills.
- For bulk coverage, click Auto Explore: pick 1D, Size, or 2D, then how many steps per axis (2–16). The tuner sweeps the bracket and records every clean hit.
- Hover a filled cell to see
W,H, freq, and cents-to-note. Click any filled cell to load and play that shape.
- Discover or Build a few notes first (see previous recipe).
- Switch to Refine mode. Same grid view, but cells are now clickable and start a Fine-W session for that cell's chromatic freq.
- Click any filled cell — the shape loads at its saved (W, H), Fine-W kicks in.
- When it converges, the cell updates with the refined values; the displayed cents tightens up.
Use Bayes-W in these situations:
- Fine-W keeps "giving up" with a suboptimal-best message — the cost surface has a local minimum the binary search keeps falling into.
- The shape has multiple plausible modes near the target, and the loudest changes with W (which confuses Fine-W's bisection).
- You want to map the W-axis as a side effect; Bayes-W's 8 LHS seeds give you a near-uniform survey for free.
- In Create / Refine mode, instead of Fine-W click Bayes-W.
- Watch the GP heatmap in the Experimental tab populate. Green = low |cents| (good).
- Convergence: typically 8–15 samples. Falls back to "budget exhausted" with best-so-far if it can't get under threshold.
- Create mode → Component combo → String.
- Set Width = 0.15 (≈ 30 cm half-length).
- Set Height = 0.05 (controls beam cross-section:
beam_w = H · 0.01= 0.5 mm, realistic). - Open the post_process panel. Drag String tension from 0 to ~100 N. The shape re-solves on release.
- Strike it. At 100 N the fundamental should land around 80–150 Hz depending on material.
- Now run Fine-W on a chromatic target. Width still controls fundamental (longer = lower); tension shifts the whole spectrum.
K_geom = T/L · ...
on every beam edge. The fundamental moves toward
(1/2L)·√(T/μ) — pure-string physics. Without tension the
string only has bending stiffness, which makes for a stiff piano-wire-
without-tension sound rather than a guitar string.
- Create mode → Component combo → Strung. You get a hard-frame / soft-panel cavity with a steel string + bridge above the top panel. Material legend shows three regions.
- Open post_process. Set String tension to 80 N.
- Strike the string. The top panel rings via the bridge — soft panels radiate stronger than the hard frame.
- Now go to the Experimental tab → Air cavity. Toggle on, set freq ≈ 120 Hz, Q ≈ 12, gain ≈ 0.6.
- Strike again. The bandpass adds a body resonance around 120 Hz — the audible "boom" of a small cavity.
- Create mode. Open the Experimental tab.
- Drag Width or Height sliders. The mass-spring preview thumbnails update in microseconds — every mode shape recomputes live.
- When you see a shape with the mode distribution you want, release the slider. That fires the full FEM solve.
- FEM thumbnails appear next to the mass-spring ones (A/B view). Compare topology directly.
- Create or Refine mode. Strike the shape so the spectrum has visible bars.
- Left-click the spectrum bar of the mode you want to tune. A glowing "Mode N pinned" banner appears.
- Click Manual-W. The binary search now targets the pinned mode's frequency, not the fundamental.
- As the shape changes, the white arrow follows the pinned mode automatically (via MAC mode-shape correlation). The arrow's tint colour-codes confidence: green ≥ 0.60, yellow ≥ 0.30, red below.
- Red arrow = mode identity changed too much. Re-click a bar to re-pin.
- Right-click in the spectrum to clear the pin. Manual-W then falls through to fundamental tracking (same as Fine-W).
02 Keyboard shortcuts
Global
| Key | What |
|---|---|
| Esc | Cascade-cancel: fine-tune → draw-mode → create/test → no-op. Held doesn't double-fire (edge-detected). |
| Space | Clear all modal energy (silence). Useful when something rings forever. |
| D | Toggle polygon-draw mode. Click vertices, press Enter to triangulate. Disabled while solving. |
| S | Cycle slow-motion: realtime → 20× slow → 200× → 5000× → realtime. |
| C | Toggle Chladni mode — viewport shows the nodal pattern of the selected mode instead of live oscillation. |
| V | Toggle 2D / 3D view of the mesh. |
| M | Toggle medium-propagation visualization (acoustic waves around the shape). |
| P | Toggle plate-bending mode (DKT elements). Forces a re-solve. |
| B | Toggle boundary-beam stiffeners along the mesh outline. |
| Home | Reset camera (zoom = 1, pan = 0, 3D orbit reset). |
Play-mode preset hotkeys
| Key | Preset shape |
|---|---|
| 1 | RECT (square) |
| 2 | CIRCLE |
| 3 | L_SHAPE |
| 4 | EQUILATERAL (triangle) |
| 5 | TUNING_FORK |
| 6 | RING |
| 7 | STRING |
| 8 | CROSS |
| 9 | STAR |
Spectrum interactions
| Action | Effect |
|---|---|
| Left-click bar | Pin that mode (in Create / Refine tuning contexts). |
| Right-click anywhere in spectrum | Clear the pin. |
| Hover | Tooltip: mode index + Hz. |
| Right-click on drift slider | Reset freq-drift to 0 semitones. |
03 Fine-tune state machine
Every fine-tune mode (1, 2, 3, 4, 5, 6, 7, 8, 9) shares the same
fine_phase_ state machine. The pick-next logic varies per
mode; the phase transitions are universal.
The pickers vary by mode:
- Mode 1, 6, 9: bisection mid-point, or HF Newton step when available.
- Mode 2: bisection on a uniform scale factor.
- Mode 3: alternating W/H bisections per round.
- Mode 4: adaptive sampler scores bracket / near-best / gap regions, picks highest.
- Mode 5: sweep then per-bracket bisection.
- Mode 7, 8: GP fit + Expected-Improvement argmax on a fine grid.
04 Threading & data flow
Three threads operate concurrently. Mutexes guard the small set of shared state; the rest is single-owner.
Render thread (60 Hz)
- Process input (keyboard, mouse).
- Drain solve worker result if ready:
modal_res_ = solve_worker_.take_result(). - Apply per-frame state pushes (drift, damping, sympathetic config, gravity).
- Read displacements via
modal_get_displacements. - Write displacement VBO → draw mesh (OpenGL).
- ImGui begin → draw_layout (tabs, panels, viewport) → end.
- Swap buffers.
Solve worker (background thread)
- Started by
solve_worker_.start(modal_obj_, num_modes)from the render thread. - Runs
modal_solve: assemble K / M → eigensolve → build ModalResult. - Result picked up by the next render frame via
is_done(). - Joined before any destroy of
modal_obj_/modal_res_to avoid use-after-free.
Audio callback (PortAudio, ~5 ms blocks)
- Drain strike events from libmodal's strike_buf, project onto modes via
compute_strike_impulse. - Run GPU synthesis (CUDA kernel) → PCM samples per mode → sum.
- Apply air-cavity bandpass (if enabled).
- Run noise gate (HP-filter envelope, smoothstep).
- Post-process: gain → low-pass → soft-clip → auto-normalize.
- Write to PortAudio output buffer.
Strike flow: a click in the viewport calls
modal_strike(res, x, y, mag). That pushes to the audio
strike_buf (drained by the audio callback) AND immediately injects into
vis_impulses for visualisation (no audio latency).
05 File layout
Top-level split: libmodal/ (pure physics + audio synthesis,
no UI) and modal_viewer/ (App, OpenGL renderer, ImGui UI).
| Path | Responsibility |
|---|---|
libmodal/include/modal/modal.h | Public C++ API surface. Start here when integrating. |
libmodal/src/modal_api.cpp | Object / Result lifecycle, all preset shapes, strike dispatch, multi-material, sympathetic, MAC primitives, HF derivatives. |
libmodal/src/fem/assembler.cpp | Pure 2D CST assembly + multi-material variant. |
libmodal/src/fem/assembler_mixed.cpp | Mixed CST + beam assembly (3 DOFs per node). |
libmodal/src/fem/element_beam.cpp | Bernoulli beam element + geometric stiffness for tension. |
libmodal/src/fem/element_cst.cpp | Constant-strain triangle element (Ke, Me). |
libmodal/src/solver/eigensolve_sparse.cpp | Sparse Lanczos eigensolve. |
libmodal/src/audio/synthesis.cu | CUDA kernel for per-sample modal sum on GPU. |
libmodal/src/mesh/preset.cpp | Polygon vertex lists for every PresetShape. |
modal_viewer/src/app/app.cpp | Main loop, strike handling, fine-tune state machine, all per-frame logic. Largest file. |
modal_viewer/src/app/gaussian_process.h | Header-only GP for Bayes-W / Bayes-WH. |
modal_viewer/src/app/mass_spring.h | Header-only mass-spring lattice + Jacobi eigensolve. |
modal_viewer/src/app/solve_worker.cpp | Background thread wrapping modal_solve. |
modal_viewer/src/audio/audio_engine.cpp | PortAudio callback. Cavity bandpass, gate, post-process. |
modal_viewer/src/ui/ui_manager.cpp | ImGui dock layout + every tab and panel. Second-largest file. |
modal_viewer/src/ui/viz_spectrum.cpp | Frequency spectrum bar chart + pin handling + glowing banner. |
modal_viewer/src/renderer/mesh_renderer.cpp | OpenGL VAO / VBO for mesh + displacements. |
06 Glossary
- Mode
- A standing-wave pattern of vibration. The shape vibrates as a sum of decoupled modes; each has a frequency, a shape, and a decay rate.
- Eigenmode
- Same thing as a mode. The "eigen" emphasises that it's the solution to the generalised eigenvalue problem
K φ = λ M φ. - K
- Stiffness matrix. Encodes how much force a unit displacement at each DOF generates. Symmetric, sparse, positive-semi-definite (3 rigid-body modes give it a 3-dim nullspace before BCs).
- M
- Mass matrix. Inertia per DOF. Diagonal-ish; we use the consistent form.
- ω (omega)
- Angular frequency in rad/s. The audible frequency in Hz is
ω / (2π). - ζ (zeta)
- Damping ratio. Decay rate of a mode is
ζ · ω; the e-folding time is1 / (ζ·ω). - α (alpha)
- Radiation efficiency per mode. Computed from the modal velocity normal to the boundary and the medium's impedance. Controls how loud each mode is in the air.
- φ (phi)
- Eigenvector. The mode's shape, stored as displacement per DOF. M-normalised so
φᵀ M φ = 1. - CST
- Constant-Strain Triangle. The simplest 2D plane-stress finite element. 3 nodes × 2 DOFs = 6 DOFs per element.
- DKT
- Discrete Kirchhoff Triangle. Plate-bending element with 3 DOFs/node (w, θx, θy). Produces out-of-plane modes.
- MAC
- Modal Assurance Criterion.
|φ_a · φ_b|² / ((φ_a · φ_a)(φ_b · φ_b)). 1 = identical shape, 0 = orthogonal. We use it for mode tracking across W changes in Manual-W. - HF
- Hellmann-Feynman theorem. Says
dλ/dp = φᵀ(dK/dp − λ dM/dp)φfor an eigenpair. Gives usdf/dWwithout doing a new eigensolve, used for Newton steps in Fine-W / Manual-W. - K_geom
- Geometric stiffness. Extra stiffness from pre-stress (tension, compression, gravity-induced strain). For a tensioned beam, scales as
T/Land stiffens the transverse motion. - GP / EI
- Gaussian Process / Expected Improvement. The GP fits a surrogate over (W, H) → |cents|; EI picks the next sample by trading off predicted improvement vs uncertainty. Drives Bayes-W and Bayes-WH.
- LHS
- Latin Hypercube Sampling. Better space coverage than uniform random for small sample counts. Used to seed Bayes-W / Bayes-WH before EI takes over.
- Lanczos
- The sparse iterative eigensolver we use. Computes the lowest N eigenvalues of
(K, M)without dense factorisation. Cheap for small N. - Rayleigh quotient
R(x) = (xᵀ K x) / (xᵀ M x). Equals the eigenvalue when x is the eigenvector; stationary at eigenvectors. Used internally by HF for second-order-accurate FD of dλ/dW.- Strike impulse
- Per-mode amplitude added when you hit the shape. Computed as the eigenvector's value at the nearest node, projected onto the strike direction (boundary normal for boundary nodes; +Y for interior).
- Helmholtz mode
- The lowest acoustic mode of an enclosed air volume connected to outside via a small opening. Has a simple closed form:
f = (c/2π)·√(A/(V·L)). We currently model this as a single bandpass; full cavity-FEM is planned. - Boundary clamp / fix_level
- How many boundary nodes are pinned to zero displacement. 0 = free-free (rigid-body modes appear), 1–3 = partial, 4 = clamped all-around (membrane-only modes).
07 Troubleshooting
2^(s/12). Either reset drift to 0 or enable
comp next to it so future fine-tune sessions
pre-compensate. (2) The instrument was tuned with the flexible
toggle on; the saved freqs are correct but the chromatic labels reflect
a relative octave, not absolute. Save with flexible off if you want
absolute pitch labels.
08 Basics: modal analysis from first principles
This section builds up from the simplest example (a plucked guitar string) to the framework the rest of the viewer uses. No prior knowledge of FEM or eigenvalues assumed — equations introduced as they become necessary, never before.
8.1 · What is modal analysis?
Pluck a guitar string. You hear a pitch (the fundamental), plus quieter higher pitches (overtones). The string isn't vibrating in just one shape — it's vibrating in many shapes at once, each with its own frequency. Each of those independent vibration patterns is a mode, and the observation that any motion of the string can be written as a sum of them is the heart of modal analysis.
Why is this useful? Because each mode evolves independently. The fundamental does its thing, the second overtone does its thing, and they don't interfere. So if we can find the modes once and know how hard each one was struck, we can compute the entire future of the vibrating object by summing analytical sine waves — no iterative physics simulation needed.
8.2 · Eigenmodes of a 1D string
The simplest case. A string of length L, tension T, and mass per length μ, pinned at both ends, has displacement v(x, t) obeying the wave equation:
Try a separated-variables solution v(x, t) = φ(x) · sin(ωt).
Substituting gives
plus the boundary conditions φ(0) = φ(L) = 0. The only
solutions are
Each φ_n is a mode shape; each
ω_n is its natural angular frequency. The
pair (φ_n, ω_n) is called an eigenmode because
the equation above is an eigenvalue problem in disguise: φ_n
is an eigenvector of the operator −(T/μ) ∂²/∂x² with
eigenvalue ω_n².
The first three modes look like:
Three things to notice that generalise to all modes, in any geometry:
- Each mode has its own frequency. Higher modes oscillate faster.
- Each mode has a fixed shape. Mode 3 always vibrates with two interior zeros (nodes); it doesn't morph into mode 2.
- Different modes don't interfere. A plucked string's motion is exactly the sum of every mode's contribution, each evolving independently in time.
8.3 · Striking, sound, and decay
A real strike is a transient: a brief force impulse at one position on the string. The string's response is the sum of mode contributions, each scaled by how much the strike "looked like" that mode:
Where A_n is the projection of the strike onto mode
n — essentially, "how much does mode n's shape match where
I struck?". Strike at the middle of the string (x = L/2):
odd modes (1, 3, 5, …) have a peak there, so they ring loud; even
modes (2, 4, …) have a zero there, so they ring not at all. This is
why striking different parts of an instrument changes its timbre.
Real materials dissipate energy. We model that with a damping ratio
ζ_n per mode, giving an exponential envelope:
Two consequences worth absorbing:
- Higher modes decay faster at the same
ζ, because the exponentζ ω tgrows quicker withω. That's why a struck bell's bright initial timbre mellows out over time — the overtones die first. - Decay time is
1 / (ζ ω). Forζ = 0.01and an audible 440 Hz mode (ω ≈ 2765 rad/s), that's ~36 ms — a percussive sound. Forζ = 0.0001it's 3.6 seconds — a bell.
To turn vibration into sound, every mode shape radiates into
the surrounding air with an efficiency α_n that depends
on its shape and the medium. The final audio signal is the radiated
pressure at the listener position; in the viewer we approximate this
as a weighted sum where each mode's contribution scales by
α_n:
α_n is computed from the modal velocity normal to the
boundary and the medium's impedance. This is why setting the
Acoustic Medium to vacuum-like properties silences the output — the
structure still vibrates, it just doesn't radiate.
8.4 · Going from 1D to 2D shapes
A 1D string has modes with a single index n: shape number 1, 2, 3, …, ordered by frequency. A 2D shape has modes indexed by two numbers in the simplest case (number of bulges along x, number along y), and ordered modes start to mix the two indices. For a rectangular plate fixed at the edges:
The patterns of zero displacement are nodal lines; for higher modes they form intricate curves. In Chladni's classical experiment (the demo this viewer's Chladni-mode C toggle is named for), sand sprinkled on a plate gathers along these nodal lines because they're the only places that don't shake the sand off.
For arbitrary shapes (not just rectangles), the modes can't be written as products of 1D sines — they're whatever shape comes out of solving the 2D wave equation with the actual boundary. Closed-form solutions rarely exist. We need a numerical method: FEM.
8.5 · FEM: from continuous PDE to a matrix problem
The 2D wave equation we want to solve is
in some 2D region, with material parameters E (stiffness), ρ (density), and boundary conditions on the region's edge. Three steps turn this into something a computer can chew on:
Step 1: mesh the shape
Cut the 2D region into a triangulation. Each triangle has three nodes
at its corners. The displacement field u(x, t) is approximated
as linear interpolation between the displacement values at
those nodes. So instead of a continuous unknown function we have a
finite list of unknowns: u_i(t), one per node DOF.
Triangulation in this viewer is done with shewchuk's Triangle library,
invoked via mesh::triangulate in
libmodal/src/mesh/triangulator.cpp.
Step 2: assemble K and M
For each triangle, you can derive (from the wave equation + linear
interpolation) a small element stiffness matrix
K_e (6×6 for the 2D CST element — 3 nodes × 2 DOFs) and
an element mass matrix M_e. The full
system is built by summing contributions from every element into
global sparse matrices:
K encodes how much force each unit displacement produces;
M encodes inertia. Both are sparse because each node only
couples to its triangle neighbours.
Assembly happens in libmodal/src/fem/assembler.cpp (pure
2D CST) and assembler_mixed.cpp (CST + beam elements for
strings). The element matrices themselves come from
element_cst.cpp and element_beam.cpp.
Step 3: eigensolve
For free vibration (no external force, f = 0)
we look for solutions of the form u(t) = φ · sin(ωt).
Substituting into M ü + K u = 0 gives
Dropping the time factor leaves the generalised eigenvalue problem:
Solve this and you get a discrete set of eigenpairs
(ω_k, φ_k) — exactly the modes from §8.2, but for an
arbitrary 2D geometry instead of a 1D string. We solve typically
24 – 120 of the lowest modes using sparse Lanczos iteration in
libmodal/src/solver/eigensolve_sparse.cpp.
M
isn't the identity. A standard eigenvalue problem looks like
A φ = λ φ — what we have is K φ = ω² M φ,
a generalised eigenvalue problem. It's still solvable cheaply because
M is positive-definite, so we can reduce to standard
form via Cholesky factorisation of M, but the algorithm is slightly
different from the basic power method.
8.6 · How a click becomes a sound
End-to-end trace of what happens when you click the viewport. This is the complete pipeline; every step touches a specific file in the codebase.
Three details that surprise newcomers:
- Visual and audio are decoupled. They share strike events but run on different threads with different time bases. Visual uses scaled time (S key cycles slow-motion), audio always runs at real time. Each path stores its own copy of recent impulses.
- Audio synthesis is analytical, not simulated. The
CUDA kernel computes the exact sum
Σ α_k A_k exp(−ζω t) sin(ω_d t)at every PCM sample, with no time-stepping. No accumulated error, no stability concerns, and trivially parallel across samples. - The strike acts on one node. We snap the world
click to the nearest mesh node, then project that node's eigenvector
entry onto the strike direction (outward boundary normal for
boundary nodes, +Y for interior). The whole strike-to-modes
projection lives in
compute_strike_impulse.
8.7 · Why the same machinery handles strings, plates, and bells
Everything in §8.5 and §8.6 was general. The viewer has different
elements (CST triangle, Bernoulli beam, DKT plate) but each
one just contributes its own little K_e and M_e
block during assembly. After assembly the eigenvalue problem looks
identical regardless of which elements were used. Same Lanczos solve,
same modal sum for synthesis.
That's why adding tension to strings (§7) was a small change: tension
contributes an extra term K_geom to the beam element's
K_e. Everything downstream — eigensolve, MAC, HF
derivatives, audio synthesis — is unchanged. The element layer is
where the physics lives; everything above it operates on abstract
sparse matrices.
Multi-material support (§5) works the same way: each triangle's
element matrix is computed with its own material, then the assembler
sums all elements into a single global K and M.
The eigensolver doesn't know or care that the material varies — it
just sees a different sparse pattern of stiffness values.
8.8 · Deeper: Hellmann-Feynman, MAC, geometric stiffness
With the basics in place, the three "advanced" results in the rest of the viewer become short corollaries.
Why Hellmann-Feynman is cheap
Conventional derivative df/dW: change W, redo the entire
eigensolve, finite-difference. Cost: one extra eigensolve per
derivative.
Hellmann-Feynman insight: for an eigenpair (λ, φ) of
K φ = λ M φ with M-normalisation φᵀ M φ = 1,
No new eigensolve. Cost: one matrix re-assembly at perturbed W (much cheaper than eigendecomposition) plus a quadratic-form evaluation. Total ~5% of an eigensolve. We use this for Newton-step Fine-W / Manual-W via the Rayleigh-quotient formulation, which handles M-normalisation automatically (see features §3).
MAC mode tracking
Two eigenvectors φ_a and φ_b from different
solves are "the same mode" if their shapes correlate strongly. The
Modal Assurance Criterion measures this with a single scalar in
[0, 1]:
The square handles sign flips (eigenvectors are only defined up to sign). Above 0.6 means "clearly the same mode topology"; below 0.3 means the modes diverged — the geometry change crossed a mode-identity boundary and the tracker needs a fresh reference. Used by Manual-W to auto-retrack the pinned mode across W changes.
Geometric stiffness from tension
A taut string's pitch comes from tension, not bending. Why? The transverse equation for a beam under axial tension T is
When T · L² ≫ EI the bending term EI ∂⁴v/∂x⁴
is negligible and the equation collapses to the pure-string wave
equation from §8.2. Frequencies follow f_n = (n/2L) · √(T/μ).
In FEM terms, the tension contributes a geometric stiffness
matrix K_geom = T/L · [transverse-block] that adds to the
usual bending stiffness:
High tension → geometric term dominates → string-like behaviour.
Zero tension → pure bending beam. Same eigensolve handles both
regimes because the only thing that changed is the values in
K.
09 Further reading
Modal synthesis
- Adrien, J.-M. "The Missing Link: Modal Synthesis" — the foundational paper on the analytical sum-of-modes approach to audio.
- van den Doel, K. & Pai, D. "The Sounds of Physical Shapes" — modal sound generation for shape-aware audio in graphics.
- O'Brien, J., Cook, P., Essl, G. "Synthesizing Sounds from Physically Based Motion" — coupling FEM to audio in real time.
Finite element method
- Cook, R. Concepts and Applications of Finite Element Analysis. Canonical reference; CST, beam, plate elements all derive here.
- Bathe, K.-J. Finite Element Procedures. The standard graduate text. Sparse eigensolve methods covered in depth.
Mode tracking and sensitivity
- Allemang, R. "The Modal Assurance Criterion — Twenty Years of Use and Abuse" (Sound and Vibration, 2003). Practical guide to MAC.
- Fox, R., Kapoor, M. "Rates of Change of Eigenvalues and Eigenvectors". Original Hellmann-Feynman application to FEM eigenproblems.
Bayesian optimisation
- Rasmussen, C., Williams, C. Gaussian Processes for Machine Learning. Free online; chapters 2–5 cover what we use.
- Frazier, P. "A Tutorial on Bayesian Optimization" (arXiv 1807.02811). EI, acquisition strategies, when BO works.
Acoustic radiation
- Fahy, F. Sound and Structural Vibration. Structure → air coupling, radiation efficiency, near vs far field.
- Rossing, T. Science of Sound. Approachable, covers Helmholtz, instrument-cavity physics, anisotropic materials.
This codebase
features.html— what's here, command-reference style.roadmap.html— upcoming work, longer-term trajectory.project-structure.html— full code tree with descriptions.