/** * The available scenes: their MATLAB source, and the parameters the host * offers each one. * * Same split as the models (src/mgpu/registry.ts): what the medium *is* lives * in the .m, and the sliders around it live here. A scene also gets `x`, `y` * (metres), `L`, `h` (metres), `c0` (metres per second — the speed of sound * in air), `npts`, `nx` and `ny` for free — see src/scene/scene.ts. * * Every scene here takes its speed contrasts (`cin`, `cwall`, `dn`, `amp`) as * plain ratios to `c0` rather than as absolute speeds, so a slider means the * same thing whatever the background is. Its lengths (`R`, `side`, `gap`, * `w`, ...) are metres, and its rates (`absorb`) are inverse seconds. */ import type { ParamSpec, Params } from '../mgpu/registry.ts'; import diskSource from '../../scenes/disk.m?raw'; import roomSource from '../../scenes/room.m?raw'; import slitSource from '../../scenes/slit.m?raw'; import lensSource from '../../scenes/lens.m?raw'; import speckleSource from '../../scenes/speckle.m?raw'; export interface MScene { key: string; label: string; blurb: string; params: ParamSpec[]; /** * Model parameters this scene wants set when it is chosen — a source * position, usually. A scene cannot reach into the model's parameters, and * should not: they belong to different files. But a room is no use with the * source outside it, so a scene may say what it would like, and the app * applies it as if the sliders had been moved by hand. */ suggest?: Params; /** Where this scene would like the microphone. Inside the room, for a room. * Applied like `suggest`, and movable afterwards. */ mic?: { x: number; y: number }; source: string; } const disk: MScene = { key: 'disk', label: 'Disk', blurb: 'One circular scatterer — the case with an exact series solution.', params: [ { key: 'cin', label: 'speed inside (×c0)', value: 0.5, min: 0.2, max: 3, step: 0.05, hint: 'Ratio to the background speed. Well above 1 is nearly rigid, well below nearly pressure-release, and exactly 1 is no scatterer at all.', }, { key: 'R', label: 'radius (m)', value: 0.75, min: 0.1, max: 2.5, step: 0.05 }, { key: 'absorb', label: 'absorption (1/s)', value: 0, min: 0, max: 1500, step: 50, hint: 'Damping inside the disk. Turn it up and the scatterer swallows what enters it instead of ringing.', }, ], source: diskSource, }; const room: MScene = { key: 'room', label: 'Room', blurb: 'An enclosure with a doorway: echoes, modes, and something to listen to.', params: [ { key: 'cwall', label: 'wall speed (×c0)', value: 0.15, min: 0.05, max: 6, step: 0.05, hint: 'Ratio to the background speed. Below 1 the wall is slower than the room and reflects without costing timestep; above 1 it is rigid, and the timestep drops to match. Either way what reflects is |cwall-1|/(cwall+1).', }, { key: 'side', label: 'room half-width (m)', value: 1.5, min: 0.5, max: 2.5, step: 0.05, hint: 'The room is 2×side across — a real room, not a fraction of the domain.', }, { key: 'gap', label: 'doorway (m)', value: 0.9, min: 0, max: 2, step: 0.05, hint: 'Width of the opening in the right-hand wall — 0.9 m is a typical interior door. At 0 the room is sealed and the sound never leaves.', }, { key: 'thick', label: 'wall thickness (m)', value: 0.1, min: 0.03, max: 0.3, step: 0.01 }, { key: 'absorb', label: 'wall absorption (1/s)', value: 70, min: 0, max: 1500, step: 10, hint: 'Damping inside the wall. It swallows whatever gets in rather than letting it rattle around in there, and it is what sets how long the room rings. At 0 a sealed room rings almost forever.', }, ], // A click from a point inside the room, off-centre so it does not excite // only the symmetric modes, heard from the far corner. 700 Hz on a 1.5 m // half-width room is a wavelength of about half a metre — small enough to // show interference between the direct sound and the walls, and (at the // app's default 512-point grid over a 10 m domain) resolved to about 25 // cells per wavelength. suggest: { point: 1, x0: -0.7, y0: 0.5, w: 0.03, f: 700, tw: 0.003, t0: 0.015, cw: 0 }, mic: { x: 0.9, y: -0.7 }, source: roomSource, }; const slit: MScene = { key: 'slit', label: 'Two slits', blurb: 'A hard screen with two apertures: diffraction and interference.', params: [ { key: 'cwall', label: 'wall speed (×c0)', value: 4, min: 1.5, max: 8, step: 0.1, hint: 'Ratio to the background speed.', }, { key: 'gap', label: 'slit width (m)', value: 0.4, min: 0.05, max: 2, step: 0.02 }, { key: 'sep', label: 'slit separation (m)', value: 2, min: 0.5, max: 4, step: 0.05 }, { key: 'thick', label: 'screen thickness (m)', value: 0.3, min: 0.05, max: 1, step: 0.02 }, ], suggest: { point: 0, f: 400, tw: 0.008, t0: 0.04, cw: 0, x0: -3 }, source: slitSource, }; const lens: MScene = { key: 'lens', label: 'Lens', blurb: 'A smooth slow patch that refracts a plane wave to a focus.', params: [ { key: 'dn', label: 'speed dip (fraction of c0)', value: 0.35, min: 0, max: 0.8, step: 0.01, hint: 'Fraction of the background speed the centre of the lens is slower by. Deeper means a shorter focal length.', }, { key: 'w', label: 'lens width (m)', value: 1.5, min: 0.5, max: 3, step: 0.05 }, { key: 'x0', label: 'lens x (m)', value: -1, min: -3, max: 3, step: 0.1 }, ], suggest: { point: 0, f: 400, tw: 0.008, t0: 0.04, cw: 0, x0: -3 }, source: lensSource, }; const speckle: MScene = { key: 'speckle', label: 'Random medium', blurb: 'Weak random structure everywhere: multiple scattering, and a coda.', params: [ { key: 'amp', label: 'contrast (fraction of c0)', value: 0.15, min: 0, max: 0.4, step: 0.01 }, { key: 'kc', label: 'structure scale (rad/m)', value: 6, min: 1, max: 20, step: 0.5, hint: 'Centre wavenumber (2π / wavelength) of the random field. Scattering is strongest when this is comparable to the wave’s own.', }, { key: 'seed', label: 'seed', value: 1, min: 1, max: 9999, step: 1, reseed: true }, ], suggest: { point: 0, f: 400, tw: 0.008, t0: 0.04, cw: 0, x0: -3 }, source: speckleSource, }; // The room first: it is the one that shows the most, and the one the // microphone is for. export const mScenes: MScene[] = [room, disk, slit, lens, speckle]; export const mSceneByKey = (key: string): MScene | undefined => mScenes.find((s) => s.key === key); export const defaultSceneParams = (s: MScene): Params => Object.fromEntries(s.params.map((p) => [p.key, p.value]));