// Everything the time loop needs, built once from a configuration: profile, mesh, shell, time // step, hit schedule and radiation, plus a human-readable summary of the set-up. import type { SimulationConfig } from "../config/schema.ts"; import { buildPatch, buildSchedule, freeContactTime, patchEnergy, type ContactPatch, type HitSchedule } from "./exciter.ts"; import { CymbalProfile } from "./geometry.ts"; import { diskMesh, type Mesh } from "./mesh.ts"; import { buildRadiation, type Radiation } from "./radiation.ts"; import { buildShell, type Shell } from "./shell.ts"; export interface Setup { config: SimulationConfig; profile: CymbalProfile; mesh: Mesh; shell: Shell; fs: number; dt: number; substeps: number; nSamples: number; nSteps: number; patches: ContactPatch[]; schedule: HitSchedule; radiation: Radiation; summary: SetupSummary; } export interface SetupSummary { nodes: number; elements: number; dofs: number; mass: number; height: number; rimWarp: number; dents: number; hMin: number; hMax: number; maxSlope: number; slopeWarning: boolean; omegaMax: number; limitingR: number; limitingH: number; dt: number; substeps: number; nSteps: number; setupSeconds: number; hits: { time: number; kind: string; points: number; r: number; kTotal: number; alpha: number; mass: number; velocity: number; energy: number; freeContact: number; directBelow: number; late: boolean }[]; } /** Build the set-up; ``progress`` is awaited between phases so a UI can repaint. */ export async function buildSetup(config: SimulationConfig, progress?: (phase: string) => Promise | void): Promise { const t0 = performance.now(); const step = async (s: string) => { if (progress) await progress(s); }; await step("profile and mesh"); const profile = new CymbalProfile(config.profile, config.imperfections); const mesh = diskMesh(config.profile.radius, config.mesh.element_size, config.mesh.smoothing); await step(`shell assembly (${mesh.n} nodes, ${mesh.e} elements)`); const shell = buildShell(mesh, profile, config.material, config.numerics); const num = config.numerics, fs = num.fs; const substeps = Math.ceil(1 / (fs * num.cfl_safety * shell.dtStable)); const dt = 1 / (fs * substeps); const nSamples = Math.ceil(num.duration * fs); const nSteps = nSamples * substeps; await step("hits and radiation"); const patches = config.hits.map((h) => buildPatch(h, mesh, profile)); const schedule = buildSchedule(patches, dt); const radiation = buildRadiation(mesh, profile, shell.z0Node, config.listener, fs, substeps); let zMax = -Infinity, rimMin = Infinity, rimMax = -Infinity, hMin = Infinity, hMax = -Infinity; for (const z of shell.z0Node) zMax = Math.max(zMax, z); for (const b of mesh.boundary) { rimMin = Math.min(rimMin, shell.z0Node[b]); rimMax = Math.max(rimMax, shell.z0Node[b]); } for (const h of shell.hE) { hMin = Math.min(hMin, h); hMax = Math.max(hMax, h); } const le = shell.limitingElement; const summary: SetupSummary = { nodes: mesh.n, elements: mesh.e, dofs: 5 * mesh.n, mass: shell.mass, height: zMax, rimWarp: rimMax - rimMin, dents: profile.dentX.length, hMin, hMax, maxSlope: shell.maxSlope, slopeWarning: shell.maxSlope > config.profile.max_slope, omegaMax: shell.omegaMax, limitingR: Math.hypot(mesh.centroid[2 * le], mesh.centroid[2 * le + 1]), limitingH: shell.hE[le], dt, substeps, nSteps, setupSeconds: 0, hits: patches.map((p) => { const tau = freeContactTime(p); return { time: p.time, kind: p.hit.exciter.kind, points: p.x.length, r: p.hit.r, kTotal: p.kTotal, alpha: p.alpha, mass: p.mass, velocity: p.velocity, energy: patchEnergy(p), freeContact: tau, directBelow: 0.5 / tau, late: p.time >= num.duration }; }), }; summary.setupSeconds = (performance.now() - t0) / 1000; return { config, profile, mesh, shell, fs, dt, substeps, nSamples, nSteps, patches, schedule, radiation, summary }; }