/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
Adding a way to reset simulation from same random IC.
Owen Melia <owenjmelia@gmail.com> committed commit ca3795529376 parent c8e230a Browse files
4 changed files+110−4
index.htmlmodified+2−1View file
@@ -330,12 +330,13 @@
330330 </div>
331331 <div class="controls ctrl-group" data-group="playback">
332332 <button id="runpause" class="primary">Run</button>
333+ <button id="restart" title="Rewind to the initial condition this run started from, without drawing a new one">Restart</button>
333334 </div>
334335 <div class="controls ctrl-group" data-group="benchmark">
335336 <button id="benchmark">Benchmark</button>
336337 </div>
337338 <div class="controls ctrl-group" data-group="seed">
338- <label title="Wavelength of the smooth random field the initial condition is seeded from (chebfun's randnfun3, restricted to the surface). An absolute length in the surface's own units, as in chebfun — not a fraction of its size — so smaller means finer features to grow from. Nothing caps it but memory and patience — 0.02 takes about 3 s to seed, 0.01 about 25 s — but it is only *useful* down to about 2*pi/lmax (0.1 at lmax 63): below that the field carries more detail than the grid holds, init's analys discards it, and the seed gets weaker rather than finer. Raise lmax to go finer.">seed λ
339+ <label title="Wavelength of the smooth random field generating the initial condition">Initial condition wavelength λ
339340 <input id="lam3" type="number" min="0" step="0.05" value="0.5">
340341 </label>
341342 <button id="reseed">Re-seed</button>
src/compare/compareRun.tsmodified+56−1View file
@@ -145,6 +145,11 @@ export class CompareRun {
145145 #opts: CompareOptions;
146146 #rows: Row[] = [];
147147 #fileRow: FileRow | null = null;
148+ /** What restart() reloads: the file's fixed state if opts.refFile is set,
149+ * otherwise a snapshot of the coarsest variant's state as of the last
150+ * (re-)seed — see the capture in create() and in reseed()'s plain branch. */
151+ #initial: Record<string, Float32Array>;
152+ #initialLmax: number;
148153 /** Base steps taken since the initial state — the refFile clock. */
149154 #stepsDone = 0;
150155 /** True once a refFile run has reached the file's end time. */
@@ -178,6 +183,8 @@ export class CompareRun {
178183 rangeBars: { fill: (lo: number, hi: number) => void }[];
179184 frameSteps: number;
180185 note: string;
186+ initial: Record<string, Float32Array>;
187+ initialLmax: number;
181188 }) {
182189 this.#opts = init.opts;
183190 this.#rows = init.rows;
@@ -189,6 +196,8 @@ export class CompareRun {
189196 this.#note = init.note;
190197 this.#morph = init.opts.morph;
191198 this.#ranges = init.opts.model.species.map(() => ({ lo: NaN, hi: NaN }));
199+ this.#initial = init.initial;
200+ this.#initialLmax = init.initialLmax;
192201 }
193202
194203 get variants(): Variant[] {
@@ -264,6 +273,13 @@ export class CompareRun {
264273 for (const s of sessions) await s.setDisplayGrid(nlat, nphi);
265274
266275 // ---- one initial condition, on every grid ---------------------------
276+ // Also what restart() reloads later — the file's fixed state, or (for
277+ // the plain case) a snapshot of the coarsest variant's own state,
278+ // taken after seeding it: the same lowest-lmax session sharedNoise
279+ // itself draws from, so prolonging it up to any other variant later is
280+ // always widening a band, never narrowing one.
281+ let initial: Record<string, Float32Array>;
282+ let initialLmax: number;
267283 if (opts.refFile) {
268284 // The file's exact spectral state, prolonged into each variant's band.
269285 // Exact, not approximate: the state is band-limited at the file's lmax
@@ -273,6 +289,8 @@ export class CompareRun {
273289 for (const s of sessions) {
274290 s.loadState(prolongState(opts.refFile.initial, model.state, opts.refFile.lmax, s.cfg.lmax));
275291 }
292+ initial = opts.refFile.initial;
293+ initialLmax = opts.refFile.lmax;
276294 } else {
277295 opts.onStatus('seeding all variants from one band-limited perturbation…');
278296 const noise = await sharedNoise(sessions, model.seedAmp, opts.seed);
@@ -280,6 +298,10 @@ export class CompareRun {
280298 // One at a time: a seed submits its whole mode sum in pieces, and there
281299 // is nothing to gain from interleaving several variants' worth of it.
282300 for (let i = 0; i < sessions.length; i++) await sessions[i].seedWith(noise[i], modes);
301+ let coarsest = sessions[0];
302+ for (const s of sessions) if (s.cfg.lmax < coarsest.cfg.lmax) coarsest = s;
303+ initial = await coarsest.readState();
304+ initialLmax = coarsest.cfg.lmax;
283305 }
284306
285307 // ---- the mesh, shared; the surface, per variant ---------------------
@@ -325,7 +347,7 @@ export class CompareRun {
325347 ` · ops/step ${ops.join(', ')}`;
326348
327349 const run = new CompareRun({
328- opts, rows, fileRow, topo, weights, rangeBars, frameSteps, note,
350+ opts, rows, fileRow, topo, weights, rangeBars, frameSteps, note, initial, initialLmax,
329351 });
330352 await run.draw();
331353 run.#observeResize();
@@ -374,6 +396,39 @@ export class CompareRun {
374396 if (this.#disposed) return;
375397 await sessions[i].seedWith(noise[i], modes);
376398 }
399+ if (this.#disposed) return;
400+ // This draw becomes what restart() rewinds to from now on — see the
401+ // identical selection in create(). Recaptured here rather than left
402+ // pointing at the pre-reseed field.
403+ let coarsest = sessions[0];
404+ for (const s of sessions) if (s.cfg.lmax < coarsest.cfg.lmax) coarsest = s;
405+ this.#initial = await coarsest.readState();
406+ this.#initialLmax = coarsest.cfg.lmax;
407+ }
408+ this.#t = 0;
409+ this.#stepsDone = 0;
410+ this.#finished = false;
411+ for (const r of this.#ranges) {
412+ r.lo = NaN;
413+ r.hi = NaN;
414+ }
415+ await this.draw();
416+ this.#status();
417+ if (!this.#disposed && wasRunning) this.setRunning(true);
418+ }
419+
420+ /** Rewind every variant to the saved initial condition — the file's fixed
421+ * state, or (for the plain case) the last (re-)seed, not necessarily the
422+ * very first one — without drawing anything new. */
423+ async restart(): Promise<void> {
424+ const wasRunning = this.#running;
425+ this.#running = false;
426+ while (this.#pumping) await nextFrame();
427+ if (this.#disposed) return;
428+ for (const r of this.#rows) {
429+ r.session.loadState(
430+ prolongState(this.#initial, this.#opts.model.state, this.#initialLmax, r.session.cfg.lmax),
431+ );
377432 }
378433 this.#t = 0;
379434 this.#stepsDone = 0;
src/main.tsmodified+37−2View file
@@ -53,6 +53,7 @@ const elLmax = $<HTMLSelectElement>('lmax');
5353 const elOversample = $<HTMLSelectElement>('oversample');
5454 const elColormap = $<HTMLSelectElement>('colormap');
5555 const elRunPause = $<HTMLButtonElement>('runpause');
56+const elRestart = $<HTMLButtonElement>('restart');
5657 const elBenchmark = $<HTMLButtonElement>('benchmark');
5758 const elReseed = $<HTMLButtonElement>('reseed');
5859 const elLam3 = $<HTMLInputElement>('lam3');
@@ -276,6 +277,11 @@ let posBuf: Float32Array | null = null;
276277 * mode. While it is non-null there is no `session`: the study owns one per
277278 * variant, and the panels area is its grid. */
278279 let compareRun: CompareRun | null = null;
280+/** `session`'s spectral state as of the last (re-)seed — what "Restart"
281+ * rewinds to. Captured fresh each time a new field is actually established
282+ * (rebuild/reseed), not just once, so Restart reflects the run's current
283+ * starting point rather than permanently the very first draw. */
284+let initialState: Record<string, Float32Array> | null = null;
279285
280286 const source = (): string => editedSource ?? model.source;
281287 const geomSource = (): string => editedGeomSource ?? geometry.source;
@@ -523,6 +529,10 @@ elReseed.addEventListener('click', () => {
523529 updateCommand();
524530 void reseed();
525531 });
532+elRestart.addEventListener('click', () => {
533+ setRunning(false);
534+ void restart();
535+});
526536 elResetView.addEventListener('click', () => {
527537 compareRun?.resetView();
528538 for (const s of scenes) s.resetCamera();
@@ -783,6 +793,9 @@ async function rebuild(): Promise<void> {
783793 if (gen !== generation) return;
784794
785795 await session.seed(seed);
796+ if (gen !== generation) return;
797+ initialState = await session.readState();
798+ if (gen !== generation) return;
786799
787800 const plan = session.describe();
788801 elCompiled.textContent =
@@ -817,6 +830,24 @@ async function reseed(): Promise<void> {
817830 const gen = generation;
818831 await session.seed(seed);
819832 if (gen !== generation) return;
833+ initialState = await session.readState();
834+ if (gen !== generation) return;
835+ for (const r of ranges) {
836+ r.lo = NaN;
837+ r.hi = NaN;
838+ }
839+ await draw();
840+ updateStats();
841+}
842+
843+/** Rewind to the field this run is currently starting from — the last
844+ * (re-)seed, not necessarily the very first one — without drawing a new
845+ * one. Unlike reseed(), the seed value and lam3 are untouched, so nothing
846+ * the CLI command line encodes changes. */
847+async function restart(): Promise<void> {
848+ if (compareRun) return compareRun.restart();
849+ if (!session || !initialState) return;
850+ session.loadState(initialState);
820851 for (const r of ranges) {
821852 r.lo = NaN;
822853 r.hi = NaN;
@@ -1018,7 +1049,7 @@ function submitSteps(n: number): void {
10181049 function setMovieUi(on: boolean): void {
10191050 const locked = [
10201051 elModel, elGeometry, elMorph, elNiter, elLmax, elOversample, elColormap,
1021- elRunPause, elBenchmark, elReseed, elRecompile, elRevert, elEditorFile,
1052+ elRunPause, elRestart, elBenchmark, elReseed, elRecompile, elRevert, elEditorFile,
10221053 elMovieSpeed, elMovieRes, elMovieRotate, elMovieToggle,
10231054 ];
10241055 for (const el of locked) el.disabled = on;
@@ -1386,7 +1417,11 @@ const MODE_GROUPS: Record<Mode, readonly GroupName[]> = {
13861417 simulate: ['surface', 'surface-params', 'solver', 'display', 'playback', 'benchmark', 'seed', 'movie'],
13871418 'compute-effort': ['surface', 'surface-params', 'display', 'playback', 'seed'],
13881419 'vs-sphere': [], // unreachable — the button is disabled, no listener ever calls setMode with this
1389- 'vs-upload': ['display', 'playback', 'seed'],
1420+ // No `seed` here: nothing in that group does anything useful against a
1421+ // loaded file (lam3 is silently absorbed, and Restart already covers what
1422+ // Re-seed would otherwise be doing — reloading the file's fixed initial
1423+ // state) — see CompareRun.restart().
1424+ 'vs-upload': ['display', 'playback'],
13901425 };
13911426
13921427 function setModeButtons(mode: Mode): void {
src/mgpu/session.tsmodified+15−0View file
@@ -397,6 +397,21 @@ export class ModelSession {
397397 return this.gpu.read(name);
398398 }
399399
400+ /**
401+ * Read every state species back to the CPU, in the shape `loadState`
402+ * consumes — the capture side of that method's reload, so a caller can
403+ * hold onto the current spectral state and restore it exactly later
404+ * (e.g. a "restart to this run's initial condition" control). One at a
405+ * time, not `Promise.all`: every `read()` copies into the same shared
406+ * readback buffer (`GpuModel#readback`, model.ts:448-452), so two in
407+ * flight at once race `mapAsync` against each other's `unmap`.
408+ */
409+ async readState(): Promise<Record<string, Float32Array>> {
410+ const out: Record<string, Float32Array> = {};
411+ for (const name of this.model.state) out[name] = await this.read(name);
412+ return out;
413+ }
414+
400415 /**
401416 * Read species `k` at render resolution (`viewSht`'s grid): the spectral
402417 * state synthesized there. The models define each species as synth of its
moveopenescclose