/ concept-collection / turing-surface-cache
concept-collection / turing-surface-cache
turing-surface-cache / src / cache / spec.ts
84 lines · 3.4 KBCodeBlameHistory
2 * The cache spec: the JSON object that fully identifies one solution, and the
3 * mapping from it to an object name in the cloud cache.
4 *
5 * The name is the SHA-256 of the spec's canonical JSON serialization (keys
6 * sorted recursively, numbers as ECMAScript shortest-round-trip strings, which
7 * the language specifies exactly). The serialization includes the app name and
8 * a format version, so a change to what a spec means changes every hash. The
9 * object path also carries the app name, version, and model in the clear —
10 * `turing-surface-cache/v1/schnakenberg/<sha256>.h5` — so that future cleanup
11 * (lifecycle rules, prefix deletes, per-model sweeps) never has to open a file
12 * to know what it belongs to.
13 */
14import type { Params } from '../mgpu/registry.ts';
16export const APP_NAME = 'turing-surface-cache';
17/** Bump together with the `v1` path segment below. */
18export const FORMAT_VERSION = 1;
20export interface CacheSpec {
21 app: typeof APP_NAME;
22 formatVersion: typeof FORMAT_VERSION;
23 model: string;
24 /** The model's own parameters, dt included. */
25 params: Params;
26 geometry: string;
27 geometryParams: Params;
28 lmax: number;
29 niter: number;
30 /** Wavelength of the seeded random field. */
31 lam3: number;
32 seed: number;
33 /** Physical end time; steps = tEnd / dt, which must be an integer. */
34 tEnd: number;
37/** Timesteps from t = 0 to the spec's end time. Throws if tEnd is not an
38 * exact multiple of dt — the discrete lists are chosen so it always is. */
39export function stepsFor(spec: CacheSpec): number {
40 const dt = spec.params.dt;
41 if (!(dt > 0)) throw new Error(`bad dt ${dt}`);
42 const steps = Math.round(spec.tEnd / dt);
43 if (Math.abs(steps * dt - spec.tEnd) > 1e-9 * spec.tEnd) {
44 throw new Error(`tEnd ${spec.tEnd} is not a multiple of dt ${dt}`);
45 }
46 return steps;
49/** JSON with every object's keys sorted, at every level. */
50export function canonicalJson(value: unknown): string {
51 if (value === null || typeof value !== 'object') return JSON.stringify(value);
52 if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
53 const keys = Object.keys(value as Record<string, unknown>).sort();
54 const body = keys
55 .map((k) => `${JSON.stringify(k)}:${canonicalJson((value as Record<string, unknown>)[k])}`)
56 .join(',');
57 return `{${body}}`;
61 * WebCrypto. A global in every browser, and in node from version 19; node 18
62 * has the same implementation but only under `node:crypto`, and a machine with
63 * an idle GPU is quite likely to be running whatever its distribution shipped.
64 */
65async function subtle(): Promise<SubtleCrypto> {
66 if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;
67 if (__NODE_BUILD__) return (await import('node:crypto')).webcrypto.subtle as SubtleCrypto;
68 throw new Error('WebCrypto is not available');
4f822e1turing-surface-cache: reaction-diffusion solutions at a chosen end time, shared through a cloud cacheJeremy Magland 71export async function sha256Hex(text: string): Promise<string> {
d3fa654Say what is wrong when node is too old, and run on node 18Jeremy Magland 72 const digest = await (await subtle()).digest('SHA-256', new TextEncoder().encode(text));
4f822e1turing-surface-cache: reaction-diffusion solutions at a chosen end time, shared through a cloud cacheJeremy Magland 73 return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
76/**
77 * Object name under the bucket's upload prefix (tmpbucket prepends
78 * `tmpbucket/`, so the public URL is
79 * https://tempory.net/tmpbucket/<this>).
80 */
81export async function cacheFileName(spec: CacheSpec): Promise<string> {
82 const hash = await sha256Hex(canonicalJson(spec));
83 return `${APP_NAME}/v${FORMAT_VERSION}/${spec.model}/${hash}.h5`;