diff --git a/docs/sim-specs/continuous-ca/lenia/implementation-plan.md b/docs/sim-specs/continuous-ca/lenia/implementation-plan.md new file mode 100644 index 00000000..4ec49db9 --- /dev/null +++ b/docs/sim-specs/continuous-ca/lenia/implementation-plan.md @@ -0,0 +1,288 @@ +# Flow Lenia ecosystem implementation plan + +> Companion execution ledger for `spec-web-ecosystem.md`. Each chunk is intended to finish in one +> implementation context and leave a tested boundary for the next `/compact` continuation. + +## Chunk 1 — f64 scientific oracle (M1) + +Status: **implemented; validation recorded below** + +- preserve the existing Taichi point-splat package and capture contract; +- add the versioned `flow-lenia-ecosystem-v1` configuration; +- freeze the primary three-ring cutoff, bell response, Sobel convention, pressure gate, component + clamp, and finite-square overlap equations; +- implement direct periodic convolution and full multi-channel reference stepping; +- implement all five localized inheritance rules and seed-addressed patch mutation; +- add analytical anchors, property tests, and a small cross-language JSON fixture. + +Primary implementation: + +- `packages/flow-lenia/flow_lenia/ecosystem_config.py` +- `packages/flow-lenia/flow_lenia/ecosystem_reference.py` +- `packages/flow-lenia/tests/test_ecosystem_reference.py` +- `packages/flow-lenia/tests/test_ecosystem_properties.py` +- `packages/flow-lenia/tests/test_ecosystem_golden.py` + +## Chunk 2 — WebGPU feasibility and package foundation (M0) + +Status: **implemented; 256² desktop default frozen from the measured M0 result** + +- audit and extract the repository's shared Stockham FFT surface; +- scaffold `packages/flow-lenia/web/` with the standard lifecycle, capture, and test hooks; +- add a batch/plane dimension to the FFT prototype; +- prototype the `dd=5` destination gather at mass-only and full state bandwidth; +- benchmark 128² and 256², inventory buffers/bindings/dispatches, and publish the M0 note; +- freeze 128² or 256² as the measured default rather than assuming it. + +Exit boundary: browser smoke loads, M0 benchmark artifacts are reproducible, and no architectural +limit remains unknown. + +Primary implementation: + +- `packages/flow-lenia/web/src/fft-batch.ts` +- `packages/flow-lenia/web/src/probe.ts` +- `packages/flow-lenia/web/src/shaders/` +- `packages/flow-lenia/web/src/inventory.ts` +- `packages/flow-lenia/web/scripts/run-browser-benchmark.mjs` +- `packages/flow-lenia/web/artifacts/m0-browser-benchmark.json` +- `m0-webgpu-benchmark.md` + +## Chunk 3 — WebGPU Organism Lab (M2) + +Status: **implemented; numerical, structural, determinism, performance, and capture gates pass** + +- implement batched convolution, growth, affinity, pressure, displacement, and faithful gather; +- add density/channel/affinity/flow scientific render modes; +- add exact-seed reset, capture, mass ledger, and compact PROVE diagnostics; +- compare GPU intermediates and short rollouts against the chunk-1 fixture/oracle; +- add long-horizon structural gates before enabling localized genomes. + +Exit boundary: the full base model runs interactively and passes the declared CPU–GPU gates. + +Primary implementation: + +- `packages/flow-lenia/web/src/model/solver.ts` +- `packages/flow-lenia/web/src/model/config.ts` +- `packages/flow-lenia/web/src/model/kernels.ts` +- `packages/flow-lenia/web/src/shaders/organism_*.wgsl` +- `packages/flow-lenia/web/src/shaders/reintegrate_organism.wgsl` +- `packages/flow-lenia/web/src/render/renderer.ts` +- `packages/flow-lenia/web/src/prove.ts` +- `packages/flow-lenia/web/src/prove/organism-fixture.json` +- `packages/flow-lenia/web/artifacts/m2-browser-gates.json` +- `m2-organism-validation.md` + +## Chunk 4 — interaction and visual laboratory (M3) + +Status: **implemented; interaction, ledger, card, accessibility, and render-integrity gates pass** + +- add camera, time controls, pipette/add/erase/stir/inspect tools, and event scheduling; +- add trails, flow glyphs, contours, pressure/flux views, kernel inset, and response plot; +- ship six stable organism/ablation cards with accessible keyboard, pointer, and touch controls; +- verify render-only effects never write scientific state. + +Exit boundary: a visitor can form, perturb, inspect, compare, and reset an organism without paper +knowledge. + +Primary implementation: + +- `packages/flow-lenia/web/src/model/events.ts` +- `packages/flow-lenia/web/src/shaders/organism_events.wgsl` +- `packages/flow-lenia/web/src/experiments/cards.ts` +- `packages/flow-lenia/web/src/render/renderer.ts` +- `packages/flow-lenia/web/src/render/overlays.ts` +- `packages/flow-lenia/web/src/prove-m3.ts` +- `packages/flow-lenia/web/artifacts/m3-browser-gates.json` +- `m3-visual-laboratory-validation.md` + +## Chunk 5 — localized ecosystem (M4) + +Status: **implemented; five-rule numerical, determinism, mutation, ecosystem, identity-dilution, +render-integrity, memory, and performance gates pass** + +- add packed `H`, `Q`, fingerprint, lineage, and flags ping-pong buffers; +- port average, whole, gene-wise, best-affinity, and negotiation gather specializations; +- add mutation patches, lineage ring, diversity metrics, and phenotype/lineage views; +- ship three-founder, negotiation-sea, and identity-dilution comparisons; +- extend numerical and determinism gates to every inheritance rule. + +Exit boundary: localized ecosystems are reproducible, mass-conserving, and separately falsifiable. + +## Chunk 6 — Arena Lab and productization (M5–M6) + +Status: **implemented; Arena, reload, canonical-capture, responsive, public-deploy, quality, +determinism, integrity, memory, and performance gates pass** + +- add soft walls, corridors, islands, affinity fields, storms, gates, and region metrics; +- ship corridor-divergence, maze, and recovery experiments plus versioned import/export; +- finish responsive UX, landing copy, canonical captures, posters, provenance, and browser matrix; +- integrate the package into the public `.io` catalogue only after correctness, performance, + quality, determinism, and integrity gates pass. + +Exit boundary: the spec's release acceptance criteria are satisfied and deployment-ready. + +Primary implementation: + +- `packages/flow-lenia/web/src/model/arena.ts` +- `packages/flow-lenia/web/src/model/experiment-io.ts` +- `packages/flow-lenia/web/src/shaders/arena_perceive.wgsl` +- `packages/flow-lenia/web/src/shaders/arena_events.wgsl` +- `packages/flow-lenia/web/src/experiments/arena-cards.ts` +- `packages/flow-lenia/web/src/arena-app.ts` +- `packages/flow-lenia/web/src/prove-m6.ts` +- `packages/flow-lenia/web/artifacts/m6-browser-release-gates.json` +- `packages/flow-lenia/web/artifacts/m6-canonical-capture-index.json` +- `m5-m6-arena-release-validation.md` + +## Chunk 1 validation + +Run from the repository root: + +```bash +uv run ruff check packages/flow-lenia/flow_lenia packages/flow-lenia/tests +uv run pytest packages/flow-lenia/tests -q +PYTHONPATH=packages/flow-lenia .venv/bin/mypy \ + --config-file packages/flow-lenia/pyproject.toml \ + packages/flow-lenia/flow_lenia/ecosystem_config.py \ + packages/flow-lenia/flow_lenia/ecosystem_reference.py +``` + +The final validation result for this chunk is recorded in the completing agent response. Taichi may +emit a non-failing offline-cache lock warning in the restricted workspace; it does not change test +results. + +## Chunk 2 validation + +Run from `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run build +``` + +The real-browser reference artifact and reproduction command are recorded in +`m0-webgpu-benchmark.md`. The repository standard browser driver must also reach the WebGPU ready +flag, mount the shared settings panel, invoke its capture button, and extract a non-empty capture. + +## Chunk 3 validation + +Run the static and build gates from `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run build +``` + +Regenerate and verify the oracle fixture plus the complete Python regression from the repository +root: + +```bash +PYTHONPATH=packages/flow-lenia uv run python packages/flow-lenia/scripts/generate_m2_fixture.py +uv run ruff check packages/flow-lenia/flow_lenia packages/flow-lenia/tests \ + packages/flow-lenia/scripts/generate_m2_fixture.py +uv run pytest packages/flow-lenia/tests -q +``` + +The real-browser numerical/structural/performance artifact and the standard capture-driver result +are recorded in `m2-organism-validation.md`. + +## Chunk 4 validation + +Run the static regressions and build from `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run check:m3 +npm run build +``` + +Run the real-browser laboratory gate with local browser paths: + +```bash +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium \ + npm run gate:m3 +``` + +The standard repository capture driver must also load the default 256² application, mount the +shared settings panel, invoke capture, and extract a non-empty state. Results and declared M3 +tolerances are recorded in `m3-visual-laboratory-validation.md`. + +## Chunk 5 validation + +Regenerate the inheritance fixture and run the complete static regression from the repository root: + +```bash +PYTHONPATH=packages/flow-lenia uv run python packages/flow-lenia/scripts/generate_m4_fixture.py +uv run ruff check packages/flow-lenia/flow_lenia packages/flow-lenia/tests \ + packages/flow-lenia/scripts/generate_m2_fixture.py \ + packages/flow-lenia/scripts/generate_m4_fixture.py +uv run pytest packages/flow-lenia/tests -q +``` + +Run the WebGPU static/build gates from `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run check:m3 +npm run check:m4 +npm run build +``` + +Run the real-browser localized-ecosystem gate with local browser paths: + +```bash +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium \ + npm run gate:m4 +``` + +The standard capture driver must additionally open `?ecosystem=1`, capture mass plus H/Q and packed +identity state, and extract a non-empty state. Results and declared M4 tolerances are recorded in +`m4-localized-ecosystem-validation.md`. + +## Chunk 6 validation + +Run the complete static/build regression from `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run check:m3 +npm run check:m4 +npm run check:m6 +npm run build +``` + +Run the real-browser Arena/release gate with local browser paths: + +```bash +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium \ + npm run gate:m6 +``` + +Validate the exact public-deploy path and assembled catalogue from the repository root: + +```bash +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium \ + uv run --no-sync python tools/productization/web-deploy/pipeline.py validate \ + --artifacts /tmp/flow-lenia-web-release --sim flow-lenia --skip-build --json +node tools/productization/web-deploy/web/pages/check-links.mjs +``` + +The real-browser result, exact tolerances, canonical hash index, compatibility posture, poster/live +asset provenance, and complete regression are recorded in `m5-m6-arena-release-validation.md`. + +## Continuation contract + +All planned chunks are complete. Further work is a separately scoped extension rather than an +implicit Chunk 7; the release exclusions in `spec-web-ecosystem.md` remain exclusions. + +Do not begin a later chunk while an earlier exit boundary is red. Preserve unrelated user files and +the legacy Taichi behavior throughout. diff --git a/docs/sim-specs/continuous-ca/lenia/m0-webgpu-benchmark.md b/docs/sim-specs/continuous-ca/lenia/m0-webgpu-benchmark.md new file mode 100644 index 00000000..e8d7eb69 --- /dev/null +++ b/docs/sim-specs/continuous-ca/lenia/m0-webgpu-benchmark.md @@ -0,0 +1,148 @@ +# Flow Lenia M0 WebGPU feasibility result + +> **Verdict:** PASS on the reference desktop. Freeze **256²** as the desktop default and retain +> **128²** as the adaptive tier. +> +> **Scope:** dominant-path architecture probe, not a claim that the M2 model is implemented. The +> executable measures batched C=3/K=9 FFT fan-out and faithful `dd=5` finite-square transport. It +> intentionally omits growth, affinity, pressure, diagnostics, and rendering. + +## Measured environment + +- Date: 2026-07-18. +- OS: Ubuntu 26.04 LTS, Linux 7.0.0, x86-64. +- Browser: headless Chromium 150, Dawn/ANGLE Vulkan. +- Adapter reported by WebGPU: AMD, RDNA 2. The browser withheld the device/description strings. +- Timing source: WebGPU `timestamp-query`, 3 warm-up iterations and 12 measured samples per row. +- Prototype: f32, C=3, K=9, `dd=5`, `sigma=0.65`, 8×8 destination tiles. + +The committed raw artifact is +[`packages/flow-lenia/web/artifacts/m0-browser-benchmark.json`](../../../../packages/flow-lenia/web/artifacts/m0-browser-benchmark.json). +The benchmark driver serves the production bundle over localhost, waits for the standard ready +flag, and calls the browser's exported M0 hook. + +## Results + +Independent rows have independent warm-up and sample windows, so combined-path timing need not be +the arithmetic sum of separately measured rows. + +| Grid | Workload | Dispatches | p50 ms | p95 ms | +|---:|---|---:|---:|---:| +| 128² | batched FFT + C→K spectral expansion | 29 | 0.298 | 0.316 | +| 128² | mass-only faithful gather | 1 | 0.039 | 0.041 | +| 128² | full-state faithful gather | 1 | 0.066 | 0.069 | +| 128² | FFT + mass-only gather | 30 | 0.338 | 0.369 | +| 128² | FFT + full-state gather | 30 | 0.362 | 0.381 | +| 256² | batched FFT + C→K spectral expansion | 33 | 1.173 | 1.199 | +| 256² | mass-only faithful gather | 1 | 0.180 | 0.200 | +| 256² | full-state faithful gather | 1 | 0.226 | 0.230 | +| 256² | FFT + mass-only gather | 34 | 1.309 | 1.362 | +| 256² | FFT + full-state gather | 34 | 1.184 | 1.252 | + +The dominant-path 256² full-state p95 is 1.252 ms against the provisional 33.3 ms desktop budget. +That leaves substantial room for the omitted pointwise model, diagnostics, and render passes; M2 +must still measure the complete solver and may revise the product tier if the full frame graph +invalidates this headroom. + +## Correctness anchors + +The browser ran these 128² GPU anchors before accepting a timing result: + +| Anchor | Measured | M0 ceiling | Result | +|---|---:|---:|---| +| C=3 forward FFT → K=9 identity spectra → inverse FFT, max absolute error | 2.3520e-7 | 5e-4 | PASS | +| Mass-only gather, worst per-channel relative ledger residual | 4.7225e-8 | 5e-5 | PASS | +| Full-state gather, worst per-channel relative ledger residual | 4.7225e-8 | 5e-5 | PASS | +| Uniform H/Q field preservation, max absolute error | 2.0266e-7 | 5e-5 | PASS | +| Uniform identity preservation | exact | exact | PASS | + +These are feasibility anchors. The M2 CPU–GPU gate remains responsible for comparing real kernel +responses, growth, flow, and short rollouts against the f64 oracle. + +## Buffer inventory + +The projected complete M4 allocation is conservative and retains complex kernel spectra. Small +uniform, reduction, event, and readback buffers are excluded from the per-cell table but are far +below one MiB. + +| Buffer family | Bytes/cell | Count | 256² MiB | +|---|---:|---:|---:| +| mass vec4f | 16 | 2 | 2.00 | +| derived transport source: mass + displacement x/y | 48 | 1 | 3.00 | +| K=9 complex FFT workspace | 72 | 2 | 9.00 | +| K=9 complex kernel spectra | 72 | 1 | 4.50 | +| K=9 real responses | 36 | 1 | 2.25 | +| three-channel affinity | 16 | 1 | 1.00 | +| flow x/y vec4f | 16 | 2 | 2.00 | +| H: three vec4f records | 48 | 2 | 6.00 | +| Q: three vec4f records | 48 | 2 | 6.00 | +| identity vec4u | 16 | 2 | 2.00 | +| **total** | **604** | | **37.75** | + +The 128² projection is 9.4375 MiB. The largest single 256² buffer is one nine-plane complex FFT +buffer at 4.50 MiB. The executable M0 probe itself allocates approximately 31.5 MiB at 256². + +## Binding and dispatch decision + +The repository shared numerical core in `common/common-web/src/fft-wgsl.ts` did not need to change. +`fft-batch.ts` adds plane-aware 2D addressing around that butterfly. A stage binds two storage +buffers and batches all active planes into one dispatch: + +- C=3 forward: `2 log2(N)` dispatches; +- C→K spectral expansion: one dispatch with three storage bindings; +- K=9 inverse: `2 log2(N)` dispatches; +- faithful gather: one dispatch. + +The full gather would exceed the portable eight-storage-buffer floor if mass and both flow +components were separately bound beside mass/H/Q/identity outputs. M0 resolves this by producing a +derived `transport source` scratch buffer with three vec4f records per cell: mass, displacement x, +and displacement y. Full gather then binds exactly eight storage buffers: + +1. transport source; +2. next mass; +3. current H; +4. next H; +5. current Q; +6. next Q; +7. current identity; +8. next identity. + +Canonical mass, H, Q, and identity remain structure-of-arrays; only the one-pass transport input is +packed. Mass-only gather binds two storage buffers. Neither specialization uses float atomics. + +## Browser limits observed + +| Limit | Observed | Required by 256² architecture | +|---|---:|---:| +| `maxBufferSize` | 256 MiB | 4.50 MiB largest buffer | +| `maxStorageBufferBindingSize` | 128 MiB | 4.50 MiB | +| `maxStorageBuffersPerShaderStage` | 8 | 8 | +| `maxBindingsPerBindGroup` | 1000 | 9 including the uniform | +| `maxComputeInvocationsPerWorkgroup` | 256 | 128 FFT / 64 gather | +| `maxComputeWorkgroupStorageSize` | 16 KiB | 0 in M0; portable tiling ceiling frozen at 16 KiB | +| `maxComputeWorkgroupsPerDimension` | 65535 | 2304 for the largest FFT stage dispatch | + +No architectural limit remains unknown for M2. Full-state gather sits exactly on the portable +storage-binding floor, so M2 must keep purpose-specific pipeline layouts and must not add another +storage binding to that pass. + +## Reproduction + +From the repository root: + +```bash +cd packages/flow-lenia/web +npm install +npm run typecheck +npm run check:m0 +npm run build + +PLAYWRIGHT_MODULE="$PWD/../../../tools/productization/web-deploy/web/headless/node_modules/playwright/index.js" \ +CHROME_BIN=/snap/bin/chromium \ +node scripts/run-browser-benchmark.mjs dist artifacts/m0-browser-benchmark.json +``` + +The repository's standard browser driver also passed: real WebGPU engaged, time-to-ready 42 ms, the +settings panel mounted, its capture control completed, and a one-frame diagnostic capture was +exported. The benchmark artifact is hardware-specific by design; reruns should be committed only +with their reported environment and without replacing this reference measurement silently. diff --git a/docs/sim-specs/continuous-ca/lenia/m2-organism-validation.md b/docs/sim-specs/continuous-ca/lenia/m2-organism-validation.md new file mode 100644 index 00000000..fea03918 --- /dev/null +++ b/docs/sim-specs/continuous-ca/lenia/m2-organism-validation.md @@ -0,0 +1,91 @@ +# Flow Lenia M2 Organism Lab validation + +Date: 2026-07-18 + +## Implemented scientific path + +M2 implements the complete global-parameter organism model from +`flow-lenia-ecosystem-v1`: + +1. pack three mass channels and run one plane-batched forward 2D Stockham FFT; +2. multiply by nine precomputed, complex, discretely normalized radial-kernel spectra; +3. run one plane-batched inverse transform for all nine responses; +4. evaluate the frozen bell response and reduce weighted target-channel affinity; +5. compute the unnormalized periodic Sobel affinity and density gradients, pressure gate, flow, + and component-clamped displacement; +6. perform the exact `dd=5` finite-square destination gather with fixed candidate order. + +The step uses f32, has 37 compute dispatches at 256², and is normally encoded in one compute pass +and one queue submission. The organism specialization owns no `H`, `Q`, fingerprint, lineage, or +identity buffers. The density, channels, affinity, and flow render pipelines bind every scientific +buffer as read-only. + +## CPU–GPU numerical gate + +`scripts/generate_m2_fixture.py` evaluates the M1 pure-NumPy f64 oracle and quantizes only the +committed expected arrays to f32. The browser compares: + +- all perception, growth, affinity, alpha, flow, and displacement intermediates for a smooth + periodic field; +- one-step and four-step mass for that field; +- displacement and one-/four-step mass for a seam-loaded adversarial field; +- displacement and one-/four-step mass for a crowded pressure adversary. + +The committed Chromium/Dawn run recorded these worst errors: + +| Field | Max absolute error | Declared tolerance | +|---|---:|---:| +| perception | 2.98e-8 | 1.0e-6 | +| growth | 4.17e-7 | 1.0e-5 | +| affinity | 2.98e-7 | 1.0e-5 | +| alpha | 7.45e-9 | 2.0e-7 | +| flow | 8.20e-7 | 2.0e-5 | +| displacement, worst adversary | 7.75e-7 | 4.0e-6 | +| one-step mass, worst adversary | 1.19e-6 | 1.0e-5 | +| four-step mass, worst adversary | 8.94e-7 | 2.0e-5 | + +The largest four-step fixture ledger residual was 2.26e-7. The tolerances are frozen in the +generated fixture and checked statically; they are not inferred from a passing browser result. + +## Structural, determinism, and performance gates + +The structural test executes the seeded 128², C=3, K=9 organism for 256 steps twice from the exact +same initial bytes. The measured reference result was: + +- identical final SHA-256 hashes on both same-adapter replays; +- relative mass drift 1.3534e-5 against a 5e-5 limit; +- zero negative or non-finite values; +- zero displacement-clamped cells; +- maximum density 2.1249 and maximum displacement component 0.5749. + +The complete 256² solver, including growth/affinity/pressure and faithful gather rather than the M0 +dominant-path proxy, measured 3.30 ms p95 over 16 queue-completion samples. Its owned allocation is +26.01 MiB, including FFT pass uniforms, below the 33.3 ms and 128 MiB desktop budgets. This local +reference measurement used headless Chromium 150 with Dawn/ANGLE Vulkan on the reported AMD RDNA2 +adapter. It is not a cross-device performance promise. + +## Reproduction + +From `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run build +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium \ + npm run gate:m2 +``` + +The measured report is committed at +`packages/flow-lenia/web/artifacts/m2-browser-gates.json`. The standard repository browser driver +also loads the default 256² app, mounts the shared panel, invokes exact-seed capture, and extracts a +non-empty scientific-state bundle. Two fresh browser contexts produced identical step-32 mass +hashes during M2 completion. + +## Boundary for M3 + +M2 deliberately does not include localized genomes, mutation, environment editing, camera/tools, +trails, contours, pressure/flux overlays, inspectors, or experiment cards. Those are later +milestones. M3 must keep all visual effects read-only with respect to the scientific state and must +not weaken the M2 gates. diff --git a/docs/sim-specs/continuous-ca/lenia/m3-visual-laboratory-validation.md b/docs/sim-specs/continuous-ca/lenia/m3-visual-laboratory-validation.md new file mode 100644 index 00000000..28e43235 --- /dev/null +++ b/docs/sim-specs/continuous-ca/lenia/m3-visual-laboratory-validation.md @@ -0,0 +1,106 @@ +# Flow Lenia M3 visual laboratory validation + +Date: 2026-07-18 + +## Implemented laboratory surface + +M3 keeps the M2 global-parameter solver and adds a deterministic manipulation and observation +layer: + +- fixed-step event scheduling for pipette, add, erase, and stir brushes; +- an explicit open-system GPU ledger for add/erase and mass-closed displacement impulses for + pipette/stir; +- pause, single step, 0.25×–4× time controls, pan, pinch/wheel zoom, reset, and occupied-mass fit; +- read-only local sampling of mass, density, affinity, pressure alpha, flow, displacement, clamp, + all nine kernel perceptions, and all nine growth responses; +- organism glow, channels, affinity, flow, pressure, and flux views plus optional contours, adaptive + flow glyphs, and render-only trails; +- a spatial three-ring kernel inset and bell-response plot with the inspected cell plotted on the + curve; +- keyboard, pointer, secondary-button, wheel, pinch, and long-press radial touch-tool paths. + +The six authored cards are Affinity swimmer, Spinner collision, Dividing droplets, Pressure +ablation, Sigma phase, and Expansion trap. Pressure ablation and Sigma phase allocate a second +solver lazily and advance both sides synchronously from identical mass bytes. All card source notes +identify them as Bit-Physics-authored transformations of the frozen ecosystem-v1 configuration; +no third-party preset data or media is redistributed. + +## Event and ledger contract + +Events are sorted by requested step and consumed before the relevant scientific operation: + +1. add and erase execute before spectral perception; +2. normal Flow Lenia perception and pressure compute transport; +3. pipette and stir add component-bounded displacement impulses; +4. the unchanged exact finite-square destination gather transports all mass. + +At most 32 records share a boundary. Add and erase update the mass buffer cell-locally and tally +their actual f32 deltas into commutative atomic u32 counters at 1/65,536 mass-unit resolution. +Metrics reconcile current mass against initial mass plus credited additions minus debited removals. +Pipette and stir do not touch those counters because their displacement still passes through the +closed gather. + +The committed same-adapter event replay schedules add, erase, pipette, and stir across four fixed +boundaries, runs 32 steps twice, and produces identical final hashes: + +`036e16fa6686c61cdcfc3425933b30e6c30cae011e3fc5ec7845c50c2a7d22b5` + +It credited 2.14484 mass units, debited 10.52699, and ended with relative ledger error 1.40e-6. +The separate 24-step pipette/stir case left both open counters exactly zero and ended at 1.27e-6 +relative drift. M3 uses a 1.2e-4 structural ceiling for open-tool/card gates to honestly include +fixed-point ledger quantization; the unchanged M2 closed-model gate retains its tighter 5e-5 limit. + +## Six-card and ablation gates + +Each authored 128² card runs for 64 steps with zero negative or non-finite values. Relative ledger +errors ranged from 1.10e-6 to 3.40e-6, and every card stayed below the declared clamp bound. Both +comparison cards also ran their synchronized second side: + +| Card | Primary max density | Comparison max density | Absolute peak delta | Result | +|---|---:|---:|---:|---| +| Pressure ablation | 2.4974 | 11.5226, pressure off | 9.0252 | stable and divergent | +| Sigma phase | 3.8272, sigma 0.38 | 3.0711, sigma 1.05 | 0.7561 | stable and divergent | + +The divergence check ensures the comparison is a causal solver ablation rather than two labels on +one rendered state. These outcomes are demonstrations for the authored regimes, not general claims +about agency, reproduction, or evolution. + +## Render-only integrity and product gate + +Every scientific renderer binding is `read-only-storage`. Scene rendering writes a presentation-only +texture; two additional presentation textures accumulate and display trails. A browser gate hashes +the live scientific mass, changes mode, contours, glyphs, persistence, and camera, renders eight +frames, then hashes mass again. The before/after bytes are identical. + +The browser product gate also verifies six experiment cards, five tools, six scientific views, two +inspector plots, keyboard-focus metadata, and five radial touch choices. A 256² synchronized solver +pair owns 52.02 MiB, below the provisional 128 MiB budget before localized genomes. The committed +environment is headless Chromium 150 with Dawn/ANGLE Vulkan on the reported AMD RDNA2 adapter. + +## Reproduction + +From `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run check:m3 +npm run build +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium \ + npm run gate:m3 +``` + +The measured report is committed at +`packages/flow-lenia/web/artifacts/m3-browser-gates.json`. `npm run gate:m2` remains an independent +regression of the f64-derived M2 numerical, long-horizon, and 256² performance gates. The standard +repository browser driver mounted the shared panel in 203 ms and 195 ms in two fresh contexts and +extracted one non-empty step-32 state from each default 256² run. Their scientific mass payloads +shared SHA-256 `8390ef4611ad0d2758ea301611790d024eaa15aee5420b20401a3c5ac98fe1d3`; +manifest timestamps and wall-clock fields intentionally differed. + +## Boundary for M4 + +M3 does not add localized `H`/`Q`, fingerprint, lineage, identity, mutation, inheritance, or +environment fields. M4 may add those buffers and specialized gathers only after preserving the M2 +scientific gates, M3 event ledger, and render-integrity contract. diff --git a/docs/sim-specs/continuous-ca/lenia/m4-localized-ecosystem-validation.md b/docs/sim-specs/continuous-ca/lenia/m4-localized-ecosystem-validation.md new file mode 100644 index 00000000..fc88bee8 --- /dev/null +++ b/docs/sim-specs/continuous-ca/lenia/m4-localized-ecosystem-validation.md @@ -0,0 +1,154 @@ +# Flow Lenia M4 localized-ecosystem validation + +> Status: **PASS** on the recorded Chromium/Dawn reference adapter. This note closes Chunk 5 of +> `implementation-plan.md`; Arena Lab and release productization remain M5–M6 work. + +## Implemented boundary + +M4 adds a separate localized ecosystem specialization without allocating genome state in Organism +Lab. Each ecosystem cell carries two ping-ponged arrays of three `vec4f` records for nine H genes +and nine Q genes, plus a ping-ponged `vec4u` identity containing the two-u32 fingerprint, lineage, +and flags. The scientific mass path remains the M2 three-channel/nine-kernel affinity, pressure, +clamp, and exact finite-square destination gather. + +The localized gather binds exactly eight storage buffers: the transport source, next mass, current +and next H, current and next Q, and current and next identity. The transport record grows from the +M0 three-`vec4f` estimate to six records so the already-computed nine contextual growth values can +reach best-affinity and negotiation scoring without a ninth storage binding. This is one 96-byte +scratch record per cell and does not add persistent scientific state. + +Five compute pipelines specialize one frozen streaming gather module: + +- mass-weighted H/Q average with a mixed-lineage sentinel; +- whole-genome mass reservoir selection; +- one independently addressed mass reservoir for each of 18 H/Q genes; +- deterministic best contextual H·I selection with first-candidate tie break; +- contextual negotiation using `beta * incoming_mass * Q·I` plus streaming Gumbel-max. + +The WGSL implementation emulates the oracle's SplitMix64 counter hash with two u32 lanes. Counters +are addressed by seed, step, destination, candidate, and gene; selection does not depend on GPU +scheduling. Candidate H/Q and identity are loaded only after positive overlap mass is established. + +## Mutation and ecosystem instrumentation + +Mutation is a fixed-step event over a contiguous toroidal patch. One seed-addressed Gaussian delta +vector is applied to all matching cells of the sampled parent lineage, H/Q is clamped to `[-2, 2]`, +and the patch receives one deterministic child fingerprint and lineage ID. A 128-record lineage ring +stores parent, child, step, centroid, affected mass, radius, and all 18 deltas. A small fixed-point GPU +counter records affected mass at the mutation boundary; it is diagnostic-only and never changes +scientific mass. Mutated flags also retain a compact parent-hue key so child lineage colors remain a +small deterministic offset from their parent while exact IDs remain separately inspectable. + +Low-cadence ecosystem readback reports exact-lineage count and mass, the top lineage, Shannon +lineage diversity, mixed-identity mass, quantized H/Q phenotype clusters and entropy, and mutation +and extinction event counts. “Lineage,” “fingerprint,” and “phenotype” remain explicitly operational +simulation definitions rather than biological species. + +The Ecosystem Lab is entered through `?ecosystem=1` or the in-canvas lab switch. It includes: + +- three-founder and negotiation-sea cards; +- a synchronized three-pane average/gene-wise/negotiation identity-dilution comparison; +- exact lineage and approximate phenotype views plus density and flow views; +- sample and coherent mutate tools, radius/scale controls, fixed-step time controls, and keyboard + access; +- ecosystem capture containing mass, H, Q, packed identity values, schema, rule, seed, step, and + diversity diagnostics. + +All three cards and visual parameters are Bit-Physics-authored and use no redistributed third-party +preset or media data. + +## CPU–GPU numerical gate + +`scripts/generate_m4_fixture.py` derives a compact 16² localized fixture from the M1 f64 oracle. It +freezes the same initial mass, three founder genomes, contextual growth, displacement, and mutation +event for all five rules. Expected floating fields cross the browser boundary as f32. + +Declared ceilings are `1.2e-5` absolute for mass and H/Q and `6e-5` relative for the closed mass +ledger. The recorded maximum errors are: + +| Rule | mass max abs | H max abs | Q max abs | lineage / flag mismatches | +|---|---:|---:|---:|---:| +| average | 1.94e-7 | 1.79e-7 | 2.38e-7 | 0 / 0 | +| whole | 1.94e-7 | 0 | 0 | 0 / 0 | +| gene-wise | 1.94e-7 | 0 | 0 | 0 / 0 | +| best | 1.94e-7 | 0 | 0 | 0 / 0 | +| negotiation | 1.94e-7 | 0 | 0 | 0 / 0 | + +Every rule has a `5.15e-8` relative one-step mass residual against its f64-derived expected field. +The frozen mutation child identity and all 18 f32 deltas match exactly; its affected patch contains +`16.19374` mass units at the fixed-point boundary and its one-step relative mass drift is `4.74e-8`. + +## Determinism, ecosystem, and falsification gates + +Each inheritance specialization runs two independent 32-step 128² replays. SHA-256 covers mass, H, +Q, and identity, and all five replay pairs are byte-identical on the recorded adapter. No +cross-vendor bit-identity claim is made. + +Longer authored-card gates remain finite, non-negative, unclamped, and within the declared `1.5e-4` +closed-ledger ceiling: + +- three founders, whole-genome, 48 steps: three exact lineages and `2.55e-6` relative drift; +- negotiation sea, 96 steps: all three scheduled mutation events, seven active lineages, and + `5.08e-6` relative drift; +- identity dilution, 48 steps per synchronized rule: stable under average, gene-wise, and + negotiation with approximately `2.54e-6` relative drift. + +Identity dilution is separately falsifiable rather than a cosmetic comparison. Average and +gene-wise produce essentially all mixed-sentinel mass while negotiation preserves exact selected +lineages. Their measured phenotype-bin counts are 139, 396, and 4 respectively. The gate requires +distinct outcomes and requires average mixed mass to exceed negotiation by at least one percentage +point. + +Cycling lineage, phenotype, density, and flow render modes leaves mass, H, Q, and identity byte +exact. The product-surface gate records three experiments, five rules, four views, two tools, three +comparison panes, and a keyboard-focusable canvas. + +## Reference browser measurement + +Committed artifact: `packages/flow-lenia/web/artifacts/m4-browser-gates.json`. + +Recorded environment: + +- Chromium 150 / Dawn WebGPU; +- AMD RDNA2 adapter through ANGLE Vulkan; +- 256², C=3, K=9, `dd=5`, negotiation gather; +- queue-completion timing after warm-up. + +The final run records a complete 256² allocation of **43.01 MiB**, below the provisional 128 MiB +budget, and a **19.3 ms p95** complete negotiation step over eight queue-completion samples. The +33.3 ms gate is the durable requirement; this one machine's timing is not a universal performance +claim. + +## Reproduction + +From the repository root: + +```bash +PYTHONPATH=packages/flow-lenia uv run python packages/flow-lenia/scripts/generate_m4_fixture.py +uv run ruff check packages/flow-lenia/flow_lenia packages/flow-lenia/tests \ + packages/flow-lenia/scripts/generate_m2_fixture.py \ + packages/flow-lenia/scripts/generate_m4_fixture.py +uv run pytest packages/flow-lenia/tests -q +``` + +From `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run check:m3 +npm run check:m4 +npm run build + +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium npm run gate:m4 +``` + +The real-browser gate rebuilds `artifacts/m4-browser-gates.json` and writes a visual inspection image +to `/tmp/flow-lenia-m4-ui.png` by default. + +## Next boundary + +M4 does not implement editable affinity environments, walls, corridors, islands, storms, regional +metrics, import/export, or public-catalogue integration. Those remain Chunk 6 (M5–M6); localized +ecosystem correctness no longer blocks them. diff --git a/docs/sim-specs/continuous-ca/lenia/m5-m6-arena-release-validation.md b/docs/sim-specs/continuous-ca/lenia/m5-m6-arena-release-validation.md new file mode 100644 index 00000000..5d4e5f44 --- /dev/null +++ b/docs/sim-specs/continuous-ca/lenia/m5-m6-arena-release-validation.md @@ -0,0 +1,140 @@ +# Flow Lenia M5–M6 Arena and release validation + +Date: 2026-07-18 +Model: `flow-lenia-ecosystem-v1` +Environment schema: `flow-lenia-arena-environment-v1` +Experiment schema: `flow-lenia-arena-experiment-v1` + +## Release boundary + +Arena Lab extends the separately gated M4 ecosystem solver only when created with explicit Arena +resources. Organism Lab still allocates no genome or environment fields, and Ecosystem Lab retains +its measured 43.0107 MiB 256² allocation. Arena adds two scientific arrays—one `vec4f` field and one +`u32` region label—plus small uniforms/event records. The complete measured 256² Arena allocation +is 44.2613 MiB. + +The environment is deliberately a soft-affinity model: + +- field `x` is an authored or painted signed affinity; +- field `y` is a negative soft-wall contribution; +- field `z` is a negative gate contribution multiplied by the closed/open schedule; +- field `w` is a render/diagnostic wall opacity and never feeds transport; +- an analytic orbiting attractor and three-lobed storm pulse add signed affinity at a fixed step; +- the resulting scalar is multiplied by explicit per-channel response and added to `V_c` before + the unchanged Sobel pressure, displacement clamp, and conservative destination gather. + +There is no hard collision, reflected boundary, resource metabolism, or mass source/sink. Public +copy labels walls as soft affinity and recovery as a regional-distribution proxy, not biological +fitness or resilience. + +## Product surface + +The release ships three authored, repository-licensed Arena cards with no imported data: + +| Card | Controlled mechanism | Primary metric | +|---|---|---| +| Corridor divergence | Two founder regions and a central gate that opens at step 48 | Region abundance before/after reconnection | +| Maze navigation | Authored soft-wall maze and slowly orbiting positive-affinity beacon | Target-region abundance and wall exposure | +| Storm recovery | Frozen three-lobed affinity pulse over steps 40–63 | Retained closed mass and regional-distribution recovery | + +Six direct tools—sample, mutate, attract, repel, wall and erase—share primary/secondary, Shift, +Alt, wheel, pointer and keyboard semantics. Five read-only scientific views expose lineage, +phenotype, channels, flow and the fixed signed environment scale. A low-cadence panel displays all +three region masses, environment range, gate/storm state, a recovery trace, active lineage masses, +and mutation edges. + +Complete experiment export stores packed mass, H, Q, identity, environment, regions, dynamics, +lineage ring, seed, fixed step, mixing rule, closed open-system ledger and authored provenance. +Import rejects incompatible grid/model versions, lengths, rules, modified dynamics and SHA-256. The release gate +restores every byte and proves another 24 steps remain byte-identical on the same adapter. + +## Committed browser result + +The primary artifact is +`packages/flow-lenia/web/artifacts/m6-browser-release-gates.json`, schema +`flow-lenia-m6-release-gates-v1`. It was measured in headless Chromium 150 through Dawn/ANGLE +Vulkan on AMD RDNA2. + +| Gate | Measured result | Criterion | Verdict | +|---|---:|---:|---| +| Zero environment | mass/H/Q/identity byte-exact vs M4 specialization | exact | PASS | +| Constant-field affinity | max absolute residual `2.9802322387695312e-8` | `≤ 2e-6` | PASS | +| Timed gate | closed at 47, open at 48 | exact schedule | PASS | +| GPU affinity paint | environment bytes changed; mass remained closed | both required | PASS | +| Corridor replay | 96 steps × 2, byte-exact; drift `5.0977e-6` | drift `≤ 1.5e-4` | PASS | +| Maze replay | 96 steps × 2, byte-exact; drift `5.0721e-6` | drift `≤ 1.5e-4` | PASS | +| Storm replay | 96 steps × 2, byte-exact; drift `5.0886e-6` | pulse observed; drift `≤ 1.5e-4` | PASS | +| Export/import | tamper rejected; restore exact; 24-step continuation exact | exact | PASS | +| Render integrity | all six scientific arrays byte-exact across five views | exact | PASS | +| Arena allocation | 44.2613 MiB at 256² | `< 128 MiB` | PASS | +| Complete 256² step | 8 samples, p50 3.20 ms, p95 18.90 ms | p95 `≤ 33.3 ms` | PASS | +| Adaptive smoke | 128² at 390×844, touch + reduced motion | ready, panel mounted, no viewport overflow | PASS | + +No readback or GPU-buffer allocation occurs in the simulation hot loop. Metrics, inspection, +capture, export and explicit proof runs are low-cadence or user-requested synchronization points. + +## Capture and provenance + +The Arena standard capture runs 72 exact steps and contains six fields: mass, H, Q, packed identity +values, packed environment values and region values. It also records the model/environment schema, +card provenance and the SHA-256 of the eight scientific/render WGSL sources. The committed +`m6-canonical-capture-index.json` pins: + +- the M2 128²/256-step Organism structural state hash; +- the M4 128²/32-step whole-inheritance ecosystem state hash; +- the M6 128²/72-step Arena browser-capture state and shader hashes. + +The catalogue poster and live VP9 loop are generated from the same built bundle, seed 42, authored +Maze Navigation card, and fixed frame schedules in `make-posters.mjs` / `make-loops.mjs`. The PNG is +48 KiB and the loop is 396 KiB, below the 1.5 MiB per-simulation live-asset budget. They are +presentation assets and never feed a solver or gate. + +## Browser and public-deploy posture + +The measured browser matrix claims Chromium desktop WebGPU and Chromium 390×844 adaptive/touch +WebGPU only. Both pass. Firefox and Safari are explicitly marked “not claimed—support varies”; no +cross-vendor bit identity is asserted. + +The standard public pipeline loaded the default full Organism Lab in two fresh Chromium profiles, +reached WebGPU readiness in 198 ms and 200 ms, emitted two byte-identical step-32 mass captures, and +passed the new-canonical verifier. The public capture measured relative mass drift +`1.6940432e-6`, relative ledger error `1.6940403e-6`, zero negative/non-finite cells, zero clamp +fraction, and maximum displacement `0.53417`. The assembled 21-simulation site then resolved all +467 internal links and all 22 landing/per-sim favicons. + +## Reproduction + +From `packages/flow-lenia/web/`: + +```bash +npm run typecheck +npm run check:m0 +npm run check:m2 +npm run check:m3 +npm run check:m4 +npm run check:m6 +npm run build +PLAYWRIGHT_MODULE=/path/to/playwright CHROME_BIN=/path/to/chromium npm run gate:m6 +``` + +From the repository root: + +```bash +uv run ruff check packages/flow-lenia/flow_lenia packages/flow-lenia/tests \ + packages/flow-lenia/scripts/generate_m2_fixture.py \ + packages/flow-lenia/scripts/generate_m4_fixture.py \ + tools/productization/web-deploy/pipeline.py \ + tools/productization/web-deploy/verify.py +uv run pytest packages/flow-lenia/tests tools/productization/web-deploy/smoke -q +PYTHONPATH=packages/flow-lenia .venv/bin/mypy \ + --config-file packages/flow-lenia/pyproject.toml \ + packages/flow-lenia/flow_lenia/ecosystem_config.py \ + packages/flow-lenia/flow_lenia/ecosystem_reference.py +node tools/productization/web-deploy/web/pages/check-links.mjs +``` + +Real-browser M2, M3 and M4 regression gates were rerun after the Arena changes. All remained green: +M2 preserved its f64-derived numerical cases and byte-exact 256-step replay; M3 retained all six +cards, event ledger and render integrity; M4 retained all five inheritance gates, three ecosystems, +mutation identity and the identity-dilution falsification. The legacy Taichi behavior and its +captures were not changed. diff --git a/docs/sim-specs/continuous-ca/lenia/spec-web-ecosystem.md b/docs/sim-specs/continuous-ca/lenia/spec-web-ecosystem.md new file mode 100644 index 00000000..baca5691 --- /dev/null +++ b/docs/sim-specs/continuous-ca/lenia/spec-web-ecosystem.md @@ -0,0 +1,968 @@ +# Flow Lenia Ecosystem — WebGPU implementation specification + +> **Status:** IMPLEMENTED — research/audit, M1 f64 scientific oracle, M0 WebGPU feasibility, M2 +> Organism Lab, M3 visual laboratory, M4 localized ecosystems, M5 Arena Lab, and M6 +> productization are complete. See +> `implementation-plan.md`, `m0-webgpu-benchmark.md`, `m2-organism-validation.md`, and +> `m3-visual-laboratory-validation.md`, `m4-localized-ecosystem-validation.md`, and +> `m5-m6-arena-release-validation.md`. +> **Target:** a new interactive browser simulation at `packages/flow-lenia/web/`. +> **Scientific lineage:** Flow Lenia (Plantec et al., 2022/2025), Reintegration Tracking, +> multi-kernel/multi-channel Lenia, and the localized-parameter ecosystem experiments of +> Etcheverry et al. (2025–2026). +> **Relationship to the landed package:** additive. The existing Taichi package and +> `spec-frontier-flow.md` remain the verified conservative-transport primitive. This specification +> adds the published affinity, density-pressure, square-distribution reintegration, multi-channel +> kernels, localized parameters, mixing, mutation, environment, and WebGPU product surface. +> **Date:** 2026-07-18. + +## 1. Ship outcome + +Ship a visually rich, scientifically legible Flow Lenia ecosystem laboratory that runs entirely in +the browser. A visitor must be able to: + +1. grow and disturb mass-conserving Flow Lenia organisms; +2. see how affinity gradients, density pressure, and conservative transport create motion; +3. introduce localized, heritable rule parameters and watch lineages collide, mix, mutate, spread, + fragment, or disappear; +4. sculpt soft environments such as walls, corridors, islands, and affinity landscapes; +5. inspect mass, flow, affinity, kernel response, lineage, and ecosystem metrics without leaving + the canvas; +6. replay a deterministic seed and export/import a compact experiment; +7. distinguish the faithful model, optional experimental extensions, and purely artistic render + effects. + +The default experience is an organism-scale Flow Lenia scene with immediate motion and a clear +visual identity. “Ecosystem mode” then reveals genotype color, mutation, mixing, lineage, and +environment controls. The UI should teach by direct manipulation rather than require prior +artificial-life knowledge. + +This is a synthetic artificial-life model. It is not a model of biological cells, biochemical +kinetics, disease, or an ecological population in physical units. Terms such as organism, genome, +species, feeding, and evolution are operational metaphors whose in-simulation definitions must be +available in the information panel. + +## 2. Existing repository baseline + +### 2.1 What is already trustworthy + +The current `packages/flow-lenia/` implementation establishes a small, rigorous transport oracle: + +- one non-negative scalar mass field; +- a normalized Gaussian convolution; +- affinity flow given by a central-difference gradient; +- periodic boundaries; +- forward bilinear point splatting; +- f64, serial Taichi CPU execution; +- property tests for mass conservation and non-negativity; +- a zero-flow pointwise identity anchor; +- deterministic canonical capture and testkit integration. + +Its conservation proof is reusable: if every source redistributes all of its non-negative mass +through non-negative weights whose destination sum is one, total mass is conserved to floating-point +summation error. + +### 2.2 What it does not yet implement + +The current package is intentionally a frontier primitive, not the complete published Flow Lenia +model. It omits: + +- Lenia’s multi-ring kernels and bell-shaped growth responses; +- multiple channels and source-to-target kernel connections; +- the density-dependent pressure term; +- finite square distributions and exact overlap-based reintegration; +- localized kernel weights carried with matter; +- genome selection, mixing, mutation, and lineage; +- environment affinity fields, obstacles, food, and ecosystem diagnostics; +- a WebGPU application and public-site integration. + +The new work must not silently redefine the old package as though those features were already +verified. + +### 2.3 Reusable browser infrastructure + +The repository already provides the major product and GPU building blocks: + +- `common/common-web/src/fft-wgsl.ts`: radix-2 Stockham FFT WGSL with driver-stable trigonometric + approximation; +- `packages/heat-equation/web/src/solver.ts`: a working 2D FFT ping-pong and bind-layout pattern; +- `packages/physarum/web/`: fixed-point atomic deposition and agent-field visualization patterns; +- shared panel, colormap, capture, canonical-reference, and readiness helpers; +- the `packages//web` Vite app convention; +- WebGPU Chromium smoke/capture validation and the 13-gate productization flow. + +The current FFT requires power-of-two dimensions. The initial supported simulation sizes are +therefore 128, 256, and an opt-in 512 cinematic tier. + +## 3. Research synthesis + +### 3.1 Academic model lineage + +| Work | Contribution used here | Implementation consequence | +|---|---|---| +| [Flow-Lenia, ALIFE 2022 preprint](https://arxiv.org/abs/2212.07906) and [2025 Artificial Life journal article](https://direct.mit.edu/artl/article/31/2/228/130572/Flow-Lenia-Emergent-Evolutionary-Dynamics-in-Mass) | Recasts Lenia growth as an affinity field, adds density pressure, conserves matter through Reintegration Tracking, and localizes parameters. | Defines the faithful simulation core and conservation contract. | +| [Reintegration Tracking](https://michaelmoroz.github.io/Reintegration-Tracking/) | Represents each cell as a finite distribution and analytically reintegrates its overlap with destination cells. | Use exact uniform-square overlap in a destination-gather compute pass. | +| [Lenia and Expanded Universe](https://direct.mit.edu/isal/article/doi/10.1162/isal_a_00297/98400/Lenia-and-Expanded-Universe) | Multi-kernel, multi-channel rules produce differentiated and interacting structures. | Default ecosystem uses three channels and nine connected kernels. | +| [Exploring Flow Lenia Universes](https://arxiv.org/html/2505.15998) | Curiosity search, localized genomes, alternative mixing rules, mutations, walls/corridors, long-horizon evolutionary-activity and complexity measurements. | Separates ecosystem mechanics from the organism solver and motivates negotiation, stochastic gene-wise inheritance, lineage, and environmental experiments. | +| [Sensorimotor Lenia](https://arxiv.org/abs/2402.10236) | Obstacles, perturbations, curriculum, and diversity search expose robust agency. | Include obstacle arenas, disturbance tests, tracking, and recovery diagnostics. | +| [Flow-Lenia.png](https://arxiv.org/abs/2408.06374) | Genetic search with multiscale compression complexity and polar rerasterization to avoid radial-symmetry bias. | Add lightweight live complexity proxies; reserve compression scoring and search for offline tooling. | +| [Large-scale evolutionary Lenia](https://arxiv.org/abs/2304.05639) | Local genotypes can produce implicit reproduction and selection, but unconstrained runs may be dominated by fast expanders. | Supply spatial structure and explicit resource experiments; do not equate expansion with open-ended evolution. | +| [Quality Diversity through AI Feedback](https://arxiv.org/abs/2406.04235) | MAP-Elites and learned/hand-designed descriptors can curate diverse Lenia behaviors. | Reserve an archive/discovery gallery as a later mode, not part of the hot simulation loop. | + +The most recent ecosystem study reports that mixing rule matters: negotiation sustained the greatest +measured non-neutral evolutionary activity in its experiments; stochastic gene-wise inheritance was +also active; simple averaging created diffuse, continuously blended parameter identities that showed +almost no exact-genome activity. Its ecosystem search used 256 × 256 worlds, three channels, nine +kernels, and 10,000-step runs. Those are empirical results for the study’s regimes, not universal +guarantees. The application should make the rules comparable under identical seeds. + +### 3.2 Research-engineering and industry-adjacent implementations + +This survey found no mature commercial Flow Lenia simulator to copy. The relevant “industry” +evidence is research-lab engineering around accelerator-native artificial life: + +| Implementation | Relevant lesson | +|---|---| +| [Official Flow Lenia JAX code](https://github.com/erwanplantec/FlowLenia) and [model notes](https://sites.google.com/view/flowlenia/model) | Primary executable reference for kernels, growth, pressure, Reintegration Tracking, and local parameter transport. | +| [Exploring-Flowlenia](https://github.com/Thomick/Exploring-Flowlenia) and its [current companion archive](https://developmentalsystems.org/Flow-Lenia-Universes-Journal/) | Current ecosystem reference: mixing variants, mutation, walls, food experiments, evolutionary metrics, curiosity search, and a browsable discovery manifold. | +| [Google DeepMind publication page](https://deepmind.google/research/publications/106327/) | Confirms active research-lab interest and the journal version’s scope. | +| [Google Research Particle Lenia](https://google-research.github.io/self-organising-systems/particle-lenia/) | Strong browser UX precedent: direct manipulation, reproducible configurations, and PNG previews that carry a full creature configuration. | +| [CAX](https://github.com/maxencefaldor/cax) / [paper](https://arxiv.org/abs/2410.02651) | Modular perceive/update decomposition, vectorization, scan, and selectable convolution backends are useful architectural patterns. | +| [Leniax](https://github.com/morgangiraud/leniax) / [documentation](https://leniax.readthedocs.io/en/latest/overview.html) | Batch simulation, differentiation, evolution, and reproducible configuration should remain possible outside the interactive renderer. | + +The official JAX repositories are scientific references, not browser code dependencies. Equations, +small golden fixtures, and documented preset values may be independently reimplemented. Source must +not be copied without reviewing and recording license compatibility. + +### 3.3 Community implementations and product lessons + +| Project | Feature lesson | Provenance constraint | +|---|---|---| +| [Lenia portal](https://chakazul.github.io/lenia.html) and [Chakazul/Lenia](https://github.com/Chakazul/Lenia) | Species galleries, immediate preset switching, FFT history, and evolutionary exploration make a difficult model approachable. | Treat the species catalogue as reference material; verify licenses before redistributing presets or media. | +| [Open Lenia](https://openlenia.github.io/) | A clean public entry point and accessible explanation are as important as solver depth. | Design inspiration only unless assets are explicitly compatible. | +| [Adrian Margel’s WebGL experiment](https://adrianmargel.ca/projects/lenia/) and [GPL repository](https://github.com/AdrianMargel/flow-lenia) | Genotype hue, density lighting, mutation-by-click, image adhesion, trails, zoom, and material coupling make the field visually expressive. | The project is GPL-3.0 and scientifically diverges from the published affinity model. Do not copy code or shaders; independently implement only general interaction ideas. | +| [Exploring Flow Lenia archive](https://developmentalsystems.org/Flow-Lenia-Universes-Journal/) | Pan/zoom discovery maps, video previews, goal metadata, and export make generated behaviors explorable. | A future discovery surface, not a requirement for the first simulator release. | + +Scientific reference mode must never be replaced by a visually attractive but undocumented “fluid +Lenia” variant. Experimental coupling, blur, trails, and lighting belong behind labeled render or +model toggles. + +## 4. Model from first principles + +### 4.1 Intuition + +Ordinary Lenia repeatedly creates or removes activation according to how closely a local +neighborhood matches a preferred density. Flow Lenia instead reads that response as a potential: +matter moves uphill in favorable affinity. When matter becomes dense, a pressure term pushes it away +from crowded regions. The complete mass distribution is moved and reintegrated; it is not incremented +or decremented. + +An ecosystem appears when the rule weights are fields carried by that matter. Colliding material can +inherit one incoming rule vector, combine vectors, or choose a vector according to contextual +affinity. Local mutation creates new rule vectors. Differential persistence and spread can then be +measured without an explicit birth/death loop. + +### 4.2 State and topology + +For a square N × N torus: + +- A_c(x) ≥ 0 is mass in channel c, with c in [0, C). +- H_k(x) is the localized weight of kernel k carried by matter. +- Q_k(x) is an optional localized negotiation/mixing weight. +- Z(x) is a packed identity record: a 64-bit genome fingerprint, lineage ID, and identity flags. +- E_c(x) is a static or slowly changing external affinity field. + +Release defaults: + +| Parameter | Default | Notes | +|---|---:|---| +| N | 256 desktop, 128 adaptive | Power of two | +| C | 3 | Renderable as RGB and consistent with ecosystem references | +| K | 9 | Three source-to-target connections per channel | +| dt | 0.2 | Reference-scale preset value | +| dd | 5 cells | Reintegration search radius | +| sigma | 0.65 cell | Uniform-square half-width | +| theta_A | 2.0 | Density-pressure scale | +| n | 2.0 | Density-pressure exponent | +| boundary | periodic | The only closed, verified release boundary | +| seed | explicit u32 pair | Controls initialization, mixing, and mutation | + +Every shipped preset records all values. “Reference” presets preserve values from a cited source; +curated presets identify their Bit-Physics authoring origin. + +### 4.3 Kernel perception + +Kernel k reads source channel s(k), influences target channel t(k), and is a normalized radial +mixture of three rings: + +K_k(r) = S(r) Σ_j b_kj exp(-(r - a_kj)² / w_kj), then K_k ← K_k / Σ_x K_k(x). + +S smoothly suppresses the kernel outside its nominal radius. Kernel radius, relative radius, ring +locations a, widths w, and amplitudes b are global preset parameters. The exact reference formula, +cutoff, centering, and discrete normalization must be frozen in the CPU oracle before GPU work. + +Perception and growth are: + +U_k(x) = K_k ∗ A_s(k)(x) + +I_k(x) = 2 exp[-0.5 ((U_k(x) - m_k) / s_k)²] - 1. + +The target-channel affinity is: + +V_c(x) = Σ over k where t(k)=c of H_k(x) I_k(x) + E_c(x). + +For organism mode, H_k is a global constant and no genome buffers are needed. Ecosystem mode makes +H local and advected. + +### 4.4 Pressure and flow + +Let rho(x) = Σ_c A_c(x). The crowding gate and flow are: + +alpha_c(x) = clamp((A_c(x) / theta_A)^n, 0, 1) + +F_c(x) = (1 - alpha_c(x)) grad V_c(x) - alpha_c(x) grad rho(x). + +The first term follows affinity. The second moves dense material down the total-density gradient. +The faithful reference uses the same Sobel gradient convention and boundary wrapping as the official +code. A central-difference comparison may exist as a labeled experiment because the landed Taichi +primitive uses it, but it is not the reference default. + +Displacement is dt F_c, component-clamped so that every source distribution remains within the +declared dd gather neighborhood. The UI reports the fraction of cells hitting this clamp; a sustained +high clamp rate means the chosen timestep or field strength is invalid. + +### 4.5 Conservative reintegration + +Each source cell p and channel c becomes a uniform axis-aligned square centered at +p + dt F_c(p), with half-width sigma. For a destination cell q, let W(q, p, c) be the exact overlap +area between that square and q’s unit cell, divided by 4 sigma²: + +A'_c(q) = Σ_p A_c(p) W(q, p, c). + +On the torus, all overlap pieces are wrapped and: + +- W is non-negative; +- Σ_q W(q, p, c) = 1 for every source; +- therefore Σ_q,c A'_c(q) = Σ_p,c A_c(p), to floating-point accumulation error; +- non-negative input remains non-negative; +- zero displacement is the identity only for the reference sigma/cell geometry whose overlap is + frozen by the oracle. General sigma may introduce controlled numerical diffusion even at zero + flow. + +The browser implements destination gather, not source scatter. Each destination examines the fixed +(2dd + 1)² source neighborhood and sums contributions in a stable order. This matches the analytical +model, needs no float atomics, and makes reproducibility tractable. + +Never substitute backward semi-Lagrangian advection in reference mode: it is convenient and smooth +but not mass conservative. A point-distribution bilinear splat may later be offered as a separately +named fast transport comparison, never as “the same” solver. + +### 4.6 Localized inheritance + +For destination q, the mass arriving from source p is: + +M(p → q) = Σ_c A_c(p) W(q, p, c). + +The genome mixer chooses or combines H, Q, and Z from candidates with M > 0: + +| Rule | Definition | Expected character | +|---|---|---| +| None / fixed | Global H; no local genome | Faithful organism laboratory | +| Mass-weighted average | H' = Σ M H / Σ M | Smooth diffuse blends; destroys discrete identity | +| Whole-genome stochastic | Select one source with probability M / Σ M | Maintains coherent inherited vectors | +| Gene-wise stochastic | Select each component independently by mass | Recombines traits and can sustain variation | +| Dot / best affinity | Select highest contextual score with stable tie break | Deterministic competitive selection | +| Negotiation | Sample from softmax of beta M S, where S = Σ_k I_k Q_k | Context-sensitive inheritance used by the recent ecosystem study | + +Stochastic rules use stateless counter-based hashing keyed by seed, step, destination, candidate, and +gene. They are repeatable for a fixed adapter and configuration; they do not depend on scheduling. +Ordinary mass-proportional sampling uses a streaming weighted reservoir. Negotiation uses streaming +Gumbel-max over its logits, avoiding a candidate array and an explicit softmax normalization. + +The gather pass must not materialize 121 candidate genomes per destination. It streams candidates: + +- weighted averaging accumulates moments; +- deterministic selection keeps best score and source index; +- stochastic selection uses weighted reservoir sampling; +- gene-wise selection maintains one reservoir per gene; +- negotiation computes the candidate logit and updates a Gumbel-max winner on the fly. + +Genome buffers are fetched only after overlap mass is known to be positive. + +### 4.7 Mutation and lineage + +Mutation is an event, not per-cell white noise: + +1. a pointer brush, scheduled event, or low-rate ecological trigger selects a contiguous patch of + matter; +2. one deterministic Gaussian delta is applied to selected H and/or Q components with bounded + parameter transforms; +3. the patch receives one new lineage ID derived from parent ID, event index, and seed; +4. the event is appended to a bounded lineage ring with parent, child, time, centroid, mass, and + parameter delta. + +This preserves visible lineages. Independently mutating every cell would make nearly every floating +vector unique and render “species count” meaningless. + +Selection-based rules copy genome fingerprint and lineage. Gene-wise recombination hashes the chosen +gene-parent identities into a new genome fingerprint and marks ancestry as mixed. Weighted averaging +bit-hashes the resulting H/Q vector and uses a mixed-lineage sentinel; exact-genome activity is +disabled by default because almost every blended cell can become unique. Mutation creates both a new +fingerprint and lineage event. + +For visualization, a selection lineage is exact by ID. A genome fingerprint is a reproducible +64-bit proxy whose collision risk is reported, while verification can compare the complete H/Q bits. +A phenotype cluster is a quantized distance bin in H/Q space. None is automatically a biological +species. The UI labels all definitions. + +### 4.8 Open-system extensions + +The verified core is closed and conserves matter. These optional actions make it open: + +- add/remove mass brush; +- decay; +- food/resource conversion; +- absorbing boundaries; +- scripted source/sink fields. + +Every open-system step writes an explicit mass ledger: + +mass_after = mass_before + user_added - user_removed + resource_converted - decay - absorbed. + +A closed “pipette” brush should be the default sculpting tool: it transfers mass from a donor ring to +the pointer center rather than creating mass. Food is a post-release extension. If implemented, it +must track organism mass, resource mass, and combined matter separately. + +## 5. Product modes and scope + +### 5.1 Release modes + +1. **Organism Lab** — global parameters, no genomes. The simplest faithful full Flow Lenia solver. +2. **Ecosystem Lab** — localized H/Q, lineage, selectable mixing, mutation, and ecosystem metrics. +3. **Arena Lab** — Ecosystem Lab plus editable affinity walls, corridors, islands, and scripted + perturbations. +4. **Compare Lab** — two synchronized views share initial state and seed but differ by one model + parameter, such as pressure, mixing rule, sigma, or mutation. + +Organism Lab is both the onboarding path and the numerical reference surface. Ecosystem mechanics +cannot block shipping or validating the base solver. + +### 5.2 Release exclusions + +Do not put these in the initial critical path: + +- online IMGEP, MAP-Elites, or population-scale evolutionary search; +- differentiable optimization; +- food/resource metabolism; +- hard no-flux walls; +- fluid/Navier–Stokes coupling; +- arbitrary imported shader code; +- claims of open-ended evolution; +- mobile 256² guarantees; +- bit-identical behavior across different GPU vendors. + +They remain explicit stretch tracks after the core gate is green. + +## 6. Interactive mechanics + +### 6.1 Pointer tools + +| Tool | Direct effect | Conservation | +|---|---|---| +| Pipette / move | Draw from an annulus and deposit under brush | Closed | +| Add matter | Paint chosen channel or sampled organism | Open; ledgered | +| Erase matter | Remove proportional channel mass | Open; ledgered | +| Stir | Add a temporary external affinity dipole or displacement impulse | Closed after impulse | +| Mutate | Create one mutation patch and lineage event | Mass closed | +| Sample | Inspect local A, V, F, H, Q, lineage, and neighborhood response | Read only | +| Clone | Copy selected patch to another position, subtracting source by default | Closed by default | +| Soft wall | Paint negative affinity environment | Mass closed | +| Affinity paint | Paint attractive/repulsive scalar environment | Mass closed | +| Track | Lock camera and graphs to a selected lineage or connected mass component | Read only | + +Pointer semantics must remain stable: primary applies, secondary reverses/removes, wheel changes +radius, Shift changes strength, and Alt samples where possible. Touch exposes a radial tool selector. + +### 6.2 Environment mechanics + +- soft circular and polygonal obstacles; +- maze and corridor stamps; +- founder islands separated by editable passages; +- radial, linear, checker, noise, and image-derived affinity fields; +- slowly rotating or translating attractors; +- a “storm” pulse that perturbs affinity without changing mass; +- periodic gates that connect and isolate populations; +- optional per-channel environment response; +- torus seam preview so users understand wraparound. + +Soft walls are negative affinity, not hard collision geometry. Label them accordingly. Hard walls +require reflected or locally renormalized Reintegration Tracking and their own conservation proof. + +### 6.3 Experiment cards + +Every card loads a preset, a camera, an optional event timeline, and success metrics: + +1. **Affinity swimmer** — a solitary translating body; inspect flow and pressure. +2. **Spinner collision** — counter-rotating organisms collide and recover or merge. +3. **Dividing droplets** — fragmentation and regrowth without implicit biological claims. +4. **Three founders** — three lineages meet under each mixing rule. +5. **Negotiation sea** — mutation pulses and contextual inheritance. +6. **Corridor divergence** — isolated founders reconnect after a timed gate. +7. **Maze navigation** — moving affinity source beyond soft walls. +8. **Storm recovery** — standardized perturbation with time-to-recover graph. +9. **Pressure ablation** — synchronized comparison with alpha pressure on/off. +10. **Sigma phase** — particle-like to field-like reintegration sweep. +11. **Identity dilution test** — average versus gene-wise versus negotiation mixing. +12. **Expansion trap** — demonstrates why occupied area alone is not evolutionary progress. + +At least six cards must ship with authored, stable seeds. Research-derived presets need source, +license, parameter transformation, and expected-behavior notes. + +### 6.4 Time, camera, and sharing + +- pause, single step, 0.25× to maximum stable steps/frame; +- 2D pan/zoom, follow lineage, reset, and fit occupied mass; +- optional torus ghost copies near seams; +- render-only trails with adjustable persistence; +- a sparse rewind ring using compressed CPU checkpoints at low cadence; +- URL-encoded preset/seed/tool state when compact; +- binary or JSON experiment export/import; +- image export with a human-readable metadata sidecar. + +Encoding a complete state inside a PNG, inspired by Particle Lenia, is a stretch goal after the +basic state format is versioned. + +## 7. Visual system + +### 7.1 Required render modes + +1. **Organism glow** — density drives opacity/luminance; channel mixture drives hue. +2. **Lineage color** — stable lineage ID hashes to a perceptually spaced hue; density controls + luminance and alpha. +3. **Phenotype color** — a projection of H/Q into color, with a legend and warning that nearby color + is only an approximate parameter relationship. +4. **Channels** — direct RGB visualization of three mass channels. +5. **Affinity** — signed diverging map of V_c with isolines. +6. **Flow** — vectors or short streamlets over density; arrow sampling adapts to zoom. +7. **Pressure** — alpha and grad rho overlay. +8. **Flux** — recently transported mass and clamp hot spots. +9. **Environment** — soft walls and external fields, independently adjustable. + +The default combines lineage hue, density glow, subtle gradient-based relief, and low-persistence +flow trails. Lighting and bloom are render-only and must never feed the solver. + +### 7.2 Visually expressive but honest effects + +- gradient-normal relief and rim lighting; +- density-dependent emissive bloom; +- age/persistence tint from a render-only exponential field; +- mutation pulse rings and short lineage trails; +- velocity-aligned streaks; +- affinity contour animation; +- collision flashes based on incoming lineage diversity; +- split-screen ablations; +- selected-lineage silhouette and center-of-mass trail; +- kernel-ring inset and live response curve; +- magnifying lens that shows cells, mass distributions, and flow vectors. + +All diagnostic modes must use fixed numeric legends. Artistic exposure, bloom, and trail controls +must not change metric computation. + +### 7.3 Color identity + +Lineage color is derived from a stable hash into OKLCH-like evenly separated hues, clamped to the +display gamut. Parent and child hues remain related by using a small deterministic hue offset for +mutations, but a separate outline pattern distinguishes exact IDs. Avoid red/green-only semantics. + +## 8. Ecosystem instrumentation + +### 8.1 Always available + +- total, per-channel, added, removed, and converted mass; +- relative mass drift against the ledger; +- min/max density and NaN/non-finite count; +- maximum flow and displacement-clamp fraction; +- occupied fraction above a documented threshold; +- center-of-mass speed on the torus; +- active lineage count and top lineage mass; +- Shannon diversity over lineage mass; +- phenotype-cluster diversity; +- mutation and extinction event counts; +- frame time, simulation step time, and active quality tier. + +### 8.2 Research diagnostics + +- non-neutral evolutionary activity using the referenced mass-change definition; +- spatial multiscale entropy or matter-distribution descriptors; +- temporal novelty of downsampled density; +- recovery time and retained mass after a scripted disturbance; +- lineage transition graph; +- kernel response histograms; +- environment-region abundance for founder-island experiments. + +MP4 or PNG compression complexity is an offline/export analysis because codec cost and browser +implementation differences make it unsuitable as a live scientific metric. The live entropy/novelty +proxies must not be labeled “compression complexity.” + +Metrics are reduced on GPU and read back at a low cadence. No GPU-to-CPU synchronization is allowed +in the per-step hot loop. + +## 9. WebGPU architecture + +### 9.1 State layout + +Use structure-of-arrays and ping-pong buffers: + +- `mass[2]`: vec4f per cell; xyz are the three channels; +- `genomeH[2]`: three vec4f records per cell for nine H values; +- `genomeQ[2]`: three vec4f records per cell for nine Q values; +- `identity[2]`: vec4u per cell containing a two-u32 genome fingerprint, lineage ID, and flags; +- batched complex FFT ping/pong buffers; +- complex precomputed kernel spectra; +- kernel responses I; +- target affinity V; +- flow x/y packed by channel; +- environment fields; +- small reduction and event buffers. + +The transport/mixing pipeline binds current and next mass, H, Q, and identity: eight storage buffers, +matching the portable per-stage floor used by this repository. Other pipelines use smaller, +purpose-specific explicit layouts. Organism mode allocates no per-cell genome buffers. + +At 256², the complete ecosystem state plus FFT workspace should remain below a provisional 128 MiB +GPU-memory budget. The probe milestone must publish actual byte counts by buffer. + +### 9.2 Per-step frame graph + +1. Apply queued environment, closed-brush, open-source/sink, and mutation events at a fixed step + boundary. +2. Pack the C mass channels into one batched complex FFT buffer. +3. Execute one batched forward 2D FFT across all C planes. +4. Multiply source spectra by each precomputed kernel spectrum, producing K planes. +5. Execute one batched inverse 2D FFT across all K planes. +6. Normalize, evaluate growth I_k, and reduce weighted target affinities V_c. +7. Compute Sobel gradients, density pressure, flow, and displacement clamp diagnostics. +8. Destination-gather Reintegration Tracking; simultaneously resolve genome mixing and lineage. +9. Apply post-transport mutation parameter bounds and open-system accounting. +10. Reduce diagnostics at their requested cadence. +11. Render from the newly completed state. + +All work for the simulation step is encoded into one command encoder and normally one queue +submission. Rendering may share that encoder when the app structure permits. + +### 9.3 FFT batching + +A naive C=3, K=9 implementation would run three forward and nine inverse transforms independently. +At 256² and 16 one-dimensional Stockham stages per 2D transform, that is approximately 192 FFT +dispatches per simulation step. + +Extend the shared FFT kernel with a batch/plane dimension so each stage processes every active plane: + +- 16 dispatches for all forward source-channel planes; +- pointwise kernel expansion; +- 16 dispatches for all inverse kernel-response planes. + +The core path is then approximately 32 FFT dispatches plus packing, spectral multiply, growth, +gradient, transport, and diagnostics. Cache kernel spectra and rebuild them only while paused or at +a deliberately throttled cadence. Radial kernels should have a nearly real spectrum after the +correct discrete center shift, but the reference path retains both components. A real-only spectrum +is allowed only after its imaginary residual and CPU–GPU error are gated. + +Kernel shape, radius, m, and s remain global. Only H/Q are local in the initial ecosystem. Per-cell +kernel shapes would invalidate FFT convolution and are out of scope. + +### 9.4 Reintegration optimization + +The faithful gather examines 121 candidates per destination at dd=5. Optimize without changing the +sum: + +- wrap coordinates with integer arithmetic and a power-of-two mask; +- compute overlap mass before loading genome data; +- branch out when overlap is exactly zero; +- keep candidate reduction order fixed; +- tile only source mass in portable workgroup memory; +- leave flow in global memory if a mass+flow halo would exceed the 16 KiB portable workgroup floor; +- use an 8×8 destination tile and benchmark 16×4 and 8×16 alternatives; +- pack channels in vec4; +- use f32 fused arithmetic consistently and document compiler sensitivity; +- specialize pipelines for organism mode, averaged genome, stochastic genome, and negotiation rather + than placing every rule in one branch-heavy shader. + +Do not use floating-point atomic scatter. [WGSL atomic types](https://www.w3.org/TR/WGSL/#atomic-types) +are portable only for i32/u32. A later fixed-point u32 point-splat variant may reuse the Physarum +pattern, but it changes the transport distribution and adds quantization. + +### 9.5 Scheduler and precision + +- Solver state, FFT, transport, and metrics use f32. +- f16 may be used only for optional render intermediates after image comparison. +- Fixed simulation dt never changes to hide a slow frame. +- The scheduler performs an integer number of simulation steps and may hold/interpolate rendering. +- No readback, `mapAsync`, pipeline creation, buffer allocation, or kernel-spectrum rebuild occurs + in the hot step. +- Static parameter changes are coalesced and applied at a step boundary. +- A hidden developer overlay reports dispatch count, allocated bytes, readbacks, and timing. + +### 9.6 Performance tiers and go/no-go budgets + +These are design budgets to measure, not current performance claims: + +| Tier | Configuration | Product target | +|---|---|---| +| Adaptive | 128², C=3, K=9, faithful gather | p95 step ≤ 16.7 ms on reference desktop; usable ≥30 steps/s on representative integrated GPU | +| Desktop | 256², C=3, K=9, faithful gather | p95 step ≤ 33.3 ms; GPU memory <128 MiB | +| Cinematic | 512² or expensive overlays | Explicit slow/record mode; no real-time promise | + +Use timestamp queries when supported and a portable queue-completion benchmark otherwise. Publish +reference browser, OS, adapter, resolution, mode, and warm-up. If the 256² target fails, default to +128² rather than silently switching to a non-conservative solver. + +### 9.7 Proposed browser package map + +Keep model, presentation, and verification separable: + +| Path under packages/flow-lenia/web/src | Responsibility | +|---|---| +| main.ts / app.ts | Lifecycle, device selection, fixed-step scheduler, panel wiring | +| model/config.ts | Versioned configuration schema, validation, safe bounds, preset migration | +| model/solver.ts | Resource ownership and the per-step command graph | +| model/fft-batch.ts | Batched wrapper around the shared Stockham core | +| model/kernels.ts | CPU kernel construction, normalization, spectrum cache and provenance | +| shaders/perceive.wgsl | Pack, spectral multiply, normalize, growth and affinity | +| shaders/flow.wgsl | Sobel gradient, pressure, flow and clamp diagnostics | +| shaders/reintegrate-*.wgsl | Specialized organism and inheritance gather pipelines | +| shaders/events.wgsl | Brushes, mutation patches and environment events | +| metrics/reduce.ts | GPU reductions, low-cadence readback and ledger reconciliation | +| render/renderer.ts | Scientific and artistic render modes; no solver writes | +| render/overlays.ts | Flow, contours, lineage, inspector, graphs and legends | +| presets/ | Authored/reference presets plus source and license metadata | +| experiments/ | Cards, timelines, expected metrics and explanatory copy | +| capture.ts | Standard capture hook, schema versioning, export/import | +| prove.ts | Live invariants, canonical comparison and developer diagnostics | + +The configuration has four explicit namespaces: model, ecosystem, environment, and render. Render +configuration is excluded from the scientific state hash. Every serialized experiment stores its +schema version, seed, fixed step, open-system ledger, and preset provenance. + +## 10. CPU reference and verification + +### 10.1 Reference implementation + +Add a new f64 reference beside, not inside, the existing simplified behavior: + +- `flow_lenia/ecosystem_config.py`; +- `flow_lenia/ecosystem_reference.py`; +- direct periodic convolution for small grids; +- exact ring kernel/growth/Sobel/pressure equations; +- destination-gather square Reintegration Tracking; +- deterministic localized mixing and mutation; +- small JSON golden fixtures. + +Keep the existing `FlowLeniaSim`, invariants, capture, and performance ledger valid. The new API and +capture schema use a distinct name such as `flow-lenia-ecosystem-v1`. + +### 10.2 Independent anchors + +At minimum: + +1. discrete kernel sums to one and matches frozen radial samples; +2. growth is exactly 1 at U=m and matches hand-computed off-center values; +3. uniform affinity and density produce zero flow; +4. a hand-derived pressure-gradient case has the expected direction and magnitude; +5. one translated square has analytically known four/nine-cell overlap weights; +6. closed torus transport conserves total and per-channel mass to honest f64/f32 tolerances; +7. non-negative mass remains non-negative; +8. a constant incoming genome remains constant under every mixing rule; +9. selection rules return only valid incoming genomes where their definition requires it; +10. fixed seed/event mutation produces the frozen child lineage and parameter delta. + +### 10.3 Property tests + +Generate bounded random grids, flows, sigma, kernels, channels, and candidate genomes: + +- mass ledger closes; +- per-channel transport mass closes; +- no negative or non-finite mass; +- overlap weights are non-negative and source-normalized; +- torus translation equivariance; +- zero-field flow; +- no-mutation lineage IDs come only from incoming candidates; +- weighted-average genomes remain in the componentwise incoming convex hull; +- stochastic samplers are repeatable for the same seed; +- displacement never exceeds dd-sigma after clamp. + +Tolerance is derived from operation count and measured residual distribution. It may not be widened +to absorb a systematic drift. + +### 10.4 CPU–GPU gates + +Use two complementary gates: + +**Short-horizon numerical gate** + +- 32² or 64²; +- reference and at least three adversarial fixtures; +- compare kernel response, affinity, flow, one transport step, and a short rollout; +- component-specific absolute/relative tolerances; +- save the first divergent intermediate on failure. + +**Long-horizon structural gate** + +- 128², C=3, K=9; +- 256–1,024 steps depending on CI budget; +- require no non-finite/negative values, a closed mass ledger, bounded clamp rate, valid lineage mass, + and reproducible same-adapter metrics; +- compare structural summaries rather than pointwise equality after chaotic divergence. + +The reference-mode solver uses no atomics and stateless random hashing. Same-browser, same-adapter, +same-configuration runs are expected to be byte-identical, but that claim must be measured before it +is registered. Cross-vendor bit identity is not claimed. + +### 10.5 Falsification controls + +Developer-only comparisons should intentionally demonstrate: + +- backward advection loses/gains mass; +- removing pressure causes high-density collapse or clamp saturation in selected presets; +- weighted averaging dilutes discrete identity faster than stochastic/negotiation rules in the + documented test; +- disabling mutation prevents new lineage IDs; +- an unnormalized kernel changes response magnitude; +- an open brush changes mass exactly by its ledger entry. + +These are diagnostics, not permanent public novelty toggles. + +### 10.6 Capture and public PROVE surface + +- expose the standard capture hook and `window.__bitPhysicsReady`; +- commit one organism and one ecosystem canonical reference; +- include state/config schema version, seed, adapter, shader hash, mode, and open-system ledger; +- run a small live verification on the visitor’s GPU; +- show mass drift, non-negativity, short-horizon reference residual, determinism status, and model + variant in a compact PROVE panel; +- never hide a failed invariant behind an attractive image. + +## 11. UI information architecture + +### 11.1 Primary controls + +- preset and experiment card; +- play/pause/step/speed; +- tool, radius, and strength; +- render mode, exposure, trails, and overlays; +- Organism/Ecosystem/Arena/Compare mode; +- mixing rule and mutation enable; +- reset exact seed and randomize seed. + +### 11.2 Advanced controls + +Group by causal role: + +- **Perception:** radius, ring shape, source/target connections; +- **Response:** m, s, H; +- **Flow:** dt, pressure theta/n, displacement clamp; +- **Transport:** sigma, dd, boundary; +- **Inheritance:** rule, beta, Q, mutation rate/scale/patch; +- **Environment:** channel response, wall/field strength; +- **Quality:** resolution, steps/frame, metric cadence. + +Unsafe combinations display a specific warning such as “12.4% of cells are displacement-clamped; +reduce dt or affinity strength,” not a generic unstable label. + +### 11.3 Inspection + +The inspect lens shows: + +- channel mass and total density; +- affinity and pressure contribution; +- flow direction, magnitude, and clamped displacement; +- lineage/phenotype identity and parent event; +- H/Q bars; +- contributing kernel growth values; +- local incoming lineage mixture after the last step. + +The kernel inspector displays the spatial rings, normalized spectrum, growth curve, and the sampled +cell’s location on that curve. + +## 12. Milestones + +### M0 — feasibility probe + +- port/extend the shared FFT to batched planes; +- benchmark 128² and 256² with C=3/K=9; +- prototype faithful dd=5 gather at mass-only and full-genome bandwidth; +- inventory buffer bytes, storage bindings, dispatches, and browser limits; +- decide default resolution from measurements. + +Exit: no unknown architectural blocker and a published benchmark note. + +### M1 — full f64 scientific oracle + +- freeze equations and reference presets; +- implement direct convolution, growth, Sobel pressure, exact reintegration; +- add anchors, property tests, and golden fixtures; +- add localized mixing and mutation tests without changing the old package. + +Exit: reference suite green and independently reviewable. + +### M2 — WebGPU Organism Lab + +- batched FFT perception; +- growth, affinity, pressure, faithful gather; +- density/channels/affinity/flow renders; +- CPU–GPU short and structural gates; +- exact-seed reset and capture. + +Exit: full published base model passes gates before ecosystem complexity is enabled. + +**Implementation status:** complete. The browser ships the four required M2 scientific views, +exact-seed reset/capture, a low-cadence mass ledger, f64-derived intermediate and rollout checks, +two same-adapter 256-step structural replays, and a measured complete-step 256² performance gate. +See `m2-organism-validation.md` for the frozen tolerances and results. + +### M3 — interaction and visual laboratory + +- tools, camera, trails, inspector, kernel view; +- six stable organism/ablation cards; +- mass ledger and performance overlay; +- accessible touch/keyboard controls. + +Exit: a new visitor can form, perturb, inspect, and reset a system without reading the paper. + +**Implementation status:** complete. The browser schedules open and closed tools at fixed step +boundaries, reconciles add/erase with an explicit GPU ledger, preserves pipette/stir through the +faithful gather, and supplies camera/time controls, trails, contours, glyphs, pressure/flux views, +cell and kernel inspection, accessible input paths, and six authored organism/ablation cards. Two +cards run synchronized causal comparisons. The committed browser gate covers exact event replay, +all card rollouts, comparison divergence, product-surface mounting, and byte-exact render-only +integrity. See `m3-visual-laboratory-validation.md`. + +### M4 — localized ecosystem + +- H/Q and lineage state; +- averaging, whole-genome, gene-wise, best-affinity, and negotiation pipelines; +- patch mutation and lineage ring; +- lineage/phenotype render modes and core diversity metrics; +- three-founder and identity-dilution comparison cards. + +Exit: ecosystem rules are reproducible, mass-conserving, and separately falsifiable. + +**Implementation status:** complete. Ecosystem mode allocates packed H/Q and identity ping-pong +state only in its dedicated solver, compiles all five inheritance rules as gather specializations at +the portable eight-storage binding floor, and adds deterministic mutation patches, a bounded +lineage ring, exact-lineage and approximate-phenotype metrics/views, and three authored ecosystem +cards. The committed browser gate covers every rule against the f64 fixture, byte-exact +same-adapter replay, mutation identity and affected mass, card stability, identity-dilution +divergence, render-only integrity, the product surface, and the 256² memory/timing budgets. See +`m4-localized-ecosystem-validation.md`. + +### M5 — Arena Lab + +- soft walls, corridors, islands, affinity painting, timed gates; +- corridor-divergence, maze, storm-recovery cards; +- environment-region metrics and lineage graph; +- export/import versioned experiments. + +Exit: long-horizon environmental experiments survive reload and canonical capture. + +**Implementation status:** complete. Arena mode is an opt-in ecosystem specialization: authored +and GPU-painted scalar environments add per-channel affinity before the unchanged density-pressure +and finite-square transport path. It supplies labeled soft walls, timed gates, an orbiting +attractor, a scripted mass-neutral storm, three region maps, region-abundance/recovery histories, +and a bounded lineage transition graph. Complete-state JSON export carries packed mass, H/Q, +identity, environment, regions, dynamics, lineage history, fixed step, seed, ledger and provenance; +SHA-256 is verified before import, and the committed gate proves byte-exact restore plus continued +same-adapter evolution. See `m5-m6-arena-release-validation.md`. + +### M6 — productization + +- responsive panel and landing copy; +- poster/live/canonical assets; +- browser smoke, quality, performance, determinism, and integrity gates; +- add to the public site only after references and provenance are committed. + +**Implementation status:** complete. The release has a responsive 128² adaptive/256² desktop +surface, keyboard/pointer/touch Arena tools, versioned canonical capture and shader hashes, authored +poster/live assets, honest browser-compatibility copy, a dedicated deploy verifier, and a public +catalogue card. Catalogue integration occurred only after the M0/M2/M3/M4/M6 gates, the standard +two-profile capture driver, and the assembled-site link audit were green. + +## 13. Acceptance criteria + +Release requires all of the following: + +- the browser default is the full affinity/pressure/square-reintegration model, not the existing + point-splat simplification; +- Organism Lab passes the f64–f32 numerical and long-horizon structural gates; +- a closed torus run keeps mass within its declared measured tolerance and never creates negative + mass; +- open tools reconcile against the explicit mass ledger; +- same-adapter fixed-seed ecosystem runs satisfy the measured determinism claim; +- all five inheritance rules have unit/property coverage and visible rule labels; +- at least six organism and four ecosystem/arena experiment cards are stable; +- inspect, affinity, flow, lineage, channels, and density render modes work; +- no hot-loop readback or allocation is observed; +- adaptive 128² meets the published reference-device budget, or the measured default is revised in + this specification; +- source, license, and transformation provenance exists for every non-original preset/asset; +- public copy makes no biological, chemical, medical, or open-ended-evolution guarantee; +- the existing Taichi package, old capture, tests, and `spec-frontier-flow.md` remain valid. + +## 14. Risks and decisions + +| Risk | Decision / mitigation | +|---|---| +| “Flow Lenia” name hides multiple incompatible implementations | Expose exact model variant; default to the published affinity/pressure/reintegration equations. | +| FFT dispatch or memory dominates | Batch planes per stage, cache real spectra, power-of-two tiers, one submission, benchmark at M0. | +| Reintegration gather is bandwidth heavy | Fixed dd, mass-first branch, specialized mixers, vec4 packing, portable mass tiling. | +| Browser has no float atomics | Faithful destination gather; no atomic scatter in reference mode. | +| Local genomes explode identity count | Event-level patch mutation plus stable lineage IDs and separately defined phenotype clusters. | +| Averaging erases discrete identity | Mark blended ancestry, disable misleading exact-genome activity by default, and make mixing a controlled comparison. | +| Chaotic divergence frustrates GPU comparison | Short numerical gates plus long structural invariants; no unjustified pointwise long-horizon tolerance. | +| Pretty effects contaminate science | Render-only effect buffers and explicit model-vs-render controls. | +| Obstacles break conservation | Ship soft affinity walls first; hard walls require a new proof and tests. | +| Expansion is mistaken for evolution | Show diversity, lineage, NNEA, environment, and ablations; state metric limitations. | +| Community code has incompatible licensing | Reimplement from papers/equations; record provenance; do not copy GPL shaders/code. | +| Research presets are fragile | Freeze exact versions and source notes; maintain authored fallback presets. | + +## 15. Resolved implementation choices + +- **Full model versus current primitive:** full model, with the primitive retained as an independent + transport reference. +- **Transport:** finite uniform-square Reintegration Tracking by destination gather. +- **Convolution:** batched 2D Stockham FFT derived from the repository’s shared core. +- **Default topology:** periodic torus. +- **Default scale:** 256² desktop, frozen by the measured M0 result; 128² adaptive. +- **Channels/kernels:** C=3, K=9. +- **Localized parameters:** kernel weights H first; separate Q for negotiation; kernel shapes remain + global. +- **Randomness:** stateless, seed-addressed GPU hashing. +- **Mutation:** contiguous event patches with explicit lineage. +- **Walls:** soft affinity fields in release; hard collision walls deferred. +- **Food:** deferred open-system extension. +- **Evolution search:** deferred offline/worker feature. +- **Visual identity:** lineage hue plus density light, with scientific overlays. +- **Scientific posture:** measurable artificial-life dynamics, not a biological simulator or proof + of open-ended evolution. + +## 16. Primary references + +1. Plantec et al., [Flow-Lenia: Towards open-ended evolution in cellular automata through mass + conservation and parameter localization](https://arxiv.org/abs/2212.07906), ALIFE 2022. +2. Plantec et al., [Flow-Lenia: Emergent Evolutionary Dynamics in Mass Conservative Continuous + Cellular Automata](https://direct.mit.edu/artl/article/31/2/228/130572/Flow-Lenia-Emergent-Evolutionary-Dynamics-in-Mass), + Artificial Life 31(2), 2025. +3. [Official Flow Lenia implementation](https://github.com/erwanplantec/FlowLenia). +4. Moroz, [Reintegration Tracking](https://michaelmoroz.github.io/Reintegration-Tracking/). +5. Etcheverry et al., [Exploring Flow Lenia Universes](https://arxiv.org/html/2505.15998), current + version consulted July 2026. +6. [Exploring-Flowlenia implementation](https://github.com/Thomick/Exploring-Flowlenia). +7. Chan, [Lenia and Expanded Universe](https://direct.mit.edu/isal/article/doi/10.1162/isal_a_00297/98400/Lenia-and-Expanded-Universe). +8. Hamon et al., [Discovering Sensorimotor Agency in Cellular Automata using Diversity Search](https://arxiv.org/abs/2402.10236). +9. Faldor et al., [CAX: Cellular Automata Accelerated in JAX](https://arxiv.org/abs/2410.02651). +10. [WebGPU specification limits](https://www.w3.org/TR/webgpu/#limits) and + [WGSL atomic types](https://www.w3.org/TR/WGSL/#atomic-types). + +Research conclusions in this document describe the cited configurations and implementations as of +the date above. They are design evidence, not a claim that every behavior generalizes to all Flow +Lenia parameter regimes. diff --git a/packages/flow-lenia/README.md b/packages/flow-lenia/README.md index 7499525c..edba174b 100644 --- a/packages/flow-lenia/README.md +++ b/packages/flow-lenia/README.md @@ -24,6 +24,77 @@ zero-flow invariants are **flow-agnostic** (properties of the reintegration tran the reintegration uses the bilinear-splat (point-distribution) limit of the paper's uniform square distribution `D` (both redistribute the full mass). +## Full ecosystem oracle + +The package also contains a separate pure-NumPy f64 target named +`flow-lenia-ecosystem-v1`. It implements the multi-channel published model without changing the +Taichi primitive above: + +- normalized three-ring perception kernels and bell growth; +- target-channel affinity plus density-gated Sobel pressure; +- exact finite uniform-square Reintegration Tracking by periodic destination gather; +- local `H`/`Q` rule fields with average, whole-genome, gene-wise, best-affinity, and negotiation + inheritance; +- deterministic contiguous-patch mutation and lineage identity. + +The entry points are `FlowLeniaEcosystemConfig`, `step_reference`, +`reintegrate_with_genomes`, and `mutate_patch`. Frozen cross-language fixtures live under +`tests/fixtures/flow-lenia-ecosystem-v1.json`; compact M2 and M4 browser fixtures are generated from +the same oracle. + +## WebGPU ecosystem implementation + +`web/` contains the completed M0 foundation, M2 Organism Lab, M3 Visual Laboratory, M4 +localized Ecosystem Lab, M5 Arena Lab, and M6 public release for the full ecosystem model: + +- a plane-batched 2D Stockham wrapper around the repository's shared FFT butterfly; +- C=3 to K=9 spectral fan-out; +- faithful `dd=5` finite-square destination gather in mass-only and full-state bandwidth variants; +- explicit portable buffer/binding/dispatch inventories; +- standard browser lifecycle, capture, and benchmark hooks; +- the full global-parameter C=3/K=9 growth, affinity, Sobel pressure, displacement, and conservative + transport step; +- exact-seed reset/capture plus density, channels, affinity, and flow scientific views; +- compact live PROVE diagnostics, f64-derived intermediate/rollout fixtures, and a same-adapter + long-horizon determinism gate; +- deterministic fixed-step pipette, add, erase, and stir tools with an explicit open-mass ledger; +- pan/zoom/fit and fixed-rate time controls, read-only cell/kernel/response inspection, trails, + contours, flow glyphs, pressure, and flux views; +- six authored organism/ablation cards, including synchronized pressure and finite-square sigma + comparisons, with keyboard, pointer, pinch, and radial touch controls. +- packed per-cell H/Q plus fingerprint, lineage, and flags with genome-free organism allocation; +- specialized average, whole-genome, gene-wise, best-affinity, and negotiation gather pipelines at + the portable eight-storage binding floor; +- deterministic contiguous mutation patches, a bounded parent/child lineage ring, exact-lineage and + quantized-phenotype diversity metrics, and read-only lineage/phenotype views; +- three-founder, negotiation-sea, and three-pane identity-dilution experiment cards. +- opt-in per-cell authored affinity, soft-wall, timed-gate, and region fields that preserve the M4 + allocation and behavior when Arena mode is absent; +- GPU affinity/wall/erase brushes, a slowly orbiting attractor, and a scripted three-lobed storm, + all upstream of the unchanged pressure/transport path and all mass-neutral; +- corridor-divergence, maze-navigation, and storm-recovery cards with region abundance, recovery + history, and a bounded lineage transition graph; +- complete-state `flow-lenia-arena-experiment-v1` JSON export/import with SHA-256 validation and + byte-exact same-adapter continuation after restore; +- a responsive Arena product surface, six-field canonical capture, shader provenance hash, + poster/live catalogue assets, and a dedicated two-run public-deploy gate. + +The committed Chromium/Dawn reference measurement passes the FFT, conservation, uniform-genome, +portable-limit, memory, and browser-contract probes. It freezes 256² as the desktop default and +128² as the adaptive tier. See `docs/sim-specs/continuous-ca/lenia/m0-webgpu-benchmark.md` for the +measured feasibility scope. The complete M2 gate is documented in +`docs/sim-specs/continuous-ca/lenia/m2-organism-validation.md`; the measured 256² organism step +retains the desktop timing and memory budgets. M3 event/card/render validation is recorded in +`docs/sim-specs/continuous-ca/lenia/m3-visual-laboratory-validation.md`; a synchronized 256² pair +owns 52.02 MiB and render-only changes leave scientific mass byte-exact. M4 validation is recorded +in `docs/sim-specs/continuous-ca/lenia/m4-localized-ecosystem-validation.md`; the complete 256² +localized solver is below the 128 MiB allocation budget and every mixing rule is separately checked +against the f64 oracle and same-adapter deterministic replay. M5–M6 validation is recorded in +`docs/sim-specs/continuous-ca/lenia/m5-m6-arena-release-validation.md`; it covers environment +anchors, all three deterministic Arena cards, exact reload/continuation, region and lineage +instrumentation, render integrity, responsive smoke, canonical capture, public deployment, and the +desktop memory/timing budgets. + ## Determinism vs conservation (distinct) Run-to-run determinism is **bit-exact** (Taichi CPU single-thread serial fixes the `ti.atomic_add` diff --git a/packages/flow-lenia/flow_lenia/__init__.py b/packages/flow-lenia/flow_lenia/__init__.py index 750853cf..305ee0aa 100644 --- a/packages/flow-lenia/flow_lenia/__init__.py +++ b/packages/flow-lenia/flow_lenia/__init__.py @@ -9,8 +9,30 @@ parent-vs-frontier REFRAMED to the invariant posture). This is the SOUND home of the Phase-3 plain-Lenia ``mass_approximately_conserved`` invariant FALSIFIED under Quad4 (re-routed, not widened). + +The separate ``flow-lenia-ecosystem-v1`` NumPy oracle exposes the full multi-channel +affinity/pressure/finite-square model without changing this legacy Taichi behavior. """ +from .ecosystem_config import ( + MODEL_VARIANT, + FlowLeniaEcosystemConfig, + KernelSpec, + default_ecosystem_config, + organism_reference_config, +) +from .ecosystem_reference import ( + EcosystemState, + GenomeFields, + MixingRule, + MutationEvent, + StepDiagnostics, + build_kernels, + mutate_patch, + reintegrate_square, + reintegrate_with_genomes, + step_reference, +) from .forward import ( FlowLeniaConfig, affinity_gradient, @@ -23,13 +45,28 @@ from .sim import FlowLeniaSim __all__ = [ + "MODEL_VARIANT", + "EcosystemState", "FlowLeniaConfig", + "FlowLeniaEcosystemConfig", "FlowLeniaSim", + "GenomeFields", + "KernelSpec", + "MixingRule", + "MutationEvent", + "StepDiagnostics", "affinity_gradient", + "build_kernels", + "default_ecosystem_config", "gaussian_kernel", "initial_mass", "mass_non_negative", + "mutate_patch", + "organism_reference_config", "reintegrate", + "reintegrate_square", + "reintegrate_with_genomes", + "step_reference", "total_mass", "total_mass_conserved", ] diff --git a/packages/flow-lenia/flow_lenia/ecosystem_config.py b/packages/flow-lenia/flow_lenia/ecosystem_config.py new file mode 100644 index 00000000..87e12d52 --- /dev/null +++ b/packages/flow-lenia/flow_lenia/ecosystem_config.py @@ -0,0 +1,228 @@ +"""Versioned configuration for the full Flow Lenia ecosystem reference model. + +This module is deliberately separate from :mod:`flow_lenia.forward`. The older module is the +small point-splat transport primitive used by the Taichi acceptance suite; the dataclasses below +freeze the multi-channel affinity/pressure/uniform-square model that the browser implementation +will target. + +The radial kernel follows the primary JAX implementation by Plantec et al.: + +``sigmoid(-10 * (d - 1)) * sum(b_j * exp(-(d-a_j)^2 / w_j))`` + +where ``d`` is distance divided by ``(R + 15) * r_k``. The seemingly unusual ``+15`` is retained +as an explicit, versioned parameter so imported reference rules can be represented without a +silent transform. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +MODEL_VARIANT = "flow-lenia-ecosystem-v1" + +__all__ = [ + "MODEL_VARIANT", + "FlowLeniaEcosystemConfig", + "KernelSpec", + "default_ecosystem_config", + "organism_reference_config", +] + + +@dataclass(frozen=True) +class KernelSpec: + """One normalized perception kernel and its source/target connection.""" + + source: int + target: int + relative_radius: float + growth_mean: float + growth_width: float + weight: float + ring_centers: tuple[float, float, float] + ring_amplitudes: tuple[float, float, float] + ring_widths: tuple[float, float, float] + + def validate(self, channels: int) -> None: + """Raise :class:`ValueError` when the kernel cannot define a finite rule.""" + if not 0 <= self.source < channels: + raise ValueError(f"kernel source {self.source} outside [0, {channels})") + if not 0 <= self.target < channels: + raise ValueError(f"kernel target {self.target} outside [0, {channels})") + if self.relative_radius <= 0.0: + raise ValueError("relative_radius must be positive") + if self.growth_width <= 0.0: + raise ValueError("growth_width must be positive") + if any(width <= 0.0 for width in self.ring_widths): + raise ValueError("ring widths must be positive") + if all(amplitude == 0.0 for amplitude in self.ring_amplitudes): + raise ValueError("a kernel needs at least one non-zero ring amplitude") + + +@dataclass(frozen=True) +class FlowLeniaEcosystemConfig: + """Frozen equations and safe bounds for ``flow-lenia-ecosystem-v1``. + + Arrays use plane-major layout ``(channels, grid, grid)`` in the NumPy oracle. The reference + topology is an even square torus, matching the power-of-two browser tiers. + """ + + grid: int + channels: int + kernels: tuple[KernelSpec, ...] + base_radius: float = 10.0 + radius_offset: float = 15.0 + cutoff_sharpness: float = 10.0 + dt: float = 0.2 + gather_radius: int = 5 + square_half_width: float = 0.65 + density_threshold: float = 2.0 + density_exponent: float = 2.0 + seed: int = 42 + + def __post_init__(self) -> None: + if self.grid < 4 or self.grid % 2 != 0: + raise ValueError("grid must be even and at least 4") + if self.channels < 1: + raise ValueError("channels must be positive") + if not self.kernels: + raise ValueError("at least one kernel is required") + if self.base_radius + self.radius_offset <= 0.0: + raise ValueError("base_radius + radius_offset must be positive") + if self.cutoff_sharpness <= 0.0: + raise ValueError("cutoff_sharpness must be positive") + if self.dt <= 0.0: + raise ValueError("dt must be positive") + if self.gather_radius < 1: + raise ValueError("gather_radius must be positive") + if self.square_half_width <= 0.0: + raise ValueError("square_half_width must be positive") + if self.square_half_width >= self.gather_radius: + raise ValueError("square_half_width must be smaller than gather_radius") + if self.grid <= 2 * self.gather_radius: + raise ValueError("grid must exceed the gather neighborhood diameter") + if self.density_threshold <= 0.0: + raise ValueError("density_threshold must be positive") + if self.density_exponent <= 0.0: + raise ValueError("density_exponent must be positive") + for kernel in self.kernels: + kernel.validate(self.channels) + + @property + def max_displacement(self) -> float: + """Largest allowed displacement component for the fixed gather neighborhood.""" + return float(self.gather_radius - self.square_half_width) + + +def _kernel( + source: int, + target: int, + relative_radius: float, + mean: float, + width: float, + weight: float, + centers: tuple[float, float, float], + amplitudes: tuple[float, float, float], + ring_widths: tuple[float, float, float], +) -> KernelSpec: + return KernelSpec( + source=source, + target=target, + relative_radius=relative_radius, + growth_mean=mean, + growth_width=width, + weight=weight, + ring_centers=centers, + ring_amplitudes=amplitudes, + ring_widths=ring_widths, + ) + + +def organism_reference_config(*, grid: int = 32, seed: int = 42) -> FlowLeniaEcosystemConfig: + """Small one-channel, three-kernel rule used by the oracle and future GPU gates. + + This is a Bit-Physics authored numerical fixture, not a claim that the parameters reproduce a + named organism from an external catalogue. + """ + kernels = ( + _kernel( + 0, + 0, + 0.62, + 0.24, + 0.070, + 0.70, + (0.20, 0.52, 0.82), + (0.8, 1.0, 0.45), + (0.020, 0.035, 0.025), + ), + _kernel( + 0, + 0, + 0.43, + 0.18, + 0.055, + 0.35, + (0.28, 0.66, 0.90), + (1.0, 0.5, 0.2), + (0.025, 0.040, 0.020), + ), + _kernel( + 0, + 0, + 0.28, + 0.31, + 0.090, + -0.18, + (0.15, 0.48, 0.76), + (0.3, 1.0, 0.7), + (0.018, 0.030, 0.030), + ), + ) + return FlowLeniaEcosystemConfig(grid=grid, channels=1, kernels=kernels, seed=seed) + + +def default_ecosystem_config(*, grid: int = 32, seed: int = 42) -> FlowLeniaEcosystemConfig: + """Three-channel/nine-kernel reference topology for ecosystem implementation work. + + The connection counts match the official example matrix ``[[2,1,0],[0,2,1],[1,0,2]]``. + Numerical ring parameters are deterministic Bit-Physics fixtures and carry no biological + interpretation. + """ + connections = ( + (0, 0), + (0, 0), + (0, 1), + (1, 1), + (1, 1), + (1, 2), + (2, 0), + (2, 2), + (2, 2), + ) + radii = (0.72, 0.48, 0.63, 0.70, 0.44, 0.61, 0.59, 0.68, 0.42) + means = (0.22, 0.31, 0.19, 0.24, 0.34, 0.17, 0.21, 0.26, 0.32) + widths = (0.070, 0.085, 0.060, 0.075, 0.090, 0.052, 0.065, 0.080, 0.088) + weights = (0.65, -0.18, 0.42, 0.61, -0.16, 0.38, 0.40, 0.58, -0.14) + center_sets = ( + (0.18, 0.50, 0.82), + (0.27, 0.61, 0.89), + (0.15, 0.46, 0.76), + ) + amplitude_sets = ((0.7, 1.0, 0.5), (1.0, 0.55, 0.25), (0.35, 1.0, 0.72)) + ring_width_sets = ((0.020, 0.035, 0.025), (0.025, 0.040, 0.020), (0.018, 0.030, 0.030)) + kernels = tuple( + _kernel( + source, + target, + radii[index], + means[index], + widths[index], + weights[index], + center_sets[index % 3], + amplitude_sets[index % 3], + ring_width_sets[index % 3], + ) + for index, (source, target) in enumerate(connections) + ) + return FlowLeniaEcosystemConfig(grid=grid, channels=3, kernels=kernels, seed=seed) diff --git a/packages/flow-lenia/flow_lenia/ecosystem_reference.py b/packages/flow-lenia/flow_lenia/ecosystem_reference.py new file mode 100644 index 00000000..7dd5ab7d --- /dev/null +++ b/packages/flow-lenia/flow_lenia/ecosystem_reference.py @@ -0,0 +1,819 @@ +"""Pure NumPy f64 oracle for the full Flow Lenia ecosystem model. + +The implementation favors transparent equations and stable iteration order over speed. It is the +scientific target for the WebGPU solver, not the browser runtime itself. In particular, transport +is the finite uniform-square Reintegration Tracking gather rather than the older point-splat +primitive in :mod:`flow_lenia.forward`. +""" + +from __future__ import annotations + +import hashlib +import math +from dataclasses import dataclass +from typing import Literal + +import numpy as np +from numpy.typing import NDArray + +from .ecosystem_config import FlowLeniaEcosystemConfig, KernelSpec + +FloatArray = NDArray[np.float64] +UInt32Array = NDArray[np.uint32] +UInt64Array = NDArray[np.uint64] +MixingRule = Literal["average", "whole", "gene-wise", "best", "negotiation"] + +IDENTITY_MIXED = np.uint32(1 << 0) +IDENTITY_MUTATED = np.uint32(1 << 1) +MIXED_LINEAGE = np.uint32(0xFFFFFFFF) + +__all__ = [ + "IDENTITY_MIXED", + "IDENTITY_MUTATED", + "MIXED_LINEAGE", + "EcosystemState", + "GenomeFields", + "MixingRule", + "MutationEvent", + "StepDiagnostics", + "build_kernels", + "clamp_displacement", + "compute_flow", + "direct_periodic_convolution", + "growth_response", + "make_uniform_genomes", + "mutate_patch", + "perceive", + "radial_kernel", + "reintegrate_square", + "reintegrate_with_genomes", + "sobel_periodic", + "square_overlap_weight", + "step_reference", +] + + +@dataclass(frozen=True) +class GenomeFields: + """Localized rule and identity planes, all indexed as ``[..., x, y]``.""" + + h: FloatArray + q: FloatArray + fingerprint: UInt64Array + lineage: UInt32Array + flags: UInt32Array + + +@dataclass(frozen=True) +class EcosystemState: + """Mass plus localized rules for an ecosystem reference step.""" + + mass: FloatArray + genomes: GenomeFields + + +@dataclass(frozen=True) +class StepDiagnostics: + """Intermediates used by independent anchors and CPU/GPU divergence reports.""" + + perception: FloatArray + growth: FloatArray + affinity: FloatArray + alpha: FloatArray + flow: FloatArray + displacement: FloatArray + clamp_mask: NDArray[np.bool_] + + +@dataclass(frozen=True) +class MutationEvent: + """One deterministic, contiguous mutation event for the bounded lineage log.""" + + event_index: int + parent_lineage: int + child_lineage: int + child_fingerprint: int + center: tuple[int, int] + radius: float + affected_mass: float + delta_h: tuple[float, ...] + delta_q: tuple[float, ...] + + +def _grid_coordinates(grid: int) -> NDArray[np.int64]: + half = grid // 2 + return np.arange(-half, half, dtype=np.int64) + + +def radial_kernel(config: FlowLeniaEcosystemConfig, spec: KernelSpec) -> FloatArray: + """Return one centered, discretely normalized three-ring kernel. + + Index ``(grid//2, grid//2)`` is zero displacement. Use :func:`numpy.fft.ifftshift` before an + FFT; :func:`direct_periodic_convolution` consumes this centered representation directly. + """ + axis = _grid_coordinates(config.grid).astype(np.float64) + xx, yy = np.meshgrid(axis, axis, indexing="ij") + radius = (config.base_radius + config.radius_offset) * spec.relative_radius + distance = np.sqrt(xx * xx + yy * yy) / radius + cutoff = 0.5 * (np.tanh((-config.cutoff_sharpness * (distance - 1.0)) / 2.0) + 1.0) + rings = np.zeros_like(distance) + for center, amplitude, width in zip( + spec.ring_centers, spec.ring_amplitudes, spec.ring_widths, strict=True + ): + rings += amplitude * np.exp(-((distance - center) ** 2) / width) + kernel = cutoff * rings + normalization = float(kernel.sum(dtype=np.float64)) + if not math.isfinite(normalization) or normalization <= 0.0: + raise ValueError("kernel normalization must be finite and positive") + return np.ascontiguousarray(kernel / normalization, dtype=np.float64) + + +def build_kernels(config: FlowLeniaEcosystemConfig) -> FloatArray: + """Build all centered normalized kernels as ``(kernel, x, y)``.""" + return np.ascontiguousarray( + np.stack([radial_kernel(config, spec) for spec in config.kernels]), dtype=np.float64 + ) + + +def direct_periodic_convolution(field: FloatArray, centered_kernel: FloatArray) -> FloatArray: + """Direct f64 periodic convolution used as an independent small-grid FFT oracle.""" + if field.ndim != 2 or centered_kernel.shape != field.shape: + raise ValueError("field and centered kernel must be equally sized 2-D arrays") + grid = field.shape[0] + if field.shape[1] != grid or grid % 2 != 0: + raise ValueError("the reference convolution requires an even square field") + out = np.zeros_like(field, dtype=np.float64) + axis = _grid_coordinates(grid) + for kernel_i, offset_i in enumerate(axis): + for kernel_j, offset_j in enumerate(axis): + weight = float(centered_kernel[kernel_i, kernel_j]) + if weight != 0.0: + out += weight * np.roll(field, (int(offset_i), int(offset_j)), axis=(0, 1)) + return np.ascontiguousarray(out, dtype=np.float64) + + +def growth_response( + values: FloatArray, mean: float | FloatArray, width: float | FloatArray +) -> FloatArray: + """Lenia bell response ``2 exp(-0.5 ((U-m)/s)^2) - 1``.""" + widths = np.asarray(width, dtype=np.float64) + if np.any(widths <= 0.0): + raise ValueError("growth widths must be positive") + z = (values - mean) / widths + return np.ascontiguousarray(2.0 * np.exp(-0.5 * z * z) - 1.0, dtype=np.float64) + + +def _validate_mass(mass: FloatArray, config: FlowLeniaEcosystemConfig) -> FloatArray: + array = np.ascontiguousarray(mass, dtype=np.float64) + expected = (config.channels, config.grid, config.grid) + if array.shape != expected: + raise ValueError(f"mass shape {array.shape} does not match {expected}") + if not np.all(np.isfinite(array)): + raise ValueError("mass must be finite") + if np.any(array < 0.0): + raise ValueError("mass must be non-negative") + return array + + +def perceive( + mass: FloatArray, + config: FlowLeniaEcosystemConfig, + *, + kernels: FloatArray | None = None, + local_h: FloatArray | None = None, + environment: FloatArray | None = None, +) -> tuple[FloatArray, FloatArray, FloatArray]: + """Compute kernel perception ``U``, growth ``I``, and channel affinity ``V``.""" + mass = _validate_mass(mass, config) + if kernels is None: + kernels = build_kernels(config) + expected_kernels = (len(config.kernels), config.grid, config.grid) + if kernels.shape != expected_kernels: + raise ValueError(f"kernel shape {kernels.shape} does not match {expected_kernels}") + + perception = np.empty(expected_kernels, dtype=np.float64) + growth = np.empty_like(perception) + affinity = np.zeros_like(mass) + if local_h is not None and local_h.shape != expected_kernels: + raise ValueError(f"local_h shape {local_h.shape} does not match {expected_kernels}") + + for index, spec in enumerate(config.kernels): + perception[index] = direct_periodic_convolution(mass[spec.source], kernels[index]) + growth[index] = growth_response(perception[index], spec.growth_mean, spec.growth_width) + weight: float | FloatArray = spec.weight if local_h is None else local_h[index] + affinity[spec.target] += weight * growth[index] + + if environment is not None: + if environment.shape != mass.shape or not np.all(np.isfinite(environment)): + raise ValueError("environment must be a finite field matching mass") + affinity += environment + return ( + np.ascontiguousarray(perception), + np.ascontiguousarray(growth), + np.ascontiguousarray(affinity), + ) + + +_SOBEL_AXIS_1 = np.array( + [[1.0, 0.0, -1.0], [2.0, 0.0, -2.0], [1.0, 0.0, -1.0]], + dtype=np.float64, +) +_SOBEL_AXIS_0 = _SOBEL_AXIS_1.T.copy() + + +def _convolve_3x3_periodic(field: FloatArray, stencil: FloatArray) -> FloatArray: + out = np.zeros_like(field, dtype=np.float64) + for stencil_i, offset_i in enumerate((-1, 0, 1)): + for stencil_j, offset_j in enumerate((-1, 0, 1)): + out += stencil[stencil_i, stencil_j] * np.roll(field, (offset_i, offset_j), axis=(0, 1)) + return out + + +def sobel_periodic(field: FloatArray) -> FloatArray: + """Official unnormalized Sobel convention with periodic boundaries. + + The result shape is ``(2, x, y)``. Component 0 differentiates array axis 0 and component 1 + differentiates axis 1. The stencil is intentionally not divided by eight because the primary + Flow Lenia implementation is also unnormalized. + """ + if field.ndim != 2 or field.shape[0] != field.shape[1]: + raise ValueError("Sobel input must be a square 2-D field") + return np.ascontiguousarray( + np.stack( + [ + _convolve_3x3_periodic(field, _SOBEL_AXIS_0), + _convolve_3x3_periodic(field, _SOBEL_AXIS_1), + ] + ), + dtype=np.float64, + ) + + +def compute_flow( + mass: FloatArray, + affinity: FloatArray, + config: FlowLeniaEcosystemConfig, +) -> tuple[FloatArray, FloatArray]: + """Compute pressure-gated channel flow and the crowding gate ``alpha``.""" + mass = _validate_mass(mass, config) + if affinity.shape != mass.shape or not np.all(np.isfinite(affinity)): + raise ValueError("affinity must be a finite field matching mass") + density_gradient = sobel_periodic(mass.sum(axis=0, dtype=np.float64)) + alpha = np.clip((mass / config.density_threshold) ** config.density_exponent, 0.0, 1.0) + flow = np.empty((config.channels, 2, config.grid, config.grid), dtype=np.float64) + for channel in range(config.channels): + affinity_gradient = sobel_periodic(affinity[channel]) + flow[channel] = ( + affinity_gradient * (1.0 - alpha[channel])[None, :, :] + - density_gradient * alpha[channel][None, :, :] + ) + return np.ascontiguousarray(flow), np.ascontiguousarray(alpha) + + +def clamp_displacement( + flow: FloatArray, config: FlowLeniaEcosystemConfig +) -> tuple[FloatArray, NDArray[np.bool_]]: + """Convert flow to displacement and component-clamp it to the gather proof domain.""" + expected = (config.channels, 2, config.grid, config.grid) + if flow.shape != expected or not np.all(np.isfinite(flow)): + raise ValueError(f"flow must be finite with shape {expected}") + raw = config.dt * flow + displacement = np.clip(raw, -config.max_displacement, config.max_displacement) + clamped = np.any(displacement != raw, axis=1) + return np.ascontiguousarray(displacement), np.ascontiguousarray(clamped) + + +def _periodic_delta(delta: float, grid: int) -> float: + return float((delta + 0.5 * grid) % grid - 0.5 * grid) + + +def _overlap_length(center_delta: float, half_width: float) -> float: + return float( + np.clip( + 0.5 + half_width - abs(center_delta), + 0.0, + min(1.0, 2.0 * half_width), + ) + ) + + +def square_overlap_weight( + destination: tuple[int, int], + source: tuple[int, int], + displacement: tuple[float, float], + *, + grid: int, + half_width: float, +) -> float: + """Exact normalized overlap of one moved uniform square with one unit destination cell.""" + if half_width <= 0.0: + raise ValueError("half_width must be positive") + source_center_i = source[0] + 0.5 + displacement[0] + source_center_j = source[1] + 0.5 + displacement[1] + destination_center_i = destination[0] + 0.5 + destination_center_j = destination[1] + 0.5 + delta_i = _periodic_delta(destination_center_i - source_center_i, grid) + delta_j = _periodic_delta(destination_center_j - source_center_j, grid) + overlap_i = _overlap_length(delta_i, half_width) + overlap_j = _overlap_length(delta_j, half_width) + return overlap_i * overlap_j / (4.0 * half_width * half_width) + + +def _validate_displacement( + displacement: FloatArray, config: FlowLeniaEcosystemConfig +) -> FloatArray: + expected = (config.channels, 2, config.grid, config.grid) + array = np.ascontiguousarray(displacement, dtype=np.float64) + if array.shape != expected or not np.all(np.isfinite(array)): + raise ValueError(f"displacement must be finite with shape {expected}") + if np.any(np.abs(array) > config.max_displacement + 1e-12): + raise ValueError("displacement exceeds the fixed gather neighborhood") + return array + + +def _candidate_weights( + destination_i: int, + destination_j: int, + source_i: int, + source_j: int, + displacement: FloatArray, + config: FlowLeniaEcosystemConfig, +) -> FloatArray: + weights = np.empty(config.channels, dtype=np.float64) + for channel in range(config.channels): + weights[channel] = square_overlap_weight( + (destination_i, destination_j), + (source_i, source_j), + ( + float(displacement[channel, 0, source_i, source_j]), + float(displacement[channel, 1, source_i, source_j]), + ), + grid=config.grid, + half_width=config.square_half_width, + ) + return weights + + +def reintegrate_square( + mass: FloatArray, + displacement: FloatArray, + config: FlowLeniaEcosystemConfig, +) -> FloatArray: + """Destination-gather finite-square Reintegration Tracking on a torus.""" + mass = _validate_mass(mass, config) + displacement = _validate_displacement(displacement, config) + out = np.zeros_like(mass, dtype=np.float64) + dd = config.gather_radius + for destination_i in range(config.grid): + for destination_j in range(config.grid): + for offset_i in range(-dd, dd + 1): + source_i = (destination_i + offset_i) % config.grid + for offset_j in range(-dd, dd + 1): + source_j = (destination_j + offset_j) % config.grid + weights = _candidate_weights( + destination_i, + destination_j, + source_i, + source_j, + displacement, + config, + ) + out[:, destination_i, destination_j] += mass[:, source_i, source_j] * weights + return np.ascontiguousarray(out, dtype=np.float64) + + +def step_reference( + mass: FloatArray, + config: FlowLeniaEcosystemConfig, + *, + kernels: FloatArray | None = None, + local_h: FloatArray | None = None, + environment: FloatArray | None = None, +) -> tuple[FloatArray, StepDiagnostics]: + """Advance one full affinity/pressure/uniform-square reference step.""" + perception, growth, affinity = perceive( + mass, + config, + kernels=kernels, + local_h=local_h, + environment=environment, + ) + flow, alpha = compute_flow(mass, affinity, config) + displacement, clamp_mask = clamp_displacement(flow, config) + next_mass = reintegrate_square(mass, displacement, config) + return next_mass, StepDiagnostics( + perception=perception, + growth=growth, + affinity=affinity, + alpha=alpha, + flow=flow, + displacement=displacement, + clamp_mask=clamp_mask, + ) + + +def _splitmix64(value: int) -> int: + mask = (1 << 64) - 1 + value = (value + 0x9E3779B97F4A7C15) & mask + value = ((value ^ (value >> 30)) * 0xBF58476D1CE4E5B9) & mask + value = ((value ^ (value >> 27)) * 0x94D049BB133111EB) & mask + return (value ^ (value >> 31)) & mask + + +def _counter_hash(seed: int, step: int, destination: int, candidate: int, gene: int) -> int: + value = seed & ((1 << 64) - 1) + for component in (step, destination, candidate, gene): + value = _splitmix64(value ^ (int(component) & ((1 << 64) - 1))) + return value + + +def _unit_float(value: int) -> float: + return ((value >> 11) + 0.5) * (1.0 / (1 << 53)) + + +def _fingerprint_values(*arrays: NDArray[np.generic]) -> np.uint64: + digest = hashlib.blake2b(digest_size=8, person=b"flow-lenia-v1") + for array in arrays: + canonical = np.ascontiguousarray(array) + digest.update(str(canonical.dtype).encode("ascii")) + digest.update(np.asarray(canonical.shape, dtype=" GenomeFields: + """Create a coherent localized rule field from the config's global weights.""" + mass = _validate_mass(mass, config) + h_vector = np.asarray([spec.weight for spec in config.kernels], dtype=np.float64) + q_vector = np.full(len(config.kernels), q_value, dtype=np.float64) + h = np.broadcast_to(h_vector[:, None, None], (len(h_vector), config.grid, config.grid)).copy() + q = np.broadcast_to(q_vector[:, None, None], (len(q_vector), config.grid, config.grid)).copy() + occupied = mass.sum(axis=0) > 0.0 + fingerprint_value = _fingerprint_values(h_vector, q_vector) + fingerprint = np.where(occupied, fingerprint_value, np.uint64(0)).astype(np.uint64) + lineages = np.where(occupied, np.uint32(lineage), np.uint32(0)).astype(np.uint32) + flags = np.zeros((config.grid, config.grid), dtype=np.uint32) + return GenomeFields( + h=np.ascontiguousarray(h), + q=np.ascontiguousarray(q), + fingerprint=np.ascontiguousarray(fingerprint), + lineage=np.ascontiguousarray(lineages), + flags=np.ascontiguousarray(flags), + ) + + +def _validate_genomes(genomes: GenomeFields, config: FlowLeniaEcosystemConfig) -> None: + gene_shape = (len(config.kernels), config.grid, config.grid) + identity_shape = (config.grid, config.grid) + if genomes.h.shape != gene_shape or genomes.q.shape != gene_shape: + raise ValueError(f"H and Q must have shape {gene_shape}") + if not np.all(np.isfinite(genomes.h)) or not np.all(np.isfinite(genomes.q)): + raise ValueError("H and Q must be finite") + for field in (genomes.fingerprint, genomes.lineage, genomes.flags): + if field.shape != identity_shape: + raise ValueError(f"identity fields must have shape {identity_shape}") + + +def _same_genome( + genomes: GenomeFields, + first: tuple[int, int], + candidate: tuple[int, int], +) -> bool: + fi, fj = first + ci, cj = candidate + return bool( + genomes.fingerprint[fi, fj] == genomes.fingerprint[ci, cj] + and genomes.lineage[fi, fj] == genomes.lineage[ci, cj] + and genomes.flags[fi, fj] == genomes.flags[ci, cj] + and np.array_equal(genomes.h[:, fi, fj], genomes.h[:, ci, cj]) + and np.array_equal(genomes.q[:, fi, fj], genomes.q[:, ci, cj]) + ) + + +def reintegrate_with_genomes( + state: EcosystemState, + displacement: FloatArray, + config: FlowLeniaEcosystemConfig, + *, + rule: MixingRule, + step: int, + contextual_growth: FloatArray | None = None, + negotiation_beta: float = 1.0, +) -> EcosystemState: + """Transport mass and stream localized genome inheritance through the same gather. + + ``contextual_growth`` has shape ``(kernels, grid, grid)`` and supplies the local ``I_k`` values + for best-affinity and negotiation scoring. No candidate genome is considered unless positive + mass actually arrives from its source cell. + """ + mass = _validate_mass(state.mass, config) + displacement = _validate_displacement(displacement, config) + _validate_genomes(state.genomes, config) + if rule not in ("average", "whole", "gene-wise", "best", "negotiation"): + raise ValueError(f"unknown mixing rule: {rule}") + kernel_count = len(config.kernels) + if contextual_growth is None: + contextual_growth = np.zeros((kernel_count, config.grid, config.grid), dtype=np.float64) + if contextual_growth.shape != (kernel_count, config.grid, config.grid): + raise ValueError("contextual_growth has the wrong shape") + if not math.isfinite(negotiation_beta) or negotiation_beta < 0.0: + raise ValueError("negotiation_beta must be finite and non-negative") + + next_mass = np.zeros_like(mass) + next_h = np.zeros_like(state.genomes.h) + next_q = np.zeros_like(state.genomes.q) + next_fingerprint = np.zeros_like(state.genomes.fingerprint) + next_lineage = np.zeros_like(state.genomes.lineage) + next_flags = np.zeros_like(state.genomes.flags) + dd = config.gather_radius + gene_count = 2 * kernel_count + + for destination_i in range(config.grid): + for destination_j in range(config.grid): + destination_index = destination_i * config.grid + destination_j + total_incoming = 0.0 + first_source: tuple[int, int] | None = None + all_same = True + average_h = np.zeros(kernel_count, dtype=np.float64) + average_q = np.zeros(kernel_count, dtype=np.float64) + selected_source: tuple[int, int] | None = None + selected_gene_sources: list[tuple[int, int] | None] = [None] * gene_count + best_score = -math.inf + negotiation_score = -math.inf + candidate_index = 0 + + for offset_i in range(-dd, dd + 1): + source_i = (destination_i + offset_i) % config.grid + for offset_j in range(-dd, dd + 1): + source_j = (destination_j + offset_j) % config.grid + weights = _candidate_weights( + destination_i, + destination_j, + source_i, + source_j, + displacement, + config, + ) + channel_arrivals = mass[:, source_i, source_j] * weights + next_mass[:, destination_i, destination_j] += channel_arrivals + incoming = float(channel_arrivals.sum(dtype=np.float64)) + if incoming <= 0.0: + candidate_index += 1 + continue + + source = (source_i, source_j) + total_incoming += incoming + if first_source is None: + first_source = source + elif all_same and not _same_genome(state.genomes, first_source, source): + all_same = False + + source_h = state.genomes.h[:, source_i, source_j] + source_q = state.genomes.q[:, source_i, source_j] + average_h += incoming * source_h + average_q += incoming * source_q + + reservoir_u = _unit_float( + _counter_hash( + config.seed, + step, + destination_index, + candidate_index, + -1, + ) + ) + if selected_source is None or reservoir_u < incoming / total_incoming: + selected_source = source + + for gene in range(gene_count): + gene_u = _unit_float( + _counter_hash( + config.seed, + step, + destination_index, + candidate_index, + gene, + ) + ) + if ( + selected_gene_sources[gene] is None + or gene_u < incoming / total_incoming + ): + selected_gene_sources[gene] = source + + context = contextual_growth[:, destination_i, destination_j] + affinity_score = float(np.dot(context, source_h)) + if affinity_score > best_score: + best_score = affinity_score + if rule == "best": + selected_source = source + + negotiation_logit = ( + negotiation_beta * incoming * float(np.dot(context, source_q)) + ) + gumbel_u = _unit_float( + _counter_hash( + config.seed, + step, + destination_index, + candidate_index, + gene_count, + ) + ) + gumbel = -math.log(-math.log(gumbel_u)) + if negotiation_logit + gumbel > negotiation_score: + negotiation_score = negotiation_logit + gumbel + if rule == "negotiation": + selected_source = source + candidate_index += 1 + + if total_incoming <= 0.0 or first_source is None: + continue + if all_same: + selected_source = first_source + rule_for_cell: MixingRule = "whole" + else: + rule_for_cell = rule + + if rule_for_cell == "average": + next_h[:, destination_i, destination_j] = average_h / total_incoming + next_q[:, destination_i, destination_j] = average_q / total_incoming + next_fingerprint[destination_i, destination_j] = _fingerprint_values( + next_h[:, destination_i, destination_j], + next_q[:, destination_i, destination_j], + ) + next_lineage[destination_i, destination_j] = MIXED_LINEAGE + next_flags[destination_i, destination_j] = IDENTITY_MIXED + elif rule_for_cell == "gene-wise": + parent_fingerprints = np.empty(gene_count, dtype=np.uint64) + for gene, gene_source in enumerate(selected_gene_sources): + if gene_source is None: + raise RuntimeError("positive incoming mass left a gene reservoir empty") + source_i, source_j = gene_source + if gene < kernel_count: + next_h[gene, destination_i, destination_j] = state.genomes.h[ + gene, source_i, source_j + ] + else: + q_gene = gene - kernel_count + next_q[q_gene, destination_i, destination_j] = state.genomes.q[ + q_gene, source_i, source_j + ] + parent_fingerprints[gene] = state.genomes.fingerprint[source_i, source_j] + next_fingerprint[destination_i, destination_j] = _fingerprint_values( + parent_fingerprints + ) + next_lineage[destination_i, destination_j] = MIXED_LINEAGE + next_flags[destination_i, destination_j] = IDENTITY_MIXED + else: + if selected_source is None: + raise RuntimeError("positive incoming mass left the selector empty") + source_i, source_j = selected_source + next_h[:, destination_i, destination_j] = state.genomes.h[:, source_i, source_j] + next_q[:, destination_i, destination_j] = state.genomes.q[:, source_i, source_j] + next_fingerprint[destination_i, destination_j] = state.genomes.fingerprint[ + source_i, source_j + ] + next_lineage[destination_i, destination_j] = state.genomes.lineage[ + source_i, source_j + ] + next_flags[destination_i, destination_j] = state.genomes.flags[source_i, source_j] + + return EcosystemState( + mass=np.ascontiguousarray(next_mass), + genomes=GenomeFields( + h=np.ascontiguousarray(next_h), + q=np.ascontiguousarray(next_q), + fingerprint=np.ascontiguousarray(next_fingerprint), + lineage=np.ascontiguousarray(next_lineage), + flags=np.ascontiguousarray(next_flags), + ), + ) + + +def _normal_from_counter(seed: int, event_index: int, gene: int, lane: int) -> float: + u1 = _unit_float(_counter_hash(seed, event_index, gene, lane, 0)) + u2 = _unit_float(_counter_hash(seed, event_index, gene, lane, 1)) + return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2) + + +def _event_identity(seed: int, parent_lineage: int, event_index: int, tag: int) -> int: + value = _counter_hash(seed, event_index, parent_lineage, tag, 0) + return value + + +def _lineage_hue_key(lineage: int) -> int: + """Pack the renderer's stable parent-hue inputs into identity flag metadata.""" + value = lineage & 0xFFFFFFFF + value = ((value ^ (value >> 16)) * 0x7FEB352D) & 0xFFFFFFFF + value = ((value ^ (value >> 15)) * 0x846CA68B) & 0xFFFFFFFF + value = (value ^ (value >> 16)) & 0xFFFFFFFF + return (value & 0xFFFF) | ((lineage & 7) << 16) + + +def mutate_patch( + state: EcosystemState, + config: FlowLeniaEcosystemConfig, + *, + center: tuple[int, int], + radius: float, + event_index: int, + scale: float = 0.05, + h_bounds: tuple[float, float] = (-2.0, 2.0), + q_bounds: tuple[float, float] = (-2.0, 2.0), +) -> tuple[EcosystemState, MutationEvent]: + """Apply one seed-addressed Gaussian rule delta to a coherent toroidal lineage patch.""" + mass = _validate_mass(state.mass, config) + _validate_genomes(state.genomes, config) + if radius <= 0.0 or not math.isfinite(radius): + raise ValueError("mutation radius must be finite and positive") + if scale < 0.0 or not math.isfinite(scale): + raise ValueError("mutation scale must be finite and non-negative") + if h_bounds[0] > h_bounds[1] or q_bounds[0] > q_bounds[1]: + raise ValueError("mutation bounds must be ordered") + + center_i = center[0] % config.grid + center_j = center[1] % config.grid + density = mass.sum(axis=0, dtype=np.float64) + patch = np.zeros((config.grid, config.grid), dtype=np.bool_) + lineage_mass: dict[int, float] = {} + for i in range(config.grid): + distance_i = min(abs(i - center_i), config.grid - abs(i - center_i)) + for j in range(config.grid): + distance_j = min(abs(j - center_j), config.grid - abs(j - center_j)) + if ( + distance_i * distance_i + distance_j * distance_j <= radius * radius + and density[i, j] > 0.0 + ): + patch[i, j] = True + lineage = int(state.genomes.lineage[i, j]) + lineage_mass[lineage] = lineage_mass.get(lineage, 0.0) + float(density[i, j]) + if not lineage_mass: + raise ValueError("mutation patch contains no matter") + parent_lineage = min(lineage_mass, key=lambda lineage: (-lineage_mass[lineage], lineage)) + patch &= state.genomes.lineage == np.uint32(parent_lineage) + + kernel_count = len(config.kernels) + delta_h = np.asarray( + [ + scale * _normal_from_counter(config.seed, event_index, gene, 0) + for gene in range(kernel_count) + ], + dtype=np.float64, + ) + delta_q = np.asarray( + [ + scale * _normal_from_counter(config.seed, event_index, gene, 1) + for gene in range(kernel_count) + ], + dtype=np.float64, + ) + child_lineage = _event_identity(config.seed, parent_lineage, event_index, 0) & 0xFFFFFFFE + if child_lineage == 0: + child_lineage = 2 + child_fingerprint = _event_identity(config.seed, parent_lineage, event_index, 1) + if child_fingerprint == 0: + child_fingerprint = 1 + + next_h = state.genomes.h.copy() + next_q = state.genomes.q.copy() + for gene in range(kernel_count): + next_h[gene, patch] = np.clip(next_h[gene, patch] + delta_h[gene], h_bounds[0], h_bounds[1]) + next_q[gene, patch] = np.clip(next_q[gene, patch] + delta_q[gene], q_bounds[0], q_bounds[1]) + next_fingerprint = state.genomes.fingerprint.copy() + next_lineage = state.genomes.lineage.copy() + next_flags = state.genomes.flags.copy() + next_fingerprint[patch] = np.uint64(child_fingerprint) + next_lineage[patch] = np.uint32(child_lineage) + parent_hue_metadata = np.uint32(_lineage_hue_key(parent_lineage) << 8) + next_flags[patch] |= IDENTITY_MUTATED | parent_hue_metadata + affected_mass = float(density[patch].sum(dtype=np.float64)) + + next_state = EcosystemState( + mass=mass.copy(), + genomes=GenomeFields( + h=np.ascontiguousarray(next_h), + q=np.ascontiguousarray(next_q), + fingerprint=np.ascontiguousarray(next_fingerprint), + lineage=np.ascontiguousarray(next_lineage), + flags=np.ascontiguousarray(next_flags), + ), + ) + event = MutationEvent( + event_index=event_index, + parent_lineage=parent_lineage, + child_lineage=child_lineage, + child_fingerprint=child_fingerprint, + center=(center_i, center_j), + radius=radius, + affected_mass=affected_mass, + delta_h=tuple(float(value) for value in delta_h), + delta_q=tuple(float(value) for value in delta_q), + ) + return next_state, event diff --git a/packages/flow-lenia/scripts/generate_m2_fixture.py b/packages/flow-lenia/scripts/generate_m2_fixture.py new file mode 100644 index 00000000..17816d7f --- /dev/null +++ b/packages/flow-lenia/scripts/generate_m2_fixture.py @@ -0,0 +1,114 @@ +"""Generate the compact f32 browser conformance fixture from the f64 M1 oracle.""" + +from __future__ import annotations + +import base64 +import json +import math +from pathlib import Path + +import numpy as np +from numpy.typing import NDArray + +from flow_lenia.ecosystem_config import MODEL_VARIANT, default_ecosystem_config +from flow_lenia.ecosystem_reference import build_kernels, step_reference + +ROOT = Path(__file__).resolve().parents[3] +OUTPUT = ROOT / "packages/flow-lenia/web/src/prove/organism-fixture.json" + + +def conformance_mass(grid: int, variant: int) -> NDArray[np.float64]: + """Match ``makeConformanceMass`` after its browser f32 upload boundary.""" + mass = np.empty((3, grid, grid), dtype=np.float32) + for i in range(grid): + for j in range(grid): + for channel in range(3): + p = 2.0 * math.pi * ((channel + 1) * i + (channel + 2) * j) / grid + q = 2.0 * math.pi * ((channel + 3) * i - (channel + 1) * j) / grid + value = 0.16 + channel * 0.035 + 0.055 * math.sin(p) + 0.025 * math.cos(q) + if variant == 1 and (i < 2 or j >= grid - 2): + value += 0.24 / (channel + 1) + if ( + variant == 2 + and grid / 2 - 2 <= i <= grid / 2 + 1 + and grid / 2 - 2 <= j <= grid / 2 + 1 + ): + value += 2.4 - channel * 0.25 + mass[channel, i, j] = value + return mass.astype(np.float64) + + +def field(array: NDArray[np.generic]) -> dict[str, object]: + values = np.ascontiguousarray(array, dtype=" dict[str, object]: + config = default_ecosystem_config(grid=16, seed=91) + kernels = build_kernels(config) + initial = conformance_mass(config.grid, variant) + one_step, diagnostics = step_reference(initial, config, kernels=kernels) + rollout = one_step + for _ in range(3): + rollout, _ = step_reference(rollout, config, kernels=kernels) + fields: dict[str, object] = { + "initial_mass": field(initial), + "displacement_step_1": field(diagnostics.displacement), + "mass_step_1": field(one_step), + "mass_step_4": field(rollout), + } + if complete: + fields.update( + perception_step_1=field(diagnostics.perception), + growth_step_1=field(diagnostics.growth), + affinity_step_1=field(diagnostics.affinity), + alpha_step_1=field(diagnostics.alpha), + flow_step_1=field(diagnostics.flow), + ) + return {"name": name, "variant": variant, "fields": fields} + + +def main() -> None: + config = default_ecosystem_config(grid=16, seed=91) + document = { + "schema_version": "flow-lenia-m2-conformance-v1", + "model_variant": MODEL_VARIANT, + "oracle": "flow_lenia.ecosystem_reference f64; expected arrays quantized to f32", + "config": { + "grid": config.grid, + "channels": config.channels, + "kernels": len(config.kernels), + "seed": config.seed, + "dt": config.dt, + "dd": config.gather_radius, + "sigma": config.square_half_width, + "density_threshold": config.density_threshold, + "density_exponent": config.density_exponent, + }, + "tolerances": { + "perception_abs": 1.0e-6, + "growth_abs": 1.0e-5, + "affinity_abs": 1.0e-5, + "alpha_abs": 2.0e-7, + "flow_abs": 2.0e-5, + "displacement_abs": 4.0e-6, + "mass_step_1_abs": 1.0e-5, + "mass_step_4_abs": 2.0e-5, + "mass_relative_ledger": 5.0e-5, + }, + "cases": [ + generate_case("smooth-periodic", 0, complete=True), + generate_case("seam-loaded", 1, complete=False), + generate_case("crowded-pressure", 2, complete=False), + ], + } + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/packages/flow-lenia/scripts/generate_m4_fixture.py b/packages/flow-lenia/scripts/generate_m4_fixture.py new file mode 100644 index 00000000..ae736be2 --- /dev/null +++ b/packages/flow-lenia/scripts/generate_m4_fixture.py @@ -0,0 +1,155 @@ +"""Generate localized-inheritance and mutation fixtures from the f64 ecosystem oracle.""" + +from __future__ import annotations + +import base64 +import json +import math +from pathlib import Path + +import numpy as np +from numpy.typing import NDArray + +from flow_lenia.ecosystem_config import MODEL_VARIANT, default_ecosystem_config +from flow_lenia.ecosystem_reference import ( + EcosystemState, + GenomeFields, + MixingRule, + build_kernels, + clamp_displacement, + compute_flow, + mutate_patch, + perceive, + reintegrate_with_genomes, +) + +ROOT = Path(__file__).resolve().parents[3] +OUTPUT = ROOT / "packages/flow-lenia/web/src/prove/ecosystem-fixture.json" +RULES: tuple[MixingRule, ...] = ("average", "whole", "gene-wise", "best", "negotiation") + + +def encoded(array: NDArray[np.generic], dtype: str) -> dict[str, object]: + values = np.ascontiguousarray(array, dtype=dtype) + return { + "shape": list(values.shape), + "dtype": f"{np.dtype(dtype).name}-le-base64", + "data": base64.b64encode(values.tobytes()).decode("ascii"), + } + + +def initial_state(grid: int) -> EcosystemState: + config = default_ecosystem_config(grid=grid, seed=91) + mass = np.empty((3, grid, grid), dtype=np.float32) + h = np.empty((9, grid, grid), dtype=np.float32) + q = np.empty((9, grid, grid), dtype=np.float32) + fingerprint = np.empty((grid, grid), dtype=np.uint64) + lineage = np.empty((grid, grid), dtype=np.uint32) + flags = np.zeros((grid, grid), dtype=np.uint32) + base = np.asarray([kernel.weight for kernel in config.kernels], dtype=np.float32) + for i in range(grid): + for j in range(grid): + for channel in range(3): + p = 2.0 * math.pi * ((channel + 1) * i + (channel + 2) * j) / grid + q_wave = 2.0 * math.pi * ((channel + 3) * i - (channel + 1) * j) / grid + mass[channel, i, j] = ( + 0.17 + channel * 0.03 + 0.05 * math.sin(p) + 0.02 * math.cos(q_wave) + ) + founder = 1 + (3 * j // grid) + for gene in range(9): + phase = founder * 1.31 + gene * 0.77 + h[gene, i, j] = base[gene] + 0.11 * math.sin(phase) + q[gene, i, j] = 0.75 + 0.22 * math.cos(phase * 0.81) + fingerprint[i, j] = np.uint64(0x1020304050607000 + founder) + lineage[i, j] = np.uint32(founder) + return EcosystemState( + mass=mass.astype(np.float64), + genomes=GenomeFields( + h=h.astype(np.float64), + q=q.astype(np.float64), + fingerprint=fingerprint, + lineage=lineage, + flags=flags, + ), + ) + + +def main() -> None: + config = default_ecosystem_config(grid=16, seed=91) + state = initial_state(config.grid) + kernels = build_kernels(config) + _, growth, affinity = perceive(state.mass, config, kernels=kernels, local_h=state.genomes.h) + flow, _ = compute_flow(state.mass, affinity, config) + displacement, _ = clamp_displacement(flow, config) + cases: list[dict[str, object]] = [] + for rule in RULES: + result = reintegrate_with_genomes( + state, + displacement, + config, + rule=rule, + step=0, + contextual_growth=growth, + negotiation_beta=1.0, + ) + cases.append( + { + "rule": rule, + "mass_step_1": encoded(result.mass, "> 32, + ], + "center": list(mutation.center), + "radius": mutation.radius, + "delta_h": list(mutation.delta_h), + "delta_q": list(mutation.delta_q), + "child_flags": int( + mutated_state.genomes.flags[ + mutated_state.genomes.lineage == np.uint32(mutation.child_lineage) + ][0] + ), + }, + } + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/packages/flow-lenia/tests/fixtures/flow-lenia-ecosystem-v1.json b/packages/flow-lenia/tests/fixtures/flow-lenia-ecosystem-v1.json new file mode 100644 index 00000000..1c351556 --- /dev/null +++ b/packages/flow-lenia/tests/fixtures/flow-lenia-ecosystem-v1.json @@ -0,0 +1,70 @@ +{ + "model_variant": "flow-lenia-ecosystem-v1", + "provenance": { + "equations": "Plantec et al., Flow Lenia (ALIFE 2022; arXiv:2212.07906)", + "primary_code": "https://github.com/erwanplantec/FlowLenia", + "fixture_origin": "Bit-Physics authored numerical and analytical anchors" + }, + "kernel": { + "grid": 16, + "seed": 91, + "sums": [ + 0.9999999999999998, + 1.0000000000000002, + 0.9999999999999999 + ], + "sample_offsets": [ + [0, 0], + [1, 0], + [3, 0], + [0, 4] + ], + "samples": [ + [ + 0.0005161832106848863, + 0.0015297573772329724, + 0.004015673437432702, + 0.0038758355321612003 + ], + [ + 0.0003364192301321962, + 0.0019127864019845625, + 0.007837040827865831, + 0.005989461997136468 + ], + [ + 0.0009506435177430281, + 0.0035393697487711736, + 0.010281441434615261, + 0.010535194470401087 + ] + ] + }, + "growth": { + "mean": 0.3, + "width": 0.1, + "inputs": [0.3, 0.4, 0.5], + "outputs": [1.0, 0.21306131942526685, -0.7293294335267746] + }, + "transport": { + "grid": 8, + "source": [3, 3], + "half_width": 0.25, + "displacement": [0.4, 0.4], + "destinations": [[3, 3], [3, 4], [4, 3], [4, 4]], + "weights": [0.49, 0.21, 0.21, 0.09] + }, + "mutation": { + "seed": 91, + "event_index": 5, + "parent_lineage": 12, + "child_lineage": 564381756, + "child_fingerprint": 9100830937284941169, + "center": [0, 0], + "radius": 1.1, + "scale": 0.08, + "affected_mass": 5.0, + "delta_h": [0.15026070546818013, 0.05676989169958144, 0.16249077826232236], + "delta_q": [0.0721502061163193, -0.17240856592771034, 0.053865904676694484] + } +} diff --git a/packages/flow-lenia/tests/test_ecosystem_golden.py b/packages/flow-lenia/tests/test_ecosystem_golden.py new file mode 100644 index 00000000..34a03c38 --- /dev/null +++ b/packages/flow-lenia/tests/test_ecosystem_golden.py @@ -0,0 +1,101 @@ +"""Frozen cross-language anchors for ``flow-lenia-ecosystem-v1``.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import numpy as np + +from flow_lenia.ecosystem_config import MODEL_VARIANT, organism_reference_config +from flow_lenia.ecosystem_reference import ( + EcosystemState, + build_kernels, + growth_response, + make_uniform_genomes, + mutate_patch, + square_overlap_weight, +) + +FIXTURE_PATH = Path(__file__).parent / "fixtures" / "flow-lenia-ecosystem-v1.json" + + +def _fixture() -> dict[str, Any]: + with FIXTURE_PATH.open(encoding="utf-8") as stream: + return json.load(stream) + + +def test_model_variant_and_kernel_samples_match_frozen_fixture() -> None: + fixture = _fixture() + kernel_fixture = fixture["kernel"] + config = organism_reference_config( + grid=int(kernel_fixture["grid"]), seed=int(kernel_fixture["seed"]) + ) + kernels = build_kernels(config) + center = config.grid // 2 + samples = np.asarray( + [ + [ + kernels[index, center + di, center + dj] + for di, dj in kernel_fixture["sample_offsets"] + ] + for index in range(len(config.kernels)) + ] + ) + + assert fixture["model_variant"] == MODEL_VARIANT + np.testing.assert_allclose( + kernels.sum(axis=(1, 2)), kernel_fixture["sums"], atol=2e-16, rtol=0.0 + ) + np.testing.assert_allclose(samples, kernel_fixture["samples"], atol=2e-16, rtol=1e-14) + + +def test_growth_and_square_overlap_match_frozen_fixture() -> None: + fixture = _fixture() + growth = fixture["growth"] + response = growth_response( + np.asarray(growth["inputs"], dtype=np.float64), + float(growth["mean"]), + float(growth["width"]), + ) + np.testing.assert_allclose(response, growth["outputs"], atol=2e-16, rtol=1e-15) + + transport = fixture["transport"] + weights = [ + square_overlap_weight( + tuple(destination), + tuple(transport["source"]), + tuple(transport["displacement"]), + grid=int(transport["grid"]), + half_width=float(transport["half_width"]), + ) + for destination in transport["destinations"] + ] + np.testing.assert_allclose(weights, transport["weights"], atol=2e-15, rtol=0.0) + + +def test_seeded_mutation_event_matches_frozen_fixture() -> None: + mutation = _fixture()["mutation"] + config = organism_reference_config(grid=16, seed=int(mutation["seed"])) + mass = np.ones((1, 16, 16), dtype=np.float64) + state = EcosystemState( + mass, + make_uniform_genomes(mass, config, lineage=int(mutation["parent_lineage"])), + ) + + _, event = mutate_patch( + state, + config, + center=tuple(mutation["center"]), + radius=float(mutation["radius"]), + event_index=int(mutation["event_index"]), + scale=float(mutation["scale"]), + ) + + assert event.parent_lineage == mutation["parent_lineage"] + assert event.child_lineage == mutation["child_lineage"] + assert event.child_fingerprint == mutation["child_fingerprint"] + assert event.affected_mass == mutation["affected_mass"] + np.testing.assert_allclose(event.delta_h, mutation["delta_h"], atol=2e-16, rtol=0.0) + np.testing.assert_allclose(event.delta_q, mutation["delta_q"], atol=2e-16, rtol=0.0) diff --git a/packages/flow-lenia/tests/test_ecosystem_properties.py b/packages/flow-lenia/tests/test_ecosystem_properties.py new file mode 100644 index 00000000..0c7eef1d --- /dev/null +++ b/packages/flow-lenia/tests/test_ecosystem_properties.py @@ -0,0 +1,196 @@ +"""Property checks for square transport, pressure, and localized inheritance.""" + +from __future__ import annotations + +import numpy as np +from hypothesis import Phase, given, settings +from hypothesis import strategies as st + +from flow_lenia.ecosystem_config import FlowLeniaEcosystemConfig, KernelSpec +from flow_lenia.ecosystem_reference import ( + EcosystemState, + GenomeFields, + clamp_displacement, + make_uniform_genomes, + reintegrate_square, + reintegrate_with_genomes, + square_overlap_weight, +) + +_SETTINGS = settings( + max_examples=12, + deadline=None, + derandomize=True, + phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), +) + + +def _config( + *, channels: int = 1, half_width: float = 0.65, seed: int = 123 +) -> FlowLeniaEcosystemConfig: + kernels = tuple( + KernelSpec( + source=channel, + target=channel, + relative_radius=0.5, + growth_mean=0.25, + growth_width=0.08, + weight=0.6, + ring_centers=(0.2, 0.5, 0.8), + ring_amplitudes=(0.5, 1.0, 0.4), + ring_widths=(0.02, 0.04, 0.03), + ) + for channel in range(channels) + ) + return FlowLeniaEcosystemConfig( + grid=8, + channels=channels, + kernels=kernels, + gather_radius=1, + square_half_width=half_width, + seed=seed, + ) + + +@given( + seed=st.integers(min_value=0, max_value=10_000), + half_width=st.floats(min_value=0.15, max_value=0.85, allow_nan=False), +) +@_SETTINGS +def test_transport_mass_ledger_closes_for_bounded_random_fields( + seed: int, half_width: float +) -> None: + config = _config(channels=2, half_width=half_width, seed=seed) + rng = np.random.default_rng(seed) + mass = rng.uniform(0.0, 2.0, size=(2, 8, 8)) + displacement = rng.uniform( + -config.max_displacement, + config.max_displacement, + size=(2, 2, 8, 8), + ) + + result = reintegrate_square(mass, displacement, config) + + np.testing.assert_allclose( + result.sum(axis=(1, 2)), mass.sum(axis=(1, 2)), atol=3e-13, rtol=4e-15 + ) + assert float(result.min()) >= 0.0 + assert np.all(np.isfinite(result)) + + +@given( + displacement_i=st.floats(min_value=-0.8, max_value=0.8, allow_nan=False), + displacement_j=st.floats(min_value=-0.8, max_value=0.8, allow_nan=False), + half_width=st.floats(min_value=0.15, max_value=0.85, allow_nan=False), +) +@_SETTINGS +def test_overlap_weights_are_nonnegative_and_source_normalized( + displacement_i: float, displacement_j: float, half_width: float +) -> None: + weights = np.asarray( + [ + square_overlap_weight( + (i, j), + (3, 3), + (displacement_i, displacement_j), + grid=8, + half_width=half_width, + ) + for i in range(8) + for j in range(8) + ] + ) + + assert float(weights.min()) >= 0.0 + np.testing.assert_allclose(weights.sum(), 1.0, atol=2e-15, rtol=0.0) + + +@given( + seed=st.integers(min_value=0, max_value=10_000), + shift_i=st.integers(-3, 3), + shift_j=st.integers(-3, 3), +) +@_SETTINGS +def test_transport_is_torus_translation_equivariant(seed: int, shift_i: int, shift_j: int) -> None: + config = _config(seed=seed) + rng = np.random.default_rng(seed) + mass = rng.uniform(0.0, 1.0, size=(1, 8, 8)) + displacement = rng.uniform(-0.25, 0.25, size=(1, 2, 8, 8)) + translated_mass = np.roll(mass, (shift_i, shift_j), axis=(1, 2)) + translated_displacement = np.roll(displacement, (shift_i, shift_j), axis=(2, 3)) + + baseline = reintegrate_square(mass, displacement, config) + translated = reintegrate_square(translated_mass, translated_displacement, config) + + np.testing.assert_allclose( + translated, + np.roll(baseline, (shift_i, shift_j), axis=(1, 2)), + atol=3e-15, + rtol=3e-15, + ) + + +@given(seed=st.integers(min_value=0, max_value=10_000)) +@_SETTINGS +def test_weighted_average_genes_stay_in_incoming_convex_hull(seed: int) -> None: + config = _config(seed=seed) + mass = np.zeros((1, 8, 8), dtype=np.float64) + mass[0, 3, 3] = 0.7 + mass[0, 3, 4] = 1.3 + genomes = make_uniform_genomes(mass, config) + h = genomes.h.copy() + q = genomes.q.copy() + h[:, 3, 3], q[:, 3, 3] = -0.4, 0.2 + h[:, 3, 4], q[:, 3, 4] = 1.2, 0.9 + fingerprints = genomes.fingerprint.copy() + lineages = genomes.lineage.copy() + fingerprints[3, 3], fingerprints[3, 4] = 101, 202 + lineages[3, 3], lineages[3, 4] = 11, 22 + state = EcosystemState( + mass, + GenomeFields(h, q, fingerprints, lineages, genomes.flags), + ) + displacement = np.zeros((1, 2, 8, 8), dtype=np.float64) + + result = reintegrate_with_genomes(state, displacement, config, rule="average", step=4) + + occupied = result.mass.sum(axis=0) > 0.0 + assert np.all(result.genomes.h[:, occupied] >= -0.4) + assert np.all(result.genomes.h[:, occupied] <= 1.2) + assert np.all(result.genomes.q[:, occupied] >= 0.2) + assert np.all(result.genomes.q[:, occupied] <= 0.9) + + +@given(seed=st.integers(min_value=0, max_value=10_000)) +@_SETTINGS +def test_stateless_mixers_repeat_for_same_seed_and_step(seed: int) -> None: + config = _config(seed=seed) + rng = np.random.default_rng(seed) + mass = rng.uniform(0.0, 1.0, size=(1, 8, 8)) + genomes = make_uniform_genomes(mass, config) + h = rng.uniform(-1.0, 1.0, size=genomes.h.shape) + q = rng.uniform(-1.0, 1.0, size=genomes.q.shape) + fingerprints = np.arange(1, 65, dtype=np.uint64).reshape(8, 8) + lineages = np.arange(1, 65, dtype=np.uint32).reshape(8, 8) + state = EcosystemState(mass, GenomeFields(h, q, fingerprints, lineages, genomes.flags)) + displacement = rng.uniform(-0.2, 0.2, size=(1, 2, 8, 8)) + + for rule in ("whole", "gene-wise", "negotiation"): + first = reintegrate_with_genomes(state, displacement, config, rule=rule, step=29) + second = reintegrate_with_genomes(state, displacement, config, rule=rule, step=29) + np.testing.assert_array_equal(first.mass, second.mass) + np.testing.assert_array_equal(first.genomes.h, second.genomes.h) + np.testing.assert_array_equal(first.genomes.q, second.genomes.q) + np.testing.assert_array_equal(first.genomes.fingerprint, second.genomes.fingerprint) + + +@given(seed=st.integers(min_value=0, max_value=10_000)) +@_SETTINGS +def test_displacement_clamp_never_exceeds_proof_bound(seed: int) -> None: + config = _config(seed=seed) + flow = np.random.default_rng(seed).normal(0.0, 100.0, size=(1, 2, 8, 8)) + + displacement, clamp_mask = clamp_displacement(flow, config) + + assert float(np.max(np.abs(displacement))) <= config.max_displacement + assert np.any(clamp_mask) diff --git a/packages/flow-lenia/tests/test_ecosystem_reference.py b/packages/flow-lenia/tests/test_ecosystem_reference.py new file mode 100644 index 00000000..c8bc5dad --- /dev/null +++ b/packages/flow-lenia/tests/test_ecosystem_reference.py @@ -0,0 +1,261 @@ +"""Independent anchors for the full f64 Flow Lenia ecosystem oracle.""" + +from __future__ import annotations + +from dataclasses import replace + +import numpy as np +import pytest + +from flow_lenia.ecosystem_config import ( + FlowLeniaEcosystemConfig, + KernelSpec, + default_ecosystem_config, +) +from flow_lenia.ecosystem_reference import ( + IDENTITY_MUTATED, + EcosystemState, + GenomeFields, + MixingRule, + build_kernels, + compute_flow, + growth_response, + make_uniform_genomes, + mutate_patch, + perceive, + reintegrate_square, + reintegrate_with_genomes, + sobel_periodic, + square_overlap_weight, + step_reference, +) + + +def _kernel(*, source: int = 0, target: int = 0, weight: float = 0.7) -> KernelSpec: + return KernelSpec( + source=source, + target=target, + relative_radius=0.75, + growth_mean=0.25, + growth_width=0.08, + weight=weight, + ring_centers=(0.2, 0.55, 0.82), + ring_amplitudes=(0.7, 1.0, 0.4), + ring_widths=(0.025, 0.04, 0.03), + ) + + +def _config( + *, + channels: int = 1, + half_width: float = 0.25, + threshold: float = 2.0, + seed: int = 17, +) -> FlowLeniaEcosystemConfig: + kernels = tuple(_kernel(source=channel, target=channel) for channel in range(channels)) + return FlowLeniaEcosystemConfig( + grid=8, + channels=channels, + kernels=kernels, + base_radius=1.0, + radius_offset=0.0, + gather_radius=1, + square_half_width=half_width, + density_threshold=threshold, + seed=seed, + ) + + +def _zero_displacement(config: FlowLeniaEcosystemConfig) -> np.ndarray: + return np.zeros((config.channels, 2, config.grid, config.grid), dtype=np.float64) + + +def test_kernels_are_discretely_normalized_and_radially_symmetric() -> None: + config = _config() + kernel = build_kernels(config)[0] + center = config.grid // 2 + + assert float(kernel.sum()) == pytest.approx(1.0, abs=2e-16) + assert kernel[center + 1, center] == pytest.approx(kernel[center, center + 1]) + assert kernel[center - 1, center] == pytest.approx(kernel[center, center - 1]) + assert np.all(kernel >= 0.0) + + +def test_growth_has_exact_peak_and_hand_computed_off_center_value() -> None: + mean = 0.3 + width = 0.1 + values = np.array([mean, mean + width, mean + 2.0 * width], dtype=np.float64) + response = growth_response(values, mean, width) + + assert response[0] == 1.0 + assert response[1] == pytest.approx(2.0 * np.exp(-0.5) - 1.0, abs=1e-15) + assert response[2] == pytest.approx(2.0 * np.exp(-2.0) - 1.0, abs=1e-15) + + +def test_uniform_affinity_and_density_produce_zero_flow() -> None: + config = _config(channels=2) + mass = np.full((2, 8, 8), 0.2, dtype=np.float64) + _, _, affinity = perceive(mass, config) + flow, _ = compute_flow(mass, affinity, config) + + np.testing.assert_allclose(flow, 0.0, atol=2e-15, rtol=0.0) + + +def test_pressure_gradient_direction_and_official_sobel_magnitude() -> None: + config = replace( + _config(threshold=1.0), + kernels=(replace(_kernel(), weight=0.0),), + ) + mass = np.zeros((1, 8, 8), dtype=np.float64) + mass[0, 4, 4] = 1.0 + mass[0, 5, 4] = 2.0 + affinity = np.zeros_like(mass) + + density_gradient = sobel_periodic(mass[0]) + flow, alpha = compute_flow(mass, affinity, config) + + assert density_gradient[0, 4, 4] == 4.0 + assert density_gradient[1, 4, 4] == 0.0 + assert alpha[0, 4, 4] == 1.0 + assert flow[0, 0, 4, 4] == -4.0 + assert flow[0, 1, 4, 4] == 0.0 + + +def test_translated_half_cell_square_has_hand_derived_four_cell_weights() -> None: + config = _config(half_width=0.25) + mass = np.zeros((1, 8, 8), dtype=np.float64) + mass[0, 3, 3] = 1.0 + displacement = _zero_displacement(config) + displacement[0, :, 3, 3] = 0.4 + + result = reintegrate_square(mass, displacement, config) + + assert result[0, 3, 3] == pytest.approx(0.49, abs=2e-15) + assert result[0, 3, 4] == pytest.approx(0.21, abs=2e-15) + assert result[0, 4, 3] == pytest.approx(0.21, abs=2e-15) + assert result[0, 4, 4] == pytest.approx(0.09, abs=2e-15) + assert np.count_nonzero(result) == 4 + + +def test_broad_stationary_square_has_hand_derived_nine_cell_weights() -> None: + center_weight = square_overlap_weight((3, 3), (3, 3), (0.0, 0.0), grid=8, half_width=0.65) + edge_weight = square_overlap_weight((3, 4), (3, 3), (0.0, 0.0), grid=8, half_width=0.65) + corner_weight = square_overlap_weight((4, 4), (3, 3), (0.0, 0.0), grid=8, half_width=0.65) + + assert center_weight == pytest.approx(1.0 / 1.3**2, abs=2e-15) + assert edge_weight == pytest.approx(0.15 / 1.3**2, abs=2e-15) + assert corner_weight == pytest.approx(0.15**2 / 1.3**2, abs=2e-15) + assert center_weight + 4.0 * edge_weight + 4.0 * corner_weight == pytest.approx(1.0) + + +def test_closed_transport_conserves_each_channel_and_non_negativity() -> None: + config = _config(channels=2, half_width=0.65) + rng = np.random.default_rng(123) + mass = rng.uniform(0.0, 1.0, size=(2, 8, 8)) + displacement = rng.uniform( + -config.max_displacement, + config.max_displacement, + size=(2, 2, 8, 8), + ) + + result = reintegrate_square(mass, displacement, config) + + np.testing.assert_allclose( + result.sum(axis=(1, 2)), mass.sum(axis=(1, 2)), atol=2e-13, rtol=2e-15 + ) + assert float(result.min()) >= 0.0 + assert np.all(np.isfinite(result)) + + +def test_full_three_channel_nine_kernel_step_closes_mass_ledger() -> None: + config = default_ecosystem_config(grid=16, seed=7) + mass = np.random.default_rng(7).uniform(0.0, 0.3, size=(3, 16, 16)) + + result, diagnostics = step_reference(mass, config) + + np.testing.assert_allclose( + result.sum(axis=(1, 2)), mass.sum(axis=(1, 2)), atol=3e-13, rtol=4e-15 + ) + assert float(result.min()) >= 0.0 + assert diagnostics.perception.shape == (9, 16, 16) + assert diagnostics.growth.shape == (9, 16, 16) + assert diagnostics.affinity.shape == (3, 16, 16) + assert diagnostics.flow.shape == (3, 2, 16, 16) + assert np.all(np.isfinite(diagnostics.flow)) + + +@pytest.mark.parametrize("rule", ["average", "whole", "gene-wise", "best", "negotiation"]) +def test_constant_incoming_genome_is_constant_for_every_mixer(rule: MixingRule) -> None: + config = _config(half_width=0.65) + mass = np.full((1, 8, 8), 0.5, dtype=np.float64) + genomes = make_uniform_genomes(mass, config, q_value=0.4, lineage=27) + state = EcosystemState(mass=mass, genomes=genomes) + + result = reintegrate_with_genomes( + state, + _zero_displacement(config), + config, + rule=rule, + step=11, + ) + + np.testing.assert_array_equal(result.genomes.h, genomes.h) + np.testing.assert_array_equal(result.genomes.q, genomes.q) + np.testing.assert_array_equal(result.genomes.fingerprint, genomes.fingerprint) + np.testing.assert_array_equal(result.genomes.lineage, genomes.lineage) + + +@pytest.mark.parametrize("rule", ["whole", "best", "negotiation"]) +def test_whole_genome_selectors_only_return_incoming_identity(rule: MixingRule) -> None: + config = _config(half_width=0.65) + mass = np.zeros((1, 8, 8), dtype=np.float64) + mass[0, 3, 3] = 1.0 + mass[0, 3, 4] = 1.0 + genomes = make_uniform_genomes(mass, config) + h = genomes.h.copy() + q = genomes.q.copy() + fingerprint = genomes.fingerprint.copy() + lineage = genomes.lineage.copy() + h[:, 3, 3], q[:, 3, 3], fingerprint[3, 3], lineage[3, 3] = 0.2, 0.3, 111, 7 + h[:, 3, 4], q[:, 3, 4], fingerprint[3, 4], lineage[3, 4] = 0.8, -0.1, 222, 9 + state = EcosystemState( + mass=mass, + genomes=GenomeFields(h, q, fingerprint, lineage, genomes.flags), + ) + + result = reintegrate_with_genomes( + state, + _zero_displacement(config), + config, + rule=rule, + step=3, + contextual_growth=np.ones((1, 8, 8), dtype=np.float64), + ) + + occupied = result.mass.sum(axis=0) > 0.0 + assert set(np.unique(result.genomes.fingerprint[occupied])).issubset({111, 222}) + assert set(np.unique(result.genomes.lineage[occupied])).issubset({7, 9}) + + +def test_mutation_is_repeatable_and_changes_one_coherent_lineage_patch() -> None: + config = _config(half_width=0.65, seed=91) + mass = np.ones((1, 8, 8), dtype=np.float64) + state = EcosystemState(mass, make_uniform_genomes(mass, config, lineage=12)) + + first, first_event = mutate_patch( + state, config, center=(0, 0), radius=1.1, event_index=5, scale=0.08 + ) + second, second_event = mutate_patch( + state, config, center=(0, 0), radius=1.1, event_index=5, scale=0.08 + ) + + assert first_event == second_event + np.testing.assert_array_equal(first.genomes.h, second.genomes.h) + np.testing.assert_array_equal(first.genomes.q, second.genomes.q) + changed = first.genomes.lineage == first_event.child_lineage + assert np.count_nonzero(changed) == 5 + assert first_event.parent_lineage == 12 + assert first_event.affected_mass == 5.0 + assert np.all((first.genomes.flags[changed] & IDENTITY_MUTATED) != 0) + assert np.all(first.genomes.fingerprint[changed] == first_event.child_fingerprint) + np.testing.assert_array_equal(first.mass, mass) diff --git a/packages/flow-lenia/tests/test_m2_browser_fixture.py b/packages/flow-lenia/tests/test_m2_browser_fixture.py new file mode 100644 index 00000000..e6845a2b --- /dev/null +++ b/packages/flow-lenia/tests/test_m2_browser_fixture.py @@ -0,0 +1,79 @@ +"""Keep the compact WebGPU M2 fixture pinned to the f64 M1 oracle.""" + +from __future__ import annotations + +import base64 +import json +import math +from pathlib import Path +from typing import Any + +import numpy as np + +from flow_lenia.ecosystem_config import MODEL_VARIANT, default_ecosystem_config +from flow_lenia.ecosystem_reference import build_kernels, step_reference + +FIXTURE = Path(__file__).parents[1] / "web/src/prove/organism-fixture.json" + + +def _conformance_mass(grid: int, variant: int) -> np.ndarray[Any, np.dtype[np.float64]]: + mass = np.empty((3, grid, grid), dtype=np.float32) + for i in range(grid): + for j in range(grid): + for channel in range(3): + p = 2.0 * math.pi * ((channel + 1) * i + (channel + 2) * j) / grid + q = 2.0 * math.pi * ((channel + 3) * i - (channel + 1) * j) / grid + value = 0.16 + channel * 0.035 + 0.055 * math.sin(p) + 0.025 * math.cos(q) + if variant == 1 and (i < 2 or j >= grid - 2): + value += 0.24 / (channel + 1) + if ( + variant == 2 + and grid / 2 - 2 <= i <= grid / 2 + 1 + and grid / 2 - 2 <= j <= grid / 2 + 1 + ): + value += 2.4 - channel * 0.25 + mass[channel, i, j] = value + return mass.astype(np.float64) + + +def _decode(record: dict[str, Any]) -> np.ndarray[Any, np.dtype[np.float32]]: + values = np.frombuffer(base64.b64decode(record["data"]), dtype=" None: + fixture = json.loads(FIXTURE.read_text(encoding="utf-8")) + config = default_ecosystem_config(grid=16, seed=91) + kernels = build_kernels(config) + assert fixture["model_variant"] == MODEL_VARIANT + assert fixture["config"]["kernels"] == len(config.kernels) + + for case in fixture["cases"]: + initial = _conformance_mass(config.grid, int(case["variant"])) + expected_one, diagnostics = step_reference(initial, config, kernels=kernels) + expected_four = expected_one + for _ in range(3): + expected_four, _ = step_reference(expected_four, config, kernels=kernels) + fields = case["fields"] + np.testing.assert_array_equal(_decode(fields["initial_mass"]), initial.astype(np.float32)) + np.testing.assert_array_equal( + _decode(fields["displacement_step_1"]), + diagnostics.displacement.astype(np.float32), + ) + np.testing.assert_array_equal( + _decode(fields["mass_step_1"]), expected_one.astype(np.float32) + ) + np.testing.assert_array_equal( + _decode(fields["mass_step_4"]), expected_four.astype(np.float32) + ) + if case["name"] == "smooth-periodic": + for field_name, expected in ( + ("perception_step_1", diagnostics.perception), + ("growth_step_1", diagnostics.growth), + ("affinity_step_1", diagnostics.affinity), + ("alpha_step_1", diagnostics.alpha), + ("flow_step_1", diagnostics.flow), + ): + np.testing.assert_array_equal( + _decode(fields[field_name]), expected.astype(np.float32) + ) diff --git a/packages/flow-lenia/web/artifacts/m0-browser-benchmark.json b/packages/flow-lenia/web/artifacts/m0-browser-benchmark.json new file mode 100644 index 00000000..0acd3954 --- /dev/null +++ b/packages/flow-lenia/web/artifacts/m0-browser-benchmark.json @@ -0,0 +1,165 @@ +{ + "schemaVersion": "flow-lenia-m0-v1", + "generatedUtc": "2026-07-18T17:10:49.826Z", + "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "adapter": { + "vendor": "amd", + "architecture": "rdna-2", + "device": "", + "description": "" + }, + "timestampQuery": true, + "limits": { + "maxBufferSize": 268435456, + "maxStorageBufferBindingSize": 134217728, + "maxStorageBuffersPerShaderStage": 8, + "maxBindingsPerBindGroup": 1000, + "maxComputeInvocationsPerWorkgroup": 256, + "maxComputeWorkgroupStorageSize": 16384, + "maxComputeWorkgroupsPerDimension": 65535 + }, + "architectureFailures": [], + "verification128": { + "fftMaxAbs": 2.3520114933450742e-7, + "gatherMassRelativeResidual": 4.7225355131289265e-8, + "fullMassRelativeResidual": 4.7225355131289265e-8, + "uniformGenomeMaxAbs": 2.0265579214751028e-7, + "uniformIdentityExact": true + }, + "benchmarks": [ + { + "n": 128, + "mode": "fft", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 29, + "meanMs": 0.30115000000000003, + "p50Ms": 0.29816, + "p95Ms": 0.31616, + "minMs": 0.29136, + "maxMs": 0.31616 + }, + { + "n": 128, + "mode": "gather-mass", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 1, + "meanMs": 0.03956666666666667, + "p50Ms": 0.0394, + "p95Ms": 0.04084, + "minMs": 0.03928, + "maxMs": 0.04084 + }, + { + "n": 128, + "mode": "gather-full", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 1, + "meanMs": 0.06660333333333333, + "p50Ms": 0.06648, + "p95Ms": 0.06872, + "minMs": 0.06548, + "maxMs": 0.06872 + }, + { + "n": 128, + "mode": "step-mass", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 30, + "meanMs": 0.34109999999999996, + "p50Ms": 0.33772, + "p95Ms": 0.36868, + "minMs": 0.32792, + "maxMs": 0.36868 + }, + { + "n": 128, + "mode": "step-full", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 30, + "meanMs": 0.3659866666666667, + "p50Ms": 0.36232, + "p95Ms": 0.38072, + "minMs": 0.35604, + "maxMs": 0.38072 + }, + { + "n": 256, + "mode": "fft", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 33, + "meanMs": 1.1296, + "p50Ms": 1.17284, + "p95Ms": 1.19888, + "minMs": 1.02292, + "maxMs": 1.19888 + }, + { + "n": 256, + "mode": "gather-mass", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 1, + "meanMs": 0.18101333333333333, + "p50Ms": 0.17952, + "p95Ms": 0.2004, + "minMs": 0.17728, + "maxMs": 0.2004 + }, + { + "n": 256, + "mode": "gather-full", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 1, + "meanMs": 0.22594000000000003, + "p50Ms": 0.22592, + "p95Ms": 0.22956, + "minMs": 0.22144, + "maxMs": 0.22956 + }, + { + "n": 256, + "mode": "step-mass", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 34, + "meanMs": 1.3100933333333333, + "p50Ms": 1.30948, + "p95Ms": 1.36192, + "minMs": 1.24688, + "maxMs": 1.36192 + }, + { + "n": 256, + "mode": "step-full", + "timing": "timestamp-query", + "samples": 12, + "dispatchesPerStep": 34, + "meanMs": 1.18041, + "p50Ms": 1.18376, + "p95Ms": 1.252, + "minMs": 1.12824, + "maxMs": 1.252 + } + ], + "projectedMemory": [ + { + "n": 128, + "bytes": 9895936, + "mebibytes": 9.4375 + }, + { + "n": 256, + "bytes": 39583744, + "mebibytes": 37.75 + } + ], + "measuredDefault": 256, + "decision": "The dominant-path full-state prototype is 1.25 ms p95 at 256² and the projected complete allocation is 37.75 MiB; 256² retains the desktop budget." +} diff --git a/packages/flow-lenia/web/artifacts/m2-browser-gates.json b/packages/flow-lenia/web/artifacts/m2-browser-gates.json new file mode 100644 index 00000000..b274ab21 --- /dev/null +++ b/packages/flow-lenia/web/artifacts/m2-browser-gates.json @@ -0,0 +1,159 @@ +{ + "schemaVersion": "flow-lenia-m2-gates-v1", + "generatedUtc": "2026-07-18T18:06:47.892Z", + "fixtureSchema": "flow-lenia-m2-conformance-v1", + "environment": { + "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "adapter": { + "vendor": "amd", + "architecture": "rdna-2", + "device": "", + "description": "" + } + }, + "numericalCases": [ + { + "name": "smooth-periodic", + "fields": [ + { + "field": "perception_step_1", + "maxAbs": 2.9802322387695312e-8, + "tolerance": 0.000001, + "pass": true + }, + { + "field": "growth_step_1", + "maxAbs": 4.172325134277344e-7, + "tolerance": 0.00001, + "pass": true + }, + { + "field": "affinity_step_1", + "maxAbs": 2.980232238769531e-7, + "tolerance": 0.00001, + "pass": true + }, + { + "field": "alpha_step_1", + "maxAbs": 7.450580596923828e-9, + "tolerance": 2e-7, + "pass": true + }, + { + "field": "flow_step_1", + "maxAbs": 8.195638656616211e-7, + "tolerance": 0.00002, + "pass": true + }, + { + "field": "displacement_step_1", + "maxAbs": 1.6391277313232422e-7, + "tolerance": 0.000004, + "pass": true + }, + { + "field": "mass_step_1", + "maxAbs": 2.8312206268310547e-7, + "tolerance": 0.00001, + "pass": true + }, + { + "field": "mass_step_4", + "maxAbs": 3.129243850708008e-7, + "tolerance": 0.00002, + "pass": true + } + ], + "massRelativeResidual": 2.1820410425353305e-7, + "pass": true + }, + { + "name": "seam-loaded", + "fields": [ + { + "field": "displacement_step_1", + "maxAbs": 2.7194619178771973e-7, + "tolerance": 0.000004, + "pass": true + }, + { + "field": "mass_step_1", + "maxAbs": 3.5762786865234375e-7, + "tolerance": 0.00001, + "pass": true + }, + { + "field": "mass_step_4", + "maxAbs": 3.8743019104003906e-7, + "tolerance": 0.00002, + "pass": true + } + ], + "massRelativeResidual": 2.2551371380707505e-7, + "pass": true + }, + { + "name": "crowded-pressure", + "fields": [ + { + "field": "displacement_step_1", + "maxAbs": 7.748603820800781e-7, + "tolerance": 0.000004, + "pass": true + }, + { + "field": "mass_step_1", + "maxAbs": 0.0000011920928955078125, + "tolerance": 0.00001, + "pass": true + }, + { + "field": "mass_step_4", + "maxAbs": 8.940696716308594e-7, + "tolerance": 0.00002, + "pass": true + } + ], + "massRelativeResidual": 2.0938558535274417e-7, + "pass": true + } + ], + "structural": { + "grid": 128, + "steps": 256, + "elapsedMs": 229.19999998807907, + "firstHash": "1b3e80400f4c32128c5344144dbce5311304dd348e6be79ccb22163070d02522", + "secondHash": "1b3e80400f4c32128c5344144dbce5311304dd348e6be79ccb22163070d02522", + "byteExactSameAdapter": true, + "metrics": { + "step": 256, + "totalMass": 1979.0234330583999, + "channelMass": [ + 659.3029751258518, + 660.0061884456692, + 659.7142694868791 + ], + "relativeMassDrift": 0.000013534007704367495, + "minDensity": 2.5354286137257986e-7, + "maxDensity": 2.124877852920349, + "occupiedFraction": 0.5618896484375, + "nonFinite": 0, + "negative": 0, + "maxFlow": 2.8743743896484375, + "maxDisplacement": 0.5748748779296875, + "clampFraction": 0 + }, + "pass": true + }, + "performance": { + "grid": 256, + "samples": 16, + "timing": "queue-completion", + "p50Ms": 2.2999999821186066, + "p95Ms": 3.2999999821186066, + "allocatedBytes": 27271568, + "memoryMib": 26.008193969726562, + "pass": true + }, + "pass": true +} diff --git a/packages/flow-lenia/web/artifacts/m3-browser-gates.json b/packages/flow-lenia/web/artifacts/m3-browser-gates.json new file mode 100644 index 00000000..cdd3c10e --- /dev/null +++ b/packages/flow-lenia/web/artifacts/m3-browser-gates.json @@ -0,0 +1,308 @@ +{ + "schemaVersion": "flow-lenia-m3-gates-v1", + "environment": { + "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "adapter": { + "vendor": "amd", + "architecture": "rdna-2", + "device": "", + "description": "" + } + }, + "cards": [ + { + "id": "affinity-swimmer", + "steps": 64, + "metrics": { + "step": 64, + "totalMass": 1979.0033607313248, + "channelMass": [ + 659.2962897756562, + 659.9994680486403, + 659.7076029070282 + ], + "relativeMassDrift": 0.000003391329048449776, + "minDensity": 7.44194555979405e-9, + "maxDensity": 3.34562898427248, + "occupiedFraction": 0.52386474609375, + "nonFinite": 0, + "negative": 0, + "maxFlow": 12.712763786315918, + "maxDisplacement": 2.5425527095794678, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 1978.9966493025013, + "ledgerError": 0.006711428823564347 + }, + "pass": true + }, + { + "id": "spinner-collision", + "steps": 64, + "metrics": { + "step": 64, + "totalMass": 1562.0489078837068, + "channelMass": [ + 532.5380251221457, + 502.11328768176196, + 527.3975950797993 + ], + "relativeMassDrift": 0.0000033580402068554726, + "minDensity": 0.000006443437882808212, + "maxDensity": 2.4351633752132713, + "occupiedFraction": 0.62225341796875, + "nonFinite": 0, + "negative": 0, + "maxFlow": 7.066369533538818, + "maxDisplacement": 1.4132739305496216, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 1562.0436624782833, + "ledgerError": 0.005245405423465854 + }, + "pass": true + }, + { + "id": "dividing-droplets", + "steps": 64, + "metrics": { + "step": 64, + "totalMass": 3058.660939570317, + "channelMass": [ + 1008.1739336289696, + 1042.8598545225511, + 1007.6271514187965 + ], + "relativeMassDrift": 0.0000033691292458977462, + "minDensity": 0.000009228061315980085, + "maxDensity": 2.3200308680534363, + "occupiedFraction": 0.90606689453125, + "nonFinite": 0, + "negative": 0, + "maxFlow": 2.89229154586792, + "maxDisplacement": 0.578458309173584, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 3058.650634581011, + "ledgerError": 0.010304989305950585 + }, + "pass": true + }, + { + "id": "pressure-ablation", + "steps": 64, + "metrics": { + "step": 64, + "totalMass": 1300.1518169334588, + "channelMass": [ + 435.6444923728719, + 435.7902577711585, + 428.7170667894283 + ], + "relativeMassDrift": 0.0000033975378415764576, + "minDensity": 1.2909862889820545e-7, + "maxDensity": 2.4974355697631836, + "occupiedFraction": 0.43267822265625, + "nonFinite": 0, + "negative": 0, + "maxFlow": 3.3179423809051514, + "maxDisplacement": 0.6635884642601013, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 1300.147399633469, + "ledgerError": 0.00441729998988194 + }, + "pass": true, + "comparison": { + "label": "pressure off", + "metrics": { + "step": 64, + "totalMass": 1300.151830733471, + "channelMass": [ + 435.64450425774146, + 435.7902686772988, + 428.7170577984308 + ], + "relativeMassDrift": 0.000003408152032004455, + "minDensity": 1.2909880098277426e-7, + "maxDensity": 11.52260971069336, + "occupiedFraction": 0.43133544921875, + "nonFinite": 0, + "negative": 0, + "maxFlow": 4.378595352172852, + "maxDisplacement": 0.8757190704345703, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 1300.147399633469, + "ledgerError": 0.004431100001966115 + }, + "stateDiverged": true, + "peakDensityDelta": 9.025174140930176, + "pass": true + } + }, + { + "id": "sigma-phase", + "steps": 64, + "metrics": { + "step": 64, + "totalMass": 1978.9351317487362, + "channelMass": [ + 660.0194227517363, + 659.2940329630094, + 659.6216760339905 + ], + "relativeMassDrift": 0.0000010972946031860312, + "minDensity": 4.0385180743568206e-11, + "maxDensity": 3.8272088766098022, + "occupiedFraction": 0.49090576171875, + "nonFinite": 0, + "negative": 0, + "maxFlow": 3.3235878944396973, + "maxDisplacement": 0.6647175550460815, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 1978.937303225959, + "ledgerError": -0.0021714772228733636 + }, + "pass": true, + "comparison": { + "label": "σ 1.05", + "metrics": { + "step": 64, + "totalMass": 1978.946749482832, + "channelMass": [ + 660.0232641166424, + 659.2979583289847, + 659.6255270372051 + ], + "relativeMassDrift": 0.000004773398761817926, + "minDensity": 4.863671487775889e-8, + "maxDensity": 3.0710813403129578, + "occupiedFraction": 0.5660400390625, + "nonFinite": 0, + "negative": 0, + "maxFlow": 2.9537436962127686, + "maxDisplacement": 0.5907487273216248, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 1978.937303225959, + "ledgerError": 0.0094462568729341 + }, + "stateDiverged": true, + "peakDensityDelta": 0.7561275362968445, + "pass": true + } + }, + { + "id": "expansion-trap", + "steps": 64, + "metrics": { + "step": 64, + "totalMass": 2721.407800530891, + "channelMass": [ + 895.3550745654439, + 954.2597701030489, + 871.792955862398 + ], + "relativeMassDrift": 0.0000033963129484593688, + "minDensity": 3.0342240872616166e-11, + "maxDensity": 1.352072812616825, + "occupiedFraction": 0.67669677734375, + "nonFinite": 0, + "negative": 0, + "maxFlow": 2.714146614074707, + "maxDisplacement": 0.5428293347358704, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 2721.3985578097313, + "ledgerError": 0.009242721159807843 + }, + "pass": true + } + ], + "scheduledEvents": { + "steps": 32, + "hashA": "036e16fa6686c61cdcfc3425933b30e6c30cae011e3fc5ec7845c50c2a7d22b5", + "hashB": "036e16fa6686c61cdcfc3425933b30e6c30cae011e3fc5ec7845c50c2a7d22b5", + "byteExactSameAdapter": true, + "ledgerPass": true, + "metrics": { + "step": 32, + "totalMass": 1970.3730722183118, + "channelMass": [ + 657.5399664457245, + 656.2190782307501, + 656.614027541837 + ], + "relativeMassDrift": 0.0000014010491940933032, + "minDensity": 2.127176678291126e-9, + "maxDensity": 3.1479188054800034, + "occupiedFraction": 0.50714111328125, + "nonFinite": 0, + "negative": 0, + "maxFlow": 3.4598751068115234, + "maxDisplacement": 0.6919749975204468, + "clampFraction": 0, + "ledgerAdded": 2.14483642578125, + "ledgerRemoved": 10.526992797851562, + "expectedMass": 1970.3703116325746, + "ledgerError": 0.0027605857371781894 + } + }, + "closedImpulses": { + "steps": 24, + "ledgerUntouched": true, + "metrics": { + "step": 24, + "totalMass": 1978.7549803868164, + "channelMass": [ + 660.0894309128788, + 659.039550367442, + 659.6259991064958 + ], + "relativeMassDrift": 0.0000012696798675634296, + "minDensity": 1.4714658545453285e-9, + "maxDensity": 2.999167263507843, + "occupiedFraction": 0.50323486328125, + "nonFinite": 0, + "negative": 0, + "maxFlow": 3.5148324966430664, + "maxDisplacement": 0.7029665112495422, + "clampFraction": 0, + "ledgerAdded": 0, + "ledgerRemoved": 0, + "expectedMass": 1978.752468004645, + "ledgerError": 0.002512382171516947 + }, + "pass": true + }, + "renderIntegrity": { + "scientificStateByteExact": true, + "pass": true + }, + "productSurface": { + "experiments": 6, + "tools": 5, + "scientificViews": 6, + "inspectorPlots": 2, + "keyboardFocus": true, + "touchRadialTools": 5, + "pass": true + }, + "memory": { + "solverBytes": 27273128, + "solverMib": 26.009681701660156, + "synchronizedPairMib": 52.01936340332031, + "under128Mib": true + }, + "pass": true +} diff --git a/packages/flow-lenia/web/artifacts/m4-browser-gates.json b/packages/flow-lenia/web/artifacts/m4-browser-gates.json new file mode 100644 index 00000000..d74c82a3 --- /dev/null +++ b/packages/flow-lenia/web/artifacts/m4-browser-gates.json @@ -0,0 +1,467 @@ +{ + "schemaVersion": "flow-lenia-m4-gates-v1", + "generatedUtc": "2026-07-18T19:24:49.635Z", + "fixtureSchema": "flow-lenia-m4-conformance-v1", + "environment": { + "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "adapter": { + "vendor": "amd", + "architecture": "rdna-2", + "device": "", + "description": "" + } + }, + "numericalRules": [ + { + "rule": "average", + "massMaxAbs": 1.9371509552001953e-7, + "hMaxAbs": 1.7881393432617188e-7, + "qMaxAbs": 2.384185791015625e-7, + "lineageMismatches": 0, + "flagMismatches": 0, + "relativeMassResidual": 5.146527364864454e-8, + "pass": true + }, + { + "rule": "whole", + "massMaxAbs": 1.9371509552001953e-7, + "hMaxAbs": 0, + "qMaxAbs": 0, + "lineageMismatches": 0, + "flagMismatches": 0, + "relativeMassResidual": 5.146527364864454e-8, + "pass": true + }, + { + "rule": "gene-wise", + "massMaxAbs": 1.9371509552001953e-7, + "hMaxAbs": 0, + "qMaxAbs": 0, + "lineageMismatches": 0, + "flagMismatches": 0, + "relativeMassResidual": 5.146527364864454e-8, + "pass": true + }, + { + "rule": "best", + "massMaxAbs": 1.9371509552001953e-7, + "hMaxAbs": 0, + "qMaxAbs": 0, + "lineageMismatches": 0, + "flagMismatches": 0, + "relativeMassResidual": 5.146527364864454e-8, + "pass": true + }, + { + "rule": "negotiation", + "massMaxAbs": 1.9371509552001953e-7, + "hMaxAbs": 0, + "qMaxAbs": 0, + "lineageMismatches": 0, + "flagMismatches": 0, + "relativeMassResidual": 5.146527364864454e-8, + "pass": true + } + ], + "determinismRules": [ + { + "rule": "average", + "steps": 32, + "hashA": "00861730600af9bc84cf694c5d43d3c616b4132b3b23900631aa305c0b6a6138", + "hashB": "00861730600af9bc84cf694c5d43d3c616b4132b3b23900631aa305c0b6a6138", + "byteExactSameAdapter": true, + "metrics": { + "step": 32, + "totalMass": 1153.5578562625358, + "channelMass": [ + 416.1004699111862, + 382.77673143261285, + 354.6806549187367 + ], + "relativeMassDrift": 0.0000016921899920505227, + "minDensity": 1.1109502532323512e-12, + "maxDensity": 1.8596857488155365, + "occupiedFraction": 0.51361083984375, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.3783726692199707, + "clampFraction": 0, + "activeLineages": 0, + "topLineage": 0, + "topLineageMass": 0, + "mixedIdentityMass": 1153.557855014887, + "shannonDiversity": 0, + "phenotypeClusters": 138, + "phenotypeShannon": 1.1390639944209, + "mutationEvents": 0, + "extinctionEvents": 0 + }, + "pass": true + }, + { + "rule": "whole", + "steps": 32, + "hashA": "0b03175f813c0872907bde01dcee658694edbacda546fc7f9b7a4dbb95347348", + "hashB": "0b03175f813c0872907bde01dcee658694edbacda546fc7f9b7a4dbb95347348", + "byteExactSameAdapter": true, + "metrics": { + "step": 32, + "totalMass": 1153.5578635419467, + "channelMass": [ + 416.1004707935123, + 382.7767358701373, + 354.6806568782972 + ], + "relativeMassDrift": 0.0000016985004022689555, + "minDensity": 1.287260012125535e-12, + "maxDensity": 1.8598044216632843, + "occupiedFraction": 0.51385498046875, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.9053739905357361, + "clampFraction": 0, + "activeLineages": 3, + "topLineage": 3, + "topLineageMass": 418.31502511044744, + "mixedIdentityMass": 0, + "shannonDiversity": 1.095923373948133, + "phenotypeClusters": 4, + "phenotypeShannon": 1.0959238913979439, + "mutationEvents": 0, + "extinctionEvents": 0 + }, + "pass": true + }, + { + "rule": "gene-wise", + "steps": 32, + "hashA": "9be5cca04082e9ab4d627c5e0a0c722e5180c62af7bcb3a1ac66cec6863115c6", + "hashB": "9be5cca04082e9ab4d627c5e0a0c722e5180c62af7bcb3a1ac66cec6863115c6", + "byteExactSameAdapter": true, + "metrics": { + "step": 32, + "totalMass": 1153.5578557717886, + "channelMass": [ + 416.10047129176655, + 382.7767329110343, + 354.68065156898774 + ], + "relativeMassDrift": 0.000001691764570798074, + "minDensity": 1.5282808926795483e-12, + "maxDensity": 1.8596410155296326, + "occupiedFraction": 0.51275634765625, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.9953532218933105, + "clampFraction": 0, + "activeLineages": 0, + "topLineage": 0, + "topLineageMass": 0, + "mixedIdentityMass": 1153.5578535533648, + "shannonDiversity": 0, + "phenotypeClusters": 374, + "phenotypeShannon": 1.133618629341893, + "mutationEvents": 0, + "extinctionEvents": 0 + }, + "pass": true + }, + { + "rule": "best", + "steps": 32, + "hashA": "a02c2e0b5e6a7e327f5d8640eb53fe8ac9cf94a0130472640c3ec3ea336d28c1", + "hashB": "a02c2e0b5e6a7e327f5d8640eb53fe8ac9cf94a0130472640c3ec3ea336d28c1", + "byteExactSameAdapter": true, + "metrics": { + "step": 32, + "totalMass": 1153.5578577440288, + "channelMass": [ + 416.10047212953134, + 382.7767290367867, + 354.6806565777108 + ], + "relativeMassDrift": 0.000001693474275688026, + "minDensity": 1.3748180967214471e-14, + "maxDensity": 1.8609909415245056, + "occupiedFraction": 0.515380859375, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.9053739905357361, + "clampFraction": 0, + "activeLineages": 3, + "topLineage": 2, + "topLineageMass": 555.1556875787733, + "mixedIdentityMass": 0, + "shannonDiversity": 1.01639044297694, + "phenotypeClusters": 4, + "phenotypeShannon": 1.0173983660507655, + "mutationEvents": 0, + "extinctionEvents": 0 + }, + "pass": true + }, + { + "rule": "negotiation", + "steps": 32, + "hashA": "aef7e5f83ddb5dc29051202f157cc8dccfb2aad95ae66046de3999e97444b827", + "hashB": "aef7e5f83ddb5dc29051202f157cc8dccfb2aad95ae66046de3999e97444b827", + "byteExactSameAdapter": true, + "metrics": { + "step": 32, + "totalMass": 1153.5578517619938, + "channelMass": [ + 416.1004670050038, + 382.77673061050973, + 354.6806541464803 + ], + "relativeMassDrift": 0.000001688288540916558, + "minDensity": 1.6261505217472078e-13, + "maxDensity": 1.8600183129310608, + "occupiedFraction": 0.513671875, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.9053739905357361, + "clampFraction": 0, + "activeLineages": 3, + "topLineage": 3, + "topLineageMass": 426.79008442006494, + "mixedIdentityMass": 0, + "shannonDiversity": 1.0945587654766067, + "phenotypeClusters": 4, + "phenotypeShannon": 1.0945591079222035, + "mutationEvents": 0, + "extinctionEvents": 0 + }, + "pass": true + } + ], + "mutation": { + "childIdentityExact": true, + "deltaMaxAbs": 0, + "childPresent": true, + "childFlagsExact": true, + "lineageRingComplete": true, + "affectedMass": 16.193740844726562, + "relativeMassDrift": 4.7439243800490935e-8, + "pass": true + }, + "ecosystems": [ + { + "id": "three-founders", + "rules": [ + "whole" + ], + "metrics": [ + { + "step": 48, + "totalMass": 1153.5588427519301, + "channelMass": [ + 416.1008242376256, + 382.7770600410094, + 354.68095847329516 + ], + "relativeMassDrift": 0.000002547362585525404, + "minDensity": 1.4616891681404343e-11, + "maxDensity": 2.31387460231781, + "occupiedFraction": 0.52777099609375, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.9053739905357361, + "clampFraction": 0, + "activeLineages": 3, + "topLineage": 3, + "topLineageMass": 418.23206418039456, + "mixedIdentityMass": 0, + "shannonDiversity": 1.0959159927108764, + "phenotypeClusters": 4, + "phenotypeShannon": 1.0959164308940719, + "mutationEvents": 0, + "extinctionEvents": 0 + } + ], + "pass": true + }, + { + "id": "negotiation-sea", + "rules": [ + "negotiation" + ], + "metrics": [ + { + "step": 96, + "totalMass": 2183.413937804802, + "channelMass": [ + 710.3754433982139, + 788.621406283035, + 684.4170881235533 + ], + "relativeMassDrift": 0.000005084004354845918, + "minDensity": 0.000009431478019905626, + "maxDensity": 2.2918576076626778, + "occupiedFraction": 0.89697265625, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.7323785424232483, + "clampFraction": 0, + "activeLineages": 7, + "topLineage": 4, + "topLineageMass": 1243.1877794058591, + "mixedIdentityMass": 0, + "shannonDiversity": 1.3381267590569665, + "phenotypeClusters": 7, + "phenotypeShannon": 1.3381267590569665, + "mutationEvents": 3, + "extinctionEvents": 0 + } + ], + "pass": true + }, + { + "id": "identity-dilution", + "rules": [ + "average", + "gene-wise", + "negotiation" + ], + "metrics": [ + { + "step": 48, + "totalMass": 1112.2627128703245, + "channelMass": [ + 401.6718800627076, + 368.9046717676066, + 341.6861610400104 + ], + "relativeMassDrift": 0.000002544366777625204, + "minDensity": 1.7698411312883078e-10, + "maxDensity": 2.3725897073745728, + "occupiedFraction": 0.5279541015625, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.38747406005859375, + "clampFraction": 0, + "activeLineages": 0, + "topLineage": 0, + "topLineageMass": 0, + "mixedIdentityMass": 1112.262711809549, + "shannonDiversity": 0, + "phenotypeClusters": 139, + "phenotypeShannon": 1.1476851369071592, + "mutationEvents": 0, + "extinctionEvents": 0 + }, + { + "step": 48, + "totalMass": 1112.2627046057207, + "channelMass": [ + 401.6718733480451, + 368.9046684141239, + 341.68616284355176 + ], + "relativeMassDrift": 0.0000025369363164999564, + "minDensity": 7.537218418840474e-11, + "maxDensity": 2.3725003004074097, + "occupiedFraction": 0.52789306640625, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.9916041493415833, + "clampFraction": 0, + "activeLineages": 0, + "topLineage": 0, + "topLineageMass": 0, + "mixedIdentityMass": 1112.262702564119, + "shannonDiversity": 0, + "phenotypeClusters": 396, + "phenotypeShannon": 1.1483784475716055, + "mutationEvents": 0, + "extinctionEvents": 0 + }, + { + "step": 48, + "totalMass": 1112.2627057322047, + "channelMass": [ + 401.6718740472066, + 368.9046634073486, + 341.68616827764964 + ], + "relativeMassDrift": 0.000002537949104978109, + "minDensity": 7.586967859518623e-11, + "maxDensity": 2.37292218208313, + "occupiedFraction": 0.52838134765625, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.7958551645278931, + "clampFraction": 0, + "activeLineages": 3, + "topLineage": 3, + "topLineageMass": 438.5261055040858, + "mixedIdentityMass": 0, + "shannonDiversity": 1.088309417059643, + "phenotypeClusters": 4, + "phenotypeShannon": 1.0883097809643165, + "mutationEvents": 0, + "extinctionEvents": 0 + } + ], + "pass": true + } + ], + "identityDilution": { + "rules": [ + "average", + "gene-wise", + "negotiation" + ], + "mixedFractions": [ + 0.9999999990462906, + 0.999999998164461, + 0 + ], + "phenotypeClusters": [ + 139, + 396, + 4 + ], + "distinctOutcomes": true, + "pass": true + }, + "architecture": { + "gatherStorageBindings": 8, + "specializedPipelines": 5, + "allocatedBytes256": 45099936, + "memoryMib256": 43.010650634765625, + "under128Mib": true + }, + "performance": { + "grid": 256, + "samples": 8, + "p50Ms": 2.2000000178813934, + "p95Ms": 19.30000001192093, + "pass": true + }, + "renderIntegrity": { + "scientificStateByteExact": true, + "pass": true + }, + "productSurface": { + "experiments": 3, + "mixingRules": 5, + "views": 4, + "tools": 2, + "comparisonPanes": 3, + "keyboardFocus": true, + "pass": true + }, + "pass": true, + "captureContract": { + "schemaVersion": "1.0.0", + "step": 32, + "fields": [ + "mass", + "genome_h", + "genome_q", + "identity_u32_values" + ], + "pass": true + } +} diff --git a/packages/flow-lenia/web/artifacts/m6-browser-release-gates.json b/packages/flow-lenia/web/artifacts/m6-browser-release-gates.json new file mode 100644 index 00000000..bbd5ed59 --- /dev/null +++ b/packages/flow-lenia/web/artifacts/m6-browser-release-gates.json @@ -0,0 +1,328 @@ +{ + "schemaVersion": "flow-lenia-m6-release-gates-v1", + "generatedUtc": "2026-07-18T20:06:36.432Z", + "environment": { + "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "adapter": { + "vendor": "amd", + "architecture": "rdna-2", + "device": "", + "description": "" + } + }, + "environmentAnchors": { + "zeroFieldByteExact": true, + "affinityDeltaMaxAbs": 2.9802322387695312e-8, + "gateClosedAt47": true, + "gateOpenAt48": true, + "paintChangedEnvironment": true, + "paintConservedMass": true, + "pass": true + }, + "arenas": [ + { + "id": "corridor-divergence", + "steps": 96, + "hashA": "11d2f4f3f4479a3bc0de1453707caa9d8cc4ac37badf16822da5b88e1e888f0a", + "hashB": "11d2f4f3f4479a3bc0de1453707caa9d8cc4ac37badf16822da5b88e1e888f0a", + "byteExactSameAdapter": true, + "metrics": { + "step": 96, + "totalMass": 1155.1107641173494, + "channelMass": [ + 416.89133346476206, + 382.9433439159635, + 355.2760867366237 + ], + "relativeMassDrift": 0.000005097652937296187, + "minDensity": 1.6411362879423371e-15, + "maxDensity": 2.354529559612274, + "occupiedFraction": 0.48223876953125, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.9079796075820923, + "clampFraction": 0, + "activeLineages": 3, + "topLineage": 3, + "topLineageMass": 421.43699912096156, + "mixedIdentityMass": 0, + "shannonDiversity": 1.095635250259822, + "phenotypeClusters": 4, + "phenotypeShannon": 1.0956358028502846, + "mutationEvents": 0, + "extinctionEvents": 0, + "lineageMasses": [ + { + "lineage": 3, + "mass": 421.43699912096156 + }, + { + "lineage": 2, + "mass": 384.9614235766099 + }, + { + "lineage": 1, + "mass": 348.7123021025456 + } + ], + "regionMass": [ + 0, + 734.2507030515363, + 420.6408408944831, + 0.21922017133113086 + ], + "environmentMin": -1.3171343114227057, + "environmentMax": 0.15000242856265977, + "wallFraction": 0.125, + "gateOpen": true, + "stormAmplitude": 0 + }, + "regionMass": [ + 0, + 734.2507030515363, + 420.6408408944831, + 0.21922017133113086 + ], + "environmentRange": [ + -1.3171343114227057, + 0.15000242856265977 + ], + "stormObserved": true, + "pass": true + }, + { + "id": "maze-navigation", + "steps": 96, + "hashA": "6526904e2fc04f7ba297bc35061bb0b09c5b312fdbebb57d8125f8a55ba3f42a", + "hashB": "6526904e2fc04f7ba297bc35061bb0b09c5b312fdbebb57d8125f8a55ba3f42a", + "byteExactSameAdapter": true, + "metrics": { + "step": 96, + "totalMass": 1155.3495304884473, + "channelMass": [ + 417.1416877729417, + 382.7848128952337, + 355.42302982027184 + ], + "relativeMassDrift": 0.000005072075476008, + "minDensity": 3.079860365138178e-38, + "maxDensity": 3.3519516587257385, + "occupiedFraction": 0.5120849609375, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 1.5963716506958008, + "clampFraction": 0, + "activeLineages": 3, + "topLineage": 2, + "topLineageMass": 1011.6863709085384, + "mixedIdentityMass": 0, + "shannonDiversity": 0.3761684032957683, + "phenotypeClusters": 4, + "phenotypeShannon": 0.403066545708732, + "mutationEvents": 0, + "extinctionEvents": 0, + "lineageMasses": [ + { + "lineage": 2, + "mass": 1011.6863709085384 + }, + { + "lineage": 3, + "mass": 136.4923112111856 + }, + { + "lineage": 1, + "mass": 1.284579879393858 + } + ], + "regionMass": [ + 0, + 328.40473116050657, + 26.982694034343545, + 799.9621052936001 + ], + "environmentMin": -1.049998917319079, + "environmentMax": 0.8199055795026452, + "wallFraction": 0.22344970703125, + "gateOpen": false, + "stormAmplitude": 0 + }, + "regionMass": [ + 0, + 328.40473116050657, + 26.982694034343545, + 799.9621052936001 + ], + "environmentRange": [ + -1.049998917319079, + 0.8199055795026452 + ], + "stormObserved": true, + "pass": true + }, + { + "id": "storm-recovery", + "steps": 96, + "hashA": "c15fc5f189aa273bc364de79bea2a7b5fddbc9b71655223b0fd271d1531ca643", + "hashB": "c15fc5f189aa273bc364de79bea2a7b5fddbc9b71655223b0fd271d1531ca643", + "byteExactSameAdapter": true, + "metrics": { + "step": 96, + "totalMass": 2166.0336904354363, + "channelMass": [ + 704.626173268484, + 782.4684852065072, + 678.9390319604452 + ], + "relativeMassDrift": 0.000005088595011380846, + "minDensity": 0.00001291042576667678, + "maxDensity": 2.1976983696222305, + "occupiedFraction": 0.89617919921875, + "nonFinite": 0, + "negative": 0, + "maxDisplacement": 0.7046155333518982, + "clampFraction": 0, + "activeLineages": 5, + "topLineage": 4, + "topLineageMass": 1325.7766014241101, + "mixedIdentityMass": 0, + "shannonDiversity": 1.1550805526713914, + "phenotypeClusters": 5, + "phenotypeShannon": 1.1550805526713914, + "mutationEvents": 0, + "extinctionEvents": 0, + "lineageMasses": [ + { + "lineage": 4, + "mass": 1325.7766014241101 + }, + { + "lineage": 1, + "mass": 276.6317192700142 + }, + { + "lineage": 3, + "mass": 274.3170544878167 + }, + { + "lineage": 5, + "mass": 239.70435870945585 + }, + { + "lineage": 2, + "mass": 49.60395654403942 + } + ], + "regionMass": [ + 0, + 557.2225628585609, + 516.0716868460668, + 1092.7394407308086 + ], + "environmentMin": -0.05496687442064285, + "environmentMax": 0.05496687442064285, + "wallFraction": 0, + "gateOpen": false, + "stormAmplitude": 0 + }, + "regionMass": [ + 0, + 557.2225628585609, + 516.0716868460668, + 1092.7394407308086 + ], + "environmentRange": [ + -0.05496687442064285, + 0.05496687442064285 + ], + "stormObserved": true, + "pass": true + } + ], + "roundTrip": { + "schemaVersion": "flow-lenia-arena-experiment-v1", + "scientificSha256": "5ec27f3c8826972fe91bd4fbe2dde3d236120dc013a81322a3c45a6cf57938cb", + "restoredByteExact": true, + "continuationByteExact": true, + "lineagePreserved": true, + "environmentPreserved": true, + "tamperRejected": true, + "pass": true + }, + "architecture": { + "arenaStorageRenderBindings": 8, + "allocatedBytes256": 46411280, + "memoryMib256": 44.26124572753906, + "under128Mib": true, + "hotLoopReadbacks": 0, + "hotLoopGpuBufferAllocations": 0 + }, + "performance": { + "grid": 256, + "samples": 8, + "p50Ms": 3.2000000178813934, + "p95Ms": 18.899999976158142, + "pass": true + }, + "renderIntegrity": { + "scientificStateByteExact": true, + "pass": true + }, + "productSurface": { + "arenaExperiments": 3, + "arenaViews": 5, + "arenaTools": 6, + "ioActions": 2, + "regionMetrics": true, + "lineageGraph": true, + "keyboardFocus": true, + "responsive": true, + "pass": true + }, + "pass": true, + "captureContract": { + "schemaVersion": "1.0.0", + "step": 72, + "fields": [ + "mass", + "genome_h", + "genome_q", + "identity_u32_values", + "environment_affinity_values", + "environment_region_u32_values" + ], + "shaderHash": "f3f9abe23cdd4e1daefd929cbb0ca52b167cd551f2ad1bccf5bf685e29f30fff", + "environmentSchema": "flow-lenia-arena-environment-v1", + "pass": true + }, + "adaptiveSmoke": { + "grid": 128, + "viewport": [ + 390, + 844 + ], + "ready": true, + "panelMounted": true, + "pass": true + }, + "browserMatrix": [ + { + "browser": "Chromium", + "surface": "desktop reference + 256² measured scratch", + "webgpu": true, + "result": "PASS" + }, + { + "browser": "Chromium", + "surface": "390×844 touch + reduced motion + 128² adaptive", + "webgpu": true, + "result": "PASS" + }, + { + "browser": "Firefox/Safari", + "surface": "compatibility", + "webgpu": false, + "result": "NOT CLAIMED — support varies" + } + ] +} diff --git a/packages/flow-lenia/web/artifacts/m6-canonical-capture-index.json b/packages/flow-lenia/web/artifacts/m6-canonical-capture-index.json new file mode 100644 index 00000000..0c2d2a7a --- /dev/null +++ b/packages/flow-lenia/web/artifacts/m6-canonical-capture-index.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": "flow-lenia-m6-canonical-index-v1", + "generatedUtc": "2026-07-18T20:06:38.058Z", + "modelVariant": "flow-lenia-ecosystem-v1", + "captures": [ + { + "mode": "organism", + "reference": "m2-browser-gates.json structural replay", + "grid": 128, + "step": 256, + "stateSha256": "1b3e80400f4c32128c5344144dbce5311304dd348e6be79ccb22163070d02522", + "byteExactSameAdapter": true, + "massRelativeDrift": 0.000013534007704367495 + }, + { + "mode": "ecosystem", + "reference": "m4-browser-gates.json whole-rule replay", + "grid": 128, + "step": 32, + "stateSha256": "0b03175f813c0872907bde01dcee658694edbacda546fc7f9b7a4dbb95347348", + "byteExactSameAdapter": true, + "massRelativeDrift": 0.0000016985004022689555 + }, + { + "mode": "arena", + "reference": "standard browser capture", + "grid": 128, + "step": 72, + "stateSha256": "f41d30d72f30658eda45973a7f337e97bc0eb154e243a785e78b8725bbce8c5f", + "shaderSha256": "f3f9abe23cdd4e1daefd929cbb0ca52b167cd551f2ad1bccf5bf685e29f30fff", + "environmentSchema": "flow-lenia-arena-environment-v1", + "byteExactSameAdapter": true, + "massRelativeDrift": 0.0000038031936772616477 + } + ], + "pass": true +} diff --git a/packages/flow-lenia/web/index.html b/packages/flow-lenia/web/index.html new file mode 100644 index 00000000..3176f13b --- /dev/null +++ b/packages/flow-lenia/web/index.html @@ -0,0 +1,41 @@ + + + + + + Flow Lenia — Organism, Ecosystem & Arena Laboratory — Bit-Physics + + + + + + + + + + +
+ +
+

M3 · visual laboratory

+
density ρ · fixed scale 0 → 2
+
closed mass ledger · measuring…
+
GPU queue · measuring…
+ +
+ Ecosystem Lab +
+
+ +

Space pauses. Period advances one step. Keys 1 through 5 choose pipette, add, erase, stir, and inspect. Arrow keys move the inspection lens. Enter applies the selected tool. R resets, F fits occupied mass, plus and minus zoom.

+ +
+
initializing WebGPU…
+ + + diff --git a/packages/flow-lenia/web/package-lock.json b/packages/flow-lenia/web/package-lock.json new file mode 100644 index 00000000..6842753a --- /dev/null +++ b/packages/flow-lenia/web/package-lock.json @@ -0,0 +1,1127 @@ +{ + "name": "@bit-physics/web-flow-lenia", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@bit-physics/web-flow-lenia", + "version": "0.0.0", + "devDependencies": { + "@webgpu/types": "0.1.70", + "typescript": "6.0.3", + "vite": "6.4.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/packages/flow-lenia/web/package.json b/packages/flow-lenia/web/package.json new file mode 100644 index 00000000..9fc8e64d --- /dev/null +++ b/packages/flow-lenia/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "@bit-physics/web-flow-lenia", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Verified Flow Lenia WebGPU organism, localized ecosystem, and editable soft-affinity Arena laboratory with deterministic replay and versioned experiment sharing.", + "scripts": { + "build": "vite build", + "dev": "vite", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "check:m0": "node scripts/check-m0.mjs", + "check:m2": "node scripts/check-m2.mjs", + "gate:m2": "node scripts/run-browser-gates.mjs", + "check:m3": "node scripts/check-m3.mjs", + "gate:m3": "node scripts/run-browser-m3-gates.mjs", + "check:m4": "node scripts/check-m4.mjs", + "gate:m4": "node scripts/run-browser-m4-gates.mjs", + "check:m6": "node scripts/check-m6.mjs", + "gate:m6": "node scripts/run-browser-m6-gates.mjs" + }, + "devDependencies": { + "@webgpu/types": "0.1.70", + "typescript": "6.0.3", + "vite": "6.4.3" + } +} diff --git a/packages/flow-lenia/web/public/favicon.svg b/packages/flow-lenia/web/public/favicon.svg new file mode 100644 index 00000000..47d7b4ed --- /dev/null +++ b/packages/flow-lenia/web/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/flow-lenia/web/scripts/check-m0.mjs b/packages/flow-lenia/web/scripts/check-m0.mjs new file mode 100644 index 00000000..7b1e40ab --- /dev/null +++ b/packages/flow-lenia/web/scripts/check-m0.mjs @@ -0,0 +1,55 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const read = (path) => readFile(resolve(root, path), "utf8"); +const [fft, gather, wrapper, inventory, artifactRaw] = await Promise.all([ + read("src/shaders/batch_fft.wgsl"), + read("src/shaders/reintegrate.wgsl"), + read("src/fft-batch.ts"), + read("src/inventory.ts"), + read("artifacts/m0-browser-benchmark.json"), +]); + +const assertions = [ + [wrapper.includes("FFT_COMMON_WGSL"), "FFT wrapper imports the shared numerical core"], + [fft.includes("plane * U.n * U.n"), "FFT address surface includes an explicit plane dimension"], + [gather.includes("for (var oy = -5; oy <= 5; oy += 1)"), "gather has the fixed 11x11 dd=5 candidate loop"], + [gather.includes("4.0 * U.sigma * U.sigma"), "finite-square overlap uses the normalized 4 sigma^2 area"], + [gather.includes("fn gather_mass"), "mass-only gather specialization exists"], + [gather.includes("fn gather_full"), "full-state gather specialization exists"], + [inventory.includes("storageBindings: 8"), "full-state binding inventory freezes the portable storage floor"], +]; + +for (const [passed, label] of assertions) { + if (!passed) throw new Error(`M0 static check failed: ${label}`); + console.log(`PASS ${label}`); +} + +const n = 256; +const projectedBytes = 604 * n * n; +const largestBufferBytes = 72 * n * n; +if (projectedBytes !== 39_583_744) throw new Error("projected memory arithmetic drifted"); +if (largestBufferBytes !== 4_718_592) throw new Error("largest-buffer arithmetic drifted"); +console.log(`PASS projected 256^2 complete state ${(projectedBytes / 2 ** 20).toFixed(2)} MiB`); +console.log(`PASS largest 256^2 storage binding ${(largestBufferBytes / 2 ** 20).toFixed(2)} MiB`); + +const artifact = JSON.parse(artifactRaw); +const verification = artifact.verification128; +const desktop = artifact.benchmarks.find((row) => row.n === 256 && row.mode === "step-full"); +const artifactAssertions = [ + [artifact.schemaVersion === "flow-lenia-m0-v1", "benchmark artifact schema is pinned"], + [artifact.benchmarks.length === 10, "artifact covers five workloads at both grid tiers"], + [artifact.architectureFailures.length === 0, "reference adapter has no architecture failure"], + [verification.fftMaxAbs <= 5e-4, "browser FFT round-trip anchor passes"], + [verification.gatherMassRelativeResidual <= 5e-5, "mass-only gather ledger passes"], + [verification.fullMassRelativeResidual <= 5e-5, "full-state gather ledger passes"], + [verification.uniformGenomeMaxAbs <= 5e-5, "uniform genome anchor passes"], + [verification.uniformIdentityExact === true, "uniform identity anchor is exact"], + [desktop?.p95Ms <= 33.3, "256^2 dominant-path p95 retains the desktop budget"], + [artifact.measuredDefault === 256, "measured desktop default is frozen at 256^2"], +]; +for (const [passed, label] of artifactAssertions) { + if (!passed) throw new Error(`M0 artifact check failed: ${label}`); + console.log(`PASS ${label}`); +} diff --git a/packages/flow-lenia/web/scripts/check-m2.mjs b/packages/flow-lenia/web/scripts/check-m2.mjs new file mode 100644 index 00000000..67d0cde9 --- /dev/null +++ b/packages/flow-lenia/web/scripts/check-m2.mjs @@ -0,0 +1,57 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const read = (path) => readFile(resolve(root, path), "utf8"); +const [solver, spectral, perceive, flow, gather, renderer, fixtureRaw, artifactRaw] = await Promise.all([ + read("src/model/solver.ts"), + read("src/shaders/organism_spectral.wgsl"), + read("src/shaders/organism_perceive.wgsl"), + read("src/shaders/organism_flow.wgsl"), + read("src/shaders/reintegrate_organism.wgsl"), + read("src/shaders/render_organism.wgsl"), + read("src/prove/organism-fixture.json"), + read("artifacts/m2-browser-gates.json"), +]); + +const assertions = [ + [solver.includes("encode2d(pass, CHANNELS, -1)"), "one batched forward transform covers all mass channels"], + [solver.includes("encode2d(pass, KERNELS, 1)"), "one batched inverse transform covers all kernel responses"], + [spectral.includes("params[kernel].source"), "kernel source routing comes from the frozen connection table"], + [perceive.includes("2.0 * exp(-0.5 * z * z) - 1.0"), "growth uses the frozen bell response"], + [flow.includes("(1.0 - gate) * gradient - gate * pressure_gradient"), "flow combines affinity and density pressure"], + [flow.includes("clamp(raw, vec2(-U.max_displacement)"), "displacement is component-clamped to the proof domain"], + [gather.includes("for (var oi = -5; oi <= 5; oi += 1)"), "organism gather streams the fixed 11x11 neighborhood"], + [gather.includes("4.0 * U.sigma * U.sigma"), "organism gather uses exact normalized square overlap"], + [!solver.toLowerCase().includes("genome"), "organism mode allocates no localized genome buffers"], + [!renderer.includes("read_write"), "all renderer solver bindings are read-only"], +]; +for (const [passed, label] of assertions) { + if (!passed) throw new Error(`M2 static check failed: ${label}`); + console.log(`PASS ${label}`); +} + +const fixture = JSON.parse(fixtureRaw); +if (fixture.schema_version !== "flow-lenia-m2-conformance-v1") throw new Error("M2 fixture schema drifted"); +if (fixture.cases.length !== 3) throw new Error("M2 fixture must keep the reference plus two adversarial cases"); +console.log("PASS f64-derived fixture includes smooth, seam-loaded, and crowded-pressure cases"); + +const artifact = JSON.parse(artifactRaw); +const fieldFailures = artifact.numericalCases.flatMap((item) => item.fields.filter((field) => !field.pass)); +const gates = [ + [artifact.schemaVersion === "flow-lenia-m2-gates-v1", "browser artifact schema is pinned"], + [artifact.environment?.userAgent && artifact.environment?.adapter?.vendor, "browser and adapter provenance are recorded"], + [artifact.numericalCases.length === 3 && fieldFailures.length === 0, "all CPU-GPU intermediate and rollout comparisons pass"], + [artifact.structural.steps === 256, "long-horizon gate executes 256 steps per replay"], + [artifact.structural.byteExactSameAdapter === true, "same-adapter replay is byte-exact"], + [artifact.structural.metrics.relativeMassDrift <= 5e-5, "long-horizon mass ledger closes"], + [artifact.structural.metrics.nonFinite === 0 && artifact.structural.metrics.negative === 0, "long-horizon state stays finite and non-negative"], + [artifact.structural.metrics.clampFraction <= 0.05, "long-horizon clamp fraction stays bounded"], + [artifact.performance.grid === 256 && artifact.performance.p95Ms <= 33.3, "complete 256^2 step retains the desktop timing budget"], + [artifact.performance.allocatedBytes < 128 * 2 ** 20, "complete organism allocation retains the memory budget"], + [artifact.pass === true, "committed M2 browser verdict passes"], +]; +for (const [passed, label] of gates) { + if (!passed) throw new Error(`M2 artifact check failed: ${label}`); + console.log(`PASS ${label}`); +} diff --git a/packages/flow-lenia/web/scripts/check-m3.mjs b/packages/flow-lenia/web/scripts/check-m3.mjs new file mode 100644 index 00000000..cc5adb44 --- /dev/null +++ b/packages/flow-lenia/web/scripts/check-m3.mjs @@ -0,0 +1,51 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const read = (path) => readFile(resolve(root, path), "utf8"); +const [solver, events, renderer, renderShader, trails, cards, app, artifactRaw] = await Promise.all([ + read("src/model/solver.ts"), + read("src/shaders/organism_events.wgsl"), + read("src/render/renderer.ts"), + read("src/shaders/render_organism.wgsl"), + read("src/shaders/render_trails.wgsl"), + read("src/experiments/cards.ts"), + read("src/app.ts"), + read("artifacts/m3-browser-gates.json"), +]); +const encodeOne = solver.slice(solver.indexOf("private encodeOne"), solver.indexOf("step(count")); + +const assertions = [ + [events.includes("atomicAdd(&eventLedger[0]") && events.includes("atomicAdd(&eventLedger[1]"), "open add/erase tools write the explicit fixed-point ledger"], + [encodeOne.indexOf("this.openEventPipeline") < encodeOne.indexOf("this.packPipeline"), "open events execute before spectral perception at the fixed boundary"], + [encodeOne.indexOf("this.impulseEventPipeline") < encodeOne.indexOf("this.gatherPipeline"), "closed impulses execute before faithful destination gather"], + [events.includes("transport.displacement_x") && events.includes("UI.max_displacement"), "pipette/stir remain bounded conservative transport impulses"], + [!renderShader.includes("read_write") && renderer.includes('buffer: { type: "read-only-storage"'), "all renderer scientific-state bindings are read-only"], + [trails.includes("historyFrame") && renderer.includes("flow-lenia-m3-trail-a"), "trails live in separate presentation textures"], + [(cards.match(/id: "[^"]+",/g) ?? []).length === 6, "exactly six authored organism/ablation cards ship in M3"], + [cards.includes("pressure-ablation") && cards.includes("comparison") && cards.includes("sigma-phase"), "synchronized pressure and sigma comparisons are explicit cards"], + [app.includes('aria-keyshortcuts') && app.includes('pointerType === "touch"') && app.includes("pinchDistance"), "keyboard, pointer, and touch/pinch paths are implemented"], + [app.includes("ScientificInspector") && app.includes("fitOccupied"), "read-only inspection and occupied-mass camera fit are wired"], +]; +for (const [passed, label] of assertions) { + if (!passed) throw new Error(`M3 static check failed: ${label}`); + console.log(`PASS ${label}`); +} + +const artifact = JSON.parse(artifactRaw); +const gates = [ + [artifact.schemaVersion === "flow-lenia-m3-gates-v1", "browser artifact schema is pinned"], + [artifact.environment?.userAgent && artifact.environment?.adapter?.vendor, "browser and adapter provenance are recorded"], + [artifact.cards.length === 6 && artifact.cards.every((card) => card.pass), "all six cards retain structural stability"], + [artifact.cards.filter((card) => card.comparison).length === 2 && artifact.cards.filter((card) => card.comparison).every((card) => card.comparison.pass && card.comparison.stateDiverged), "both synchronized ablations are stable and causally diverge"], + [artifact.scheduledEvents.byteExactSameAdapter && artifact.scheduledEvents.ledgerPass, "scheduled open/closed event replay is exact and ledgered"], + [artifact.closedImpulses.ledgerUntouched && artifact.closedImpulses.pass, "pipette/stir remain closed in the ledger"], + [artifact.renderIntegrity.scientificStateByteExact, "render controls and effects leave scientific bytes unchanged"], + [artifact.productSurface.pass && artifact.productSurface.experiments === 6 && artifact.productSurface.tools === 5 && artifact.productSurface.scientificViews === 6, "complete accessible laboratory product surface is mounted"], + [artifact.memory.under128Mib, "synchronized comparison solvers retain the provisional memory budget"], + [artifact.pass === true, "committed M3 browser verdict passes"], +]; +for (const [passed, label] of gates) { + if (!passed) throw new Error(`M3 artifact check failed: ${label}`); + console.log(`PASS ${label}`); +} diff --git a/packages/flow-lenia/web/scripts/check-m4.mjs b/packages/flow-lenia/web/scripts/check-m4.mjs new file mode 100644 index 00000000..313781f0 --- /dev/null +++ b/packages/flow-lenia/web/scripts/check-m4.mjs @@ -0,0 +1,60 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const read = (path) => readFile(resolve(root, path), "utf8"); +const [solver, organismSolver, gather, mutation, renderer, cardsRaw, fixtureRaw, artifactRaw] = await Promise.all([ + read("src/model/ecosystem-solver.ts"), + read("src/model/solver.ts"), + read("src/shaders/reintegrate_ecosystem.wgsl"), + read("src/shaders/ecosystem_mutation.wgsl"), + read("src/shaders/render_ecosystem.wgsl"), + read("src/experiments/ecosystem-cards.ts"), + read("src/prove/ecosystem-fixture.json"), + read("artifacts/m4-browser-gates.json"), +]); + +const assertions = [ + [solver.includes("genomeH: [GPUBuffer, GPUBuffer]") && solver.includes("genomeQ: [GPUBuffer, GPUBuffer]") && solver.includes("identity: [GPUBuffer, GPUBuffer]"), "ecosystem H, Q, fingerprint/lineage/flags state is ping-ponged"], + [solver.includes("flow-lenia-m4-eight-storage-gather-layout"), "localized transport freezes the portable eight-storage binding layout"], + [solver.includes("constants: { MIXING_RULE: ruleIndex[rule] }"), "five inheritance rules compile as specialized gather pipelines"], + [gather.includes("override MIXING_RULE") && gather.includes("incoming > 0.0") && gather.indexOf("incoming > 0.0") < gather.indexOf("source_gene(&hIn"), "candidate genome reads occur only after positive overlap mass"], + [gather.includes("counter_hash(destination, candidate, gene"), "stochastic gene choice is stateless and counter-addressed"], + [gather.includes("- log(-log(gumbel_u))"), "negotiation uses streaming Gumbel-max"], + [mutation.includes("apply_mutation_patches") && mutation.includes("identity[g.x].z == header.w"), "mutation applies bounded deltas to contiguous parent-lineage patches"], + [solver.includes("lineageRing.length > 128"), "lineage history is a bounded ring"], + [!renderer.includes("read_write"), "lineage and phenotype renderer bindings are read-only"], + [!organismSolver.includes("ecosystem-solver") && !organismSolver.toLowerCase().includes("genome"), "organism specialization remains genome-free"], + [(cardsRaw.match(/\n id: "/g) ?? []).length === 3, "three M4 ecosystem cards are authored"], +]; +for (const [passed, label] of assertions) { + if (!passed) throw new Error(`M4 static check failed: ${label}`); + console.log(`PASS ${label}`); +} + +const fixture = JSON.parse(fixtureRaw); +if (fixture.schema_version !== "flow-lenia-m4-conformance-v1") throw new Error("M4 fixture schema drifted"); +if (fixture.cases.length !== 5 || fixture.cases.map((item) => item.rule).join(",") !== "average,whole,gene-wise,best,negotiation") throw new Error("M4 fixture must cover all five inheritance rules in frozen order"); +console.log("PASS f64-derived fixture covers every localized inheritance rule and mutation identity"); + +const artifact = JSON.parse(artifactRaw); +const gates = [ + [artifact.schemaVersion === "flow-lenia-m4-gates-v1", "browser artifact schema is pinned"], + [artifact.environment?.userAgent && artifact.environment?.adapter?.vendor, "browser and adapter provenance are recorded"], + [artifact.numericalRules.length === 5 && artifact.numericalRules.every((item) => item.pass), "all five CPU-GPU inheritance comparisons pass"], + [artifact.determinismRules.length === 5 && artifact.determinismRules.every((item) => item.byteExactSameAdapter && item.pass), "all five inheritance rules replay byte-exactly on one adapter"], + [artifact.mutation.childIdentityExact && artifact.mutation.childFlagsExact && artifact.mutation.lineageRingComplete && artifact.mutation.affectedMass > 0 && artifact.mutation.pass, "mutation delta, child identity/flags, affected mass, conservation, and lineage record pass"], + [artifact.ecosystems.length === 3 && artifact.ecosystems.every((item) => item.pass), "three-founder, negotiation-sea, and identity-dilution ecosystems remain stable"], + [artifact.identityDilution.distinctOutcomes && artifact.identityDilution.pass, "identity-dilution comparison falsifies equivalent mixing outcomes"], + [artifact.architecture.gatherStorageBindings === 8 && artifact.architecture.specializedPipelines === 5, "measured architecture retains eight bindings and five specializations"], + [artifact.architecture.allocatedBytes256 < 128 * 2 ** 20, "complete 256² ecosystem allocation retains the memory budget"], + [artifact.performance.p95Ms <= 33.3 && artifact.performance.pass, "complete 256² negotiation step retains the desktop timing budget"], + [artifact.renderIntegrity?.scientificStateByteExact === true, "lineage and phenotype rendering leaves mass and genomes byte-exact"], + [artifact.productSurface?.pass === true, "ecosystem cards, rules, tools, views, and three-pane comparison are mounted"], + [artifact.captureContract?.pass === true && artifact.captureContract?.step === 32 && artifact.captureContract?.fields?.length === 4, "standard browser capture contains mass, H, Q, and packed identity state"], + [artifact.pass === true, "committed M4 browser verdict passes"], +]; +for (const [passed, label] of gates) { + if (!passed) throw new Error(`M4 artifact check failed: ${label}`); + console.log(`PASS ${label}`); +} diff --git a/packages/flow-lenia/web/scripts/check-m6.mjs b/packages/flow-lenia/web/scripts/check-m6.mjs new file mode 100644 index 00000000..8a480d40 --- /dev/null +++ b/packages/flow-lenia/web/scripts/check-m6.mjs @@ -0,0 +1,33 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); const read = (path) => readFile(resolve(root, path), "utf8"); +const [solver, arena, events, render, cards, io, app, artifactRaw, canonicalRaw] = await Promise.all([ + read("src/model/ecosystem-solver.ts"), read("src/shaders/arena_perceive.wgsl"), read("src/shaders/arena_events.wgsl"), read("src/shaders/render_arena.wgsl"), read("src/experiments/arena-cards.ts"), read("src/model/experiment-io.ts"), read("src/arena-app.ts"), read("artifacts/m6-browser-release-gates.json"), read("artifacts/m6-canonical-capture-index.json"), +]); +const assertions = [ + [solver.includes("options?.environment === true") && solver.includes("this.arenaEnabled ? makeBuffer"), "Arena resources remain opt-in and do not change M4 allocation"], + [arena.includes("affinity.xyz += U.channelResponse.xyz * environmentValue"), "environment is additive affinity upstream of unchanged flow/transport"], + [arena.includes("authored.x + authored.y + (1.0 - U.gateOpen) * authored.z"), "soft walls and timed gates are explicit conservative fields"], + [events.includes("apply_environment_events") && events.includes("field.y = clamp"), "affinity, wall, and erase brushes execute on GPU at step boundaries"], + [!render.includes("read_write") && render.includes("@binding(8) var regionIn"), "Arena renderer has eight read-only scientific bindings"], + [(cards.match(/\n id: "/g) ?? []).length === 3, "three authored Arena experiment cards ship"], + [io.includes("flow-lenia-arena-experiment-v1") && io.includes("scientific_sha256") && io.includes("parseArenaExperiment"), "versioned experiment import verifies complete-state SHA-256"], + [app.includes("data-arena-regions") && app.includes("data-lineage-graph") && app.includes("region_mass_1"), "region metrics and lineage graph are mounted"], +]; +for (const [passed, label] of assertions) { if (!passed) throw new Error(`M6 static check failed: ${label}`); console.log(`PASS ${label}`); } +const artifact = JSON.parse(artifactRaw); const gates = [ + [artifact.schemaVersion === "flow-lenia-m6-release-gates-v1", "release artifact schema is pinned"], + [artifact.environmentAnchors?.pass, "zero-field, affinity, gate, paint, and conservation anchors pass"], + [artifact.arenas?.length === 3 && artifact.arenas.every((item) => item.pass && item.byteExactSameAdapter), "all Arena cards are stable and replay byte-exactly on one adapter"], + [artifact.roundTrip?.restoredByteExact && artifact.roundTrip?.continuationByteExact && artifact.roundTrip?.tamperRejected && artifact.roundTrip?.pass, "versioned export/import rejects tampering, restores, and continues byte-exactly"], + [artifact.architecture?.arenaStorageRenderBindings === 8 && artifact.architecture?.hotLoopReadbacks === 0 && artifact.architecture?.hotLoopGpuBufferAllocations === 0 && artifact.architecture?.under128Mib, "portable bindings, hot-loop, and memory budgets pass"], + [artifact.performance?.p95Ms <= 33.3 && artifact.performance?.pass, "256² Arena timing stays within the desktop budget"], + [artifact.renderIntegrity?.scientificStateByteExact, "all Arena render modes leave scientific buffers byte-exact"], + [artifact.productSurface?.pass, "Arena cards, tools, views, metrics, graph, IO, keyboard, and responsive surface are mounted"], + [artifact.captureContract?.pass && artifact.captureContract?.fields?.length === 6 && /^[0-9a-f]{64}$/.test(artifact.captureContract.shaderHash), "canonical Arena capture includes all six fields and shader provenance"], + [artifact.adaptiveSmoke?.grid === 128 && artifact.adaptiveSmoke?.pass, "touch-sized adaptive browser smoke passes"], + [artifact.pass === true, "committed M5–M6 release verdict passes"], +]; +for (const [passed, label] of gates) { if (!passed) throw new Error(`M6 artifact check failed: ${label}`); console.log(`PASS ${label}`); } +const canonical = JSON.parse(canonicalRaw); if (canonical.schemaVersion !== "flow-lenia-m6-canonical-index-v1" || canonical.captures?.length !== 3 || !canonical.captures.every((item) => /^[0-9a-f]{64}$/.test(item.stateSha256)) || !canonical.pass) throw new Error("M6 canonical capture index is incomplete"); console.log("PASS organism, ecosystem, and Arena canonical references are hash-pinned"); diff --git a/packages/flow-lenia/web/scripts/run-browser-benchmark.mjs b/packages/flow-lenia/web/scripts/run-browser-benchmark.mjs new file mode 100644 index 00000000..bed3f4be --- /dev/null +++ b/packages/flow-lenia/web/scripts/run-browser-benchmark.mjs @@ -0,0 +1,95 @@ +import { createServer } from "node:http"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, extname, join, normalize, resolve } from "node:path"; + +const playwright = await import(process.env.PLAYWRIGHT_MODULE ?? "playwright"); +const chromium = playwright.chromium ?? playwright.default.chromium; +const webRoot = resolve(import.meta.dirname, ".."); +const distDir = resolve(process.argv[2] ?? join(webRoot, "dist")); +const output = resolve(process.argv[3] ?? join(webRoot, "artifacts", "m0-browser-benchmark.json")); +const samples = Number(process.env.FLOW_LENIA_M0_SAMPLES ?? "12"); +const warmup = Number(process.env.FLOW_LENIA_M0_WARMUP ?? "3"); +const timeout = Number(process.env.FLOW_LENIA_M0_TIMEOUT_MS ?? "600000"); +const mime = { + ".html": "text/html", + ".js": "text/javascript", + ".css": "text/css", + ".json": "application/json", + ".map": "application/json", +}; + +const server = createServer(async (request, response) => { + try { + let path = decodeURIComponent((request.url ?? "/").split("?")[0]); + if (path === "/favicon.ico") { + response.writeHead(204); + response.end(); + return; + } + if (path === "/") path = "/index.html"; + const file = join(distDir, normalize(path).replace(/^(\.\.[/\\])+/, "")); + const body = await readFile(file); + response.writeHead(200, { "content-type": mime[extname(file)] ?? "application/octet-stream" }); + response.end(body); + } catch { + response.writeHead(404); + response.end("not found"); + } +}); + +await new Promise((resolveListen) => server.listen(0, resolveListen)); +const address = server.address(); +if (!address || typeof address === "string") throw new Error("benchmark server did not bind TCP"); +const url = `http://localhost:${address.port}/?m0=1&samples=${samples}&warmup=${warmup}`; +let exitCode = 1; +try { + const browser = await chromium.launch({ + executablePath: process.env.CHROME_BIN, + headless: false, + chromiumSandbox: false, + args: [ + "--headless=new", + "--no-sandbox", + "--disable-gpu-sandbox", + "--enable-unsafe-webgpu", + "--enable-features=Vulkan", + "--use-angle=vulkan", + "--use-vulkan", + ], + }); + const page = await browser.newPage(); + const errors = []; + page.on("console", (message) => { + if (message.type() === "error") errors.push(message.text()); + }); + page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`)); + await page.goto(url, { waitUntil: "load", timeout: 30_000 }); + const available = await page.evaluate(async () => { + if (!navigator.gpu) return false; + return Boolean(await navigator.gpu.requestAdapter()); + }); + if (!available) { + console.log("WEBGPU_UNAVAILABLE: browser adapter absent"); + exitCode = 42; + } else { + await page.waitForFunction(() => window.__bitPhysicsReady === true, undefined, { timeout }); + const report = await page.evaluate(async () => { + if (!window.__flowLeniaM0) throw new Error("Flow Lenia M0 test hook missing"); + return window.__flowLeniaM0.runSuite(); + }); + const unexpected = errors.filter((message) => !/WebGPU unavailable|requestAdapter/i.test(message)); + if (unexpected.length > 0) throw new Error(`browser errors: ${unexpected.join(" | ")}`); + await mkdir(dirname(output), { recursive: true }); + await writeFile(output, JSON.stringify(report, null, 2) + "\n"); + console.log(`M0 browser benchmark: ${output}`); + console.log(`adapter: ${report.adapter.description || report.adapter.device}`); + console.log(`measured default: ${report.measuredDefault}^2`); + exitCode = 0; + } + await browser.close(); +} catch (error) { + console.error(`M0 browser benchmark failed: ${String(error).split("\n")[0]}`); +} finally { + server.close(); +} +process.exit(exitCode); diff --git a/packages/flow-lenia/web/scripts/run-browser-gates.mjs b/packages/flow-lenia/web/scripts/run-browser-gates.mjs new file mode 100644 index 00000000..1e033c83 --- /dev/null +++ b/packages/flow-lenia/web/scripts/run-browser-gates.mjs @@ -0,0 +1,84 @@ +import { createServer } from "node:http"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, extname, join, normalize, resolve } from "node:path"; + +const playwright = await import(process.env.PLAYWRIGHT_MODULE ?? "playwright"); +const chromium = playwright.chromium ?? playwright.default.chromium; +const webRoot = resolve(import.meta.dirname, ".."); +const distDir = resolve(process.argv[2] ?? join(webRoot, "dist")); +const output = resolve(process.argv[3] ?? join(webRoot, "artifacts", "m2-browser-gates.json")); +const structuralSteps = Number(process.env.FLOW_LENIA_M2_STRUCTURAL_STEPS ?? "256"); +const timeout = Number(process.env.FLOW_LENIA_M2_TIMEOUT_MS ?? "600000"); +const mime = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".map": "application/json" }; + +const server = createServer(async (request, response) => { + try { + let path = decodeURIComponent((request.url ?? "/").split("?")[0]); + if (path === "/favicon.ico") { response.writeHead(204); response.end(); return; } + if (path === "/") path = "/index.html"; + const file = join(distDir, normalize(path).replace(/^(\.\.[/\\])+/, "")); + const body = await readFile(file); + response.writeHead(200, { "content-type": mime[extname(file)] ?? "application/octet-stream" }); + response.end(body); + } catch { + response.writeHead(404); + response.end("not found"); + } +}); + +await new Promise((resolveListen) => server.listen(0, resolveListen)); +const address = server.address(); +if (!address || typeof address === "string") throw new Error("gate server did not bind TCP"); +const url = `http://localhost:${address.port}/?grid=128&gate=1`; +let exitCode = 1; +try { + const browser = await chromium.launch({ + executablePath: process.env.CHROME_BIN, + headless: false, + chromiumSandbox: false, + args: ["--headless=new", "--no-sandbox", "--disable-gpu-sandbox", "--enable-unsafe-webgpu", "--enable-features=Vulkan", "--use-angle=vulkan", "--use-vulkan"], + }); + const page = await browser.newPage(); + const errors = []; + page.on("console", (message) => { + if (message.type() === "error") { + errors.push(message.text()); + console.log(`browser error: ${message.text()}`); + } + else if (message.text().startsWith("Flow Lenia M2 gates:")) console.log(message.text()); + }); + page.on("pageerror", (error) => { + errors.push(`pageerror: ${error.message}`); + console.log(`pageerror: ${error.message}`); + }); + await page.goto(url, { waitUntil: "load", timeout: 30_000 }); + console.log("M2 gate page loaded"); + const available = await page.evaluate(async () => Boolean(navigator.gpu && await navigator.gpu.requestAdapter())); + if (!available) { + console.log("WEBGPU_UNAVAILABLE: browser adapter absent"); + exitCode = 42; + } else { + console.log("M2 WebGPU adapter available; waiting for app ready"); + await page.waitForFunction(() => window.__bitPhysicsReady === true, undefined, { timeout }); + const report = await page.evaluate(async (steps) => { + if (!window.__flowLeniaM2) throw new Error("Flow Lenia M2 test hook missing"); + return window.__flowLeniaM2.runGates(steps); + }, structuralSteps); + const unexpected = errors.filter((message) => !/WebGPU unavailable|requestAdapter/i.test(message)); + if (unexpected.length > 0) throw new Error(`browser errors: ${unexpected.join(" | ")}`); + await mkdir(dirname(output), { recursive: true }); + await writeFile(output, JSON.stringify(report, null, 2) + "\n"); + console.log(`M2 browser gates: ${output}`); + console.log(`numerical cases: ${report.numericalCases.map((item) => `${item.name}=${item.pass}`).join(", ")}`); + console.log(`structural: ${report.structural.steps} steps × 2, exact=${report.structural.byteExactSameAdapter}, drift=${report.structural.metrics.relativeMassDrift}`); + console.log(`performance: 256^2 p95=${report.performance.p95Ms} ms, memory=${report.performance.memoryMib} MiB`); + console.log(`verdict: ${report.pass ? "PASS" : "FAIL"}`); + exitCode = report.pass ? 0 : 1; + } + await browser.close(); +} catch (error) { + console.error(`M2 browser gates failed: ${String(error).split("\n")[0]}`); +} finally { + server.close(); +} +process.exit(exitCode); diff --git a/packages/flow-lenia/web/scripts/run-browser-m3-gates.mjs b/packages/flow-lenia/web/scripts/run-browser-m3-gates.mjs new file mode 100644 index 00000000..802232f5 --- /dev/null +++ b/packages/flow-lenia/web/scripts/run-browser-m3-gates.mjs @@ -0,0 +1,85 @@ +import { createServer } from "node:http"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, extname, join, normalize, resolve } from "node:path"; + +const playwright = await import(process.env.PLAYWRIGHT_MODULE ?? "playwright"); +const chromium = playwright.chromium ?? playwright.default.chromium; +const webRoot = resolve(import.meta.dirname, ".."); +const distDir = resolve(process.argv[2] ?? join(webRoot, "dist")); +const output = resolve(process.argv[3] ?? join(webRoot, "artifacts", "m3-browser-gates.json")); +const screenshot = resolve(process.argv[4] ?? "/tmp/flow-lenia-m3-ui.png"); +const timeout = Number(process.env.FLOW_LENIA_M3_TIMEOUT_MS ?? "600000"); +const mime = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".map": "application/json" }; + +const server = createServer(async (request, response) => { + try { + let path = decodeURIComponent((request.url ?? "/").split("?")[0]); + if (path === "/favicon.ico") { response.writeHead(204); response.end(); return; } + if (path === "/") path = "/index.html"; + const file = join(distDir, normalize(path).replace(/^(\.\.[/\\])+/, "")); + const body = await readFile(file); + response.writeHead(200, { "content-type": mime[extname(file)] ?? "application/octet-stream" }); + response.end(body); + } catch { + response.writeHead(404); + response.end("not found"); + } +}); + +await new Promise((resolveListen) => server.listen(0, resolveListen)); +const address = server.address(); +if (!address || typeof address === "string") throw new Error("gate server did not bind TCP"); +const url = `http://localhost:${address.port}/?grid=128&gate=1`; +let exitCode = 1; +try { + const browser = await chromium.launch({ + executablePath: process.env.CHROME_BIN, + headless: false, + chromiumSandbox: false, + args: ["--headless=new", "--no-sandbox", "--disable-gpu-sandbox", "--enable-unsafe-webgpu", "--enable-features=Vulkan", "--use-angle=vulkan", "--use-vulkan"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, deviceScaleFactor: 1 }); + const errors = []; + page.on("console", (message) => { + if (message.type() === "error") { errors.push(message.text()); console.log(`browser error: ${message.text()}`); } + else if (message.text().startsWith("Flow Lenia M3 gates:")) console.log(message.text()); + }); + page.on("pageerror", (error) => { errors.push(`pageerror: ${error.message}`); console.log(`pageerror: ${error.message}`); }); + await page.goto(url, { waitUntil: "load", timeout: 30_000 }); + console.log("M3 gate page loaded"); + const available = await page.evaluate(async () => Boolean(navigator.gpu && await navigator.gpu.requestAdapter())); + if (!available) { + console.log("WEBGPU_UNAVAILABLE: browser adapter absent"); + exitCode = 42; + } else { + await page.waitForFunction(() => window.__bitPhysicsReady === true, undefined, { timeout }); + const report = await page.evaluate(async () => { + if (!window.__flowLeniaM3) throw new Error("Flow Lenia M3 test hook missing"); + return window.__flowLeniaM3.runGates(); + }); + await page.evaluate(async () => { + for (const id of window.__flowLeniaM3.listExperiments()) await window.__flowLeniaM3.loadExperiment(id); + await window.__flowLeniaM3.loadExperiment("pressure-ablation"); + }); + await page.screenshot({ path: screenshot, fullPage: true }); + const unexpected = errors.filter((message) => !/WebGPU unavailable|requestAdapter/i.test(message)); + if (unexpected.length > 0) throw new Error(`browser errors: ${unexpected.join(" | ")}`); + await mkdir(dirname(output), { recursive: true }); + await writeFile(output, JSON.stringify(report, null, 2) + "\n"); + console.log(`M3 browser gates: ${output}`); + console.log(`cards: ${report.cards.map((item) => `${item.id}=${item.pass}`).join(", ")}`); + console.log(`comparisons: ${report.cards.filter((item) => item.comparison).map((item) => `${item.id}:Δpeak=${item.comparison.peakDensityDelta}`).join(", ")}`); + console.log(`events: exact=${report.scheduledEvents.byteExactSameAdapter}, ledger=${report.scheduledEvents.ledgerPass}`); + console.log(`closed impulses: ${report.closedImpulses.pass}; render integrity: ${report.renderIntegrity.pass}`); + console.log(`synchronized solver pair: ${report.memory.synchronizedPairMib} MiB`); + console.log(`screenshot: ${screenshot}`); + console.log(`verdict: ${report.pass ? "PASS" : "FAIL"}`); + exitCode = report.pass ? 0 : 1; + } + await browser.close(); +} catch (error) { + console.error(`M3 browser gates failed: ${String(error).split("\n")[0]}`); +} finally { + server.close(); +} +process.exit(exitCode); diff --git a/packages/flow-lenia/web/scripts/run-browser-m4-gates.mjs b/packages/flow-lenia/web/scripts/run-browser-m4-gates.mjs new file mode 100644 index 00000000..3e5cdad2 --- /dev/null +++ b/packages/flow-lenia/web/scripts/run-browser-m4-gates.mjs @@ -0,0 +1,92 @@ +import { createServer } from "node:http"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, extname, join, normalize, resolve } from "node:path"; + +const playwright = await import(process.env.PLAYWRIGHT_MODULE ?? "playwright"); +const chromium = playwright.chromium ?? playwright.default.chromium; +const webRoot = resolve(import.meta.dirname, ".."); +const distDir = resolve(process.argv[2] ?? join(webRoot, "dist")); +const output = resolve(process.argv[3] ?? join(webRoot, "artifacts", "m4-browser-gates.json")); +const screenshot = resolve(process.argv[4] ?? "/tmp/flow-lenia-m4-ui.png"); +const timeout = Number(process.env.FLOW_LENIA_M4_TIMEOUT_MS ?? "900000"); +const mime = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".map": "application/json" }; + +const server = createServer(async (request, response) => { + try { + let path = decodeURIComponent((request.url ?? "/").split("?")[0]); + if (path === "/favicon.ico") { response.writeHead(204); response.end(); return; } + if (path === "/") path = "/index.html"; + const file = join(distDir, normalize(path).replace(/^(\.\.[/\\])+/, "")); + const body = await readFile(file); + response.writeHead(200, { "content-type": mime[extname(file)] ?? "application/octet-stream" }); + response.end(body); + } catch { response.writeHead(404); response.end("not found"); } +}); + +await new Promise((resolveListen) => server.listen(0, resolveListen)); +const address = server.address(); +if (!address || typeof address === "string") throw new Error("gate server did not bind TCP"); +const url = `http://localhost:${address.port}/?ecosystem=1&grid=128&gate=1`; +let exitCode = 1; +try { + const browser = await chromium.launch({ + executablePath: process.env.CHROME_BIN, + headless: false, + chromiumSandbox: false, + args: ["--headless=new", "--no-sandbox", "--disable-gpu-sandbox", "--enable-unsafe-webgpu", "--enable-features=Vulkan", "--use-angle=vulkan", "--use-vulkan"], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, deviceScaleFactor: 1 }); + const errors = []; + page.on("console", (message) => { + if (message.type() === "error") { errors.push(message.text()); console.log(`browser error: ${message.text()}`); } + else if (message.text().startsWith("Flow Lenia M4 gates:")) console.log(message.text()); + }); + page.on("pageerror", (error) => { errors.push(`pageerror: ${error.message}`); console.log(`pageerror: ${error.message}`); }); + await page.goto(url, { waitUntil: "load", timeout: 30_000 }); + console.log("M4 gate page loaded"); + const available = await page.evaluate(async () => Boolean(navigator.gpu && await navigator.gpu.requestAdapter())); + if (!available) { console.log("WEBGPU_UNAVAILABLE: browser adapter absent"); exitCode = 42; } + else { + await page.waitForFunction(() => window.__bitPhysicsReady === true, undefined, { timeout }); + const report = await page.evaluate(async () => { + if (!window.__flowLeniaM4) throw new Error("Flow Lenia M4 test hook missing"); + return window.__flowLeniaM4.runGates(); + }); + await page.evaluate(async () => { await window.__flowLeniaM4.loadExperiment("identity-dilution"); window.__flowLeniaM4.step(48); }); + await page.waitForTimeout(1200); + await page.screenshot({ path: screenshot, fullPage: true }); + await page.evaluate(() => { + window.__bitPhysicsCaptureReady = false; + const capture = document.querySelector('[data-bp="capture"]'); + if (!(capture instanceof HTMLButtonElement)) throw new Error("ecosystem capture button missing"); + capture.click(); + }); + await page.waitForFunction(() => window.__bitPhysicsCaptureReady === true, undefined, { timeout }); + const captureContract = await page.evaluate(() => { + const bundle = window.__bitPhysicsCapture; + const step = bundle?.steps?.[0]; + const fields = step?.state ? Object.keys(step.state) : []; + const pass = bundle?.manifest?.config?.params?.mode === "ecosystem" && step?.step === 32 && fields.includes("mass") && fields.includes("genome_h") && fields.includes("genome_q") && fields.includes("identity_u32_values"); + return { schemaVersion: bundle?.manifest?.schema_version ?? "missing", step: step?.step ?? -1, fields, pass: Boolean(pass) }; + }); + report.captureContract = captureContract; + report.pass = report.pass && captureContract.pass; + const unexpected = errors.filter((message) => !/WebGPU unavailable|requestAdapter/i.test(message)); + if (unexpected.length > 0) throw new Error(`browser errors: ${unexpected.join(" | ")}`); + await mkdir(dirname(output), { recursive: true }); + await writeFile(output, JSON.stringify(report, null, 2) + "\n"); + console.log(`M4 browser gates: ${output}`); + console.log(`numerical: ${report.numericalRules.map((item) => `${item.rule}=${item.pass}`).join(", ")}`); + console.log(`determinism: ${report.determinismRules.map((item) => `${item.rule}=${item.byteExactSameAdapter}`).join(", ")}`); + console.log(`mutation: ${report.mutation.pass}; ecosystems: ${report.ecosystems.map((item) => `${item.id}=${item.pass}`).join(", ")}`); + console.log(`identity dilution mixed fractions: ${report.identityDilution.mixedFractions.join(", ")}`); + console.log(`256²: ${report.performance.p95Ms} ms p95; ${report.architecture.memoryMib256} MiB`); + console.log(`ecosystem capture: ${report.captureContract.pass} (${report.captureContract.fields.join(", ")})`); + console.log(`screenshot: ${screenshot}`); + console.log(`verdict: ${report.pass ? "PASS" : "FAIL"}`); + exitCode = report.pass ? 0 : 1; + } + await browser.close(); +} catch (error) { console.error(`M4 browser gates failed: ${String(error).split("\n")[0]}`); } +finally { server.close(); } +process.exit(exitCode); diff --git a/packages/flow-lenia/web/scripts/run-browser-m6-gates.mjs b/packages/flow-lenia/web/scripts/run-browser-m6-gates.mjs new file mode 100644 index 00000000..4842ac24 --- /dev/null +++ b/packages/flow-lenia/web/scripts/run-browser-m6-gates.mjs @@ -0,0 +1,59 @@ +import { createServer } from "node:http"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, extname, join, normalize, resolve } from "node:path"; + +const playwright = await import(process.env.PLAYWRIGHT_MODULE ?? "playwright"); +const chromium = playwright.chromium ?? playwright.default.chromium; +const webRoot = resolve(import.meta.dirname, ".."); +const distDir = resolve(process.argv[2] ?? join(webRoot, "dist")); +const output = resolve(process.argv[3] ?? join(webRoot, "artifacts", "m6-browser-release-gates.json")); +const screenshot = resolve(process.argv[4] ?? "/tmp/flow-lenia-m6-arena-ui.png"); +const canonicalOutput = resolve(process.argv[5] ?? join(webRoot, "artifacts", "m6-canonical-capture-index.json")); +const timeout = Number(process.env.FLOW_LENIA_M6_TIMEOUT_MS ?? "1200000"); +const mime = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".map": "application/json", ".woff2": "font/woff2" }; + +const server = createServer(async (request, response) => { + try { let path = decodeURIComponent((request.url ?? "/").split("?")[0]); if (path === "/favicon.ico") { response.writeHead(204); response.end(); return; } if (path === "/") path = "/index.html"; const file = join(distDir, normalize(path).replace(/^(\.\.[/\\])+/, "")); const body = await readFile(file); response.writeHead(200, { "content-type": mime[extname(file)] ?? "application/octet-stream" }); response.end(body); } + catch { response.writeHead(404); response.end("not found"); } +}); + +await new Promise((resolveListen) => server.listen(0, resolveListen)); const address = server.address(); if (!address || typeof address === "string") throw new Error("M6 gate server did not bind TCP"); const base = `http://localhost:${address.port}/`; +let exitCode = 1; +try { + const browser = await chromium.launch({ executablePath: process.env.CHROME_BIN, headless: false, chromiumSandbox: false, args: ["--headless=new", "--no-sandbox", "--disable-gpu-sandbox", "--enable-unsafe-webgpu", "--enable-features=Vulkan", "--use-angle=vulkan", "--use-vulkan"] }); + const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, deviceScaleFactor: 1, reducedMotion: "reduce" }); const page = await context.newPage(); const errors = []; + page.on("console", (message) => { if (message.type() === "error") { errors.push(message.text()); console.log(`browser error: ${message.text()}`); } else if (message.text().startsWith("Flow Lenia M6")) console.log(message.text()); }); page.on("pageerror", (error) => { errors.push(`pageerror: ${error.message}`); console.log(`pageerror: ${error.message}`); }); + await page.goto(`${base}?arena=1&grid=128&gate=1&preset=corridor-divergence`, { waitUntil: "load", timeout: 30_000 }); console.log("M6 Arena gate page loaded"); + const available = await page.evaluate(async () => Boolean(navigator.gpu && await navigator.gpu.requestAdapter())); + if (!available) { console.log("WEBGPU_UNAVAILABLE: browser adapter absent"); exitCode = 42; } + else { + await page.waitForFunction(() => window.__bitPhysicsReady === true, undefined, { timeout }); + const report = await page.evaluate(async () => { if (!window.__flowLeniaM6) throw new Error("Flow Lenia M6 hook missing"); return window.__flowLeniaM6.runGates(); }); + await page.evaluate(async () => { await window.__flowLeniaM6.loadExperiment("maze-navigation"); window.__flowLeniaM6.step(96); }); await page.waitForTimeout(1200); await page.screenshot({ path: screenshot, fullPage: true }); + await page.evaluate(() => { window.__bitPhysicsCaptureReady = false; const capture = document.querySelector('[data-bp="capture"]'); if (!(capture instanceof HTMLButtonElement)) throw new Error("Arena capture button missing"); capture.click(); }); await page.waitForFunction(() => window.__bitPhysicsCaptureReady === true, undefined, { timeout }); + const captureContract = await page.evaluate(async () => { + const bundle = window.__bitPhysicsCapture; const step = bundle?.steps?.[0]; const fields = step?.state ? Object.keys(step.state) : []; const params = bundle?.manifest?.config?.params; const encoded = new TextEncoder().encode(JSON.stringify(step?.state ?? {})); const digest = await crypto.subtle.digest("SHA-256", encoded); const stateSha256 = Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join(""); const pass = params?.mode === "arena" && step?.step === 72 && fields.includes("mass") && fields.includes("genome_h") && fields.includes("genome_q") && fields.includes("identity_u32_values") && fields.includes("environment_affinity_values") && fields.includes("environment_region_u32_values") && params?.environment_schema === "flow-lenia-arena-environment-v1" && /^[0-9a-f]{64}$/.test(params?.shader_sha256 ?? ""); + return { schemaVersion: bundle?.manifest?.schema_version ?? "missing", step: step?.step ?? -1, fields, shaderHash: params?.shader_sha256 ?? "", environmentSchema: params?.environment_schema ?? "", stateSha256, diagnostics: step?.diagnostics ?? {}, pass: Boolean(pass) }; + }); + report.captureContract = { schemaVersion: captureContract.schemaVersion, step: captureContract.step, fields: captureContract.fields, shaderHash: captureContract.shaderHash, environmentSchema: captureContract.environmentSchema, pass: captureContract.pass }; + const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1, isMobile: true, hasTouch: true, reducedMotion: "reduce" }); const mobilePage = await mobile.newPage(); await mobilePage.goto(`${base}?arena=1&grid=128&gate=1&preset=storm-recovery`, { waitUntil: "load", timeout: 30_000 }); await mobilePage.waitForFunction(() => window.__bitPhysicsReady === true, undefined, { timeout }); const adaptiveSmoke = await mobilePage.evaluate(() => { const stage = document.querySelector("#organism-lab")?.getBoundingClientRect(); const panelMounted = Boolean(document.querySelector("[data-bp-panel]")); const ready = window.__bitPhysicsReady === true && Boolean(window.__flowLeniaM6); const pass = ready && panelMounted && window.__flowLeniaM6.grid === 128 && Boolean(stage && stage.width <= innerWidth + 1 && stage.height <= innerHeight + 1); return { grid: window.__flowLeniaM6?.grid ?? -1, viewport: [innerWidth, innerHeight], ready, panelMounted, pass }; }); await mobile.close(); report.adaptiveSmoke = adaptiveSmoke; + report.browserMatrix = [ + { browser: "Chromium", surface: "desktop reference + 256² measured scratch", webgpu: true, result: report.performance.pass && report.architecture.under128Mib ? "PASS" : "FAIL" }, + { browser: "Chromium", surface: "390×844 touch + reduced motion + 128² adaptive", webgpu: true, result: adaptiveSmoke.pass ? "PASS" : "FAIL" }, + { browser: "Firefox/Safari", surface: "compatibility", webgpu: false, result: "NOT CLAIMED — support varies" }, + ]; + report.pass = report.pass && captureContract.pass && adaptiveSmoke.pass; + const unexpected = errors.filter((message) => !/WebGPU unavailable|requestAdapter/i.test(message)); if (unexpected.length > 0) throw new Error(`browser errors: ${unexpected.join(" | ")}`); + const [m2, m4] = await Promise.all([readFile(join(webRoot, "artifacts", "m2-browser-gates.json"), "utf8").then(JSON.parse), readFile(join(webRoot, "artifacts", "m4-browser-gates.json"), "utf8").then(JSON.parse)]); + const canonical = { schemaVersion: "flow-lenia-m6-canonical-index-v1", generatedUtc: new Date().toISOString(), modelVariant: "flow-lenia-ecosystem-v1", captures: [ + { mode: "organism", reference: "m2-browser-gates.json structural replay", grid: m2.structural.grid, step: m2.structural.steps, stateSha256: m2.structural.firstHash, byteExactSameAdapter: m2.structural.byteExactSameAdapter, massRelativeDrift: m2.structural.metrics.relativeMassDrift }, + { mode: "ecosystem", reference: "m4-browser-gates.json whole-rule replay", grid: 128, step: m4.determinismRules.find((item) => item.rule === "whole").steps, stateSha256: m4.determinismRules.find((item) => item.rule === "whole").hashA, byteExactSameAdapter: m4.determinismRules.find((item) => item.rule === "whole").byteExactSameAdapter, massRelativeDrift: m4.determinismRules.find((item) => item.rule === "whole").metrics.relativeMassDrift }, + { mode: "arena", reference: "standard browser capture", grid: 128, step: captureContract.step, stateSha256: captureContract.stateSha256, shaderSha256: captureContract.shaderHash, environmentSchema: captureContract.environmentSchema, byteExactSameAdapter: report.arenas.every((item) => item.byteExactSameAdapter), massRelativeDrift: captureContract.diagnostics.mass_relative_drift }, + ], pass: m2.pass && m4.pass && captureContract.pass && report.arenas.every((item) => item.byteExactSameAdapter) }; + await mkdir(dirname(output), { recursive: true }); await writeFile(output, JSON.stringify(report, null, 2) + "\n"); await writeFile(canonicalOutput, JSON.stringify(canonical, null, 2) + "\n"); + console.log(`M6 browser gates: ${output}`); console.log(`Arena cards: ${report.arenas.map((item) => `${item.id}=${item.pass}`).join(", ")}`); console.log(`round trip: restored=${report.roundTrip.restoredByteExact}, continuation=${report.roundTrip.continuationByteExact}`); console.log(`256² Arena: ${report.performance.p95Ms} ms p95; ${report.architecture.memoryMib256} MiB`); console.log(`capture: ${captureContract.pass}; adaptive: ${adaptiveSmoke.pass}`); console.log(`canonical index: ${canonicalOutput}`); console.log(`screenshot: ${screenshot}`); console.log(`verdict: ${report.pass ? "PASS" : "FAIL"}`); exitCode = report.pass ? 0 : 1; + } + await context.close(); await browser.close(); +} catch (error) { console.error(`M6 browser gates failed: ${String(error).split("\n")[0]}`); } +finally { server.close(); } +process.exit(exitCode); diff --git a/packages/flow-lenia/web/src/app.ts b/packages/flow-lenia/web/src/app.ts new file mode 100644 index 00000000..fabe9b05 --- /dev/null +++ b/packages/flow-lenia/web/src/app.ts @@ -0,0 +1,745 @@ +import "../../../../common/common-web/src/theme.css"; +import { exposeCapture, field, isCapturing, resetCapture } from "../../../../common/common-web/src/capture-export.js"; +import type { CaptureBundle } from "../../../../common/common-web/src/capture-export.js"; +import { createSettingsPanel } from "../../../../common/common-web/src/panel-shell.js"; +import type { PanelShell } from "../../../../common/common-web/src/panel-shell.js"; +import { EXPERIMENT_CARDS, makeExperimentMass, scheduleCardTimeline } from "./experiments/cards.js"; +import type { ExperimentCard, ExperimentModelControls } from "./experiments/cards.js"; +import { MODEL_VARIANT, organismConfig } from "./model/config.js"; +import type { BrushEventKind } from "./model/events.js"; +import { makeSeededOrganismMass } from "./model/seed.js"; +import { FlowLeniaOrganismSolver } from "./model/solver.js"; +import type { SolverMetrics } from "./model/solver.js"; +import { runM2Gates } from "./prove.js"; +import type { M2GateReport } from "./prove.js"; +import { runM3Gates } from "./prove-m3.js"; +import type { M3GateReport } from "./prove-m3.js"; +import { ScientificInspector } from "./render/overlays.js"; +import { OrganismRenderer } from "./render/renderer.js"; +import type { RenderMode } from "./render/renderer.js"; +import "./style.css"; + +type Tool = "pipette" | "add" | "erase" | "stir" | "inspect"; + +interface M2Hook { + runGates: (steps?: number) => Promise; + reset: (seed?: number) => void; + metrics: () => Promise; + setRenderMode: (mode: RenderMode) => void; + latestGateReport: M2GateReport | null; + grid: number; + allocatedBytes: number; + dispatchesPerStep: number; +} + +interface M3Hook { + runGates: () => Promise; + loadExperiment: (id: string) => Promise; + queueEvent: (kind: BrushEventKind, row: number, column: number) => void; + listExperiments: () => readonly string[]; + latestGateReport: M3GateReport | null; +} + +const boot = document.getElementById("boot") as HTMLDivElement; +const stage = document.getElementById("organism-lab") as HTMLElement; +const canvas = document.getElementById("view") as HTMLCanvasElement; +const legend = document.getElementById("legend") as HTMLDivElement; +const ledgerHud = document.getElementById("ledger") as HTMLDivElement; +const perfHud = document.getElementById("perf") as HTMLDivElement; +const compareLabel = document.getElementById("compare-label") as HTMLDivElement; +const experimentNote = document.getElementById("experiment-note") as HTMLDivElement; +const inspectorHost = document.getElementById("inspector-host") as HTMLDivElement; +const touchTools = document.getElementById("touch-tools") as HTMLDivElement; +const m0Probe = document.getElementById("m0-probe") as HTMLElement; +m0Probe.hidden = true; + +const RENDER_MODES: readonly RenderMode[] = ["density", "channels", "affinity", "flow", "pressure", "flux"]; +const LEGEND_TEXT: Record = { + density: "organism glow · density ρ fixed scale 0 → 2", + channels: "mass channels · C₀/C₁/C₂ direct RGB", + affinity: "affinity V₀ · diverging scale −1.5 → +1.5", + flow: "displacement dt·F₀ · direction hue, magnitude 0 → 0.8", + pressure: "pressure gate α₀ 0 → 1 · density-gradient intensity", + flux: "mass × displacement · cyan 0 → 1; red = clamp", +}; + +function setBoot(message: string): void { + boot.textContent = message; + boot.style.display = message ? "block" : "none"; +} + +function captureBundle( + state: Float32Array, + metrics: SolverMetrics, + n: number, + seed: number, + card: ExperimentCard, + started: number, +): CaptureBundle { + return { + manifest: { + schema_version: "1.0.0", + sim: { name: "flow-lenia", category: "continuous-ca", variant: MODEL_VARIANT }, + stack: { name: "webgpu-f32", version: "0.3.0", build_id: "flow-lenia-visual-lab-m3" }, + config: { + tier: n === 256 ? "reference" : "test", + dims: [n, n], + dtype: "f32", + seed, + params: { + channels: 3, + kernels: 9, + dt: 0.2, + dd: 5, + sigma: card.model.sigma, + theta_A: card.model.pressure ? 2 : "disabled-ablation", + density_exponent: 2, + experiment: card.id, + scheduled_events: card.timeline.length, + ledger_added: metrics.ledgerAdded, + ledger_removed: metrics.ledgerRemoved, + }, + }, + run: { step_count: metrics.step, capture_interval: metrics.step, wall_clock_seconds: (performance.now() - started) / 1000, start_utc: new Date().toISOString() }, + payload: { format: "hdf5", path: `flow-lenia-${card.id}-${n}sq-seed${seed}-step${metrics.step}.h5`, checksum: `sha256:${"0".repeat(64)}` }, + determinism: { claimed: "bit-exact-same-hw", atomic_ops: metrics.ledgerAdded > 0 || metrics.ledgerRemoved > 0, subgroup_ops: false }, + }, + steps: [{ + step: metrics.step, + state: { mass: field(state, [3, n, n], "f32") }, + diagnostics: { + total_mass: metrics.totalMass, + expected_mass: metrics.expectedMass, + open_added: metrics.ledgerAdded, + open_removed: metrics.ledgerRemoved, + mass_ledger_error: metrics.ledgerError, + mass_relative_drift: metrics.relativeMassDrift, + min_density: metrics.minDensity, + max_density: metrics.maxDensity, + occupied_fraction: metrics.occupiedFraction, + max_flow: metrics.maxFlow, + max_displacement: metrics.maxDisplacement, + clamp_fraction: metrics.clampFraction, + non_finite: metrics.nonFinite, + negative: metrics.negative, + }, + }], + }; +} + +function makeButton(label: string, pressed = false): HTMLButtonElement { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = label; + button.setAttribute("aria-pressed", String(pressed)); + return button; +} + +function labeledRange(labelText: string, minimum: number, maximum: number, step: number, value: number): { row: HTMLLabelElement; input: HTMLInputElement; output: HTMLOutputElement } { + const row = document.createElement("label"); + row.className = "fl-range"; + const label = document.createElement("span"); + label.textContent = labelText; + const input = document.createElement("input"); + input.type = "range"; + input.min = String(minimum); + input.max = String(maximum); + input.step = String(step); + input.value = String(value); + const output = document.createElement("output"); + output.textContent = String(value); + row.append(label, input, output); + return { row, input, output }; +} + +function diagnosticRows(metrics: SolverMetrics, solver: FlowLeniaOrganismSolver, frameMs: number, comparison?: SolverMetrics): Array<{ label: string; value: string }> { + const rows = [ + { label: "step", value: metrics.step.toLocaleString() }, + { label: "mass / expected", value: `${metrics.totalMass.toFixed(4)} / ${metrics.expectedMass.toFixed(4)}` }, + { label: "open ledger + / −", value: `${metrics.ledgerAdded.toFixed(3)} / ${metrics.ledgerRemoved.toFixed(3)}` }, + { label: "ledger error / relative", value: `${metrics.ledgerError.toExponential(2)} / ${metrics.relativeMassDrift.toExponential(2)}` }, + { label: "channel mass", value: metrics.channelMass.map((value) => value.toFixed(2)).join(" · ") }, + { label: "density min / max", value: `${metrics.minDensity.toFixed(4)} / ${metrics.maxDensity.toFixed(4)}` }, + { label: "max flow / displacement", value: `${metrics.maxFlow.toFixed(3)} / ${metrics.maxDisplacement.toFixed(3)}` }, + { label: "clamp / occupied", value: `${(100 * metrics.clampFraction).toFixed(3)}% / ${(100 * metrics.occupiedFraction).toFixed(1)}%` }, + { label: "finite / non-negative", value: metrics.nonFinite === 0 && metrics.negative === 0 ? "PASS" : `FAIL ${metrics.nonFinite}/${metrics.negative}` }, + { label: "frame CPU / GPU allocation", value: `${frameMs.toFixed(2)} ms / ${(solver.allocatedBytes / 2 ** 20).toFixed(2)} MiB` }, + { label: "core dispatches / step", value: String(solver.dispatchesPerStep) }, + ]; + if (comparison) rows.splice(7, 0, { label: "comparison peak / occupied", value: `${comparison.maxDensity.toFixed(3)} / ${(100 * comparison.occupiedFraction).toFixed(1)}%` }); + return rows; +} + +async function start(): Promise { + setBoot("requesting WebGPU adapter…"); + if (!navigator.gpu) throw new Error("WebGPU unavailable in this browser"); + const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); + if (!adapter) throw new Error("WebGPU adapter unavailable"); + const device = await adapter.requestDevice(); + const adapterInfo = adapter.info; + const gateEnvironment: M2GateReport["environment"] = { + userAgent: navigator.userAgent, + adapter: { + vendor: adapterInfo.vendor ?? "unknown", + architecture: adapterInfo.architecture ?? "unknown", + device: adapterInfo.device ?? "unknown", + description: adapterInfo.description ?? "unknown", + }, + }; + device.addEventListener("uncapturederror", (event) => { + const message = (event as GPUUncapturedErrorEvent).error.message; + console.error(`Flow Lenia M3 WebGPU error: ${message}`); + setBoot(`GPU error: ${message}`); + }); + const context = canvas.getContext("webgpu") as GPUCanvasContext | null; + if (!context) throw new Error("WebGPU canvas context unavailable"); + const format = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ device, format, alphaMode: "opaque" }); + + const query = new URLSearchParams(location.search); + const deviceMemory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 8; + const adaptive = matchMedia("(max-width: 720px)").matches || deviceMemory < 4; + const requested = Number.parseInt(query.get("grid") ?? (adaptive ? "128" : "256"), 10); + const n = requested === 128 ? 128 : 256; + const config = organismConfig(n, 42); + setBoot(`compiling the ${n}² organism solver…`); + const solver = await FlowLeniaOrganismSolver.create(device, config); + const renderer = new OrganismRenderer(device, context, canvas, format, solver); + const inspector = new ScientificInspector(inspectorHost); + let comparisonSolver: FlowLeniaOrganismSolver | null = null; + let currentCard = EXPERIMENT_CARDS[0] as ExperimentCard; + let seed = 42; + let paused = query.get("gate") === "1"; + let speed = 1; + let timeAccumulator = 0; + let selectedTool: Tool = "pipette"; + let brushRadius = Math.max(4, n / 22); + let brushStrength = 1; + let selectedChannel = 3; + let inspectPosition: readonly [number, number] = [n / 2, n / 2]; + let telemetryPending = false; + let inspectionPending = false; + let inspectionToken = 0; + let lastTelemetry = 0; + let lastFrame = performance.now(); + let frameMs = 0; + let panel!: PanelShell; + let loadingCard = 0; + + const applyModel = (target: FlowLeniaOrganismSolver, controls: ExperimentModelControls): void => { + target.setPressureEnabled(controls.pressure); + target.setSquareHalfWidth(controls.sigma); + }; + + const resetTarget = (target: FlowLeniaOrganismSolver, card: ExperimentCard, nextSeed: number, controls: ExperimentModelControls): void => { + target.reset(makeExperimentMass(n, card, nextSeed)); + applyModel(target, controls); + for (const event of scheduleCardTimeline(card, n)) target.queueEvent(event); + }; + + const resetActive = (nextSeed = seed): void => { + seed = nextSeed >>> 0; + resetTarget(solver, currentCard, seed, currentCard.model); + if (currentCard.comparison && comparisonSolver) resetTarget(comparisonSolver, currentCard, seed, currentCard.comparison.model); + renderer.clearTrails(); + }; + + const setCameraFromCard = (card: ExperimentCard): void => { + renderer.setCamera({ centerRow: card.camera.center[0] * n, centerColumn: card.camera.center[1] * n, zoom: card.camera.zoom }); + }; + + const stepBoth = (count = 1): void => { + solver.step(count); + if (renderer.hasComparison() && comparisonSolver) comparisonSolver.step(count); + }; + + const updateInspection = async (row: number, column: number): Promise => { + inspectPosition = [row, column]; + renderer.setInspection(row, column, brushRadius); + const token = ++inspectionToken; + if (inspectionPending) return; + inspectionPending = true; + try { + const reading = await solver.inspect(row, column); + if (token === inspectionToken) inspector.update(reading); + } finally { + inspectionPending = false; + if (token !== inspectionToken) void updateInspection(inspectPosition[0], inspectPosition[1]); + } + }; + + const loadExperiment = async (card: ExperimentCard): Promise => { + const generation = ++loadingCard; + panel?.setStatus(`loading ${card.title}…`); + currentCard = card; + if (card.comparison && !comparisonSolver) { + setBoot("compiling synchronized comparison solver…"); + comparisonSolver = await FlowLeniaOrganismSolver.create(device, config); + if (generation !== loadingCard) return; + } + resetActive(seed); + renderer.setComparisonSolver(card.comparison ? comparisonSolver : null); + stepBoth(); + setCameraFromCard(card); + setRenderMode(card.view); + compareLabel.hidden = !card.comparison; + compareLabel.textContent = card.comparison ? `LEFT · ${card.model.pressure ? "reference pressure" : `σ ${card.model.sigma}`} | RIGHT · ${card.comparison.label}` : ""; + experimentNote.innerHTML = `${card.title}${card.description}${card.observation}`; + document.querySelectorAll("[data-experiment]").forEach((button) => button.setAttribute("aria-pressed", String(button.dataset.experiment === card.id))); + panel?.setNarration(`${card.title} · ${card.observation}`, "event"); + panel?.setStatus(`${card.title} reset exactly at seed ${seed}`); + setBoot(""); + }; + + resetActive(); + solver.step(); + + const updateMetrics = async (): Promise => { + const metrics = await solver.metrics(); + const comparison = renderer.hasComparison() && comparisonSolver ? await comparisonSolver.metrics() : undefined; + panel.setDiagnostics(diagnosticRows(metrics, solver, frameMs, comparison)); + ledgerHud.textContent = `ledger ${metrics.totalMass.toFixed(2)} = ${metrics.expectedMass.toFixed(2)} · ε ${metrics.ledgerError.toExponential(1)}`; + perfHud.textContent = `${frameMs.toFixed(1)} ms frame · ${speed.toFixed(2)}× · ${n}²${comparison ? " × 2" : ""}`; + const pass = metrics.relativeMassDrift <= 8e-5 && metrics.nonFinite === 0 && metrics.negative === 0; + panel.setVerdict({ gate: "open-tool ledger + finite/non-negative state", verdict: pass ? "PASS" : "FAIL", pass }); + return metrics; + }; + + const makeCapture = async (): Promise => { + panel.setCaptureEnabled(false); + panel.setStatus(`resetting ${currentCard.title} and running 32 canonical steps…`); + resetCapture(); + const started = performance.now(); + resetActive(panel.getState().seed); + stepBoth(32); + await device.queue.onSubmittedWorkDone(); + const [readback, metrics] = await Promise.all([solver.readback(), solver.metrics()]); + exposeCapture(captureBundle(readback.mass, metrics, n, seed, currentCard, started), { download: false }); + panel.setStatus(`capture ready — ${currentCard.title}, seed ${seed}, step ${metrics.step}, ledger ε ${metrics.ledgerError.toExponential(2)}`); + panel.setCaptureEnabled(true); + }; + + panel = createSettingsPanel("Flow Lenia · Visual Lab", { + caption: "Form and perturb conservative Flow Lenia matter, then inspect how affinity, density pressure, and finite-square transport create motion.", + initial: { tier: n === 256 ? "reference" : "test", seed }, + tiers: n === 256 ? ["reference"] : ["test"], + onCapture: makeCapture, + onChange: (state) => { seed = state.seed >>> 0; void loadExperiment(currentCard); }, + modes: { initial: paused ? "study" : "play", onMode: (mode) => { paused = mode === "study"; } }, + study: { + diagnostics: [ + { label: "grid", value: `${n}²` }, + { label: "model", value: MODEL_VARIANT }, + { label: "kernel normalization", value: solver.spatialKernelSums.every((sum) => Math.abs(sum - 1) < 2e-6) ? "PASS" : "FAIL" }, + ], + honesty: { + faithful: "three-channel/nine-kernel spectral perception, bell affinity, Sobel density pressure, component-clamped flow, and exact finite-square destination gather", + simplified: "this organism specialization keeps global parameters and allocates no genomes. Localized inheritance and lineage live in Ecosystem Lab; editable soft-affinity environments live in Arena Lab", + measured: "open brushes use a GPU atomic fixed-point ledger; metrics and inspection read back only at low cadence or on explicit sampling", + }, + verdict: { gate: "M3 event, ledger, card, and render-integrity gates", verdict: "RUN TO VERIFY", pass: false }, + links: [ + { label: "model specification", href: "../../../../docs/sim-specs/continuous-ca/lenia/spec-web-ecosystem.md" }, + { label: "implementation ledger", href: "../../../../docs/sim-specs/continuous-ca/lenia/implementation-plan.md" }, + ], + }, + }); + + const experimentGroup = panel.addGroup("experiments", { hint: "Six authored, exact-seed organism and ablation cards. Compare cards run synchronized solvers." }); + const experimentGrid = document.createElement("div"); + experimentGrid.className = "fl-experiment-grid"; + for (const card of EXPERIMENT_CARDS) { + const button = makeButton(card.title, card === currentCard); + button.className = "fl-experiment-card"; + button.dataset.experiment = card.id; + button.title = `${card.description} Expected observation: ${card.observation}`; + const small = document.createElement("small"); + small.textContent = card.short; + button.appendChild(small); + button.addEventListener("click", () => { void loadExperiment(card); }); + experimentGrid.appendChild(button); + } + experimentGroup.appendChild(experimentGrid); + + const timeGroup = panel.addGroup("time + camera", { hint: "Fixed scientific steps; trails and camera are presentation-only." }); + const timeControls = document.createElement("div"); + timeControls.className = "fl-controls fl-controls-3"; + const pauseButton = makeButton(paused ? "play" : "pause"); + pauseButton.setAttribute("aria-keyshortcuts", "Space"); + const stepButton = makeButton("step"); + stepButton.setAttribute("aria-keyshortcuts", "."); + const fitButton = makeButton("fit mass"); + fitButton.setAttribute("aria-keyshortcuts", "F"); + timeControls.append(pauseButton, stepButton, fitButton); + const speedRow = document.createElement("label"); + speedRow.className = "fl-select-row"; + speedRow.textContent = "speed"; + const speedSelect = document.createElement("select"); + for (const value of [0.25, 0.5, 1, 2, 4]) { + const option = document.createElement("option"); + option.value = String(value); + option.textContent = `${value}×`; + option.selected = value === speed; + speedSelect.appendChild(option); + } + speedSelect.addEventListener("change", () => { speed = Number(speedSelect.value); }); + speedRow.appendChild(speedSelect); + const cameraControls = document.createElement("div"); + cameraControls.className = "fl-controls fl-controls-3"; + const zoomOut = makeButton("− zoom"); + const resetCamera = makeButton("reset view"); + const zoomIn = makeButton("+ zoom"); + cameraControls.append(zoomOut, resetCamera, zoomIn); + timeGroup.append(timeControls, speedRow, cameraControls); + + const toolGroup = panel.addGroup("matter tools", { hint: "Primary applies; secondary reverses; wheel changes radius; Shift doubles strength; Alt samples." }); + const toolControls = document.createElement("div"); + toolControls.className = "fl-controls fl-tool-controls"; + const toolButtons = new Map(); + (["pipette", "add", "erase", "stir", "inspect"] as const).forEach((tool, index) => { + const button = makeButton(tool, tool === selectedTool); + button.setAttribute("aria-keyshortcuts", String(index + 1)); + button.addEventListener("click", () => selectTool(tool)); + toolButtons.set(tool, button); + toolControls.appendChild(button); + }); + const radiusControl = labeledRange("radius", 2, Math.floor(n / 3), 1, Math.round(brushRadius)); + const strengthControl = labeledRange("strength", 0.25, 2, 0.05, brushStrength); + const channelRow = document.createElement("div"); + channelRow.className = "fl-channel-row"; + const channelButtons = ["C₀", "C₁", "C₂", "mix"].map((label, channel) => { + const button = makeButton(label, channel === selectedChannel); + button.addEventListener("click", () => { + selectedChannel = channel; + channelButtons.forEach((candidate, candidateChannel) => candidate.setAttribute("aria-pressed", String(candidateChannel === channel))); + renderer.setChannel(Math.min(channel, 2)); + }); + channelRow.appendChild(button); + return button; + }); + toolGroup.append(toolControls, radiusControl.row, strengthControl.row, channelRow); + + const renderGroup = panel.addGroup("scientific view", { open: false, hint: "Effects use separate render textures and read-only scientific bindings." }); + const renderControls = document.createElement("div"); + renderControls.className = "fl-controls fl-controls-3"; + const renderButtons = new Map(); + for (const mode of RENDER_MODES) { + const button = makeButton(mode, mode === "density"); + button.dataset.renderMode = mode; + button.addEventListener("click", () => setRenderMode(mode)); + renderButtons.set(mode, button); + renderControls.appendChild(button); + } + const exposureControl = labeledRange("exposure", 0.4, 3, 0.05, 1.65); + const trailControl = labeledRange("trail persistence", 0, 0.97, 0.01, 0.84); + const overlayControls = document.createElement("div"); + overlayControls.className = "fl-controls"; + const contourButton = makeButton("contours", false); + const glyphButton = makeButton("flow glyphs", false); + overlayControls.append(contourButton, glyphButton); + renderGroup.append(renderControls, exposureControl.row, trailControl.row, overlayControls); + + const proveGroup = panel.addGroup("PROVE", { open: false, hint: "M2 preserves model numerics; M3 adds event determinism, ledger, six-card, and render-only integrity gates." }); + const proveM3Button = document.createElement("button"); + proveM3Button.type = "button"; + proveM3Button.className = "fl-prove-button"; + proveM3Button.textContent = "Run M3 laboratory gates"; + const proveM2Button = document.createElement("button"); + proveM2Button.type = "button"; + proveM2Button.className = "fl-prove-button fl-secondary"; + proveM2Button.textContent = "Re-run M2 scientific gates"; + proveGroup.append(proveM3Button, proveM2Button); + + function selectTool(tool: Tool): void { + selectedTool = tool; + toolButtons.forEach((button, candidate) => button.setAttribute("aria-pressed", String(candidate === tool))); + canvas.dataset.tool = tool; + panel.setNarration(`${tool} selected · ${tool === "inspect" ? "read-only lens" : "events apply at the next fixed step boundary"}`); + touchTools.hidden = true; + } + + function setRenderMode(mode: RenderMode): void { + renderer.setMode(mode); + legend.textContent = LEGEND_TEXT[mode]; + renderButtons.forEach((button, candidate) => button.setAttribute("aria-pressed", String(candidate === mode))); + } + + const setPaused = (next: boolean): void => { + paused = next; + pauseButton.textContent = paused ? "play" : "pause"; + pauseButton.setAttribute("aria-pressed", String(paused)); + }; + + const advanceOnce = (): void => { + if (isCapturing()) return; + stepBoth(); + renderer.render(); + }; + + const fitOccupied = async (): Promise => { + const state = await solver.readback(); + let minRow = n; let maxRow = 0; let minColumn = n; let maxColumn = 0; let found = false; + for (let row = 0; row < n; row += 1) for (let column = 0; column < n; column += 1) { + const cell = row * n + column; + const density = (state.mass[cell] as number) + (state.mass[n * n + cell] as number) + (state.mass[2 * n * n + cell] as number); + if (density > 1e-3) { found = true; minRow = Math.min(minRow, row); maxRow = Math.max(maxRow, row); minColumn = Math.min(minColumn, column); maxColumn = Math.max(maxColumn, column); } + } + if (!found) return; + const span = Math.max(12, maxRow - minRow + 1, maxColumn - minColumn + 1); + renderer.setCamera({ centerRow: (minRow + maxRow) / 2, centerColumn: (minColumn + maxColumn) / 2, zoom: Math.min(8, n / (span * 1.22)) }); + }; + + pauseButton.addEventListener("click", () => setPaused(!paused)); + stepButton.addEventListener("click", advanceOnce); + fitButton.addEventListener("click", () => { void fitOccupied(); }); + zoomOut.addEventListener("click", () => renderer.zoom(0.8)); + zoomIn.addEventListener("click", () => renderer.zoom(1.25)); + resetCamera.addEventListener("click", () => setCameraFromCard(currentCard)); + radiusControl.input.addEventListener("input", () => { brushRadius = Number(radiusControl.input.value); radiusControl.output.textContent = brushRadius.toFixed(0); renderer.setInspection(inspectPosition[0], inspectPosition[1], brushRadius); }); + strengthControl.input.addEventListener("input", () => { brushStrength = Number(strengthControl.input.value); strengthControl.output.textContent = brushStrength.toFixed(2); }); + exposureControl.input.addEventListener("input", () => { const value = Number(exposureControl.input.value); exposureControl.output.textContent = value.toFixed(2); renderer.setExposure(value); }); + trailControl.input.addEventListener("input", () => { const value = Number(trailControl.input.value); trailControl.output.textContent = value.toFixed(2); renderer.setTrailPersistence(value); }); + contourButton.addEventListener("click", () => { const enabled = contourButton.getAttribute("aria-pressed") !== "true"; contourButton.setAttribute("aria-pressed", String(enabled)); renderer.setContours(enabled); }); + glyphButton.addEventListener("click", () => { const enabled = glyphButton.getAttribute("aria-pressed") !== "true"; glyphButton.setAttribute("aria-pressed", String(enabled)); renderer.setFlowGlyphs(enabled); }); + + const queueToolEvent = (tool: Tool, row: number, column: number, secondary: boolean, shift: boolean, direction: readonly [number, number] = [0, 0]): void => { + if (tool === "inspect") { void updateInspection(row, column); return; } + let kind: BrushEventKind = tool; + if (tool === "add" && secondary) kind = "erase"; + if (tool === "erase" && secondary) kind = "add"; + const multiplier = brushStrength * (shift ? 2 : 1); + const strength = kind === "add" ? 0.045 * multiplier + : kind === "erase" ? 0.18 * multiplier + : 1.15 * multiplier * n / 256; + const event = { + kind, + x: row, + y: column, + radius: brushRadius, + strength, + channel: selectedChannel, + directionX: direction[0], + directionY: direction[1], + polarity: secondary ? -1 : 1, + } as const; + solver.queueEvent(event); + if (renderer.hasComparison() && comparisonSolver) comparisonSolver.queueEvent(event); + panel.setNarration(`${secondary ? "reverse " : ""}${tool} queued at step ${solver.stepCount} · fixed-boundary ${kind === "add" || kind === "erase" ? "open ledger" : "mass-closed impulse"}`, "event"); + renderer.setInspection(row, column, brushRadius); + if (paused) advanceOnce(); + }; + + let panning = false; + let spaceHeld = false; + let previousPointer: { x: number; y: number; row: number; column: number; time: number } | null = null; + const pointers = new Map(); + let pinchDistance = 0; + let longPressTimer = 0; + + const showTouchTools = (clientX: number, clientY: number): void => { + const rect = stage.getBoundingClientRect(); + touchTools.style.left = `${clientX - rect.left}px`; + touchTools.style.top = `${clientY - rect.top}px`; + touchTools.hidden = false; + }; + for (const tool of ["pipette", "add", "erase", "stir", "inspect"] as const) { + const button = makeButton(tool, tool === selectedTool); + button.addEventListener("click", (event) => { event.stopPropagation(); selectTool(tool); }); + touchTools.appendChild(button); + } + + canvas.addEventListener("contextmenu", (event) => event.preventDefault()); + canvas.addEventListener("pointerdown", (event) => { + canvas.focus({ preventScroll: true }); + canvas.setPointerCapture(event.pointerId); + pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }); + if (pointers.size === 2) { + const pair = [...pointers.values()]; + pinchDistance = Math.hypot((pair[0]?.x ?? 0) - (pair[1]?.x ?? 0), (pair[0]?.y ?? 0) - (pair[1]?.y ?? 0)); + return; + } + panning = event.button === 1 || spaceHeld; + const [row, column] = renderer.screenToWorld(event.clientX, event.clientY); + previousPointer = { x: event.clientX, y: event.clientY, row, column, time: performance.now() }; + if (event.pointerType === "touch") longPressTimer = window.setTimeout(() => showTouchTools(event.clientX, event.clientY), 520); + if (!panning) { + if (event.altKey) void updateInspection(row, column); + else queueToolEvent(selectedTool, row, column, event.button === 2, event.shiftKey); + } + }); + canvas.addEventListener("pointermove", (event) => { + if (!pointers.has(event.pointerId)) return; + pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }); + if (pointers.size >= 2) { + window.clearTimeout(longPressTimer); + const pair = [...pointers.values()]; + const distance = Math.hypot((pair[0]?.x ?? 0) - (pair[1]?.x ?? 0), (pair[0]?.y ?? 0) - (pair[1]?.y ?? 0)); + if (pinchDistance > 0) renderer.zoom(distance / pinchDistance); + pinchDistance = distance; + return; + } + if (!previousPointer) return; + const [row, column] = renderer.screenToWorld(event.clientX, event.clientY); + const movedPixels = Math.hypot(event.clientX - previousPointer.x, event.clientY - previousPointer.y); + if (movedPixels > 8) window.clearTimeout(longPressTimer); + if (panning) { + const rect = canvas.getBoundingClientRect(); + const camera = renderer.getCamera(); + renderer.pan(-(event.clientY - previousPointer.y) / rect.height * n / camera.zoom, -(event.clientX - previousPointer.x) / rect.width * n / camera.zoom); + } else if (event.buttons !== 0 && performance.now() - previousPointer.time > 28 && movedPixels > 3) { + const direction: readonly [number, number] = [row - previousPointer.row, column - previousPointer.column]; + if (event.altKey) void updateInspection(row, column); + else queueToolEvent(selectedTool, row, column, (event.buttons & 2) !== 0, event.shiftKey, direction); + previousPointer.time = performance.now(); + } else if (selectedTool === "inspect") { + void updateInspection(row, column); + } else { + renderer.setInspection(row, column, brushRadius); + inspectPosition = [row, column]; + } + previousPointer = { x: event.clientX, y: event.clientY, row, column, time: previousPointer.time }; + }); + const endPointer = (event: PointerEvent): void => { + window.clearTimeout(longPressTimer); + pointers.delete(event.pointerId); + if (pointers.size < 2) pinchDistance = 0; + panning = false; + previousPointer = null; + }; + canvas.addEventListener("pointerup", endPointer); + canvas.addEventListener("pointercancel", endPointer); + canvas.addEventListener("wheel", (event) => { + event.preventDefault(); + if (event.ctrlKey || event.metaKey) renderer.zoom(Math.exp(-event.deltaY * 0.002)); + else { + brushRadius = Math.max(2, Math.min(n / 3, brushRadius * Math.exp(-event.deltaY * 0.0015))); + radiusControl.input.value = String(Math.round(brushRadius)); + radiusControl.output.textContent = brushRadius.toFixed(0); + renderer.setInspection(inspectPosition[0], inspectPosition[1], brushRadius); + } + }, { passive: false }); + + document.addEventListener("keydown", (event) => { + if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement || event.target instanceof HTMLButtonElement) return; + if (event.key === " ") { event.preventDefault(); spaceHeld = true; setPaused(!paused); } + else if (event.key === ".") advanceOnce(); + else if (event.key >= "1" && event.key <= "5") selectTool((["pipette", "add", "erase", "stir", "inspect"] as const)[Number(event.key) - 1] as Tool); + else if (event.key.toLowerCase() === "r") { resetActive(); stepBoth(); } + else if (event.key.toLowerCase() === "f") void fitOccupied(); + else if (event.key === "+" || event.key === "=") renderer.zoom(1.25); + else if (event.key === "-") renderer.zoom(0.8); + else if (event.key.toLowerCase() === "c") contourButton.click(); + else if (event.key.toLowerCase() === "g") glyphButton.click(); + else if (event.key === "Enter") queueToolEvent(selectedTool, inspectPosition[0], inspectPosition[1], false, event.shiftKey); + else if (event.key.startsWith("Arrow")) { + event.preventDefault(); + const delta = event.shiftKey ? 5 : 1; + const row = inspectPosition[0] + (event.key === "ArrowDown" ? delta : event.key === "ArrowUp" ? -delta : 0); + const column = inspectPosition[1] + (event.key === "ArrowRight" ? delta : event.key === "ArrowLeft" ? -delta : 0); + void updateInspection((row + n) % n, (column + n) % n); + } else if (event.key === "Escape") touchTools.hidden = true; + }); + document.addEventListener("keyup", (event) => { if (event.key === " ") spaceHeld = false; }); + + const renderOnlyIntegrityCheck = async (): Promise => { + const before = (await solver.readback()).mass; + const state = renderer.getPresentationState(); + renderer.setMode("pressure"); renderer.setContours(true); renderer.setFlowGlyphs(true); renderer.setTrailPersistence(0.91); renderer.zoom(1.17); + for (let frame = 0; frame < 8; frame += 1) renderer.render(); + await device.queue.onSubmittedWorkDone(); + const after = (await solver.readback()).mass; + renderer.restorePresentationState(state); + if (before.length !== after.length) return false; + for (let index = 0; index < before.length; index += 1) if (before[index] !== after[index]) return false; + return true; + }; + + const productSurfaceCheck = (): M3GateReport["productSurface"] => { + const result = { + experiments: document.querySelectorAll("[data-experiment]").length, + tools: toolButtons.size, + scientificViews: document.querySelectorAll("[data-render-mode]").length, + inspectorPlots: inspector.element.querySelectorAll("canvas").length, + keyboardFocus: canvas.tabIndex === 0 && canvas.getAttribute("aria-describedby") === "lab-help", + touchRadialTools: touchTools.querySelectorAll("button").length, + pass: false, + }; + result.pass = result.experiments === 6 && result.tools === 5 && result.scientificViews === 6 + && result.inspectorPlots === 2 && result.keyboardFocus && result.touchRadialTools === 5; + return result; + }; + + const m2Hook: M2Hook = { + latestGateReport: null, + grid: n, + allocatedBytes: solver.allocatedBytes, + dispatchesPerStep: solver.dispatchesPerStep, + reset: (nextSeed = seed) => { solver.reset(makeSeededOrganismMass(n, nextSeed)); solver.setPressureEnabled(true); solver.setSquareHalfWidth(0.65); solver.step(); }, + metrics: () => solver.metrics(), + setRenderMode, + runGates: async (steps = 256) => { + panel.setStatus(`running M2 gates (2 × ${steps} structural steps)…`); + proveM2Button.disabled = true; + const wasPaused = paused; setPaused(true); + try { + const report = await runM2Gates(device, steps, gateEnvironment); + m2Hook.latestGateReport = report; + panel.setStatus(report.pass ? "M2 numerical and structural gates PASS" : "M2 gate failure — inspect report hook"); + return report; + } finally { setPaused(wasPaused); proveM2Button.disabled = false; } + }, + }; + (globalThis as typeof globalThis & { __flowLeniaM2?: M2Hook }).__flowLeniaM2 = m2Hook; + + const m3Hook: M3Hook = { + latestGateReport: null, + listExperiments: () => EXPERIMENT_CARDS.map((card) => card.id), + loadExperiment: async (id) => { const card = EXPERIMENT_CARDS.find((candidate) => candidate.id === id); if (!card) throw new Error(`unknown experiment ${id}`); await loadExperiment(card); }, + queueEvent: (kind, row, column) => queueToolEvent(kind === "pipette" || kind === "add" || kind === "erase" || kind === "stir" ? kind : "inspect", row, column, false, false), + runGates: async () => { + panel.setStatus("running M3 event, ledger, six-card, and render-integrity gates…"); + proveM3Button.disabled = true; + const wasPaused = paused; setPaused(true); + try { + const report = await runM3Gates(device, gateEnvironment, renderOnlyIntegrityCheck, productSurfaceCheck); + m3Hook.latestGateReport = report; + panel.setVerdict({ gate: "event determinism + ledger + six cards + render-only integrity", verdict: report.pass ? "PASS" : "FAIL", pass: report.pass }); + panel.setStatus(report.pass ? "M3 laboratory gates PASS" : "M3 gate failure — inspect report hook"); + return report; + } finally { setPaused(wasPaused); proveM3Button.disabled = false; } + }, + }; + (globalThis as typeof globalThis & { __flowLeniaM3?: M3Hook }).__flowLeniaM3 = m3Hook; + proveM2Button.addEventListener("click", () => { void m2Hook.runGates(); }); + proveM3Button.addEventListener("click", () => { void m3Hook.runGates(); }); + + await loadExperiment(currentCard); + selectTool(selectedTool); + void updateInspection(n / 2, n / 2); + const frame = (now: number): void => { + const elapsed = Math.min(100, now - lastFrame); + lastFrame = now; + frameMs += 0.08 * (elapsed - frameMs); + if (!paused && !isCapturing()) { + timeAccumulator += speed; + const steps = Math.min(4, Math.floor(timeAccumulator)); + if (steps > 0) { stepBoth(steps); timeAccumulator -= steps; } + } + renderer.render(); + if (!telemetryPending && now - lastTelemetry > 750 && !isCapturing()) { + telemetryPending = true; + lastTelemetry = now; + void updateMetrics().finally(() => { telemetryPending = false; }); + } + requestAnimationFrame(frame); + }; + renderer.render(); + setBoot(""); + (globalThis as typeof globalThis & { __bitPhysicsReady?: boolean }).__bitPhysicsReady = true; + requestAnimationFrame(frame); +} + +void start().catch((error: unknown) => { + console.error(error); + setBoot(`Flow Lenia M3 failed: ${error instanceof Error ? error.message : String(error)}`); +}); diff --git a/packages/flow-lenia/web/src/arena-app.ts b/packages/flow-lenia/web/src/arena-app.ts new file mode 100644 index 00000000..10b10582 --- /dev/null +++ b/packages/flow-lenia/web/src/arena-app.ts @@ -0,0 +1,187 @@ +import "../../../../common/common-web/src/theme.css"; +import { exposeCapture, field, isCapturing, resetCapture } from "../../../../common/common-web/src/capture-export.js"; +import type { CaptureBundle } from "../../../../common/common-web/src/capture-export.js"; +import { createSettingsPanel } from "../../../../common/common-web/src/panel-shell.js"; +import { ARENA_CARDS, arenaCardById, makeArenaEnvironment, makeArenaState } from "./experiments/arena-cards.js"; +import type { ArenaCard } from "./experiments/arena-cards.js"; +import { ARENA_SCHEMA_VERSION } from "./model/arena.js"; +import type { ArenaBrushMode } from "./model/arena.js"; +import { MODEL_VARIANT, organismConfig } from "./model/config.js"; +import { FlowLeniaEcosystemSolver, MIXING_RULES } from "./model/ecosystem-solver.js"; +import type { EcosystemInspection, EcosystemMetrics, LineageGraph, MixingRule } from "./model/ecosystem-solver.js"; +import { buildArenaExperiment, downloadArenaExperiment, parseArenaExperiment } from "./model/experiment-io.js"; +import { arenaShaderSha256 } from "./model/shader-provenance.js"; +import { runM6Gates } from "./prove-m6.js"; +import type { M6GateReport } from "./prove-m6.js"; +import { ArenaRenderer } from "./render/arena-renderer.js"; +import type { ArenaView } from "./render/arena-renderer.js"; +import "./style.css"; + +type ArenaTool = "sample" | "mutate" | "attract" | "repel" | "wall" | "erase"; + +interface M6Hook { + runGates: () => Promise; + loadExperiment: (id: string) => Promise; + listExperiments: () => readonly string[]; + step: (count?: number) => void; + metrics: () => Promise; + exportExperiment: () => Promise; + importExperiment: (text: string) => Promise; + latestGateReport: M6GateReport | null; + allocatedBytes: number; + grid: number; +} + +const boot = document.getElementById("boot") as HTMLDivElement; +const stage = document.getElementById("organism-lab") as HTMLElement; +const canvas = document.getElementById("view") as HTMLCanvasElement; +const legend = document.getElementById("legend") as HTMLDivElement; +const ledgerHud = document.getElementById("ledger") as HTMLDivElement; +const perfHud = document.getElementById("perf") as HTMLDivElement; +const compareLabel = document.getElementById("compare-label") as HTMLDivElement; +const experimentNote = document.getElementById("experiment-note") as HTMLDivElement; +const inspectorHost = document.getElementById("inspector-host") as HTMLDivElement; +const kicker = document.querySelector(".fl-hud .fl-kicker") as HTMLParagraphElement; +const m0Probe = document.getElementById("m0-probe") as HTMLElement; +m0Probe.hidden = true; compareLabel.hidden = true; + +const VIEW_LABELS: Record = { + lineage: "lineage ID · soft walls amber · environment never writes mass", + phenotype: "H/Q projection · approximate phenotype relationship", + density: "mass channels C₀/C₁/C₂ · direct scientific field", + flow: "dt·F₀ displacement · direction hue · fixed scale", + environment: "signed affinity: cyan attract · orange repel · amber soft wall", +}; + +function setBoot(message: string): void { boot.textContent = message; boot.style.display = message ? "block" : "none"; } +function button(label: string, pressed = false): HTMLButtonElement { const element = document.createElement("button"); element.type = "button"; element.textContent = label; element.setAttribute("aria-pressed", String(pressed)); return element; } +function selectRow(label: string, values: readonly string[], selected: string): { row: HTMLLabelElement; select: HTMLSelectElement } { const row = document.createElement("label"); row.className = "fl-select-row"; row.append(label); const select = document.createElement("select"); for (const value of values) { const option = document.createElement("option"); option.value = value; option.textContent = value; option.selected = value === selected; select.appendChild(option); } row.appendChild(select); return { row, select }; } + +function inspectionMarkup(reading: EcosystemInspection): string { + const lineage = reading.lineage === 0xffff_ffff ? "mixed sentinel" : String(reading.lineage); + return ``; +} + +function graphMarkup(graph: LineageGraph): string { + const nodes = graph.nodes.slice(0, 12); + const max = Math.max(...nodes.map((node) => node.mass), 1e-9); + const nodeMarkup = nodes.map((node) => `
L${node.lineage}${node.mass.toFixed(2)}
`).join(""); + const edges = graph.edges.slice(-8).map((edge) => `
  • L${edge.parent} → L${edge.child}step ${edge.step}
  • `).join("") || "
  • no mutation transitions yetuse mutate to add an edge
  • "; + return `
    ${nodeMarkup}
      ${edges}
    `; +} + +function diagnostics(metrics: EcosystemMetrics, solver: FlowLeniaEcosystemSolver, frameMs: number): Array<{ label: string; value: string }> { + const region = metrics.regionMass ?? [0, 0, 0, 0]; + return [ + { label: "step / rule", value: `${metrics.step.toLocaleString()} / ${solver.getMixingRule()}` }, + { label: "mass / relative drift", value: `${metrics.totalMass.toFixed(4)} / ${metrics.relativeMassDrift.toExponential(2)}` }, + { label: "region 1 / 2 / 3", value: `${region[1].toFixed(2)} / ${region[2].toFixed(2)} / ${region[3].toFixed(2)}` }, + { label: "environment min / max", value: `${(metrics.environmentMin ?? 0).toFixed(3)} / ${(metrics.environmentMax ?? 0).toFixed(3)}` }, + { label: "gate / storm amplitude", value: `${metrics.gateOpen ? "OPEN" : "closed"} / ${(metrics.stormAmplitude ?? 0).toFixed(3)}` }, + { label: "lineages / phenotype bins", value: `${metrics.activeLineages} / ${metrics.phenotypeClusters}` }, + { label: "occupied / clamp", value: `${(100 * metrics.occupiedFraction).toFixed(1)}% / ${(100 * metrics.clampFraction).toFixed(3)}%` }, + { label: "finite / non-negative", value: metrics.nonFinite === 0 && metrics.negative === 0 ? "PASS" : `FAIL ${metrics.nonFinite}/${metrics.negative}` }, + { label: "frame / allocation", value: `${frameMs.toFixed(2)} ms / ${(solver.allocatedBytes / 2 ** 20).toFixed(2)} MiB` }, + ]; +} + +async function captureBundle(state: Awaited>, metrics: EcosystemMetrics, card: ArenaCard, solver: FlowLeniaEcosystemSolver, n: number, seed: number, started: number): Promise { + const shaderHash = await arenaShaderSha256(); const graph = solver.lineageGraph(metrics); + return { + manifest: { + schema_version: "1.0.0", + sim: { name: "flow-lenia", category: "continuous-ca", variant: MODEL_VARIANT }, + stack: { name: "webgpu-f32", version: "0.6.0", build_id: "flow-lenia-arena-release-m6" }, + config: { tier: n === 256 ? "reference" : "adaptive", dims: [n, n], dtype: "f32", seed, params: { mode: "arena", experiment: card.id, mixing_rule: solver.getMixingRule(), channels: 3, kernels: 9, dt: 0.2, dd: 5, sigma: 0.65, environment_schema: ARENA_SCHEMA_VERSION, shader_sha256: shaderHash, provenance: card.provenance } }, + run: { step_count: metrics.step, capture_interval: metrics.step, wall_clock_seconds: (performance.now() - started) / 1000, start_utc: new Date().toISOString() }, + payload: { format: "hdf5", path: `flow-lenia-arena-${card.id}-seed${seed}-step${metrics.step}.h5`, checksum: `sha256:${"0".repeat(64)}` }, + determinism: { claimed: "bit-exact-same-hw", atomic_ops: false, subgroup_ops: false }, + }, + steps: [{ + step: metrics.step, + state: { + mass: field(state.mass, [3, n, n], "f32"), genome_h: field(state.h, [9, n, n], "f32"), genome_q: field(state.q, [9, n, n], "f32"), + identity_u32_values: field(Float64Array.from(state.identity), [n, n, 4], "f64"), + environment_affinity_values: field(state.environment ?? new Float32Array(n * n * 4), [n, n, 4], "f32"), + environment_region_u32_values: field(Float64Array.from(state.environmentRegions ?? new Uint32Array(n * n)), [n, n], "f64"), + }, + diagnostics: { total_mass: metrics.totalMass, mass_relative_drift: metrics.relativeMassDrift, active_lineages: metrics.activeLineages, lineage_shannon: metrics.shannonDiversity, phenotype_clusters: metrics.phenotypeClusters, mixed_identity_mass: metrics.mixedIdentityMass, region_mass_1: metrics.regionMass?.[1] ?? 0, region_mass_2: metrics.regionMass?.[2] ?? 0, region_mass_3: metrics.regionMass?.[3] ?? 0, environment_min: metrics.environmentMin ?? 0, environment_max: metrics.environmentMax ?? 0, wall_fraction: metrics.wallFraction ?? 0, gate_open: metrics.gateOpen ? 1 : 0, storm_amplitude: metrics.stormAmplitude ?? 0, lineage_nodes: graph.nodes.length, lineage_edges: graph.edges.length, non_finite: metrics.nonFinite, negative: metrics.negative }, + }], + }; +} + +async function start(): Promise { + kicker.textContent = "M5–M6 · Arena Lab"; + const labSwitch = document.getElementById("lab-switch") as HTMLAnchorElement; labSwitch.textContent = "Organism Lab"; labSwitch.href = "?"; + stage.setAttribute("aria-label", "Flow Lenia Arena Laboratory"); stage.classList.add("fl-arena-stage"); + canvas.setAttribute("aria-label", "Interactive Flow Lenia arena. Paint soft affinity environments, sample cells, and mutate coherent lineage patches."); + const help = document.getElementById("lab-help"); if (help) help.textContent = "Space pauses. Period steps. I samples, M mutates, A attracts, P repels, W paints soft walls, E erases, R resets, plus and minus zoom. Primary applies; secondary reverses or removes."; + setBoot("requesting WebGPU adapter…"); if (!navigator.gpu) throw new Error("WebGPU unavailable in this browser"); + const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); if (!adapter) throw new Error("WebGPU adapter unavailable"); + const device = await adapter.requestDevice(); const info = adapter.info; const environment: M6GateReport["environment"] = { userAgent: navigator.userAgent, adapter: { vendor: info.vendor ?? "unknown", architecture: info.architecture ?? "unknown", device: info.device ?? "unknown", description: info.description ?? "unknown" } }; + device.addEventListener("uncapturederror", (event) => { const message = (event as GPUUncapturedErrorEvent).error.message; console.error(`Flow Lenia M6 WebGPU error: ${message}`); setBoot(`GPU error: ${message}`); }); + const context = canvas.getContext("webgpu") as GPUCanvasContext | null; if (!context) throw new Error("WebGPU canvas context unavailable"); const format = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device, format, alphaMode: "opaque" }); + const query = new URLSearchParams(location.search); const adaptive = matchMedia("(max-width: 720px)").matches || ((navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 8) < 4; const requested = Number.parseInt(query.get("grid") ?? (adaptive ? "128" : "256"), 10); const n = requested === 128 ? 128 : 256; let seed = Number.parseInt(query.get("seed") ?? "42", 10) >>> 0; + let card = arenaCardById(query.get("preset") ?? ARENA_CARDS[0]?.id ?? "corridor-divergence"); const config = organismConfig(n, seed); setBoot(`compiling the ${n}² Arena environment…`); + const solver = await FlowLeniaEcosystemSolver.create(device, config, card.mixing, { environment: true }); const renderer = new ArenaRenderer(device, context, canvas, format); renderer.setSolver(solver); + let paused = query.get("gate") === "1"; let speed = 1; let selectedTool: ArenaTool = (query.get("tool") as ArenaTool | null) ?? "sample"; let brushRadius = Math.max(4, n / 18); let strength = 0.55; let frameMs = 0; let lastFrame = performance.now(); let lastMetrics = 0; let telemetryPending = false; let lastInspection: readonly [number, number] = [n / 2, n / 2]; let recoveryBaseline: number[] | null = null; const recoveryHistory: Array<{ step: number; score: number }> = []; + const arenaMetricsHost = document.createElement("aside"); arenaMetricsHost.className = "fl-arena-metrics"; arenaMetricsHost.setAttribute("data-arena-regions", "true"); arenaMetricsHost.innerHTML = `
    REGIONS + RECOVERY
    LINEAGE GRAPH
    `; stage.appendChild(arenaMetricsHost); + + const resetExact = (): void => { solver.setMixingRule(card.mixing); solver.reset(makeArenaState(n, card, seed), makeArenaEnvironment(n, card)); recoveryBaseline = null; recoveryHistory.length = 0; }; + let panel = createSettingsPanel("Flow Lenia · Arena Lab", { + caption: "Sculpt conservative soft-affinity environments, open timed passages, and measure lineage abundance by authored region.", + initial: { tier: n === 256 ? "reference" : "test", seed }, tiers: n === 256 ? ["reference"] : ["test"], + onCapture: async () => { panel.setCaptureEnabled(false); resetCapture(); const started = performance.now(); resetExact(); solver.step(72); await device.queue.onSubmittedWorkDone(); const [state, metrics] = await Promise.all([solver.readback(), solver.metrics()]); exposeCapture(await captureBundle(state, metrics, card, solver, n, seed, started), { download: false }); panel.setStatus(`Arena canonical capture ready — ${card.title}, step ${metrics.step}`); panel.setCaptureEnabled(true); }, + onChange: (state) => { if ((state.seed >>> 0) !== seed) panel.setStatus("Arena solver seed is fixed at boot; reload with the requested seed to preserve stateless hashes"); }, + modes: { initial: paused ? "study" : "play", onMode: (mode) => { paused = mode === "study"; } }, + study: { + diagnostics: [{ label: "grid", value: `${n}²` }, { label: "environment", value: "authored field + soft wall + gate + scripted pulse" }, { label: "render bindings", value: "8 read-only storage · portable floor" }], + honesty: { faithful: "environment affinity is added before the published pressure/flow and finite-square reintegration transport; mass and localized H/Q use the unchanged M4 gather", simplified: "walls are soft negative affinity, regions are authored labels, and recovery is a distribution proxy—not hard geometry, fitness, biology, or open-ended evolution", measured: "region, diversity, and lineage metrics read back only at low cadence; no synchronization, allocation, or pipeline creation occurs in the per-step hot loop" }, + verdict: { gate: "Arena conservation + determinism + reload + release gates", verdict: "RUN TO VERIFY", pass: false }, + links: [{ label: "model specification", href: "../../../../docs/sim-specs/continuous-ca/lenia/spec-web-ecosystem.md" }, { label: "implementation ledger", href: "../../../../docs/sim-specs/continuous-ca/lenia/implementation-plan.md" }], + }, + }); + + const setView = (view: ArenaView): void => { renderer.setView(view); legend.textContent = VIEW_LABELS[view]; document.querySelectorAll("[data-arena-view]").forEach((element) => element.setAttribute("aria-pressed", String(element.dataset.arenaView === view))); }; + const setTool = (tool: ArenaTool): void => { selectedTool = tool; canvas.dataset.tool = tool; document.querySelectorAll("[data-arena-tool]").forEach((element) => element.setAttribute("aria-pressed", String(element.dataset.arenaTool === tool))); query.set("tool", tool); history.replaceState(null, "", `?${query.toString()}`); }; + const inspect = async (row: number, column: number): Promise => { lastInspection = [row, column]; renderer.setInspection(row, column, brushRadius); const reading = await solver.inspect(row, column); inspectorHost.innerHTML = inspectionMarkup(reading); return reading; }; + const applyTool = async (row: number, column: number, reverse: boolean, shifted: boolean, alt: boolean): Promise => { + if (alt || selectedTool === "sample") { await inspect(row, column); return; } + if (selectedTool === "mutate") { const reading = await inspect(row, column); if (reverse || reading.lineage === 0 || reading.lineage === 0xffff_ffff || reading.density <= 1e-8) { panel.setStatus("mutation needs an exact, occupied parent lineage"); return; } solver.queueMutation({ row, column, radius: brushRadius, parentLineage: reading.lineage, scale: Math.min(0.2, strength * 0.1) }); } + else { + let mode: ArenaBrushMode = selectedTool === "wall" ? "wall" : selectedTool === "erase" ? "erase" : "affinity"; let signed = strength * (shifted ? 1.8 : 1); + if (selectedTool === "repel") signed *= -1; if (reverse) { if (selectedTool === "wall") mode = "erase"; else signed *= -1; } + solver.queueEnvironmentEvent({ row, column, radius: brushRadius, strength: signed, mode }); + } + if (paused) solver.step(); panel.setNarration(`${reverse ? "secondary" : "primary"} ${selectedTool} at ${Math.floor(row)}, ${Math.floor(column)}`, "event"); + }; + + const drawRecovery = (metrics: EcosystemMetrics): void => { + const region = [...(metrics.regionMass ?? [0, 0, 0, 0])].slice(1); const total = Math.max(region.reduce((sum, value) => sum + value, 0), 1e-30); const normalized = region.map((value) => value / total); if (!recoveryBaseline) recoveryBaseline = [...normalized]; const score = 1 - 0.5 * normalized.reduce((sum, value, index) => sum + Math.abs(value - (recoveryBaseline?.[index] ?? 0)), 0); recoveryHistory.push({ step: metrics.step, score }); if (recoveryHistory.length > 120) recoveryHistory.shift(); + const bars = arenaMetricsHost.querySelector(".fl-region-bars") as HTMLDivElement; bars.innerHTML = region.map((value, index) => `
    R${index + 1}${value.toFixed(1)}
    `).join(""); + const graph = arenaMetricsHost.querySelector("[data-lineage-graph]") as HTMLDivElement; graph.innerHTML = `
    LINEAGE GRAPH
    ${graphMarkup(solver.lineageGraph(metrics))}`; + const plot = arenaMetricsHost.querySelector("canvas") as HTMLCanvasElement; const ctx = plot.getContext("2d"); if (ctx) { ctx.clearRect(0, 0, plot.width, plot.height); ctx.strokeStyle = "#1f3a40"; ctx.beginPath(); ctx.moveTo(0, 7); ctx.lineTo(plot.width, 7); ctx.stroke(); ctx.strokeStyle = "#68ddc2"; ctx.lineWidth = 1.5; ctx.beginPath(); recoveryHistory.forEach((point, index) => { const x = recoveryHistory.length <= 1 ? 0 : index / (recoveryHistory.length - 1) * plot.width; const y = (1 - Math.max(0, Math.min(1, point.score))) * (plot.height - 8) + 4; if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); } + }; + const updateMetrics = async (): Promise => { const metrics = await solver.metrics(); panel.setDiagnostics(diagnostics(metrics, solver, frameMs)); const region = metrics.regionMass ?? [0, 0, 0, 0]; ledgerHud.textContent = `closed mass ${metrics.totalMass.toFixed(2)} · ε ${metrics.relativeMassDrift.toExponential(1)} · R ${region.slice(1).map((value) => value.toFixed(0)).join("/")}`; perfHud.textContent = `${frameMs.toFixed(1)} ms frame · ${speed.toFixed(2)}× · ${n}² · gate ${metrics.gateOpen ? "open" : "closed"}`; const pass = metrics.relativeMassDrift <= 1.5e-4 && metrics.nonFinite === 0 && metrics.negative === 0; panel.setVerdict({ gate: "Arena closed ledger + finite localized state", verdict: pass ? "PASS" : "FAIL", pass }); drawRecovery(metrics); return metrics; }; + const loadCard = async (next: ArenaCard): Promise => { card = next; resetExact(); setView(next.view); ruleSelect.select.value = next.mixing; experimentNote.innerHTML = `${next.title}${next.description}${next.observation} · Success: ${next.successMetric}`; document.querySelectorAll("[data-arena-card]").forEach((element) => element.setAttribute("aria-pressed", String(element.dataset.arenaCard === next.id))); query.set("preset", next.id); history.replaceState(null, "", `?${query.toString()}`); panel.setStatus(`${next.title} reset exactly at seed ${seed}`); }; + + const experimentGroup = panel.addGroup("Arena experiments", { hint: "Three authored cards isolate passages, moving affinity, and a standardized mass-neutral perturbation." }); const cardGrid = document.createElement("div"); cardGrid.className = "fl-experiment-grid"; for (const item of ARENA_CARDS) { const element = button(item.title, item.id === card.id); element.className = "fl-experiment-card"; element.dataset.arenaCard = item.id; const small = document.createElement("small"); small.textContent = item.short; element.appendChild(small); element.addEventListener("click", () => { void loadCard(item); }); cardGrid.appendChild(element); } experimentGroup.appendChild(cardGrid); + const environmentGroup = panel.addGroup("environment tools", { hint: "Primary applies; secondary reverses/removes. Every event is applied at a fixed simulation-step boundary and never touches mass." }); const toolControls = document.createElement("div"); toolControls.className = "fl-controls fl-controls-3 fl-arena-tools"; for (const tool of ["sample", "mutate", "attract", "repel", "wall", "erase"] as const) { const element = button(tool, selectedTool === tool); element.dataset.arenaTool = tool; element.addEventListener("click", () => setTool(tool)); toolControls.appendChild(element); } environmentGroup.appendChild(toolControls); + const radiusInput = document.createElement("input"); radiusInput.type = "range"; radiusInput.min = "2"; radiusInput.max = String(Math.floor(n / 3)); radiusInput.value = String(Math.round(brushRadius)); const radiusRow = document.createElement("label"); radiusRow.className = "fl-select-row"; radiusRow.append("brush radius", radiusInput); radiusInput.addEventListener("input", () => { brushRadius = Number(radiusInput.value); renderer.setInspection(lastInspection[0], lastInspection[1], brushRadius); }); environmentGroup.appendChild(radiusRow); + const strengthInput = document.createElement("input"); strengthInput.type = "range"; strengthInput.min = "0.05"; strengthInput.max = "1.5"; strengthInput.step = "0.05"; strengthInput.value = String(strength); const strengthRow = document.createElement("label"); strengthRow.className = "fl-select-row"; strengthRow.append("field strength", strengthInput); strengthInput.addEventListener("input", () => { strength = Number(strengthInput.value); }); environmentGroup.appendChild(strengthRow); + const inheritanceGroup = panel.addGroup("inheritance", { hint: "Environment changes affinity only; localized H/Q still follows one of the five separately gated M4 rules." }); const ruleSelect = selectRow("mixing rule", MIXING_RULES, card.mixing); inheritanceGroup.appendChild(ruleSelect.row); ruleSelect.select.addEventListener("change", () => { solver.setMixingRule(ruleSelect.select.value as MixingRule); panel.setStatus(`inheritance set to ${solver.getMixingRule()}; state is retained`); }); + const viewGroup = panel.addGroup("scientific view", { hint: "Environment uses a fixed signed map. Amber walls remain visible in the other views as a render-only overlay." }); const viewControls = document.createElement("div"); viewControls.className = "fl-controls fl-controls-3"; for (const view of ["lineage", "phenotype", "density", "flow", "environment"] as const) { const element = button(view, view === card.view); element.dataset.arenaView = view; element.addEventListener("click", () => setView(view)); viewControls.appendChild(element); } viewGroup.appendChild(viewControls); + const shareGroup = panel.addGroup("replay + sharing", { hint: "JSON carries complete packed mass, H/Q, identity, environment, regions, lineage history, step, seed, provenance, and a verified SHA-256." }); const ioControls = document.createElement("div"); ioControls.className = "fl-controls"; const exportButton = button("export JSON"); exportButton.dataset.arenaIo = "export"; const importButton = button("import JSON"); importButton.dataset.arenaIo = "import"; ioControls.append(exportButton, importButton); shareGroup.appendChild(ioControls); const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "application/json,.json"; fileInput.hidden = true; shareGroup.appendChild(fileInput); + exportButton.addEventListener("click", () => { void (async () => { const document = await buildArenaExperiment(await solver.packedSnapshot(), card); downloadArenaExperiment(document); panel.setStatus(`exported ${document.schema_version} · sha ${document.scientific_sha256.slice(0, 12)}…`); })(); }); importButton.addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", () => { const file = fileInput.files?.[0]; if (!file) return; void (async () => { const parsed = await parseArenaExperiment(await file.text()); if (parsed.snapshot.n !== n || parsed.snapshot.seed !== config.seed) throw new Error(`import needs grid ${n} and seed ${config.seed}; received ${parsed.snapshot.n}/${parsed.snapshot.seed}`); card = arenaCardById(parsed.document.experiment.id); solver.restorePackedSnapshot(parsed.snapshot); renderer.setView(card.view); ruleSelect.select.value = parsed.snapshot.mixingRule; recoveryBaseline = null; recoveryHistory.length = 0; panel.setStatus(`import verified · sha ${parsed.document.scientific_sha256.slice(0, 12)}… · step ${parsed.snapshot.step}`); })().catch((error: unknown) => panel.setStatus(`import rejected: ${error instanceof Error ? error.message : String(error)}`)); }); + const timeGroup = panel.addGroup("time + release verification", { hint: "Fixed dt; adaptive quality changes grid only at boot and never changes simulation equations." }); const timeControls = document.createElement("div"); timeControls.className = "fl-controls fl-controls-3"; const pauseButton = button(paused ? "play" : "pause"); const stepButton = button("step"); const resetButton = button("reset"); timeControls.append(pauseButton, stepButton, resetButton); timeGroup.appendChild(timeControls); pauseButton.addEventListener("click", () => { paused = !paused; pauseButton.textContent = paused ? "play" : "pause"; }); stepButton.addEventListener("click", () => { solver.step(); paused = true; pauseButton.textContent = "play"; }); resetButton.addEventListener("click", resetExact); const speedRow = selectRow("speed", ["0.25", "0.5", "1", "2", "4"], "1"); speedRow.select.addEventListener("change", () => { speed = Number(speedRow.select.value); }); timeGroup.appendChild(speedRow.row); const proveButton = button("run complete M5–M6 gates"); proveButton.className = "fl-prove-button"; timeGroup.appendChild(proveButton); + + const renderIntegrityCheck = async (): Promise => { const before = await solver.packedSnapshot(); const previous = renderer.getView(); for (const view of ["lineage", "phenotype", "density", "flow", "environment"] as const) { renderer.setView(view); renderer.render(performance.now() / 1000); } await device.queue.onSubmittedWorkDone(); const after = await solver.packedSnapshot(); renderer.setView(previous); const equal = (a: ArrayBufferView, b: ArrayBufferView): boolean => { const x = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); const y = new Uint8Array(b.buffer, b.byteOffset, b.byteLength); return x.length === y.length && x.every((value, index) => value === y[index]); }; return equal(before.mass, after.mass) && equal(before.h, after.h) && equal(before.q, after.q) && equal(before.identity, after.identity) && equal(before.environment, after.environment) && equal(before.regions, after.regions); }; + const hook: M6Hook = { latestGateReport: null, allocatedBytes: solver.allocatedBytes, grid: n, listExperiments: () => ARENA_CARDS.map((item) => item.id), loadExperiment: async (id) => loadCard(arenaCardById(id)), step: (count = 1) => solver.step(count), metrics: updateMetrics, exportExperiment: async () => JSON.stringify(await buildArenaExperiment(await solver.packedSnapshot(), card)), importExperiment: async (text) => { const parsed = await parseArenaExperiment(text); solver.restorePackedSnapshot(parsed.snapshot); }, runGates: async () => { const wasPaused = paused; paused = true; proveButton.disabled = true; panel.setStatus("running Arena anchors, three deterministic cards, reload, render, memory, timing, and product gates…"); try { const report = await runM6Gates(device, environment, renderIntegrityCheck); hook.latestGateReport = report; panel.setVerdict({ gate: "Arena conservation + determinism + reload + release gates", verdict: report.pass ? "PASS" : "FAIL", pass: report.pass }); panel.setStatus(report.pass ? "M5–M6 Arena release gates PASS" : "M6 gate failure — inspect window.__flowLeniaM6.latestGateReport"); return report; } finally { paused = wasPaused; proveButton.disabled = false; } } }; + (globalThis as typeof globalThis & { __flowLeniaM6?: M6Hook }).__flowLeniaM6 = hook; proveButton.addEventListener("click", () => { void hook.runGates(); }); + canvas.addEventListener("contextmenu", (event) => event.preventDefault()); let pointerDown = false; let lastPaint = ""; canvas.addEventListener("pointerdown", (event) => { pointerDown = true; canvas.setPointerCapture(event.pointerId); const [row, column] = renderer.worldFromCanvas(event.clientX, event.clientY); lastPaint = `${Math.floor(row)},${Math.floor(column)}`; void applyTool(row, column, event.button === 2, event.shiftKey, event.altKey); }); canvas.addEventListener("pointerup", () => { pointerDown = false; lastPaint = ""; }); canvas.addEventListener("pointermove", (event) => { const [row, column] = renderer.worldFromCanvas(event.clientX, event.clientY); renderer.setInspection(row, column, brushRadius); if (pointerDown && selectedTool !== "sample" && selectedTool !== "mutate") { const key = `${Math.floor(row / 2)},${Math.floor(column / 2)}`; if (key !== lastPaint) { lastPaint = key; void applyTool(row, column, (event.buttons & 2) !== 0, event.shiftKey, event.altKey); } } }); canvas.addEventListener("wheel", (event) => { event.preventDefault(); if (event.ctrlKey || event.metaKey) renderer.setZoom(renderer.getZoom() * Math.exp(-event.deltaY * 0.002)); else { brushRadius = Math.max(2, Math.min(n / 3, brushRadius * Math.exp(-event.deltaY * 0.0015))); radiusInput.value = String(Math.round(brushRadius)); renderer.setInspection(lastInspection[0], lastInspection[1], brushRadius); } }, { passive: false }); + document.addEventListener("keydown", (event) => { if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement || event.target instanceof HTMLButtonElement) return; if (event.key === " ") { event.preventDefault(); paused = !paused; pauseButton.textContent = paused ? "play" : "pause"; } else if (event.key === ".") solver.step(); else if (event.key.toLowerCase() === "i") setTool("sample"); else if (event.key.toLowerCase() === "m") setTool("mutate"); else if (event.key.toLowerCase() === "a") setTool("attract"); else if (event.key.toLowerCase() === "p") setTool("repel"); else if (event.key.toLowerCase() === "w") setTool("wall"); else if (event.key.toLowerCase() === "e") setTool("erase"); else if (event.key.toLowerCase() === "r") resetExact(); else if (event.key === "+" || event.key === "=") renderer.setZoom(renderer.getZoom() * 1.25); else if (event.key === "-") renderer.setZoom(renderer.getZoom() * 0.8); }); + await loadCard(card); setTool(selectedTool); void inspect(n / 2, n / 2); let accumulator = 0; const frame = (now: number): void => { const elapsed = Math.min(100, now - lastFrame); lastFrame = now; frameMs += 0.08 * (elapsed - frameMs); if (!paused && !isCapturing()) { accumulator += speed; const steps = Math.min(4, Math.floor(accumulator)); if (steps > 0) { solver.step(steps); accumulator -= steps; } } renderer.render(now / 1000); if (!telemetryPending && now - lastMetrics > 1200 && !isCapturing()) { telemetryPending = true; lastMetrics = now; void updateMetrics().finally(() => { telemetryPending = false; }); } requestAnimationFrame(frame); }; + setBoot(""); (globalThis as typeof globalThis & { __bitPhysicsReady?: boolean }).__bitPhysicsReady = true; requestAnimationFrame(frame); +} + +void start().catch((error: unknown) => { console.error(error); setBoot(`Flow Lenia M6 failed: ${error instanceof Error ? error.message : String(error)}`); }); diff --git a/packages/flow-lenia/web/src/ecosystem-app.ts b/packages/flow-lenia/web/src/ecosystem-app.ts new file mode 100644 index 00000000..2cf0bcba --- /dev/null +++ b/packages/flow-lenia/web/src/ecosystem-app.ts @@ -0,0 +1,305 @@ +import "../../../../common/common-web/src/theme.css"; +import { exposeCapture, field, isCapturing, resetCapture } from "../../../../common/common-web/src/capture-export.js"; +import type { CaptureBundle } from "../../../../common/common-web/src/capture-export.js"; +import { createSettingsPanel } from "../../../../common/common-web/src/panel-shell.js"; +import { ECOSYSTEM_CARDS, ecosystemCardById, makeEcosystemState, scheduledMutations } from "./experiments/ecosystem-cards.js"; +import type { EcosystemCard, EcosystemView } from "./experiments/ecosystem-cards.js"; +import { MODEL_VARIANT, organismConfig } from "./model/config.js"; +import { FlowLeniaEcosystemSolver, MIXING_RULES } from "./model/ecosystem-solver.js"; +import type { EcosystemInspection, EcosystemMetrics, MixingRule } from "./model/ecosystem-solver.js"; +import { runM4Gates } from "./prove-m4.js"; +import type { M4GateReport } from "./prove-m4.js"; +import { EcosystemRenderer } from "./render/ecosystem-renderer.js"; +import "./style.css"; + +interface M4Hook { + runGates: () => Promise; + loadExperiment: (id: string) => Promise; + listExperiments: () => readonly string[]; + setMixingRule: (rule: MixingRule) => void; + step: (count?: number) => void; + metrics: () => Promise; + latestGateReport: M4GateReport | null; + allocatedBytes: number; + grid: number; +} + +const boot = document.getElementById("boot") as HTMLDivElement; +const stage = document.getElementById("organism-lab") as HTMLElement; +const canvas = document.getElementById("view") as HTMLCanvasElement; +const legend = document.getElementById("legend") as HTMLDivElement; +const ledgerHud = document.getElementById("ledger") as HTMLDivElement; +const perfHud = document.getElementById("perf") as HTMLDivElement; +const compareLabel = document.getElementById("compare-label") as HTMLDivElement; +const experimentNote = document.getElementById("experiment-note") as HTMLDivElement; +const inspectorHost = document.getElementById("inspector-host") as HTMLDivElement; +const kicker = document.querySelector(".fl-hud .fl-kicker") as HTMLParagraphElement; +const m0Probe = document.getElementById("m0-probe") as HTMLElement; +m0Probe.hidden = true; + +const VIEW_LABELS: Record = { + lineage: "lineage ID · stable hash hue · mixed ancestry is hatched", + phenotype: "phenotype projection · approximate H/Q relationship, not species", + density: "mass channels · C₀/C₁/C₂ direct RGB", + flow: "displacement dt·F₀ · direction hue, magnitude fixed scale", +}; + +function setBoot(message: string): void { boot.textContent = message; boot.style.display = message ? "block" : "none"; } + +function button(label: string, pressed = false): HTMLButtonElement { + const element = document.createElement("button"); + element.type = "button"; + element.textContent = label; + element.setAttribute("aria-pressed", String(pressed)); + return element; +} + +function selectRow(label: string, values: readonly string[], selected: string): { row: HTMLLabelElement; select: HTMLSelectElement } { + const row = document.createElement("label"); + row.className = "fl-select-row"; + row.append(label); + const select = document.createElement("select"); + for (const value of values) { const option = document.createElement("option"); option.value = value; option.textContent = value; option.selected = value === selected; select.appendChild(option); } + row.appendChild(select); + return { row, select }; +} + +function inspectMarkup(reading: EcosystemInspection): string { + const lineage = reading.lineage === 0xffff_ffff ? "mixed sentinel" : String(reading.lineage); + return ``; +} + +function diagnostics(metrics: readonly EcosystemMetrics[], solvers: readonly FlowLeniaEcosystemSolver[], frameMs: number): Array<{ label: string; value: string }> { + const first = metrics[0]; + if (!first) return []; + const rows = [ + { label: "step / rule", value: `${first.step.toLocaleString()} / ${solvers[0]?.getMixingRule()}` }, + { label: "mass / relative drift", value: `${first.totalMass.toFixed(4)} / ${first.relativeMassDrift.toExponential(2)}` }, + { label: "active / top lineage mass", value: `${first.activeLineages} / ${first.topLineageMass.toFixed(3)}` }, + { label: "lineage / phenotype H", value: `${first.shannonDiversity.toFixed(3)} / ${first.phenotypeShannon.toFixed(3)}` }, + { label: "phenotype bins / mixed mass", value: `${first.phenotypeClusters} / ${first.mixedIdentityMass.toFixed(3)}` }, + { label: "mutation / extinction events", value: `${first.mutationEvents} / ${first.extinctionEvents}` }, + { label: "occupied / clamp", value: `${(100 * first.occupiedFraction).toFixed(1)}% / ${(100 * first.clampFraction).toFixed(3)}%` }, + { label: "finite / non-negative", value: first.nonFinite === 0 && first.negative === 0 ? "PASS" : `FAIL ${first.nonFinite}/${first.negative}` }, + { label: "frame / allocation", value: `${frameMs.toFixed(2)} ms / ${(solvers.reduce((sum, solver) => sum + solver.allocatedBytes, 0) / 2 ** 20).toFixed(2)} MiB` }, + ]; + metrics.slice(1).forEach((item, index) => rows.splice(3 + index, 0, { label: `pane ${index + 2} ${solvers[index + 1]?.getMixingRule()}`, value: `mixed ${(100 * item.mixedIdentityMass / Math.max(item.totalMass, 1e-30)).toFixed(1)}% · H ${item.shannonDiversity.toFixed(2)}` })); + return rows; +} + +function captureBundle(state: Awaited>, metrics: EcosystemMetrics, card: EcosystemCard, rule: MixingRule, n: number, seed: number, started: number): CaptureBundle { + return { + manifest: { + schema_version: "1.0.0", + sim: { name: "flow-lenia", category: "continuous-ca", variant: MODEL_VARIANT }, + stack: { name: "webgpu-f32", version: "0.4.0", build_id: "flow-lenia-localized-ecosystem-m4" }, + config: { tier: n === 256 ? "reference" : "test", dims: [n, n], dtype: "f32", seed, params: { mode: "ecosystem", experiment: card.id, mixing_rule: rule, channels: 3, kernels: 9, dt: 0.2, dd: 5, sigma: 0.65, mutation_events: metrics.mutationEvents } }, + run: { step_count: metrics.step, capture_interval: metrics.step, wall_clock_seconds: (performance.now() - started) / 1000, start_utc: new Date().toISOString() }, + payload: { format: "hdf5", path: `flow-lenia-ecosystem-${card.id}-${rule}-seed${seed}-step${metrics.step}.h5`, checksum: `sha256:${"0".repeat(64)}` }, + determinism: { claimed: "bit-exact-same-hw", atomic_ops: false, subgroup_ops: false }, + }, + steps: [{ + step: metrics.step, + state: { mass: field(state.mass, [3, n, n], "f32"), genome_h: field(state.h, [9, n, n], "f32"), genome_q: field(state.q, [9, n, n], "f32"), identity_u32_values: field(Float64Array.from(state.identity), [n, n, 4], "f64") }, + diagnostics: { total_mass: metrics.totalMass, mass_relative_drift: metrics.relativeMassDrift, active_lineages: metrics.activeLineages, top_lineage_mass: metrics.topLineageMass, lineage_shannon: metrics.shannonDiversity, phenotype_clusters: metrics.phenotypeClusters, phenotype_shannon: metrics.phenotypeShannon, mutation_events: metrics.mutationEvents, extinction_events: metrics.extinctionEvents, mixed_identity_mass: metrics.mixedIdentityMass, non_finite: metrics.nonFinite, negative: metrics.negative }, + }], + }; +} + +async function start(): Promise { + kicker.textContent = "M4 · localized ecosystem"; + const labSwitch = document.getElementById("lab-switch") as HTMLAnchorElement; + labSwitch.textContent = "Arena Lab"; + labSwitch.href = "?arena=1"; + stage.setAttribute("aria-label", "Flow Lenia Ecosystem Laboratory"); + canvas.setAttribute("aria-label", "Interactive localized Flow Lenia ecosystem. Click to sample or mutate coherent lineage patches."); + setBoot("requesting WebGPU adapter…"); + if (!navigator.gpu) throw new Error("WebGPU unavailable in this browser"); + const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); + if (!adapter) throw new Error("WebGPU adapter unavailable"); + const device = await adapter.requestDevice(); + const info = adapter.info; + const environment: M4GateReport["environment"] = { userAgent: navigator.userAgent, adapter: { vendor: info.vendor ?? "unknown", architecture: info.architecture ?? "unknown", device: info.device ?? "unknown", description: info.description ?? "unknown" } }; + device.addEventListener("uncapturederror", (event) => { const message = (event as GPUUncapturedErrorEvent).error.message; console.error(`Flow Lenia M4 WebGPU error: ${message}`); setBoot(`GPU error: ${message}`); }); + const context = canvas.getContext("webgpu") as GPUCanvasContext | null; + if (!context) throw new Error("WebGPU canvas context unavailable"); + const format = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ device, format, alphaMode: "opaque" }); + const query = new URLSearchParams(location.search); + const adaptive = matchMedia("(max-width: 720px)").matches || ((navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 8) < 4; + const requested = Number.parseInt(query.get("grid") ?? (adaptive ? "128" : "256"), 10); + const n = requested === 128 ? 128 : 256; + const config = organismConfig(n, 42); + setBoot(`compiling the ${n}² localized ecosystem…`); + const primary = await FlowLeniaEcosystemSolver.create(device, config, "whole"); + let solvers: FlowLeniaEcosystemSolver[] = [primary]; + const renderer = new EcosystemRenderer(device, context, canvas, format); + renderer.setSolvers(solvers); + let card = ECOSYSTEM_CARDS[0] as EcosystemCard; + let seed = 42; + let paused = query.get("gate") === "1"; + let speed = 1; + let selectedTool: "sample" | "mutate" = "sample"; + let brushRadius = Math.max(4, n / 18); + let mutationScale = 0.055; + let frameMs = 0; + let lastFrame = performance.now(); + let lastMetrics = 0; + let telemetryPending = false; + let lastInspection: readonly [number, number] = [n / 2, n / 2]; + let panel = createSettingsPanel("Flow Lenia · Ecosystem Lab", { + caption: "Compare localized rule inheritance, create coherent mutation patches, and inspect exact lineage and approximate phenotype diversity.", + initial: { tier: n === 256 ? "reference" : "test", seed }, + tiers: n === 256 ? ["reference"] : ["test"], + onCapture: async () => { + panel.setCaptureEnabled(false); resetCapture(); const started = performance.now(); resetAll(); stepAll(32); await device.queue.onSubmittedWorkDone(); + const [state, metrics] = await Promise.all([primary.readback(), primary.metrics()]); + exposeCapture(captureBundle(state, metrics, card, primary.getMixingRule(), n, seed, started), { download: false }); + panel.setStatus(`ecosystem capture ready — ${card.title}, ${primary.getMixingRule()}, step ${metrics.step}`); panel.setCaptureEnabled(true); + }, + onChange: (state) => { seed = state.seed >>> 0; resetAll(); }, + modes: { initial: paused ? "study" : "play", onMode: (mode) => { paused = mode === "study"; } }, + study: { + diagnostics: [{ label: "grid", value: `${n}²` }, { label: "localized state", value: "H₉ + Q₉ + fingerprint + lineage + flags" }, { label: "gather bindings", value: "8 storage · portable floor" }], + honesty: { + faithful: "localized H/Q is carried through the same exact finite-square mass gather; all five inheritance rules follow the frozen f64 oracle semantics", + simplified: "lineage IDs, genome fingerprints, phenotype bins, and biological language are operational simulation definitions, not biological species or cells", + measured: "ecosystem metrics and inspection read back at low cadence; rendering binds scientific buffers read-only and never writes solver state", + }, + verdict: { gate: "five-rule numerical + deterministic + conservation + mutation gates", verdict: "RUN TO VERIFY", pass: false }, + links: [{ label: "model specification", href: "../../../../docs/sim-specs/continuous-ca/lenia/spec-web-ecosystem.md" }, { label: "M4 implementation ledger", href: "../../../../docs/sim-specs/continuous-ca/lenia/implementation-plan.md" }], + }, + }); + + const ensureSolverCount = async (count: number): Promise => { + while (solvers.length < count) { setBoot(`compiling synchronized ecosystem ${solvers.length + 1}/${count}…`); solvers.push(await FlowLeniaEcosystemSolver.create(device, config, "whole")); } + renderer.setSolvers(solvers.slice(0, count)); + setBoot(""); + }; + const activeSolvers = (): FlowLeniaEcosystemSolver[] => solvers.slice(0, card.comparisonRules?.length ?? 1); + const resetAll = (): void => { + const initial = makeEcosystemState(n, card, seed); + const rules = card.comparisonRules ?? [primary.getMixingRule()]; + activeSolvers().forEach((solver, index) => { solver.setMixingRule(rules[index] ?? card.mixing); solver.reset(initial); for (const event of scheduledMutations(card, n)) solver.queueMutation(event); }); + }; + const stepAll = (count = 1): void => activeSolvers().forEach((solver) => solver.step(count)); + const setView = (view: EcosystemView): void => { renderer.setView(view); legend.textContent = VIEW_LABELS[view]; document.querySelectorAll("[data-ecosystem-view]").forEach((element) => element.setAttribute("aria-pressed", String(element.dataset.ecosystemView === view))); }; + const updateInspection = async (row: number, column: number): Promise => { + lastInspection = [row, column]; renderer.setInspection(row, column, brushRadius); const reading = await primary.inspect(row, column); inspectorHost.innerHTML = inspectMarkup(reading); return reading; + }; + const applyMutation = async (row: number, column: number): Promise => { + const reading = await updateInspection(row, column); + if (reading.lineage === 0 || reading.density <= 1e-8) { panel.setStatus("mutation patch contains no matter"); return; } + primary.queueMutation({ row, column, radius: brushRadius, scale: mutationScale, parentLineage: reading.lineage }); + if (paused) primary.step(); + panel.setNarration(`mutation queued from lineage ${reading.lineage} at ${Math.floor(row)}, ${Math.floor(column)}`, "event"); + }; + const updateMetrics = async (): Promise => { + const metrics = await Promise.all(activeSolvers().map((solver) => solver.metrics())); + panel.setDiagnostics(diagnostics(metrics, activeSolvers(), frameMs)); + const first = metrics[0] as EcosystemMetrics; + ledgerHud.textContent = `closed mass ${first.totalMass.toFixed(2)} · ε ${first.relativeMassDrift.toExponential(1)} · lineages ${first.activeLineages}`; + perfHud.textContent = `${frameMs.toFixed(1)} ms frame · ${speed.toFixed(2)}× · ${n}²${metrics.length > 1 ? ` × ${metrics.length}` : ""}`; + const pass = metrics.every((item) => item.relativeMassDrift <= 1.5e-4 && item.nonFinite === 0 && item.negative === 0); + panel.setVerdict({ gate: "closed ledger + localized identity validity", verdict: pass ? "PASS" : "FAIL", pass }); + return metrics; + }; + const loadCard = async (next: EcosystemCard): Promise => { + card = next; + await ensureSolverCount(next.comparisonRules?.length ?? 1); + primary.setMixingRule(next.mixing); + resetAll(); + stepAll(); + setView(next.view); + ruleSelect.select.value = next.mixing; + ruleSelect.select.disabled = Boolean(next.comparisonRules); + compareLabel.hidden = !next.comparisonRules; + compareLabel.textContent = next.comparisonRules ? next.comparisonRules.map((rule, index) => `PANE ${index + 1} · ${rule}`).join(" | ") : ""; + experimentNote.innerHTML = `${next.title}${next.description}${next.observation}`; + document.querySelectorAll("[data-ecosystem-card]").forEach((element) => element.setAttribute("aria-pressed", String(element.dataset.ecosystemCard === next.id))); + panel.setStatus(`${next.title} reset exactly at seed ${seed}`); + }; + + const experimentGroup = panel.addGroup("ecosystem experiments", { hint: "Three authored cards isolate inheritance, contextual negotiation, and identity dilution." }); + const cardGrid = document.createElement("div"); cardGrid.className = "fl-experiment-grid"; + for (const item of ECOSYSTEM_CARDS) { const element = button(item.title, item.id === card.id); element.className = "fl-experiment-card"; element.dataset.ecosystemCard = item.id; const small = document.createElement("small"); small.textContent = item.short; element.appendChild(small); element.addEventListener("click", () => { void loadCard(item); }); cardGrid.appendChild(element); } + experimentGroup.appendChild(cardGrid); + + const inheritanceGroup = panel.addGroup("inheritance + mutation", { hint: "The same incoming overlap mass drives every rule. Stochastic choices are seed/step/destination/candidate/gene addressed." }); + const ruleSelect = selectRow("mixing rule", MIXING_RULES, card.mixing); inheritanceGroup.appendChild(ruleSelect.row); + for (const option of ruleSelect.select.options) option.dataset.mixingRule = option.value; + ruleSelect.select.addEventListener("change", () => { primary.setMixingRule(ruleSelect.select.value as MixingRule); resetAll(); panel.setStatus(`inheritance changed to ${primary.getMixingRule()} under the same seed`); }); + const mutationControls = document.createElement("div"); mutationControls.className = "fl-controls"; + const sampleButton = button("sample", true); const mutateButton = button("mutate"); sampleButton.dataset.ecosystemTool = "sample"; mutateButton.dataset.ecosystemTool = "mutate"; mutationControls.append(sampleButton, mutateButton); inheritanceGroup.appendChild(mutationControls); + const toolSelect = (tool: "sample" | "mutate"): void => { selectedTool = tool; sampleButton.setAttribute("aria-pressed", String(tool === "sample")); mutateButton.setAttribute("aria-pressed", String(tool === "mutate")); canvas.dataset.tool = tool === "sample" ? "inspect" : "mutate"; }; + sampleButton.addEventListener("click", () => toolSelect("sample")); mutateButton.addEventListener("click", () => toolSelect("mutate")); + const radius = document.createElement("input"); radius.type = "range"; radius.min = "2"; radius.max = String(Math.floor(n / 3)); radius.value = String(Math.round(brushRadius)); const radiusRow = document.createElement("label"); radiusRow.className = "fl-select-row"; radiusRow.append("patch radius", radius); radius.addEventListener("input", () => { brushRadius = Number(radius.value); renderer.setInspection(lastInspection[0], lastInspection[1], brushRadius); }); inheritanceGroup.appendChild(radiusRow); + const mutationStrength = document.createElement("input"); mutationStrength.type = "range"; mutationStrength.min = "0"; mutationStrength.max = "0.2"; mutationStrength.step = "0.005"; mutationStrength.value = String(mutationScale); const strengthRow = document.createElement("label"); strengthRow.className = "fl-select-row"; strengthRow.append("mutation σ", mutationStrength); mutationStrength.addEventListener("input", () => { mutationScale = Number(mutationStrength.value); }); inheritanceGroup.appendChild(strengthRow); + + const viewGroup = panel.addGroup("scientific view", { hint: "Lineage uses exact IDs. Phenotype is a labeled projection; nearby colors are not proof of relatedness." }); + const viewControls = document.createElement("div"); viewControls.className = "fl-controls fl-controls-2"; + for (const view of ["lineage", "phenotype", "density", "flow"] as const) { const element = button(view, view === card.view); element.dataset.ecosystemView = view; element.addEventListener("click", () => setView(view)); viewControls.appendChild(element); } + viewGroup.appendChild(viewControls); + + const timeGroup = panel.addGroup("time + verify", { hint: "Fixed dt; pausing never changes simulation timestep." }); + const timeControls = document.createElement("div"); timeControls.className = "fl-controls fl-controls-3"; + const pauseButton = button(paused ? "play" : "pause"); const stepButton = button("step"); const resetButton = button("reset"); timeControls.append(pauseButton, stepButton, resetButton); timeGroup.appendChild(timeControls); + pauseButton.addEventListener("click", () => { paused = !paused; pauseButton.textContent = paused ? "play" : "pause"; }); stepButton.addEventListener("click", () => { stepAll(); paused = true; pauseButton.textContent = "play"; }); resetButton.addEventListener("click", resetAll); + const speedRow = selectRow("speed", ["0.25", "0.5", "1", "2", "4"], "1"); speedRow.select.addEventListener("change", () => { speed = Number(speedRow.select.value); }); timeGroup.appendChild(speedRow.row); + const proveButton = button("run complete M4 gates"); proveButton.className = "fl-prove-button"; timeGroup.appendChild(proveButton); + + const renderIntegrityCheck = async (): Promise => { + const before = await primary.readback(); + const previous = renderer.getView(); + for (const view of ["lineage", "phenotype", "density", "flow"] as const) { renderer.setView(view); renderer.render(performance.now() / 1000); } + await device.queue.onSubmittedWorkDone(); + const after = await primary.readback(); + renderer.setView(previous); + const equal = (first: ArrayBufferView, second: ArrayBufferView): boolean => { + if (first.byteLength !== second.byteLength) return false; + const a = new Uint8Array(first.buffer, first.byteOffset, first.byteLength); + const b = new Uint8Array(second.buffer, second.byteOffset, second.byteLength); + for (let index = 0; index < a.length; index += 1) if (a[index] !== b[index]) return false; + return true; + }; + return equal(before.mass, after.mass) && equal(before.h, after.h) && equal(before.q, after.q) && equal(before.identity, after.identity); + }; + + const m4Hook: M4Hook = { + latestGateReport: null, allocatedBytes: primary.allocatedBytes, grid: n, + listExperiments: () => ECOSYSTEM_CARDS.map((item) => item.id), + loadExperiment: async (id) => loadCard(ecosystemCardById(id)), + setMixingRule: (rule) => { primary.setMixingRule(rule); ruleSelect.select.value = rule; resetAll(); }, + step: (count = 1) => stepAll(count), + metrics: updateMetrics, + runGates: async () => { + const wasPaused = paused; paused = true; proveButton.disabled = true; panel.setStatus("running all five M4 inheritance, mutation, ecosystem, determinism, memory, and timing gates…"); + try { const report = await runM4Gates(device, environment, renderIntegrityCheck); m4Hook.latestGateReport = report; panel.setVerdict({ gate: "five-rule numerical + deterministic + conservation + mutation gates", verdict: report.pass ? "PASS" : "FAIL", pass: report.pass }); panel.setStatus(report.pass ? "M4 localized ecosystem gates PASS" : "M4 gate failure — inspect window.__flowLeniaM4.latestGateReport"); return report; } + finally { paused = wasPaused; proveButton.disabled = false; } + }, + }; + (globalThis as typeof globalThis & { __flowLeniaM4?: M4Hook }).__flowLeniaM4 = m4Hook; + proveButton.addEventListener("click", () => { void m4Hook.runGates(); }); + + canvas.addEventListener("pointerdown", (event) => { canvas.setPointerCapture(event.pointerId); const [row, column] = renderer.worldFromCanvas(event.clientX, event.clientY); if (selectedTool === "mutate") void applyMutation(row, column); else void updateInspection(row, column); }); + canvas.addEventListener("pointermove", (event) => { if (event.buttons === 0) { const [row, column] = renderer.worldFromCanvas(event.clientX, event.clientY); renderer.setInspection(row, column, brushRadius); } }); + canvas.addEventListener("wheel", (event) => { event.preventDefault(); if (event.ctrlKey || event.metaKey) renderer.setZoom(renderer.getZoom() * Math.exp(-event.deltaY * 0.002)); else { brushRadius = Math.max(2, Math.min(n / 3, brushRadius * Math.exp(-event.deltaY * 0.0015))); radius.value = String(Math.round(brushRadius)); renderer.setInspection(lastInspection[0], lastInspection[1], brushRadius); } }, { passive: false }); + document.addEventListener("keydown", (event) => { if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement || event.target instanceof HTMLButtonElement) return; if (event.key === " ") { event.preventDefault(); paused = !paused; pauseButton.textContent = paused ? "play" : "pause"; } else if (event.key === ".") stepAll(); else if (event.key.toLowerCase() === "m") toolSelect("mutate"); else if (event.key.toLowerCase() === "i") toolSelect("sample"); else if (event.key.toLowerCase() === "r") resetAll(); else if (event.key === "+" || event.key === "=") renderer.setZoom(renderer.getZoom() * 1.25); else if (event.key === "-") renderer.setZoom(renderer.getZoom() * 0.8); }); + + await loadCard(card); + toolSelect("sample"); + void updateInspection(n / 2, n / 2); + let accumulator = 0; + const frame = (now: number): void => { + const elapsed = Math.min(100, now - lastFrame); lastFrame = now; frameMs += 0.08 * (elapsed - frameMs); + if (!paused && !isCapturing()) { accumulator += speed; const steps = Math.min(4, Math.floor(accumulator)); if (steps > 0) { stepAll(steps); accumulator -= steps; } } + renderer.render(now / 1000); + if (!telemetryPending && now - lastMetrics > 1200 && !isCapturing()) { telemetryPending = true; lastMetrics = now; void updateMetrics().finally(() => { telemetryPending = false; }); } + requestAnimationFrame(frame); + }; + setBoot(""); + (globalThis as typeof globalThis & { __bitPhysicsReady?: boolean }).__bitPhysicsReady = true; + requestAnimationFrame(frame); +} + +void start().catch((error: unknown) => { console.error(error); setBoot(`Flow Lenia M4 failed: ${error instanceof Error ? error.message : String(error)}`); }); diff --git a/packages/flow-lenia/web/src/experiments/arena-cards.ts b/packages/flow-lenia/web/src/experiments/arena-cards.ts new file mode 100644 index 00000000..958da916 --- /dev/null +++ b/packages/flow-lenia/web/src/experiments/arena-cards.ts @@ -0,0 +1,147 @@ +import { ECOSYSTEM_CARDS, makeEcosystemState } from "./ecosystem-cards.js"; +import type { EcosystemInitialState, MixingRule } from "../model/ecosystem-solver.js"; +import { ARENA_SCHEMA_VERSION, QUIET_ATTRACTOR, QUIET_STORM } from "../model/arena.js"; +import type { ArenaDynamics, ArenaEnvironmentState } from "../model/arena.js"; + +export type ArenaCardId = "corridor-divergence" | "maze-navigation" | "storm-recovery"; + +export interface ArenaCard { + id: ArenaCardId; + title: string; + short: string; + description: string; + observation: string; + mixing: MixingRule; + view: "lineage" | "phenotype" | "density" | "flow" | "environment"; + seedOffset: number; + duration: number; + successMetric: string; + provenance: { + origin: "Bit-Physics authored"; + sourceData: "none"; + license: "repository license"; + researchBasis: string; + }; +} + +const provenance = { + origin: "Bit-Physics authored", + sourceData: "none", + license: "repository license", + researchBasis: "Soft-affinity environments motivated by Plantec et al. 2025; parameters and fields authored independently for Bit-Physics.", +} as const; + +export const ARENA_CARDS: readonly ArenaCard[] = [ + { + id: "corridor-divergence", + title: "Corridor divergence", + short: "Founder islands + timed gate", + description: "Founder regions evolve on opposite sides of a soft-affinity barrier before a timed passage reconnects them.", + observation: "The amber seam is an affinity penalty, not collision geometry. At step 48 its central gate opens without changing mass.", + mixing: "whole", + view: "lineage", + seedOffset: 5101, + duration: 128, + successMetric: "region abundance before/after gate · closed mass ledger", + provenance, + }, + { + id: "maze-navigation", + title: "Maze navigation", + short: "Soft maze + orbiting source", + description: "A slowly orbiting positive-affinity source pulls localized matter through an authored soft-wall maze.", + observation: "Walls repel through affinity only, so sufficiently strong endogenous flow can cross them. The moving beacon never creates matter.", + mixing: "best", + view: "lineage", + seedOffset: 6113, + duration: 144, + successMetric: "target-region abundance · wall exposure · conservation", + provenance, + }, + { + id: "storm-recovery", + title: "Storm recovery", + short: "Standardized affinity pulse", + description: "A three-lobed, mass-neutral affinity storm perturbs a negotiation ecosystem on a frozen schedule.", + observation: "The recovery trace is a regional-distribution proxy, not a fitness or biological resilience claim.", + mixing: "negotiation", + view: "lineage", + seedOffset: 7127, + duration: 144, + successMetric: "retained mass · distribution recovery time after step 64", + provenance, + }, +] as const; + +function torusDelta(value: number): number { return value - Math.round(value); } +function gaussian(row: number, column: number, center: readonly [number, number], radius: number): number { + const dx = torusDelta(row - center[0]); + const dy = torusDelta(column - center[1]); + return Math.exp(-0.5 * (dx * dx + dy * dy) / (radius * radius)); +} + +function quietDynamics(): ArenaDynamics { + return { + channelResponse: [1, 0.86, 1.12], + gateOpenStep: -1, + gateCloseStep: -1, + storm: { ...QUIET_STORM, center: [...QUIET_STORM.center] }, + attractor: { ...QUIET_ATTRACTOR, center: [...QUIET_ATTRACTOR.center] }, + }; +} + +export function makeArenaEnvironment(n: number, card: ArenaCard): ArenaEnvironmentState { + const field = new Float32Array(n * n * 4); + const regions = new Uint32Array(n * n); + const dynamics = quietDynamics(); + for (let row = 0; row < n; row += 1) { + for (let column = 0; column < n; column += 1) { + const x = (row + 0.5) / n; + const y = (column + 0.5) / n; + const cell = row * n + column; + const base = cell * 4; + if (card.id === "corridor-divergence") { + const seam = Math.exp(-0.5 * ((x - 0.5) / 0.026) ** 2); + const gate = Math.exp(-0.5 * ((y - 0.5) / 0.085) ** 6); + field[base] = Math.fround(0.11 * gaussian(x, y, [0.29, 0.5], 0.24) + 0.11 * gaussian(x, y, [0.71, 0.5], 0.24)); + field[base + 1] = Math.fround(-1.35 * seam * (1 - gate)); + field[base + 2] = Math.fround(-1.35 * seam * gate); + field[base + 3] = Math.fround(Math.min(1, seam)); + regions[cell] = x < 0.46 ? 1 : x > 0.54 ? 2 : 3; + } else if (card.id === "maze-navigation") { + const border = x < 0.035 || x > 0.965 || y < 0.035 || y > 0.965; + const verticalA = Math.abs(y - 0.30) < 0.022 && !(x > 0.18 && x < 0.34) && !(x > 0.72 && x < 0.86); + const verticalB = Math.abs(y - 0.56) < 0.022 && !(x > 0.42 && x < 0.57) && !(x > 0.82 && x < 0.94); + const horizontalA = Math.abs(x - 0.34) < 0.022 && !(y > 0.08 && y < 0.21) && !(y > 0.66 && y < 0.81); + const horizontalB = Math.abs(x - 0.68) < 0.022 && !(y > 0.34 && y < 0.48) && !(y > 0.83 && y < 0.95); + const wall = border || verticalA || verticalB || horizontalA || horizontalB; + field[base + 1] = wall ? -1.05 : 0; + field[base + 3] = wall ? 1 : 0; + regions[cell] = gaussian(x, y, [0.22, 0.18], 0.18) > 0.42 ? 1 : gaussian(x, y, [0.78, 0.82], 0.18) > 0.42 ? 2 : 3; + } else { + field[base] = Math.fround(0.055 * Math.cos(2 * Math.PI * x) * Math.cos(2 * Math.PI * y)); + regions[cell] = x < 0.5 && y < 0.5 ? 1 : x >= 0.5 && y >= 0.5 ? 2 : 3; + } + } + } + if (card.id === "corridor-divergence") { + dynamics.gateOpenStep = 48; + dynamics.gateCloseStep = -1; + } else if (card.id === "maze-navigation") { + dynamics.attractor = { center: [0.74, 0.77], radius: 0.13, amplitude: 0.82, orbitRadius: 0.075, angularSpeed: 0.018, phase: 0.4 }; + dynamics.channelResponse = [1.0, 0.78, 1.18]; + } else { + dynamics.storm = { startStep: 40, duration: 24, center: [0.5, 0.5], radius: 0.31, amplitude: 1.45 }; + dynamics.channelResponse = [1.0, 0.92, 1.08]; + } + return { schemaVersion: ARENA_SCHEMA_VERSION, field, regions, dynamics }; +} + +export function makeArenaState(n: number, card: ArenaCard, seed: number): EcosystemInitialState { + const source = card.id === "storm-recovery" ? ECOSYSTEM_CARDS[1] : ECOSYSTEM_CARDS[0]; + return makeEcosystemState(n, source as (typeof ECOSYSTEM_CARDS)[number], (seed + card.seedOffset) >>> 0); +} + +export function arenaCardById(id: string): ArenaCard { + return ARENA_CARDS.find((card) => card.id === id) ?? ARENA_CARDS[0] as ArenaCard; +} diff --git a/packages/flow-lenia/web/src/experiments/cards.ts b/packages/flow-lenia/web/src/experiments/cards.ts new file mode 100644 index 00000000..9969e456 --- /dev/null +++ b/packages/flow-lenia/web/src/experiments/cards.ts @@ -0,0 +1,207 @@ +import type { BrushEventInput } from "../model/events.js"; +import { makeSeededOrganismMass } from "../model/seed.js"; + +export interface ExperimentModelControls { + pressure: boolean; + sigma: number; +} + +interface TimelineEvent { + step: number; + kind: BrushEventInput["kind"]; + row: number; + column: number; + radius256: number; + strength: number; + channel: number; + directionX?: number; + directionY?: number; +} + +export interface ExperimentCard { + id: "affinity-swimmer" | "spinner-collision" | "dividing-droplets" | "pressure-ablation" | "sigma-phase" | "expansion-trap"; + title: string; + short: string; + description: string; + observation: string; + seedOffset: number; + view: "density" | "channels" | "affinity" | "flow" | "pressure" | "flux"; + model: ExperimentModelControls; + comparison?: { label: string; model: ExperimentModelControls }; + camera: { center: readonly [number, number]; zoom: number }; + timeline: readonly TimelineEvent[]; + provenance: { source: string; license: string; transformation: string }; +} + +const authored = { + source: "Bit-Physics Flow Lenia M3 authored preset", + license: "repository license", + transformation: "constructed from the frozen ecosystem-v1 kernels; no third-party preset data or media", +} as const; + +export const EXPERIMENT_CARDS: readonly ExperimentCard[] = [ + { + id: "affinity-swimmer", + title: "Affinity swimmer", + short: "Follow a solitary body", + description: "A compact three-channel body turns bell responses into an affinity landscape and follows its gradient.", + observation: "Switch between affinity, pressure, and flow; inspect the leading rim and compare gradient direction with displacement.", + seedOffset: 0, + view: "density", + model: { pressure: true, sigma: 0.65 }, + camera: { center: [0.5, 0.5], zoom: 1.35 }, + timeline: [], + provenance: authored, + }, + { + id: "spinner-collision", + title: "Spinner collision", + short: "Perturb two mirrored bodies", + description: "Two mirrored copies approach under equal and opposite, mass-closed displacement impulses.", + observation: "Pause near contact, use the pipette to separate material, then watch whether the channel structures recover or merge.", + seedOffset: 101, + view: "flow", + model: { pressure: true, sigma: 0.65 }, + camera: { center: [0.5, 0.5], zoom: 1.05 }, + timeline: [ + { step: 36, kind: "stir", row: 0.50, column: 0.34, radius256: 34, strength: 1.25, channel: 3, directionY: 1 }, + { step: 36, kind: "stir", row: 0.50, column: 0.66, radius256: 34, strength: 1.25, channel: 3, directionY: -1 }, + ], + provenance: authored, + }, + { + id: "dividing-droplets", + title: "Dividing droplets", + short: "Fragment without biological claims", + description: "Three offset lobes exchange mass and may split, reconnect, or dissolve under the same fixed global rule.", + observation: "Use contours to see which fragments share an affinity basin. Fragmentation here is morphology, not biological reproduction.", + seedOffset: 211, + view: "affinity", + model: { pressure: true, sigma: 0.65 }, + camera: { center: [0.5, 0.5], zoom: 1.15 }, + timeline: [{ step: 54, kind: "pipette", row: 0.50, column: 0.50, radius256: 22, strength: 1.1, channel: 3 }], + provenance: authored, + }, + { + id: "pressure-ablation", + title: "Pressure ablation", + short: "Reference ↔ pressure off", + description: "A synchronized split starts from identical crowded mass. Left keeps density pressure; right removes only the alpha pressure gate.", + observation: "Compare peak density and clamp color. This is an explicit model ablation, not an artistic filter.", + seedOffset: 307, + view: "pressure", + model: { pressure: true, sigma: 0.65 }, + comparison: { label: "pressure off", model: { pressure: false, sigma: 0.65 } }, + camera: { center: [0.5, 0.40], zoom: 1.55 }, + timeline: [], + provenance: authored, + }, + { + id: "sigma-phase", + title: "Sigma phase", + short: "Narrow ↔ broad transport", + description: "A synchronized split changes only the finite-square half-width: narrow distributions at left, broad distributions at right.", + observation: "Use flux and trails to compare particle-like transport with smoother field-like reintegration.", + seedOffset: 401, + view: "flux", + model: { pressure: true, sigma: 0.38 }, + comparison: { label: "σ 1.05", model: { pressure: true, sigma: 1.05 } }, + camera: { center: [0.5, 0.40], zoom: 1.4 }, + timeline: [], + provenance: authored, + }, + { + id: "expansion-trap", + title: "Expansion trap", + short: "Area is not evolution", + description: "A diffuse annular field rapidly occupies space while retaining no localized genome or lineage state.", + observation: "Watch occupied fraction rise without interpreting it as adaptation or open-ended evolution.", + seedOffset: 509, + view: "channels", + model: { pressure: true, sigma: 0.65 }, + camera: { center: [0.5, 0.5], zoom: 0.9 }, + timeline: [], + provenance: authored, + }, +] as const; + +function addTransformed( + output: Float32Array, + source: Float32Array, + n: number, + center: readonly [number, number], + scale: number, + rotation: number, + amplitude: number, + channelShift = 0, +): void { + const cosine = Math.cos(rotation); + const sine = Math.sin(rotation); + for (let row = 0; row < n; row += 1) { + const dr = row - center[0] * n; + for (let column = 0; column < n; column += 1) { + const dc = column - center[1] * n; + const sourceRow = Math.round(n / 2 + (cosine * dr + sine * dc) / scale); + const sourceColumn = Math.round(n / 2 + (-sine * dr + cosine * dc) / scale); + const wrappedRow = ((sourceRow % n) + n) % n; + const wrappedColumn = ((sourceColumn % n) + n) % n; + const sourceCell = (wrappedRow * n + wrappedColumn) * 4; + const destinationCell = (row * n + column) * 4; + for (let channel = 0; channel < 3; channel += 1) { + const sourceChannel = (channel + channelShift) % 3; + output[destinationCell + channel] += amplitude * (source[sourceCell + sourceChannel] as number); + } + } + } +} + +export function makeExperimentMass(n: number, card: ExperimentCard, seed: number): Float32Array { + const effectiveSeed = (seed + card.seedOffset) >>> 0; + const source = makeSeededOrganismMass(n, effectiveSeed); + if (card.id === "affinity-swimmer" || card.id === "sigma-phase") return source; + const output = new Float32Array(n * n * 4); + if (card.id === "spinner-collision") { + addTransformed(output, source, n, [0.50, 0.34], 0.72, Math.PI / 2, 0.66, 0); + addTransformed(output, source, n, [0.50, 0.66], 0.72, -Math.PI / 2, 0.66, 1); + } else if (card.id === "dividing-droplets") { + addTransformed(output, source, n, [0.43, 0.43], 0.62, 0.4, 0.58, 0); + addTransformed(output, source, n, [0.57, 0.45], 0.58, -0.7, 0.54, 1); + addTransformed(output, source, n, [0.51, 0.59], 0.52, 1.8, 0.48, 2); + } else if (card.id === "pressure-ablation") { + addTransformed(output, source, n, [0.5, 0.5], 0.57, 0, 1.45, 0); + } else { + for (let row = 0; row < n; row += 1) { + const dr = row - n / 2; + for (let column = 0; column < n; column += 1) { + const dc = column - n / 2; + const radius = Math.hypot(dr, dc) / (n / 256); + const ring = 0.34 * Math.exp(-0.5 * ((radius - 55) / 16) ** 2); + const ripple = 0.76 + 0.24 * Math.cos(Math.atan2(dr, dc) * 6 + effectiveSeed * 0.001); + const cell = (row * n + column) * 4; + output[cell] = ring * ripple; + output[cell + 1] = ring * (1.0 - 0.25 * ripple); + output[cell + 2] = ring * (0.55 + 0.25 * ripple); + } + } + } + return output; +} + +export function scheduleCardTimeline(card: ExperimentCard, n: number): BrushEventInput[] { + const scale = n / 256; + return card.timeline.map((event) => ({ + kind: event.kind, + x: event.row * n, + y: event.column * n, + radius: Math.max(2, event.radius256 * scale), + strength: event.strength * scale, + channel: event.channel, + directionX: event.directionX ?? 0, + directionY: event.directionY ?? 0, + atStep: event.step, + })); +} + +export function experimentById(id: string): ExperimentCard { + return EXPERIMENT_CARDS.find((card) => card.id === id) ?? EXPERIMENT_CARDS[0] as ExperimentCard; +} diff --git a/packages/flow-lenia/web/src/experiments/ecosystem-cards.ts b/packages/flow-lenia/web/src/experiments/ecosystem-cards.ts new file mode 100644 index 00000000..1fe78346 --- /dev/null +++ b/packages/flow-lenia/web/src/experiments/ecosystem-cards.ts @@ -0,0 +1,169 @@ +import { KERNEL_SPECS } from "../model/config.js"; +import type { EcosystemInitialState, MixingRule, MutationPatchInput } from "../model/ecosystem-solver.js"; + +export type EcosystemView = "lineage" | "phenotype" | "density" | "flow"; + +export interface EcosystemCard { + id: "three-founders" | "negotiation-sea" | "identity-dilution"; + title: string; + short: string; + description: string; + observation: string; + seedOffset: number; + mixing: MixingRule; + view: EcosystemView; + comparisonRules?: readonly MixingRule[]; + mutationTimeline: readonly { + step: number; + row: number; + column: number; + radius256: number; + scale: number; + parentLineage: number; + }[]; + provenance: { origin: "Bit-Physics authored"; sourceData: "none"; license: "repository license" }; +} + +const provenance = { origin: "Bit-Physics authored", sourceData: "none", license: "repository license" } as const; + +export const ECOSYSTEM_CARDS: readonly EcosystemCard[] = [ + { + id: "three-founders", + title: "Three founders", + short: "Localized rules meet", + description: "Three coherent parameter lineages cross one another under the selected inheritance rule.", + observation: "Switch rules without changing the seed. Whole-genome selection preserves exact colored identities; averaging creates a mixed sentinel field.", + seedOffset: 1701, + mixing: "whole", + view: "lineage", + mutationTimeline: [], + provenance, + }, + { + id: "negotiation-sea", + title: "Negotiation sea", + short: "Contextual inheritance + mutation", + description: "Five founder patches occupy a dense field while deterministic mutation pulses create bounded child lineages.", + observation: "Negotiation samples contextual Q·I scores. Mutation rings change H/Q only inside coherent lineage patches and never alter mass.", + seedOffset: 2719, + mixing: "negotiation", + view: "lineage", + mutationTimeline: [ + { step: 18, row: 0.30, column: 0.29, radius256: 18, scale: 0.055, parentLineage: 1 }, + { step: 44, row: 0.68, column: 0.69, radius256: 20, scale: 0.070, parentLineage: 4 }, + { step: 76, row: 0.52, column: 0.50, radius256: 22, scale: 0.048, parentLineage: 5 }, + ], + provenance, + }, + { + id: "identity-dilution", + title: "Identity dilution", + short: "Average ↔ gene-wise ↔ negotiation", + description: "Three synchronized panes start byte-identically and differ only in localized inheritance.", + observation: "Average rapidly produces diffuse mixed identity; gene-wise recombines individual H/Q components; negotiation copies one contextual whole genome.", + seedOffset: 3911, + mixing: "average", + view: "lineage", + comparisonRules: ["average", "gene-wise", "negotiation"], + mutationTimeline: [], + provenance, + }, +] as const; + +function mix32(value: number): number { + let x = value >>> 0; + x = Math.imul(x ^ (x >>> 16), 0x7feb352d); + x = Math.imul(x ^ (x >>> 15), 0x846ca68b); + return (x ^ (x >>> 16)) >>> 0; +} + +interface Founder { + center: readonly [number, number]; + scale: number; + amplitude: number; + lineage: number; +} + +function foundersFor(card: EcosystemCard): readonly Founder[] { + if (card.id === "negotiation-sea") { + return [ + { center: [0.28, 0.28], scale: 0.15, amplitude: 0.68, lineage: 1 }, + { center: [0.27, 0.72], scale: 0.15, amplitude: 0.66, lineage: 2 }, + { center: [0.72, 0.28], scale: 0.15, amplitude: 0.64, lineage: 3 }, + { center: [0.71, 0.71], scale: 0.15, amplitude: 0.69, lineage: 4 }, + { center: [0.50, 0.50], scale: 0.18, amplitude: 0.61, lineage: 5 }, + ]; + } + return [ + { center: [0.30, 0.32], scale: 0.13, amplitude: 0.82, lineage: 1 }, + { center: [0.32, 0.69], scale: 0.13, amplitude: 0.78, lineage: 2 }, + { center: [0.69, 0.50], scale: 0.14, amplitude: 0.80, lineage: 3 }, + ]; +} + +function torusDelta(value: number, center: number): number { + const raw = value - center; + return raw - Math.round(raw); +} + +export function makeEcosystemState(n: number, card: EcosystemCard, seed: number): EcosystemInitialState { + const mass = new Float32Array(n * n * 4); + const h = new Float32Array(n * n * 12); + const q = new Float32Array(n * n * 12); + const identity = new Uint32Array(n * n * 4); + const founders = foundersFor(card); + const effectiveSeed = (seed + card.seedOffset) >>> 0; + for (let row = 0; row < n; row += 1) { + for (let column = 0; column < n; column += 1) { + const cell = row * n + column; + const x = (row + 0.5) / n; + const y = (column + 0.5) / n; + let dominant = founders[0] as Founder; + let dominantContribution = -1; + let density = 0; + for (const founder of founders) { + const dx = torusDelta(x, founder.center[0]); + const dy = torusDelta(y, founder.center[1]); + const radius = Math.hypot(dx, dy) / founder.scale; + const body = founder.amplitude * Math.exp(-2.15 * radius * radius); + const ring = 0.19 * Math.exp(-28 * (radius - 0.68) ** 2); + const contribution = body + ring; + density += contribution; + if (contribution > dominantContribution) { dominantContribution = contribution; dominant = founder; } + } + const wave = 0.92 + 0.08 * Math.cos(13 * x + 17 * y + (effectiveSeed & 1023) * 0.013); + density *= wave; + mass[cell * 4] = Math.fround(density * (0.36 + 0.11 * Math.sin(11 * y + dominant.lineage))); + mass[cell * 4 + 1] = Math.fround(density * (0.34 + 0.10 * Math.cos(9 * x - dominant.lineage))); + mass[cell * 4 + 2] = Math.fround(density * 0.30); + if (density <= 1e-7) continue; + for (let gene = 0; gene < 9; gene += 1) { + const base = KERNEL_SPECS[gene]?.weight ?? 0; + const phase = dominant.lineage * 1.731 + gene * 0.913 + (effectiveSeed & 255) * 0.001; + h[cell * 12 + gene] = Math.fround(base + 0.13 * Math.sin(phase)); + q[cell * 12 + gene] = Math.fround(0.72 + 0.28 * Math.cos(phase * 0.73)); + } + identity[cell * 4] = mix32(effectiveSeed ^ Math.imul(dominant.lineage, 0x9e3779b9)); + identity[cell * 4 + 1] = mix32(effectiveSeed ^ Math.imul(dominant.lineage, 0x85ebca6b)); + identity[cell * 4 + 2] = dominant.lineage; + identity[cell * 4 + 3] = 0; + } + } + return { mass, h, q, identity }; +} + +export function scheduledMutations(card: EcosystemCard, n: number): MutationPatchInput[] { + const scale = n / 256; + return card.mutationTimeline.map((event) => ({ + row: event.row * n, + column: event.column * n, + radius: Math.max(2, event.radius256 * scale), + scale: event.scale, + parentLineage: event.parentLineage, + atStep: event.step, + })); +} + +export function ecosystemCardById(id: string): EcosystemCard { + return ECOSYSTEM_CARDS.find((card) => card.id === id) ?? ECOSYSTEM_CARDS[0] as EcosystemCard; +} diff --git a/packages/flow-lenia/web/src/fft-batch.ts b/packages/flow-lenia/web/src/fft-batch.ts new file mode 100644 index 00000000..b8b4ff26 --- /dev/null +++ b/packages/flow-lenia/web/src/fft-batch.ts @@ -0,0 +1,132 @@ +import { FFT_COMMON_WGSL } from "../../../../common/common-web/src/fft-wgsl.js"; +import fftWgsl from "./shaders/batch_fft.wgsl?raw"; + +const WORKGROUP_SIZE = 128; + +export class BatchedFft2d { + readonly module: GPUShaderModule; + readonly allocatedBytes: number; + private readonly n: number; + private readonly log2n: number; + private readonly buffers: [GPUBuffer, GPUBuffer]; + private readonly groups = new Map(); + private readonly passGroups = new Map(); + private readonly uniformBuffers: GPUBuffer[] = []; + private readonly passBuffer: GPUBuffer; + private readonly pipeline: GPUComputePipeline; + private ping = 0; + + constructor(device: GPUDevice, n: number, buffers: [GPUBuffer, GPUBuffer], planeCounts: number[]) { + this.n = n; + this.log2n = Math.log2(n); + if (!Number.isInteger(this.log2n)) throw new Error("FFT dimension must be a power of two"); + this.buffers = buffers; + + const dataLayout = device.createBindGroupLayout({ + label: "flow-lenia-m0-fft-data", + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + ], + }); + const passLayout = device.createBindGroupLayout({ + label: "flow-lenia-m0-fft-pass", + entries: [{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }], + }); + + for (const planes of new Set(planeCounts)) { + const uniform = device.createBuffer({ + label: `flow-lenia-m0-fft-uniform-${planes}`, + size: 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + device.queue.writeBuffer(uniform, 0, new Uint32Array([n, planes, (n * n) / 2, 0])); + this.uniformBuffers.push(uniform); + for (let source = 0; source < 2; source += 1) { + this.groups.set( + `${planes}:${source}`, + device.createBindGroup({ + layout: dataLayout, + entries: [ + { binding: 0, resource: { buffer: uniform } }, + { binding: 1, resource: { buffer: buffers[source] } }, + { binding: 2, resource: { buffer: buffers[1 - source] } }, + ], + }), + ); + } + } + + const combos: Array<[string, number, number, number]> = []; + for (const axis of [0, 1]) { + for (let stage = 0; stage < this.log2n; stage += 1) { + for (const direction of [-1, 1]) { + combos.push([`${axis}:${stage}:${direction}`, axis, stage, direction]); + } + } + } + this.passBuffer = device.createBuffer({ + label: "flow-lenia-m0-fft-pass-uniforms", + size: combos.length * 256, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + this.allocatedBytes = combos.length * 256 + new Set(planeCounts).size * 16; + combos.forEach(([key, axis, stage, direction], index) => { + const raw = new ArrayBuffer(16); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + u32[0] = axis; + u32[1] = stage; + f32[2] = direction; + device.queue.writeBuffer(this.passBuffer, index * 256, raw); + this.passGroups.set( + key, + device.createBindGroup({ + layout: passLayout, + entries: [ + { binding: 0, resource: { buffer: this.passBuffer, offset: index * 256, size: 16 } }, + ], + }), + ); + }); + + this.module = device.createShaderModule({ + label: "flow-lenia-m0-batched-fft", + code: fftWgsl.replace("//__COMMON_FFT__", FFT_COMMON_WGSL), + }); + this.pipeline = device.createComputePipeline({ + label: "flow-lenia-m0-batched-fft", + layout: device.createPipelineLayout({ bindGroupLayouts: [dataLayout, passLayout] }), + compute: { module: this.module, entryPoint: "fft_pass" }, + }); + } + + get currentBuffer(): GPUBuffer { return this.buffers[this.ping]; } + get alternateBuffer(): GPUBuffer { return this.buffers[1 - this.ping]; } + get currentPing(): number { return this.ping; } + + resetPing(ping = 0): void { this.ping = ping; } + swapAfterExternalWrite(): void { this.ping = 1 - this.ping; } + + encode2d(pass: GPUComputePassEncoder, planes: number, direction: -1 | 1): void { + const butterflies = planes * this.n * this.n / 2; + for (const axis of [0, 1]) { + for (let stage = 0; stage < this.log2n; stage += 1) { + const dataGroup = this.groups.get(`${planes}:${this.ping}`); + const passGroup = this.passGroups.get(`${axis}:${stage}:${direction}`); + if (!dataGroup || !passGroup) throw new Error("missing static FFT bind group"); + pass.setPipeline(this.pipeline); + pass.setBindGroup(0, dataGroup); + pass.setBindGroup(1, passGroup); + pass.dispatchWorkgroups(Math.ceil(butterflies / WORKGROUP_SIZE)); + this.ping = 1 - this.ping; + } + } + } + + destroy(): void { + this.passBuffer.destroy(); + for (const uniform of this.uniformBuffers) uniform.destroy(); + } +} diff --git a/packages/flow-lenia/web/src/inventory.ts b/packages/flow-lenia/web/src/inventory.ts new file mode 100644 index 00000000..eb61fa1b --- /dev/null +++ b/packages/flow-lenia/web/src/inventory.ts @@ -0,0 +1,136 @@ +export const M0_CHANNELS = 3; +export const M0_KERNELS = 9; +export const M0_DD = 5; +export const M0_SIGMA = 0.65; + +export interface BufferInventoryRow { + name: string; + bytesPerCell: number; + buffers: number; + bytes: number; + purpose: string; +} + +export interface MemoryInventory { + n: number; + rows: BufferInventoryRow[]; + totalBytes: number; + largestBufferBytes: number; +} + +interface BufferPlanRow { + name: string; + bytesPerCell: number; + buffers: number; + purpose: string; +} + +// Projected M4 allocation, including arrays not needed by the M0 executable. +// It is deliberately conservative: cached complex kernel spectra retain both +// components and every ping-pong state is counted at full f32/u32 width. +const COMPLETE_ECOSYSTEM_PLAN: BufferPlanRow[] = [ + { name: "mass ping/pong", bytesPerCell: 16, buffers: 2, purpose: "vec4f mass state" }, + { name: "transport source", bytesPerCell: 48, buffers: 1, purpose: "mass + x/y displacement vec4f" }, + { name: "complex FFT ping/pong", bytesPerCell: 8 * M0_KERNELS, buffers: 2, purpose: "K complex planes" }, + { name: "kernel spectra", bytesPerCell: 8 * M0_KERNELS, buffers: 1, purpose: "K cached complex spectra" }, + { name: "kernel responses", bytesPerCell: 4 * M0_KERNELS, buffers: 1, purpose: "K real response planes" }, + { name: "affinity", bytesPerCell: 16, buffers: 1, purpose: "three channel targets in vec4f" }, + { name: "flow x/y", bytesPerCell: 16, buffers: 2, purpose: "three channel components in vec4f" }, + { name: "genome H ping/pong", bytesPerCell: 48, buffers: 2, purpose: "three vec4f records per cell" }, + { name: "genome Q ping/pong", bytesPerCell: 48, buffers: 2, purpose: "three vec4f records per cell" }, + { name: "identity ping/pong", bytesPerCell: 16, buffers: 2, purpose: "fingerprint, lineage, flags" }, +]; + +export function completeEcosystemInventory(n: number): MemoryInventory { + const cells = n * n; + const rows = COMPLETE_ECOSYSTEM_PLAN.map((row) => ({ + ...row, + bytes: row.bytesPerCell * row.buffers * cells, + })); + return { + n, + rows, + totalBytes: rows.reduce((sum, row) => sum + row.bytes, 0), + largestBufferBytes: 8 * M0_KERNELS * cells, + }; +} + +export interface BindingInventoryRow { + pipeline: string; + storageBindings: number; + uniformBindings: number; + dispatches128: number; + dispatches256: number; + note: string; +} + +export const BINDING_INVENTORY: BindingInventoryRow[] = [ + { + pipeline: "batched FFT stage", + storageBindings: 2, + uniformBindings: 2, + dispatches128: 28, + dispatches256: 32, + note: "forward C and inverse K together; two axes × log2(N) for each direction", + }, + { + pipeline: "spectral expansion", + storageBindings: 3, + uniformBindings: 1, + dispatches128: 1, + dispatches256: 1, + note: "C source spectra × cached K kernels -> K response planes", + }, + { + pipeline: "mass-only gather", + storageBindings: 2, + uniformBindings: 1, + dispatches128: 1, + dispatches256: 1, + note: "transport scratch -> next mass", + }, + { + pipeline: "full-state gather", + storageBindings: 8, + uniformBindings: 1, + dispatches128: 1, + dispatches256: 1, + note: "transport, mass out, H/Q/identity ping-pong; exactly the portable storage floor", + }, +]; + +export interface LimitSnapshot { + maxBufferSize: number; + maxStorageBufferBindingSize: number; + maxStorageBuffersPerShaderStage: number; + maxBindingsPerBindGroup: number; + maxComputeInvocationsPerWorkgroup: number; + maxComputeWorkgroupStorageSize: number; + maxComputeWorkgroupsPerDimension: number; +} + +export function snapshotLimits(limits: GPUSupportedLimits): LimitSnapshot { + return { + maxBufferSize: limits.maxBufferSize, + maxStorageBufferBindingSize: limits.maxStorageBufferBindingSize, + maxStorageBuffersPerShaderStage: limits.maxStorageBuffersPerShaderStage, + maxBindingsPerBindGroup: limits.maxBindingsPerBindGroup, + maxComputeInvocationsPerWorkgroup: limits.maxComputeInvocationsPerWorkgroup, + maxComputeWorkgroupStorageSize: limits.maxComputeWorkgroupStorageSize, + maxComputeWorkgroupsPerDimension: limits.maxComputeWorkgroupsPerDimension, + }; +} + +export function assertArchitectureFits(n: number, limits: LimitSnapshot): string[] { + const inventory = completeEcosystemInventory(n); + const failures: string[] = []; + if (inventory.largestBufferBytes > limits.maxBufferSize) failures.push("maxBufferSize"); + if (inventory.largestBufferBytes > limits.maxStorageBufferBindingSize) { + failures.push("maxStorageBufferBindingSize"); + } + if (limits.maxStorageBuffersPerShaderStage < 8) failures.push("maxStorageBuffersPerShaderStage"); + if (limits.maxBindingsPerBindGroup < 9) failures.push("maxBindingsPerBindGroup"); + if (limits.maxComputeInvocationsPerWorkgroup < 128) failures.push("maxComputeInvocationsPerWorkgroup"); + if (limits.maxComputeWorkgroupStorageSize < 16_384) failures.push("maxComputeWorkgroupStorageSize"); + return failures; +} diff --git a/packages/flow-lenia/web/src/m0-app.ts b/packages/flow-lenia/web/src/m0-app.ts new file mode 100644 index 00000000..3352e161 --- /dev/null +++ b/packages/flow-lenia/web/src/m0-app.ts @@ -0,0 +1,318 @@ +import "../../../../common/common-web/src/theme.css"; +import { exposeCapture, field, resetCapture } from "../../../../common/common-web/src/capture-export.js"; +import type { CaptureBundle } from "../../../../common/common-web/src/capture-export.js"; +import { createSettingsPanel } from "../../../../common/common-web/src/panel-shell.js"; +import type { PanelShell } from "../../../../common/common-web/src/panel-shell.js"; +import { + BINDING_INVENTORY, + assertArchitectureFits, + completeEcosystemInventory, + snapshotLimits, +} from "./inventory.js"; +import type { LimitSnapshot } from "./inventory.js"; +import { FlowLeniaM0Probe } from "./probe.js"; +import type { BenchmarkMode, BenchmarkResult, VerificationResult } from "./probe.js"; +import "./style.css"; + +interface AdapterRecord { + vendor: string; + architecture: string; + device: string; + description: string; +} + +export interface M0Report { + schemaVersion: "flow-lenia-m0-v1"; + generatedUtc: string; + userAgent: string; + adapter: AdapterRecord; + timestampQuery: boolean; + limits: LimitSnapshot; + architectureFailures: string[]; + verification128: VerificationResult; + benchmarks: BenchmarkResult[]; + projectedMemory: Array<{ n: number; bytes: number; mebibytes: number }>; + measuredDefault: 128 | 256; + decision: string; +} + +interface M0Hook { + runSuite: () => Promise; + latest: M0Report | null; + limits: LimitSnapshot; + bindingInventory: typeof BINDING_INVENTORY; +} + +const boot = document.getElementById("boot") as HTMLDivElement; +const summary = document.getElementById("summary") as HTMLParagraphElement; +const resultsElement = document.getElementById("results") as HTMLDivElement; +const m0Probe = document.getElementById("m0-probe") as HTMLElement; +const m0Canvas = document.getElementById("view") as HTMLCanvasElement; +const organismHud = document.querySelector(".fl-hud") as HTMLElement | null; +const labSwitch = document.getElementById("lab-switch") as HTMLAnchorElement | null; +m0Probe.hidden = false; +m0Canvas.hidden = true; +if (organismHud) organismHud.hidden = true; +if (labSwitch) labSwitch.hidden = true; + +function setBoot(message: string): void { + boot.textContent = message; + boot.style.display = message ? "block" : "none"; +} + +function adapterRecord(adapter: GPUAdapter): AdapterRecord { + const info = adapter.info; + return { + vendor: info.vendor ?? "unknown", + architecture: info.architecture ?? "unknown", + device: info.device ?? "unknown", + description: info.description ?? "unknown", + }; +} + +function verificationPass(verification: VerificationResult): boolean { + return ( + verification.fftMaxAbs <= 5e-4 && + verification.gatherMassRelativeResidual <= 5e-5 && + verification.fullMassRelativeResidual <= 5e-5 && + verification.uniformGenomeMaxAbs <= 5e-5 && + verification.uniformIdentityExact + ); +} + +function renderReport(report: M0Report): void { + const rows = report.benchmarks + .map( + (row) => `${row.n}² ${row.mode}${row.dispatchesPerStep}` + + `${row.p50Ms.toFixed(3)}${row.p95Ms.toFixed(3)}${row.timing}`, + ) + .join(""); + resultsElement.innerHTML = ` + + + ${rows} +
    workloaddispatchp50 msp95 msclock
    +

    + 128² shader anchors: FFT max |Δ| ${report.verification128.fftMaxAbs.toExponential(2)}, + gather mass residual ${report.verification128.fullMassRelativeResidual.toExponential(2)}, + constant genome ${report.verification128.uniformGenomeMaxAbs.toExponential(2)}. +

    +

    Measured default: ${report.measuredDefault}². ${report.decision}

    `; + summary.textContent = `${report.adapter.description || report.adapter.device}: M0 benchmark complete.`; +} + +function makeCapture(report: M0Report, wallSeconds: number): CaptureBundle { + const order = report.benchmarks.map((row) => `${row.n}:${row.mode}`); + const p95 = Float32Array.from(report.benchmarks.map((row) => row.p95Ms)); + return { + manifest: { + schema_version: "1.0.0", + sim: { name: "flow-lenia", category: "continuous-ca", variant: "ecosystem-m0-feasibility" }, + stack: { + name: "webgpu-f32", + version: "0.0.1", + build_id: `flow-lenia-m0 ${report.adapter.vendor}/${report.adapter.architecture}`, + }, + config: { + tier: "test", + dims: [report.measuredDefault, report.measuredDefault], + dtype: "f32", + seed: 42, + params: { + channels: 3, + kernels: 9, + dd: 5, + sigma: 0.65, + benchmark_order: order, + timing: report.timestampQuery ? "timestamp-query" : "queue-completion", + }, + }, + run: { + step_count: report.benchmarks.reduce((sum, row) => sum + row.samples, 0), + capture_interval: 1, + wall_clock_seconds: wallSeconds, + start_utc: report.generatedUtc, + }, + payload: { + format: "hdf5", + path: "flow-lenia-m0-browser-benchmark.h5", + checksum: "sha256:" + "0".repeat(64), + }, + determinism: { claimed: "non-deterministic", atomic_ops: false, subgroup_ops: false }, + }, + steps: [ + { + step: 0, + state: { p95_ms: field(p95, [p95.length], "f32") }, + diagnostics: { + fft_max_abs: report.verification128.fftMaxAbs, + mass_relative_residual: report.verification128.fullMassRelativeResidual, + genome_max_abs: report.verification128.uniformGenomeMaxAbs, + projected_256_mib: completeEcosystemInventory(256).totalBytes / 2 ** 20, + measured_default: report.measuredDefault, + }, + }, + ], + }; +} + +async function start(): Promise { + setBoot("requesting WebGPU adapter…"); + if (!navigator.gpu) { + setBoot("WebGPU unavailable in this browser."); + return; + } + const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); + if (!adapter) { + setBoot("WebGPU adapter unavailable."); + return; + } + const requiredFeatures: GPUFeatureName[] = []; + if (adapter.features.has("timestamp-query")) requiredFeatures.push("timestamp-query"); + const device = await adapter.requestDevice({ requiredFeatures }); + const adapterInfo = adapterRecord(adapter); + const limits = snapshotLimits(device.limits); + const architectureFailures = assertArchitectureFits(256, limits); + device.addEventListener("uncapturederror", (event) => { + const message = (event as GPUUncapturedErrorEvent).error.message; + console.error(`Flow Lenia M0 WebGPU error: ${message}`); + setBoot(`GPU error: ${message}`); + }); + void device.lost.then((info) => setBoot(`WebGPU device lost (${info.reason}): ${info.message}`)); + + // Browser smoke reaches this line only after every M0 shader and explicit + // pipeline layout has compiled successfully at the portable 128² tier. + setBoot("compiling M0 FFT and gather pipelines…"); + const preflight = await FlowLeniaM0Probe.create(device, 128); + preflight.destroy(); + + let panel: PanelShell; + let running: Promise | null = null; + const query = new URLSearchParams(location.search); + const samples = Math.max(3, Number.parseInt(query.get("samples") ?? "12", 10) || 12); + const warmup = Math.max(1, Number.parseInt(query.get("warmup") ?? "3", 10) || 3); + const modes: BenchmarkMode[] = ["fft", "gather-mass", "gather-full", "step-mass", "step-full"]; + + const runSuite = async (): Promise => { + if (running) return running; + running = (async () => { + panel.setCaptureEnabled(false); + panel.setStatus("benchmarking 128² and 256²…"); + summary.textContent = "Running correctness anchors and GPU timing samples. The UI remains still by design."; + const started = performance.now(); + const benchmarks: BenchmarkResult[] = []; + let verification128: VerificationResult | null = null; + for (const n of [128, 256]) { + setBoot(`building ${n}² probe…`); + const probe = await FlowLeniaM0Probe.create(device, n); + try { + if (n === 128) verification128 = await probe.verify(); + for (const mode of modes) { + setBoot(`benchmarking ${n}² ${mode}…`); + benchmarks.push(await probe.benchmark(mode, samples, warmup)); + } + } finally { + probe.destroy(); + } + } + if (!verification128) throw new Error("128² verification did not run"); + const desktop = benchmarks.find((row) => row.n === 256 && row.mode === "step-full"); + if (!desktop) throw new Error("256² full-state timing missing"); + const memory256 = completeEcosystemInventory(256).totalBytes; + const desktopPass = + desktop.p95Ms <= 33.3 && + memory256 < 128 * 2 ** 20 && + architectureFailures.length === 0 && + verificationPass(verification128); + const measuredDefault: 128 | 256 = desktopPass ? 256 : 128; + const decision = desktopPass + ? `The dominant-path full-state prototype is ${desktop.p95Ms.toFixed(2)} ms p95 at 256² and the projected complete allocation is ${(memory256 / 2 ** 20).toFixed(2)} MiB; 256² retains the desktop budget.` + : `256² does not retain every M0 gate (full-state p95 ${desktop.p95Ms.toFixed(2)} ms, projected ${(memory256 / 2 ** 20).toFixed(2)} MiB, architecture failures ${architectureFailures.length}); freeze 128².`; + const report: M0Report = { + schemaVersion: "flow-lenia-m0-v1", + generatedUtc: new Date().toISOString(), + userAgent: navigator.userAgent, + adapter: adapterInfo, + timestampQuery: device.features.has("timestamp-query"), + limits, + architectureFailures, + verification128, + benchmarks, + projectedMemory: [128, 256].map((n) => { + const bytes = completeEcosystemInventory(n).totalBytes; + return { n, bytes, mebibytes: bytes / 2 ** 20 }; + }), + measuredDefault, + decision, + }; + hook.latest = report; + renderReport(report); + panel.setDiagnostics([ + { label: "adapter", value: adapterInfo.description || adapterInfo.device }, + { label: "timing", value: report.timestampQuery ? "GPU timestamp query" : "queue completion" }, + { label: "FFT anchor max |Δ|", value: verification128.fftMaxAbs.toExponential(3) }, + { label: "full gather mass residual", value: verification128.fullMassRelativeResidual.toExponential(3) }, + { label: "projected 256² memory", value: `${(memory256 / 2 ** 20).toFixed(2)} MiB` }, + { label: "measured default", value: `${measuredDefault}²` }, + ]); + panel.setVerdict({ + gate: "M0 batched FFT + faithful dd=5 gather", + verdict: desktopPass ? "256² PASS" : "128² FALLBACK", + pass: verificationPass(verification128) && architectureFailures.length === 0, + }); + panel.setStatus(`benchmark ready — default ${measuredDefault}²`); + panel.setCaptureEnabled(true); + setBoot(""); + const wallSeconds = (performance.now() - started) / 1000; + resetCapture(); + exposeCapture(makeCapture(report, wallSeconds), { download: false }); + return report; + })().finally(() => { running = null; }); + return running; + }; + + panel = createSettingsPanel("Flow Lenia · M0", { + caption: "A measured architecture probe for batched spectral perception and conservative finite-square transport.", + initial: { tier: "test", seed: 42 }, + tiers: ["test"], + onCapture: async () => { await runSuite(); }, + modes: { initial: "study" }, + study: { + diagnostics: [ + { label: "adapter", value: adapterInfo.description || adapterInfo.device }, + { label: "timestamp query", value: String(device.features.has("timestamp-query")) }, + { label: "256² projected memory", value: `${(completeEcosystemInventory(256).totalBytes / 2 ** 20).toFixed(2)} MiB` }, + { label: "architecture blockers", value: architectureFailures.join(", ") || "none" }, + ], + honesty: { + faithful: "shared Stockham butterfly, C/K plane batching, exact finite-square dd=5 destination gather", + simplified: "spectral multiplier is identity and growth/pressure passes are not part of this M0 workload", + measured: "capture runs warm-up plus per-workload samples at both 128² and 256²", + }, + verdict: { + gate: "M0 compile + portable limits", + verdict: architectureFailures.length === 0 ? "READY" : "BLOCKED", + pass: architectureFailures.length === 0, + }, + links: [ + { label: "implementation ledger", href: "../../../../docs/sim-specs/continuous-ca/lenia/implementation-plan.md" }, + ], + }, + }); + + const hook: M0Hook = { + runSuite, + latest: null, + limits, + bindingInventory: BINDING_INVENTORY, + }; + (globalThis as typeof globalThis & { __flowLeniaM0?: M0Hook }).__flowLeniaM0 = hook; + summary.textContent = `Pipelines compiled on ${adapterInfo.description || adapterInfo.device}. Run the capture control to benchmark both tiers.`; + setBoot(""); + (globalThis as typeof globalThis & { __bitPhysicsReady?: boolean }).__bitPhysicsReady = true; +} + +void start().catch((error: unknown) => { + console.error(error); + setBoot(`Flow Lenia M0 failed: ${error instanceof Error ? error.message : String(error)}`); +}); diff --git a/packages/flow-lenia/web/src/main.ts b/packages/flow-lenia/web/src/main.ts new file mode 100644 index 00000000..d5d370c3 --- /dev/null +++ b/packages/flow-lenia/web/src/main.ts @@ -0,0 +1,12 @@ +const query = new URLSearchParams(location.search); +// Routed mode modules mount createSettingsPanel and publish exposeCapture only after WebGPU boot. + +if (query.get("m0") === "1") { + void import("./m0-app.js"); +} else if (query.get("arena") === "1") { + void import("./arena-app.js"); +} else if (query.get("ecosystem") === "1") { + void import("./ecosystem-app.js"); +} else { + void import("./app.js"); +} diff --git a/packages/flow-lenia/web/src/model/arena.ts b/packages/flow-lenia/web/src/model/arena.ts new file mode 100644 index 00000000..fbab1106 --- /dev/null +++ b/packages/flow-lenia/web/src/model/arena.ts @@ -0,0 +1,137 @@ +export const ARENA_SCHEMA_VERSION = "flow-lenia-arena-environment-v1" as const; + +export interface ArenaStorm { + startStep: number; + duration: number; + center: readonly [number, number]; + radius: number; + amplitude: number; +} + +export interface ArenaAttractor { + center: readonly [number, number]; + radius: number; + amplitude: number; + orbitRadius: number; + angularSpeed: number; + phase: number; +} + +export interface ArenaDynamics { + channelResponse: readonly [number, number, number]; + gateOpenStep: number; + gateCloseStep: number; + storm: ArenaStorm; + attractor: ArenaAttractor; +} + +export interface ArenaEnvironmentState { + schemaVersion: typeof ARENA_SCHEMA_VERSION; + /** Per-cell vec4: authored affinity, soft-wall affinity, closed-gate affinity, wall opacity. */ + field: Float32Array; + /** Per-cell authored region ID. Zero is unclassified; release cards use one through three. */ + regions: Uint32Array; + dynamics: ArenaDynamics; +} + +export type ArenaBrushMode = "affinity" | "wall" | "erase"; + +export interface ArenaBrushEvent { + row: number; + column: number; + radius: number; + strength: number; + mode: ArenaBrushMode; + atStep?: number; +} + +export const QUIET_STORM: ArenaStorm = { + startStep: 0, + duration: 0, + center: [0.5, 0.5], + radius: 0.2, + amplitude: 0, +}; + +export const QUIET_ATTRACTOR: ArenaAttractor = { + center: [0.5, 0.5], + radius: 0.15, + amplitude: 0, + orbitRadius: 0, + angularSpeed: 0, + phase: 0, +}; + +export function gateOpenAt(dynamics: ArenaDynamics, step: number): boolean { + if (dynamics.gateOpenStep < 0 || step < dynamics.gateOpenStep) return false; + return dynamics.gateCloseStep <= dynamics.gateOpenStep || step < dynamics.gateCloseStep; +} + +export function stormEnvelopeAt(storm: ArenaStorm, step: number): number { + if (storm.duration <= 0 || step < storm.startStep || step >= storm.startStep + storm.duration) return 0; + const phase = (step - storm.startStep + 0.5) / storm.duration; + return Math.sin(Math.PI * phase); +} + +export function attractorCenterAt(attractor: ArenaAttractor, step: number): readonly [number, number] { + const angle = attractor.phase + attractor.angularSpeed * step; + return [ + attractor.center[0] + attractor.orbitRadius * Math.cos(angle), + attractor.center[1] + attractor.orbitRadius * Math.sin(angle), + ]; +} + +function torusDelta(value: number): number { return value - Math.round(value); } + +/** CPU mirror of the Arena WGSL field evaluation, used by inspection and release gates. */ +export function arenaExternalAt( + field: Float32Array, + n: number, + row: number, + column: number, + dynamics: ArenaDynamics, + step: number, +): number { + const i = ((Math.floor(row) % n) + n) % n; + const j = ((Math.floor(column) % n) + n) % n; + const base = (i * n + j) * 4; + let value = (field[base] as number) + (field[base + 1] as number); + if (!gateOpenAt(dynamics, step)) value += field[base + 2] as number; + const x = (i + 0.5) / n; + const y = (j + 0.5) / n; + const stormEnvelope = stormEnvelopeAt(dynamics.storm, step); + if (stormEnvelope > 0 && dynamics.storm.amplitude !== 0) { + const dx = torusDelta(x - dynamics.storm.center[0]); + const dy = torusDelta(y - dynamics.storm.center[1]); + const radius = Math.max(dynamics.storm.radius, 1e-6); + const radial = Math.exp(-0.5 * (dx * dx + dy * dy) / (radius * radius)); + const angular = Math.cos(3 * Math.atan2(dy, dx) + 2 * Math.PI * stormEnvelope); + value += dynamics.storm.amplitude * stormEnvelope * radial * angular; + } + if (dynamics.attractor.amplitude !== 0) { + const center = attractorCenterAt(dynamics.attractor, step); + const dx = torusDelta(x - center[0]); + const dy = torusDelta(y - center[1]); + const radius = Math.max(dynamics.attractor.radius, 1e-6); + value += dynamics.attractor.amplitude * Math.exp(-0.5 * (dx * dx + dy * dy) / (radius * radius)); + } + return value; +} + +export function validateArenaEnvironment(state: ArenaEnvironmentState, n: number): void { + if (state.schemaVersion !== ARENA_SCHEMA_VERSION) throw new Error(`unsupported Arena environment schema: ${String(state.schemaVersion)}`); + if (state.field.length !== n * n * 4 || state.regions.length !== n * n) throw new Error("Arena environment has an invalid packed length"); + if (state.dynamics.channelResponse.length !== 3) throw new Error("Arena channel response must have exactly three entries"); + const finite = [ + ...state.dynamics.channelResponse, + state.dynamics.gateOpenStep, state.dynamics.gateCloseStep, + state.dynamics.storm.startStep, state.dynamics.storm.duration, ...state.dynamics.storm.center, + state.dynamics.storm.radius, state.dynamics.storm.amplitude, + ...state.dynamics.attractor.center, state.dynamics.attractor.radius, state.dynamics.attractor.amplitude, + state.dynamics.attractor.orbitRadius, state.dynamics.attractor.angularSpeed, state.dynamics.attractor.phase, + ]; + if (!finite.every(Number.isFinite)) throw new Error("Arena dynamics contain a non-finite value"); + if (state.dynamics.storm.radius <= 0 || state.dynamics.attractor.radius <= 0) throw new Error("Arena dynamic radii must be positive"); + for (const value of state.field) if (!Number.isFinite(value) || Math.abs(value) > 20) throw new Error("Arena field values must be finite and within [-20, 20]"); + for (const region of state.regions) if (region > 3) throw new Error("Arena release regions must be within [0, 3]"); +} diff --git a/packages/flow-lenia/web/src/model/config.ts b/packages/flow-lenia/web/src/model/config.ts new file mode 100644 index 00000000..fd7f5d6e --- /dev/null +++ b/packages/flow-lenia/web/src/model/config.ts @@ -0,0 +1,90 @@ +export const MODEL_VARIANT = "flow-lenia-ecosystem-v1" as const; +export const CHANNELS = 3; +export const KERNELS = 9; +export const GATHER_RADIUS = 5; +export const SQUARE_HALF_WIDTH = 0.65; +export const DT = 0.2; +export const DENSITY_THRESHOLD = 2.0; +export const DENSITY_EXPONENT = 2.0; +export const MAX_DISPLACEMENT = GATHER_RADIUS - SQUARE_HALF_WIDTH; + +export interface KernelSpec { + source: number; + target: number; + relativeRadius: number; + growthMean: number; + growthWidth: number; + weight: number; + ringCenters: readonly [number, number, number]; + ringAmplitudes: readonly [number, number, number]; + ringWidths: readonly [number, number, number]; +} + +const connections = [[0, 0], [0, 0], [0, 1], [1, 1], [1, 1], [1, 2], [2, 0], [2, 2], [2, 2]] as const; +const radii = [0.72, 0.48, 0.63, 0.70, 0.44, 0.61, 0.59, 0.68, 0.42] as const; +const means = [0.22, 0.31, 0.19, 0.24, 0.34, 0.17, 0.21, 0.26, 0.32] as const; +const widths = [0.070, 0.085, 0.060, 0.075, 0.090, 0.052, 0.065, 0.080, 0.088] as const; +const weights = [0.65, -0.18, 0.42, 0.61, -0.16, 0.38, 0.40, 0.58, -0.14] as const; +const centerSets = [[0.18, 0.50, 0.82], [0.27, 0.61, 0.89], [0.15, 0.46, 0.76]] as const; +const amplitudeSets = [[0.7, 1.0, 0.5], [1.0, 0.55, 0.25], [0.35, 1.0, 0.72]] as const; +const ringWidthSets = [[0.020, 0.035, 0.025], [0.025, 0.040, 0.020], [0.018, 0.030, 0.030]] as const; + +export const KERNEL_SPECS: readonly KernelSpec[] = connections.map(([source, target], index) => ({ + source, + target, + relativeRadius: radii[index] as number, + growthMean: means[index] as number, + growthWidth: widths[index] as number, + weight: weights[index] as number, + ringCenters: centerSets[index % 3] as readonly [number, number, number], + ringAmplitudes: amplitudeSets[index % 3] as readonly [number, number, number], + ringWidths: ringWidthSets[index % 3] as readonly [number, number, number], +})); + +export interface OrganismConfig { + n: number; + seed: number; + baseRadius: number; + radiusOffset: number; + cutoffSharpness: number; + dt: number; + gatherRadius: number; + squareHalfWidth: number; + densityThreshold: number; + densityExponent: number; + kernels: readonly KernelSpec[]; +} + +export function organismConfig(n: number, seed = 42): OrganismConfig { + if (n <= GATHER_RADIUS * 2 || !Number.isInteger(Math.log2(n))) { + throw new Error("Flow Lenia grid must be a power of two larger than the gather diameter"); + } + return { + n, + seed: seed >>> 0, + baseRadius: 10, + radiusOffset: 15, + cutoffSharpness: 10, + dt: DT, + gatherRadius: GATHER_RADIUS, + squareHalfWidth: SQUARE_HALF_WIDTH, + densityThreshold: DENSITY_THRESHOLD, + densityExponent: DENSITY_EXPONENT, + kernels: KERNEL_SPECS, + }; +} + +export function kernelParameterRecords(config: OrganismConfig): ArrayBuffer { + const raw = new ArrayBuffer(config.kernels.length * 32); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + config.kernels.forEach((kernel, index) => { + const offset = index * 8; + u32[offset] = kernel.source; + u32[offset + 1] = kernel.target; + f32[offset + 2] = kernel.growthMean; + f32[offset + 3] = kernel.growthWidth; + f32[offset + 4] = kernel.weight; + }); + return raw; +} diff --git a/packages/flow-lenia/web/src/model/ecosystem-solver.ts b/packages/flow-lenia/web/src/model/ecosystem-solver.ts new file mode 100644 index 00000000..9e3016cc --- /dev/null +++ b/packages/flow-lenia/web/src/model/ecosystem-solver.ts @@ -0,0 +1,888 @@ +import { BatchedFft2d } from "../fft-batch.js"; +import arenaEventsWgsl from "../shaders/arena_events.wgsl?raw"; +import arenaPerceiveWgsl from "../shaders/arena_perceive.wgsl?raw"; +import flowWgsl from "../shaders/ecosystem_flow.wgsl?raw"; +import mutationWgsl from "../shaders/ecosystem_mutation.wgsl?raw"; +import perceiveWgsl from "../shaders/ecosystem_perceive.wgsl?raw"; +import packWgsl from "../shaders/organism_pack.wgsl?raw"; +import spectralWgsl from "../shaders/organism_spectral.wgsl?raw"; +import gatherWgsl from "../shaders/reintegrate_ecosystem.wgsl?raw"; +import { CHANNELS, GATHER_RADIUS, KERNELS, kernelParameterRecords } from "./config.js"; +import type { OrganismConfig } from "./config.js"; +import { buildSpatialKernels, kernelSums } from "./kernels.js"; +import { arenaExternalAt, gateOpenAt, stormEnvelopeAt, validateArenaEnvironment } from "./arena.js"; +import type { ArenaBrushEvent, ArenaDynamics, ArenaEnvironmentState } from "./arena.js"; + +const LINEAR_WORKGROUP = 128; +const TILE = 8; +const MAX_MUTATIONS_PER_STEP = 16; +const MUTATION_RECORD_BYTES = 128; +const MAX_ENVIRONMENT_EVENTS_PER_STEP = 16; +const ENVIRONMENT_EVENT_RECORD_BYTES = 32; +const MIXED_LINEAGE = 0xffff_ffff; + +export type MixingRule = "average" | "whole" | "gene-wise" | "best" | "negotiation"; +export const MIXING_RULES: readonly MixingRule[] = ["average", "whole", "gene-wise", "best", "negotiation"]; + +const ruleIndex: Record = { + average: 0, + whole: 1, + "gene-wise": 2, + best: 3, + negotiation: 4, +}; + +export interface EcosystemInitialState { + mass: Float32Array; + h: Float32Array; + q: Float32Array; + identity: Uint32Array; +} + +export interface EcosystemReadback { + mass: Float32Array; + h: Float32Array; + q: Float32Array; + identity: Uint32Array; + perception: Float32Array; + growth: Float32Array; + affinity: Float32Array; + flow: Float32Array; + displacement: Float32Array; + clampMask: Float32Array; + environment?: Float32Array; + environmentRegions?: Uint32Array; +} + +export interface LineageEventRecord { + eventIndex: number; + parentLineage: number; + childLineage: number; + childFingerprint: readonly [number, number]; + step: number; + center: readonly [number, number]; + radius: number; + affectedMass: number; + deltaH: readonly number[]; + deltaQ: readonly number[]; +} + +export interface MutationPatchInput { + row: number; + column: number; + radius: number; + parentLineage: number; + scale?: number; + atStep?: number; +} + +interface ScheduledMutation extends LineageEventRecord { atStep: number } + +export interface EcosystemMetrics { + step: number; + totalMass: number; + channelMass: readonly [number, number, number]; + relativeMassDrift: number; + minDensity: number; + maxDensity: number; + occupiedFraction: number; + nonFinite: number; + negative: number; + maxDisplacement: number; + clampFraction: number; + activeLineages: number; + topLineage: number; + topLineageMass: number; + mixedIdentityMass: number; + shannonDiversity: number; + phenotypeClusters: number; + phenotypeShannon: number; + mutationEvents: number; + extinctionEvents: number; + lineageMasses: readonly { lineage: number; mass: number }[]; + regionMass?: readonly [number, number, number, number]; + environmentMin?: number; + environmentMax?: number; + wallFraction?: number; + gateOpen?: boolean; + stormAmplitude?: number; +} + +export interface EcosystemInspection { + cell: readonly [number, number]; + mass: readonly [number, number, number]; + density: number; + h: readonly number[]; + q: readonly number[]; + fingerprint: string; + lineage: number; + flags: number; + affinity: readonly [number, number, number]; + displacement: readonly [readonly [number, number], readonly [number, number], readonly [number, number]]; + environment?: number; + region?: number; +} + +export interface ArenaSolverOptions { environment: true } + +export interface ArenaPackedSnapshot { + schemaVersion: "flow-lenia-arena-snapshot-v1"; + n: number; + seed: number; + step: number; + mixingRule: MixingRule; + initialMass: number; + mass: Float32Array; + h: Float32Array; + q: Float32Array; + identity: Uint32Array; + environment: Float32Array; + regions: Uint32Array; + dynamics: ArenaDynamics; + lineageRing: LineageEventRecord[]; + nextMutationIndex: number; + extinctionEvents: number; +} + +export interface LineageGraph { + nodes: readonly { lineage: number; mass: number; active: boolean; mutationStep: number | null }[]; + edges: readonly { parent: number; child: number; step: number; affectedMass: number }[]; +} + +function storageEntry(binding: number, readOnly: boolean): GPUBindGroupLayoutEntry { + return { binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: readOnly ? "read-only-storage" : "storage" } }; +} + +function uniformEntry(binding = 0): GPUBindGroupLayoutEntry { + return { binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }; +} + +function splitmix64(input: bigint): bigint { + const mask = (1n << 64n) - 1n; + let value = (input + 0x9e37_79b9_7f4a_7c15n) & mask; + value = ((value ^ (value >> 30n)) * 0xbf58_476d_1ce4_e5b9n) & mask; + value = ((value ^ (value >> 27n)) * 0x94d0_49bb_1331_11ebn) & mask; + return (value ^ (value >> 31n)) & mask; +} + +function counterHash(seed: number, step: number, destination: number, candidate: number, gene: number): bigint { + const mask = (1n << 64n) - 1n; + let value = BigInt(seed >>> 0); + for (const component of [step, destination, candidate, gene]) value = splitmix64(value ^ (BigInt(component) & mask)); + return value; +} + +function unitFloat(value: bigint): number { + return Number((value >> 11n) & ((1n << 53n) - 1n)) * (1 / 2 ** 53) + 0.5 / 2 ** 53; +} + +function normalFromCounter(seed: number, eventIndex: number, gene: number, lane: number): number { + const u1 = unitFloat(counterHash(seed, eventIndex, gene, lane, 0)); + const u2 = unitFloat(counterHash(seed, eventIndex, gene, lane, 1)); + return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); +} + +function eventIdentity(seed: number, parentLineage: number, eventIndex: number, tag: number): bigint { + return counterHash(seed, eventIndex, parentLineage, tag, 0); +} + +function packMutations(events: readonly ScheduledMutation[]): ArrayBuffer { + const raw = new ArrayBuffer(MAX_MUTATIONS_PER_STEP * MUTATION_RECORD_BYTES); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + events.forEach((event, eventOffset) => { + const base = eventOffset * (MUTATION_RECORD_BYTES / 4); + u32[base] = event.center[0] >>> 0; + u32[base + 1] = event.center[1] >>> 0; + f32[base + 2] = event.radius; + u32[base + 3] = event.parentLineage >>> 0; + u32[base + 4] = event.childLineage >>> 0; + u32[base + 5] = event.childFingerprint[0] >>> 0; + u32[base + 6] = event.childFingerprint[1] >>> 0; + u32[base + 7] = event.eventIndex % 128; + for (let gene = 0; gene < KERNELS; gene += 1) { + f32[base + 8 + gene] = event.deltaH[gene] as number; + f32[base + 20 + gene] = event.deltaQ[gene] as number; + } + }); + return raw; +} + +interface ScheduledEnvironmentEvent extends ArenaBrushEvent { atStep: number; eventIndex: number } + +function packEnvironmentEvents(events: readonly ScheduledEnvironmentEvent[]): ArrayBuffer { + const raw = new ArrayBuffer(MAX_ENVIRONMENT_EVENTS_PER_STEP * ENVIRONMENT_EVENT_RECORD_BYTES); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + const mode = { affinity: 0, wall: 1, erase: 2 } as const; + events.forEach((event, eventOffset) => { + const base = eventOffset * (ENVIRONMENT_EVENT_RECORD_BYTES / 4); + f32[base] = event.row; + f32[base + 1] = event.column; + f32[base + 2] = event.radius; + f32[base + 3] = event.strength; + u32[base + 4] = mode[event.mode]; + }); + return raw; +} + +function cloneDynamics(dynamics: ArenaDynamics): ArenaDynamics { + return { + channelResponse: [...dynamics.channelResponse], + gateOpenStep: dynamics.gateOpenStep, + gateCloseStep: dynamics.gateCloseStep, + storm: { ...dynamics.storm, center: [...dynamics.storm.center] }, + attractor: { ...dynamics.attractor, center: [...dynamics.attractor.center] }, + }; +} + +export class FlowLeniaEcosystemSolver { + readonly n: number; + readonly n2: number; + readonly config: OrganismConfig; + readonly mass: [GPUBuffer, GPUBuffer]; + readonly genomeH: [GPUBuffer, GPUBuffer]; + readonly genomeQ: [GPUBuffer, GPUBuffer]; + readonly identity: [GPUBuffer, GPUBuffer]; + readonly affinity: GPUBuffer; + readonly transport: GPUBuffer; + readonly diagnostic: GPUBuffer; + readonly environment: GPUBuffer | null; + readonly environmentRegions: GPUBuffer | null; + readonly arenaEnabled: boolean; + readonly allocatedBytes: number; + readonly dispatchesPerStep: number; + readonly spatialKernelSums: number[]; + stepCount = 0; + + private readonly device: GPUDevice; + private readonly fft: BatchedFft2d; + private readonly complex: [GPUBuffer, GPUBuffer]; + private readonly kernelSpectrum: GPUBuffer; + private readonly kernelFields: GPUBuffer; + private readonly kernelParams: GPUBuffer; + private readonly gridUniform: GPUBuffer; + private readonly flowUniform: GPUBuffer; + private readonly gatherUniform: GPUBuffer; + private readonly mutationUniform: GPUBuffer; + private readonly mutationRecords: GPUBuffer; + private readonly mutationAffectedMass: GPUBuffer; + private readonly arenaUniform: GPUBuffer | null; + private readonly environmentEventUniform: GPUBuffer | null; + private readonly environmentEventRecords: GPUBuffer | null; + private readonly packPipeline: GPUComputePipeline; + private readonly spectralPipeline: GPUComputePipeline; + private readonly perceivePipeline: GPUComputePipeline; + private readonly flowPipeline: GPUComputePipeline; + private readonly gatherPipelines: Record; + private readonly mutationPipeline: GPUComputePipeline; + private readonly environmentEventPipeline: GPUComputePipeline | null; + private readonly packGroups: [GPUBindGroup, GPUBindGroup]; + private readonly spectralGroup: GPUBindGroup; + private readonly perceiveGroups: [GPUBindGroup, GPUBindGroup]; + private readonly flowGroups: [GPUBindGroup, GPUBindGroup]; + private readonly gatherGroups: [GPUBindGroup, GPUBindGroup]; + private readonly mutationGroups: [GPUBindGroup, GPUBindGroup]; + private readonly environmentEventGroup: GPUBindGroup | null; + private readonly ownedBuffers: GPUBuffer[]; + private readonly shaderModules: GPUShaderModule[]; + private ping = 0; + private initialMass = 0; + private pressureEnabled = true; + private squareHalfWidth: number; + private mixingRule: MixingRule; + private negotiationBeta = 1; + private nextMutationIndex = 1; + private mutationQueue: ScheduledMutation[] = []; + private lineageRing: LineageEventRecord[] = []; + private previousLineages = new Set(); + private extinctionEvents = 0; + private arenaDynamics: ArenaDynamics | null = null; + private environmentEventQueue: ScheduledEnvironmentEvent[] = []; + private nextEnvironmentEventIndex = 1; + + private constructor(device: GPUDevice, config: OrganismConfig, mixingRule: MixingRule, options?: ArenaSolverOptions) { + this.device = device; + this.config = config; + this.n = config.n; + this.n2 = config.n * config.n; + this.squareHalfWidth = config.squareHalfWidth; + this.mixingRule = mixingRule; + this.arenaEnabled = options?.environment === true; + const storageUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC; + const makeBuffer = (label: string, bytes: number, usage = storageUsage): GPUBuffer => device.createBuffer({ label: `flow-lenia-m4-${label}`, size: bytes, usage }); + const massBytes = this.n2 * 16; + const geneBytes = this.n2 * 48; + const complexBytes = KERNELS * this.n2 * 8; + this.mass = [makeBuffer("mass-a", massBytes), makeBuffer("mass-b", massBytes)]; + this.genomeH = [makeBuffer("h-a", geneBytes), makeBuffer("h-b", geneBytes)]; + this.genomeQ = [makeBuffer("q-a", geneBytes), makeBuffer("q-b", geneBytes)]; + this.identity = [makeBuffer("identity-a", massBytes), makeBuffer("identity-b", massBytes)]; + this.complex = [makeBuffer("complex-a", complexBytes), makeBuffer("complex-b", complexBytes)]; + this.kernelSpectrum = makeBuffer("kernel-spectra", complexBytes); + this.kernelFields = makeBuffer("kernel-fields", complexBytes); + this.affinity = makeBuffer("affinity", massBytes); + this.transport = makeBuffer("ecosystem-transport", this.n2 * 96); + this.diagnostic = makeBuffer("flow-diagnostic", this.n2 * 32); + this.kernelParams = makeBuffer("kernel-params", KERNELS * 32); + this.gridUniform = makeBuffer("grid-uniform", 16, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.flowUniform = makeBuffer("flow-uniform", 32, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.gatherUniform = makeBuffer("gather-uniform", 32, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.mutationUniform = makeBuffer("mutation-uniform", 16, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.mutationRecords = makeBuffer("mutation-records", MAX_MUTATIONS_PER_STEP * MUTATION_RECORD_BYTES); + this.mutationAffectedMass = makeBuffer("mutation-affected-mass", 128 * 4); + this.environment = this.arenaEnabled ? makeBuffer("arena-environment", massBytes) : null; + this.environmentRegions = this.arenaEnabled ? makeBuffer("arena-regions", this.n2 * 4) : null; + this.arenaUniform = this.arenaEnabled ? makeBuffer("arena-uniform", 96, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST) : null; + this.environmentEventUniform = this.arenaEnabled ? makeBuffer("arena-event-uniform", 16, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST) : null; + this.environmentEventRecords = this.arenaEnabled ? makeBuffer("arena-event-records", MAX_ENVIRONMENT_EVENTS_PER_STEP * ENVIRONMENT_EVENT_RECORD_BYTES) : null; + this.ownedBuffers = [ + ...this.mass, ...this.genomeH, ...this.genomeQ, ...this.identity, ...this.complex, + this.kernelSpectrum, this.kernelFields, this.affinity, this.transport, this.diagnostic, + this.kernelParams, this.gridUniform, this.flowUniform, this.gatherUniform, + this.mutationUniform, this.mutationRecords, this.mutationAffectedMass, + ...[this.environment, this.environmentRegions, this.arenaUniform, this.environmentEventUniform, this.environmentEventRecords].filter((buffer): buffer is GPUBuffer => buffer !== null), + ]; + this.dispatchesPerStep = 4 * Math.log2(this.n) + 5 + (this.arenaEnabled ? 1 : 0); + device.queue.writeBuffer(this.gridUniform, 0, new Uint32Array([this.n, this.n2, CHANNELS, KERNELS])); + device.queue.writeBuffer(this.kernelParams, 0, kernelParameterRecords(config)); + this.writeModelUniforms(); + this.fft = new BatchedFft2d(device, this.n, this.complex, [CHANNELS, KERNELS]); + this.allocatedBytes = this.ownedBuffers.reduce((sum, buffer) => sum + buffer.size, 0) + this.fft.allocatedBytes; + + const packLayout = device.createBindGroupLayout({ entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false)] }); + const spectralLayout = device.createBindGroupLayout({ entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, true), storageEntry(3, true), storageEntry(4, false)] }); + const perceiveLayout = device.createBindGroupLayout({ entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, true), storageEntry(3, true), storageEntry(4, false), storageEntry(5, false), ...(this.arenaEnabled ? [storageEntry(6, true)] : [])] }); + const flowLayout = device.createBindGroupLayout({ entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, true), storageEntry(3, true), storageEntry(4, false), storageEntry(5, false)] }); + const gatherLayout = device.createBindGroupLayout({ + label: "flow-lenia-m4-eight-storage-gather-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false), storageEntry(3, true), storageEntry(4, false), storageEntry(5, true), storageEntry(6, false), storageEntry(7, true), storageEntry(8, false)], + }); + const mutationLayout = device.createBindGroupLayout({ entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false), storageEntry(3, false), storageEntry(4, false), storageEntry(5, true), storageEntry(6, false)] }); + const environmentEventLayout = this.arenaEnabled ? device.createBindGroupLayout({ entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false)] }) : null; + const modules = { + pack: device.createShaderModule({ code: packWgsl }), + spectral: device.createShaderModule({ code: spectralWgsl }), + perceive: device.createShaderModule({ code: this.arenaEnabled ? arenaPerceiveWgsl : perceiveWgsl }), + flow: device.createShaderModule({ code: flowWgsl }), + gather: device.createShaderModule({ code: gatherWgsl }), + mutation: device.createShaderModule({ code: mutationWgsl }), + environmentEvents: device.createShaderModule({ code: arenaEventsWgsl }), + }; + this.shaderModules = Object.values(modules); + this.packPipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [packLayout] }), compute: { module: modules.pack, entryPoint: "pack_mass" } }); + this.spectralPipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [spectralLayout] }), compute: { module: modules.spectral, entryPoint: "expand_spectra" } }); + this.perceivePipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [perceiveLayout] }), compute: { module: modules.perceive, entryPoint: this.arenaEnabled ? "perceive_arena" : "perceive_ecosystem" } }); + this.flowPipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [flowLayout] }), compute: { module: modules.flow, entryPoint: "compute_ecosystem_flow" } }); + const gatherPipeline = (rule: MixingRule): GPUComputePipeline => device.createComputePipeline({ + label: `flow-lenia-m4-gather-${rule}`, + layout: device.createPipelineLayout({ bindGroupLayouts: [gatherLayout] }), + compute: { module: modules.gather, entryPoint: "gather_ecosystem", constants: { MIXING_RULE: ruleIndex[rule] } }, + }); + this.gatherPipelines = Object.fromEntries(MIXING_RULES.map((rule) => [rule, gatherPipeline(rule)])) as unknown as Record; + this.mutationPipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [mutationLayout] }), compute: { module: modules.mutation, entryPoint: "apply_mutation_patches" } }); + this.environmentEventPipeline = this.arenaEnabled && environmentEventLayout + ? device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [environmentEventLayout] }), compute: { module: modules.environmentEvents, entryPoint: "apply_environment_events" } }) + : null; + + const entries = (layout: GPUBindGroupLayout, resources: GPUBuffer[]): GPUBindGroup => device.createBindGroup({ layout, entries: resources.map((buffer, binding) => ({ binding, resource: { buffer } })) }); + this.packGroups = this.mass.map((mass) => entries(packLayout, [this.gridUniform, mass, this.complex[0]])) as [GPUBindGroup, GPUBindGroup]; + this.spectralGroup = entries(spectralLayout, [this.gridUniform, this.complex[0], this.kernelSpectrum, this.kernelParams, this.complex[1]]); + this.perceiveGroups = [0, 1].map((ping) => entries(perceiveLayout, [this.arenaUniform ?? this.gridUniform, this.complex[1], this.kernelParams, this.genomeH[ping] as GPUBuffer, this.kernelFields, this.affinity, ...(this.environment ? [this.environment] : [])])) as [GPUBindGroup, GPUBindGroup]; + this.flowGroups = [0, 1].map((ping) => entries(flowLayout, [this.flowUniform, this.mass[ping] as GPUBuffer, this.affinity, this.kernelFields, this.transport, this.diagnostic])) as [GPUBindGroup, GPUBindGroup]; + this.gatherGroups = [ + entries(gatherLayout, [this.gatherUniform, this.transport, this.mass[1], this.genomeH[0], this.genomeH[1], this.genomeQ[0], this.genomeQ[1], this.identity[0], this.identity[1]]), + entries(gatherLayout, [this.gatherUniform, this.transport, this.mass[0], this.genomeH[1], this.genomeH[0], this.genomeQ[1], this.genomeQ[0], this.identity[1], this.identity[0]]), + ]; + this.mutationGroups = [0, 1].map((ping) => entries(mutationLayout, [this.mutationUniform, this.mutationRecords, this.genomeH[ping] as GPUBuffer, this.genomeQ[ping] as GPUBuffer, this.identity[ping] as GPUBuffer, this.mass[ping] as GPUBuffer, this.mutationAffectedMass])) as [GPUBindGroup, GPUBindGroup]; + this.environmentEventGroup = this.arenaEnabled && environmentEventLayout && this.environmentEventUniform && this.environmentEventRecords && this.environment + ? entries(environmentEventLayout, [this.environmentEventUniform, this.environmentEventRecords, this.environment]) + : null; + const spatial = buildSpatialKernels(config); + this.spatialKernelSums = kernelSums(spatial, this.n, KERNELS); + device.queue.writeBuffer(this.complex[0], 0, spatial as unknown as BufferSource); + } + + static async create(device: GPUDevice, config: OrganismConfig, mixingRule: MixingRule = "whole", options?: ArenaSolverOptions): Promise { + device.pushErrorScope("validation"); + const solver = new FlowLeniaEcosystemSolver(device, config, mixingRule, options); + const encoder = device.createCommandEncoder({ label: "flow-lenia-m4-kernel-spectrum-build" }); + const pass = encoder.beginComputePass(); + solver.fft.resetPing(0); + solver.fft.encode2d(pass, KERNELS, -1); + pass.end(); + encoder.copyBufferToBuffer(solver.fft.currentBuffer, 0, solver.kernelSpectrum, 0, solver.kernelSpectrum.size); + device.queue.submit([encoder.finish()]); + await device.queue.onSubmittedWorkDone(); + const reports = await Promise.all([solver.fft.module, ...solver.shaderModules].map((module) => module.getCompilationInfo())); + const errors = reports.flatMap((report) => report.messages).filter((message) => message.type === "error"); + const validation = await device.popErrorScope(); + if (validation || errors.length > 0) { + solver.destroy(); + const detail = errors.map((message) => `${message.lineNum}:${message.linePos} ${message.message}`).join("\n"); + throw new Error(`${solver.arenaEnabled ? "M5" : "M4"} shader validation failed: ${validation?.message ?? detail}`); + } + return solver; + } + + private writeModelUniforms(): void { + const flowRaw = new ArrayBuffer(32); + const flowU32 = new Uint32Array(flowRaw); + const flowF32 = new Float32Array(flowRaw); + flowU32.set([this.n, this.n2, CHANNELS, 0]); + flowF32.set([this.config.dt, this.pressureEnabled ? this.config.densityThreshold : 1e9, this.config.densityExponent, GATHER_RADIUS - this.squareHalfWidth], 4); + this.device.queue.writeBuffer(this.flowUniform, 0, flowRaw); + } + + private writeGatherUniform(): void { + const raw = new ArrayBuffer(32); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + u32.set([this.n, CHANNELS, GATHER_RADIUS, this.stepCount]); + f32[4] = this.squareHalfWidth; + f32[5] = this.negotiationBeta; + u32[6] = this.config.seed >>> 0; + this.device.queue.writeBuffer(this.gatherUniform, 0, raw); + } + + private writeArenaUniform(): void { + if (!this.arenaUniform || !this.arenaDynamics) return; + const raw = new ArrayBuffer(96); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + u32.set([this.n, this.n2, CHANNELS, KERNELS]); + f32.set([...this.arenaDynamics.channelResponse, 0], 4); + f32[8] = gateOpenAt(this.arenaDynamics, this.stepCount) ? 1 : 0; + f32[9] = this.stepCount; + f32[10] = this.arenaDynamics.storm.startStep; + f32[11] = this.arenaDynamics.storm.duration; + f32.set([...this.arenaDynamics.storm.center, this.arenaDynamics.storm.radius, this.arenaDynamics.storm.amplitude], 12); + f32.set([...this.arenaDynamics.attractor.center, this.arenaDynamics.attractor.radius, this.arenaDynamics.attractor.amplitude], 16); + f32.set([this.arenaDynamics.attractor.orbitRadius, this.arenaDynamics.attractor.angularSpeed, this.arenaDynamics.attractor.phase, 1], 20); + this.device.queue.writeBuffer(this.arenaUniform, 0, raw); + } + + setMixingRule(rule: MixingRule): void { this.mixingRule = rule; } + getMixingRule(): MixingRule { return this.mixingRule; } + setNegotiationBeta(value: number): void { + if (!Number.isFinite(value) || value < 0 || value > 20) throw new Error("negotiation beta must be within [0, 20]"); + this.negotiationBeta = value; + } + setPressureEnabled(enabled: boolean): void { this.pressureEnabled = enabled; this.writeModelUniforms(); } + setSquareHalfWidth(value: number): void { + if (!Number.isFinite(value) || value < 0.3 || value > 1.25) throw new Error("square half-width must be within [0.3, 1.25]"); + this.squareHalfWidth = value; + this.writeModelUniforms(); + } + + reset(state: EcosystemInitialState, arena?: ArenaEnvironmentState): void { + if (state.mass.length !== this.n2 * 4 || state.h.length !== this.n2 * 12 || state.q.length !== this.n2 * 12 || state.identity.length !== this.n2 * 4) throw new Error("ecosystem state has an invalid packed length"); + this.initialMass = 0; + for (let cell = 0; cell < this.n2; cell += 1) this.initialMass += (state.mass[cell * 4] as number) + (state.mass[cell * 4 + 1] as number) + (state.mass[cell * 4 + 2] as number); + for (const buffer of this.mass) this.device.queue.writeBuffer(buffer, 0, state.mass as unknown as BufferSource); + for (const buffer of this.genomeH) this.device.queue.writeBuffer(buffer, 0, state.h as unknown as BufferSource); + for (const buffer of this.genomeQ) this.device.queue.writeBuffer(buffer, 0, state.q as unknown as BufferSource); + for (const buffer of this.identity) this.device.queue.writeBuffer(buffer, 0, state.identity as unknown as BufferSource); + this.ping = 0; + this.stepCount = 0; + this.nextMutationIndex = 1; + this.mutationQueue = []; + this.lineageRing = []; + this.previousLineages = new Set(); + this.extinctionEvents = 0; + this.environmentEventQueue = []; + this.nextEnvironmentEventIndex = 1; + this.device.queue.writeBuffer(this.mutationAffectedMass, 0, new Uint32Array(128)); + if (this.arenaEnabled) { + if (!arena || !this.environment || !this.environmentRegions) throw new Error("Arena solver reset requires a versioned environment"); + validateArenaEnvironment(arena, this.n); + this.arenaDynamics = cloneDynamics(arena.dynamics); + this.device.queue.writeBuffer(this.environment, 0, arena.field as unknown as BufferSource); + this.device.queue.writeBuffer(this.environmentRegions, 0, arena.regions as unknown as BufferSource); + this.writeArenaUniform(); + } else if (arena) throw new Error("Ecosystem solver was not created with Arena environment resources"); + } + + queueEnvironmentEvent(input: ArenaBrushEvent): void { + if (!this.arenaEnabled) throw new Error("environment tools require an Arena solver"); + if (!Number.isFinite(input.row) || !Number.isFinite(input.column) || !Number.isFinite(input.radius) || !Number.isFinite(input.strength)) throw new Error("environment event values must be finite"); + const event: ScheduledEnvironmentEvent = { + ...input, + row: ((input.row % this.n) + this.n) % this.n, + column: ((input.column % this.n) + this.n) % this.n, + radius: Math.max(1, Math.min(this.n / 3, input.radius)), + strength: Math.max(-2, Math.min(2, input.strength)), + atStep: Math.max(this.stepCount, input.atStep ?? this.stepCount), + eventIndex: this.nextEnvironmentEventIndex++, + }; + this.environmentEventQueue.push(event); + this.environmentEventQueue.sort((a, b) => a.atStep - b.atStep || a.eventIndex - b.eventIndex); + } + + getArenaDynamics(): ArenaDynamics | null { return this.arenaDynamics ? cloneDynamics(this.arenaDynamics) : null; } + getGateOpen(): boolean { return this.arenaDynamics ? gateOpenAt(this.arenaDynamics, this.stepCount) : false; } + getStormAmplitude(): number { return this.arenaDynamics ? this.arenaDynamics.storm.amplitude * stormEnvelopeAt(this.arenaDynamics.storm, this.stepCount) : 0; } + + queueMutation(input: MutationPatchInput): LineageEventRecord { + const eventIndex = this.nextMutationIndex++; + const scale = Math.max(0, Math.min(0.5, input.scale ?? 0.05)); + const parent = input.parentLineage >>> 0; + let childLineage = Number(eventIdentity(this.config.seed, parent, eventIndex, 0) & 0xffff_fffen) >>> 0; + if (childLineage === 0) childLineage = 2; + let fingerprint = eventIdentity(this.config.seed, parent, eventIndex, 1); + if (fingerprint === 0n) fingerprint = 1n; + const deltaH = Array.from({ length: KERNELS }, (_, gene) => Math.fround(scale * normalFromCounter(this.config.seed, eventIndex, gene, 0))); + const deltaQ = Array.from({ length: KERNELS }, (_, gene) => Math.fround(scale * normalFromCounter(this.config.seed, eventIndex, gene, 1))); + const record: ScheduledMutation = { + eventIndex, + parentLineage: parent, + childLineage, + childFingerprint: [Number(fingerprint & 0xffff_ffffn) >>> 0, Number(fingerprint >> 32n) >>> 0], + step: Math.max(this.stepCount, input.atStep ?? this.stepCount), + atStep: Math.max(this.stepCount, input.atStep ?? this.stepCount), + center: [((Math.floor(input.row) % this.n) + this.n) % this.n, ((Math.floor(input.column) % this.n) + this.n) % this.n], + radius: Math.max(1, Math.min(this.n / 3, input.radius)), + affectedMass: 0, + deltaH, + deltaQ, + }; + this.mutationQueue.push(record); + this.mutationQueue.sort((a, b) => a.atStep - b.atStep || a.eventIndex - b.eventIndex); + return record; + } + + getLineageRing(): readonly LineageEventRecord[] { return this.lineageRing; } + + step(count = 1): void { + if (!Number.isInteger(count) || count < 1) throw new Error("step count must be a positive integer"); + for (let iteration = 0; iteration < count; iteration += 1) { + const due = this.mutationQueue.filter((event) => event.atStep === this.stepCount).slice(0, MAX_MUTATIONS_PER_STEP); + const dueEnvironment = this.environmentEventQueue.filter((event) => event.atStep <= this.stepCount).slice(0, MAX_ENVIRONMENT_EVENTS_PER_STEP); + if (due.length > 0) { + this.device.queue.writeBuffer(this.mutationRecords, 0, packMutations(due)); + this.device.queue.writeBuffer(this.mutationUniform, 0, new Uint32Array([this.n, due.length, 0, 0])); + for (const event of due) this.device.queue.writeBuffer(this.mutationAffectedMass, (event.eventIndex % 128) * 4, new Uint32Array([0])); + } + if (dueEnvironment.length > 0 && this.environmentEventRecords && this.environmentEventUniform) { + this.device.queue.writeBuffer(this.environmentEventRecords, 0, packEnvironmentEvents(dueEnvironment)); + this.device.queue.writeBuffer(this.environmentEventUniform, 0, new Uint32Array([this.n, this.n2, dueEnvironment.length, 0])); + } + this.writeGatherUniform(); + this.writeArenaUniform(); + const encoder = this.device.createCommandEncoder({ label: `flow-lenia-${this.arenaEnabled ? "m5" : "m4"}-step-${this.stepCount}` }); + const pass = encoder.beginComputePass(); + if (dueEnvironment.length > 0 && this.environmentEventPipeline && this.environmentEventGroup) { + pass.setPipeline(this.environmentEventPipeline); + pass.setBindGroup(0, this.environmentEventGroup); + pass.dispatchWorkgroups(Math.ceil(this.n2 / LINEAR_WORKGROUP)); + } + if (due.length > 0) { + pass.setPipeline(this.mutationPipeline); + pass.setBindGroup(0, this.mutationGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil(this.n2 / LINEAR_WORKGROUP)); + } + if (dueEnvironment.length > 0) { + const ids = new Set(dueEnvironment.map((event) => event.eventIndex)); + this.environmentEventQueue = this.environmentEventQueue.filter((event) => !ids.has(event.eventIndex)); + } + this.fft.resetPing(0); + pass.setPipeline(this.packPipeline); + pass.setBindGroup(0, this.packGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil((CHANNELS * this.n2) / LINEAR_WORKGROUP)); + this.fft.encode2d(pass, CHANNELS, -1); + pass.setPipeline(this.spectralPipeline); + pass.setBindGroup(0, this.spectralGroup); + pass.dispatchWorkgroups(Math.ceil((KERNELS * this.n2) / LINEAR_WORKGROUP)); + this.fft.swapAfterExternalWrite(); + this.fft.encode2d(pass, KERNELS, 1); + pass.setPipeline(this.perceivePipeline); + pass.setBindGroup(0, this.perceiveGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil(this.n2 / LINEAR_WORKGROUP)); + pass.setPipeline(this.flowPipeline); + pass.setBindGroup(0, this.flowGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil(this.n / TILE), Math.ceil(this.n / TILE)); + pass.setPipeline(this.gatherPipelines[this.mixingRule]); + pass.setBindGroup(0, this.gatherGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil(this.n / TILE), Math.ceil(this.n / TILE)); + pass.end(); + this.device.queue.submit([encoder.finish()]); + if (due.length > 0) { + const ids = new Set(due.map((event) => event.eventIndex)); + this.mutationQueue = this.mutationQueue.filter((event) => !ids.has(event.eventIndex)); + this.lineageRing.push(...due); + if (this.lineageRing.length > 128) this.lineageRing.splice(0, this.lineageRing.length - 128); + } + this.ping = 1 - this.ping; + this.stepCount += 1; + } + } + + get currentMassBuffer(): GPUBuffer { return this.mass[this.ping]; } + get currentHBuffer(): GPUBuffer { return this.genomeH[this.ping]; } + get currentQBuffer(): GPUBuffer { return this.genomeQ[this.ping]; } + get currentIdentityBuffer(): GPUBuffer { return this.identity[this.ping]; } + get currentPing(): number { return this.ping; } + + private async readBuffers(requests: Array<[GPUBuffer, number]>): Promise { + const staging = requests.map(([, bytes]) => this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ })); + const encoder = this.device.createCommandEncoder({ label: "flow-lenia-m4-readback" }); + requests.forEach(([source, bytes], index) => encoder.copyBufferToBuffer(source, 0, staging[index] as GPUBuffer, 0, bytes)); + this.device.queue.submit([encoder.finish()]); + await Promise.all(staging.map((buffer) => buffer.mapAsync(GPUMapMode.READ))); + const output = staging.map((buffer) => buffer.getMappedRange().slice(0)); + staging.forEach((buffer) => { buffer.unmap(); buffer.destroy(); }); + return output; + } + + async readback(): Promise { + const requests: Array<[GPUBuffer, number]> = [ + [this.currentMassBuffer, this.n2 * 16], [this.currentHBuffer, this.n2 * 48], [this.currentQBuffer, this.n2 * 48], [this.currentIdentityBuffer, this.n2 * 16], + [this.kernelFields, KERNELS * this.n2 * 8], [this.affinity, this.n2 * 16], [this.transport, this.n2 * 96], [this.diagnostic, this.n2 * 32], + ]; + if (this.environment && this.environmentRegions) requests.push([this.environment, this.n2 * 16], [this.environmentRegions, this.n2 * 4]); + const [massRaw, hRaw, qRaw, identityRaw, kernelRaw, affinityRaw, transportRaw, diagnosticRaw, environmentRaw, regionsRaw] = await this.readBuffers(requests); + const massPacked = new Float32Array(massRaw); + const hPacked = new Float32Array(hRaw); + const qPacked = new Float32Array(qRaw); + const kernelPacked = new Float32Array(kernelRaw); + const affinityPacked = new Float32Array(affinityRaw); + const transportPacked = new Float32Array(transportRaw); + const diagnosticPacked = new Float32Array(diagnosticRaw); + const mass = new Float32Array(CHANNELS * this.n2); + const h = new Float32Array(KERNELS * this.n2); + const q = new Float32Array(KERNELS * this.n2); + const perception = new Float32Array(KERNELS * this.n2); + const growth = new Float32Array(KERNELS * this.n2); + const affinity = new Float32Array(CHANNELS * this.n2); + const flow = new Float32Array(CHANNELS * 2 * this.n2); + const displacement = new Float32Array(CHANNELS * 2 * this.n2); + const clampMask = new Float32Array(CHANNELS * this.n2); + for (let cell = 0; cell < this.n2; cell += 1) { + for (let channel = 0; channel < CHANNELS; channel += 1) { + const plane = channel * this.n2 + cell; + mass[plane] = massPacked[cell * 4 + channel] as number; + affinity[plane] = affinityPacked[cell * 4 + channel] as number; + clampMask[plane] = diagnosticPacked[cell * 8 + 4 + channel] as number; + for (let axis = 0; axis < 2; axis += 1) { + const vector = (channel * 2 + axis) * this.n2 + cell; + const value = transportPacked[cell * 24 + (axis === 0 ? 4 : 8) + channel] as number; + displacement[vector] = value; + flow[vector] = value / this.config.dt; + } + } + for (let gene = 0; gene < KERNELS; gene += 1) { + h[gene * this.n2 + cell] = hPacked[cell * 12 + gene] as number; + q[gene * this.n2 + cell] = qPacked[cell * 12 + gene] as number; + } + } + for (let index = 0; index < KERNELS * this.n2; index += 1) { + perception[index] = kernelPacked[index * 2] as number; + growth[index] = kernelPacked[index * 2 + 1] as number; + } + return { + mass, h, q, identity: new Uint32Array(identityRaw), perception, growth, affinity, flow, displacement, clampMask, + ...(environmentRaw ? { environment: new Float32Array(environmentRaw) } : {}), + ...(regionsRaw ? { environmentRegions: new Uint32Array(regionsRaw) } : {}), + }; + } + + async inspect(row: number, column: number): Promise { + const i = ((Math.floor(row) % this.n) + this.n) % this.n; + const j = ((Math.floor(column) % this.n) + this.n) % this.n; + const cell = i * this.n + j; + const staging = this.device.createBuffer({ size: this.arenaEnabled ? 260 : 240, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); + const encoder = this.device.createCommandEncoder({ label: "flow-lenia-m4-cell-inspection" }); + encoder.copyBufferToBuffer(this.currentMassBuffer, cell * 16, staging, 0, 16); + encoder.copyBufferToBuffer(this.currentHBuffer, cell * 48, staging, 16, 48); + encoder.copyBufferToBuffer(this.currentQBuffer, cell * 48, staging, 64, 48); + encoder.copyBufferToBuffer(this.currentIdentityBuffer, cell * 16, staging, 112, 16); + encoder.copyBufferToBuffer(this.affinity, cell * 16, staging, 128, 16); + encoder.copyBufferToBuffer(this.transport, cell * 96, staging, 144, 96); + if (this.environment && this.environmentRegions) { + encoder.copyBufferToBuffer(this.environment, cell * 16, staging, 240, 16); + encoder.copyBufferToBuffer(this.environmentRegions, cell * 4, staging, 256, 4); + } + this.device.queue.submit([encoder.finish()]); + await staging.mapAsync(GPUMapMode.READ); + const raw = staging.getMappedRange().slice(0); + staging.unmap(); + staging.destroy(); + const massPacked = new Float32Array(raw, 0, 4); + const hPacked = new Float32Array(raw, 16, 12); + const qPacked = new Float32Array(raw, 64, 12); + const identity = new Uint32Array(raw, 112, 4); + const affinity = new Float32Array(raw, 128, 4); + const transport = new Float32Array(raw, 144, 24); + const environment = this.arenaEnabled ? new Float32Array(raw, 240, 4) : null; + const regions = this.arenaEnabled ? new Uint32Array(raw, 256, 1) : null; + const mass = [massPacked[0], massPacked[1], massPacked[2]] as [number, number, number]; + const fingerprint = `${(identity[1] as number).toString(16).padStart(8, "0")}${(identity[0] as number).toString(16).padStart(8, "0")}`; + return { + cell: [i, j], mass, density: mass[0] + mass[1] + mass[2], + h: Array.from(hPacked.slice(0, KERNELS)), q: Array.from(qPacked.slice(0, KERNELS)), fingerprint, + lineage: identity[2] as number, flags: identity[3] as number, + affinity: [affinity[0], affinity[1], affinity[2]], + displacement: [[transport[4], transport[8]], [transport[5], transport[9]], [transport[6], transport[10]]], + ...(environment ? { environment: (environment[0] as number) + (environment[1] as number) + (this.getGateOpen() ? 0 : environment[2] as number) } : {}), + ...(regions ? { region: regions[0] as number } : {}), + }; + } + + async metrics(): Promise { + const state = await this.readback(); + const channelMass: [number, number, number] = [0, 0, 0]; + const lineageMass = new Map(); + const phenotypeMass = new Map(); + let minDensity = Number.POSITIVE_INFINITY; + let maxDensity = 0; + let occupied = 0; + let nonFinite = 0; + let negative = 0; + let maxDisplacement = 0; + let clamped = 0; + let mixedIdentityMass = 0; + const regionMass: [number, number, number, number] = [0, 0, 0, 0]; + let environmentMin = Number.POSITIVE_INFINITY; + let environmentMax = Number.NEGATIVE_INFINITY; + let wallCells = 0; + for (let cell = 0; cell < this.n2; cell += 1) { + let density = 0; + for (let channel = 0; channel < CHANNELS; channel += 1) { + const value = state.mass[channel * this.n2 + cell] as number; + if (!Number.isFinite(value)) nonFinite += 1; + if (value < 0) negative += 1; + channelMass[channel] += value; + density += value; + clamped += state.clampMask[channel * this.n2 + cell] as number; + for (let axis = 0; axis < 2; axis += 1) maxDisplacement = Math.max(maxDisplacement, Math.abs(state.displacement[(channel * 2 + axis) * this.n2 + cell] as number)); + } + minDensity = Math.min(minDensity, density); + maxDensity = Math.max(maxDensity, density); + if (state.environmentRegions) { + const region = Math.min(3, state.environmentRegions[cell] as number); + regionMass[region] += density; + } + if (state.environment && this.arenaDynamics) { + const row = Math.floor(cell / this.n); + const column = cell % this.n; + const external = arenaExternalAt(state.environment, this.n, row, column, this.arenaDynamics, this.stepCount); + environmentMin = Math.min(environmentMin, external); + environmentMax = Math.max(environmentMax, external); + if ((state.environment[cell * 4 + 3] as number) > 0.05) wallCells += 1; + } + if (density > 1e-3) occupied += 1; + if (density > 1e-8) { + const lineage = state.identity[cell * 4 + 2] as number; + if (lineage === MIXED_LINEAGE) mixedIdentityMass += density; + else if (lineage !== 0) lineageMass.set(lineage, (lineageMass.get(lineage) ?? 0) + density); + const key = [0, 4, 8].map((gene) => Math.round((state.h[gene * this.n2 + cell] as number) * 8)).concat([0, 4, 8].map((gene) => Math.round((state.q[gene * this.n2 + cell] as number) * 8))).join(":"); + phenotypeMass.set(key, (phenotypeMass.get(key) ?? 0) + density); + } + } + const totalMass = channelMass[0] + channelMass[1] + channelMass[2]; + const diversity = (masses: Iterable): number => { + let entropy = 0; + for (const value of masses) { const p = value / Math.max(totalMass, 1e-30); if (p > 0) entropy -= p * Math.log(p); } + return entropy; + }; + let topLineage = 0; + let topLineageMass = 0; + for (const [lineage, value] of lineageMass) if (value > topLineageMass || (value === topLineageMass && lineage < topLineage)) { topLineage = lineage; topLineageMass = value; } + const currentLineages = new Set(lineageMass.keys()); + if (this.previousLineages.size > 0) for (const lineage of this.previousLineages) if (!currentLineages.has(lineage)) this.extinctionEvents += 1; + this.previousLineages = currentLineages; + if (this.lineageRing.length > 0) { + const [affectedRaw] = await this.readBuffers([[this.mutationAffectedMass, 128 * 4]]); + const affected = new Uint32Array(affectedRaw as ArrayBuffer); + for (const event of this.lineageRing) event.affectedMass = (affected[event.eventIndex % 128] as number) / 65536; + } + const lineageMasses = [...lineageMass.entries()].map(([lineage, mass]) => ({ lineage, mass })).sort((a, b) => b.mass - a.mass || a.lineage - b.lineage); + return { + step: this.stepCount, totalMass, channelMass, + relativeMassDrift: Math.abs(totalMass - this.initialMass) / Math.max(this.initialMass, 1e-30), + minDensity, maxDensity, occupiedFraction: occupied / this.n2, nonFinite, negative, maxDisplacement, + clampFraction: clamped / (CHANNELS * this.n2), activeLineages: lineageMass.size, topLineage, topLineageMass, + mixedIdentityMass, shannonDiversity: diversity(lineageMass.values()), phenotypeClusters: phenotypeMass.size, + phenotypeShannon: diversity(phenotypeMass.values()), mutationEvents: this.lineageRing.length, extinctionEvents: this.extinctionEvents, + lineageMasses, + ...(state.environment ? { + regionMass, + environmentMin, + environmentMax, + wallFraction: wallCells / this.n2, + gateOpen: this.getGateOpen(), + stormAmplitude: this.getStormAmplitude(), + } : {}), + }; + } + + lineageGraph(metrics: EcosystemMetrics): LineageGraph { + const masses = new Map(metrics.lineageMasses.map((record) => [record.lineage, record.mass])); + const mutationStep = new Map(); + const ids = new Set(masses.keys()); + for (const event of this.lineageRing) { + ids.add(event.parentLineage); + ids.add(event.childLineage); + mutationStep.set(event.childLineage, event.step); + } + return { + nodes: [...ids].sort((a, b) => a - b).map((lineage) => ({ lineage, mass: masses.get(lineage) ?? 0, active: masses.has(lineage), mutationStep: mutationStep.get(lineage) ?? null })), + edges: this.lineageRing.map((event) => ({ parent: event.parentLineage, child: event.childLineage, step: event.step, affectedMass: event.affectedMass })), + }; + } + + async packedSnapshot(): Promise { + if (!this.environment || !this.environmentRegions || !this.arenaDynamics) throw new Error("packed Arena snapshots require an Arena solver"); + const [massRaw, hRaw, qRaw, identityRaw, environmentRaw, regionsRaw] = await this.readBuffers([ + [this.currentMassBuffer, this.n2 * 16], [this.currentHBuffer, this.n2 * 48], [this.currentQBuffer, this.n2 * 48], + [this.currentIdentityBuffer, this.n2 * 16], [this.environment, this.n2 * 16], [this.environmentRegions, this.n2 * 4], + ]); + return { + schemaVersion: "flow-lenia-arena-snapshot-v1", + n: this.n, + seed: this.config.seed, + step: this.stepCount, + mixingRule: this.mixingRule, + initialMass: this.initialMass, + mass: new Float32Array(massRaw), + h: new Float32Array(hRaw), + q: new Float32Array(qRaw), + identity: new Uint32Array(identityRaw), + environment: new Float32Array(environmentRaw), + regions: new Uint32Array(regionsRaw), + dynamics: cloneDynamics(this.arenaDynamics), + lineageRing: this.lineageRing.map((event) => ({ ...event, center: [...event.center], childFingerprint: [...event.childFingerprint], deltaH: [...event.deltaH], deltaQ: [...event.deltaQ] })), + nextMutationIndex: this.nextMutationIndex, + extinctionEvents: this.extinctionEvents, + }; + } + + restorePackedSnapshot(snapshot: ArenaPackedSnapshot): void { + if (!this.environment || !this.environmentRegions || !this.arenaUniform) throw new Error("Arena snapshot restore requires an Arena solver"); + if (snapshot.schemaVersion !== "flow-lenia-arena-snapshot-v1" || snapshot.n !== this.n || snapshot.seed !== this.config.seed) throw new Error("Arena snapshot model/grid/seed does not match this solver"); + if (!Number.isInteger(snapshot.step) || snapshot.step < 0 || !Number.isFinite(snapshot.initialMass) || snapshot.initialMass <= 0) throw new Error("Arena snapshot step or initial mass is invalid"); + const expected = [snapshot.mass.length === this.n2 * 4, snapshot.h.length === this.n2 * 12, snapshot.q.length === this.n2 * 12, snapshot.identity.length === this.n2 * 4, snapshot.environment.length === this.n2 * 4, snapshot.regions.length === this.n2]; + if (!expected.every(Boolean)) throw new Error("Arena snapshot contains an invalid packed field length"); + validateArenaEnvironment({ schemaVersion: "flow-lenia-arena-environment-v1", field: snapshot.environment, regions: snapshot.regions, dynamics: snapshot.dynamics }, this.n); + for (const buffer of this.mass) this.device.queue.writeBuffer(buffer, 0, snapshot.mass as unknown as BufferSource); + for (const buffer of this.genomeH) this.device.queue.writeBuffer(buffer, 0, snapshot.h as unknown as BufferSource); + for (const buffer of this.genomeQ) this.device.queue.writeBuffer(buffer, 0, snapshot.q as unknown as BufferSource); + for (const buffer of this.identity) this.device.queue.writeBuffer(buffer, 0, snapshot.identity as unknown as BufferSource); + this.device.queue.writeBuffer(this.environment, 0, snapshot.environment as unknown as BufferSource); + this.device.queue.writeBuffer(this.environmentRegions, 0, snapshot.regions as unknown as BufferSource); + this.ping = 0; + this.stepCount = snapshot.step; + this.initialMass = snapshot.initialMass; + this.mixingRule = snapshot.mixingRule; + this.arenaDynamics = cloneDynamics(snapshot.dynamics); + this.lineageRing = snapshot.lineageRing.map((event) => ({ ...event, center: [...event.center], childFingerprint: [...event.childFingerprint], deltaH: [...event.deltaH], deltaQ: [...event.deltaQ] })); + this.nextMutationIndex = snapshot.nextMutationIndex; + this.extinctionEvents = snapshot.extinctionEvents; + this.mutationQueue = []; + this.environmentEventQueue = []; + this.previousLineages = new Set(); + this.writeArenaUniform(); + } + + destroy(): void { this.fft.destroy(); for (const buffer of this.ownedBuffers) buffer.destroy(); } +} diff --git a/packages/flow-lenia/web/src/model/events.ts b/packages/flow-lenia/web/src/model/events.ts new file mode 100644 index 00000000..f227fd49 --- /dev/null +++ b/packages/flow-lenia/web/src/model/events.ts @@ -0,0 +1,45 @@ +export const MAX_STEP_EVENTS = 32; +export const EVENT_LEDGER_SCALE = 65_536; + +export type BrushEventKind = "add" | "erase" | "pipette" | "stir"; + +export interface BrushEventInput { + kind: BrushEventKind; + x: number; + y: number; + radius: number; + strength: number; + channel: number; + directionX?: number; + directionY?: number; + polarity?: number; + atStep?: number; +} + +export interface ScheduledBrushEvent extends Required { + id: number; + atStep: number; +} + +const EVENT_KIND: Record = { add: 1, erase: 2, pipette: 3, stir: 4 }; + +/** Pack the fixed 48-byte WGSL event record without relying on JS object layout. */ +export function packBrushEvents(events: readonly ScheduledBrushEvent[]): ArrayBuffer { + if (events.length > MAX_STEP_EVENTS) throw new Error(`at most ${MAX_STEP_EVENTS} events may share one step boundary`); + const raw = new ArrayBuffer(MAX_STEP_EVENTS * 48); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + events.forEach((event, index) => { + const word = index * 12; + u32[word] = EVENT_KIND[event.kind]; + u32[word + 1] = Math.max(0, Math.min(3, event.channel | 0)); + f32[word + 4] = event.x; + f32[word + 5] = event.y; + f32[word + 6] = Math.max(1, event.radius); + f32[word + 7] = Math.max(0, event.strength); + f32[word + 8] = event.directionX; + f32[word + 9] = event.directionY; + f32[word + 10] = event.polarity; + }); + return raw; +} diff --git a/packages/flow-lenia/web/src/model/experiment-io.ts b/packages/flow-lenia/web/src/model/experiment-io.ts new file mode 100644 index 00000000..f8095513 --- /dev/null +++ b/packages/flow-lenia/web/src/model/experiment-io.ts @@ -0,0 +1,170 @@ +import type { ArenaCard } from "../experiments/arena-cards.js"; +import type { ArenaPackedSnapshot, LineageEventRecord, MixingRule } from "./ecosystem-solver.js"; +import type { ArenaDynamics } from "./arena.js"; + +export const ARENA_EXPERIMENT_SCHEMA = "flow-lenia-arena-experiment-v1" as const; + +interface EncodedArray { + dtype: "f32" | "u32"; + length: number; + layout: string; + data: string; +} + +export interface ArenaExperimentDocument { + schema_version: typeof ARENA_EXPERIMENT_SCHEMA; + model_variant: "flow-lenia-ecosystem-v1"; + generated_utc: string; + scientific_sha256: string; + config: { + grid: number; + seed: number; + fixed_step: number; + mixing_rule: MixingRule; + channels: 3; + kernels: 9; + open_system_ledger: { added: 0; removed: 0; converted: 0 }; + }; + experiment: { + id: string; + title: string; + provenance: ArenaCard["provenance"]; + }; + ecosystem: { + initial_mass: number; + lineage_ring: LineageEventRecord[]; + next_mutation_index: number; + extinction_events: number; + }; + environment: { schema_version: "flow-lenia-arena-environment-v1"; dynamics: ArenaDynamics }; + state: { + mass: EncodedArray; + genome_h: EncodedArray; + genome_q: EncodedArray; + identity: EncodedArray; + environment: EncodedArray; + regions: EncodedArray; + }; +} + +function bytesOf(view: ArrayBufferView): Uint8Array { + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); +} + +function toBase64(view: ArrayBufferView): string { + const bytes = bytesOf(view); + let binary = ""; + const chunk = 0x8000; + for (let offset = 0; offset < bytes.length; offset += chunk) binary += String.fromCharCode(...bytes.subarray(offset, Math.min(bytes.length, offset + chunk))); + return btoa(binary); +} + +function fromBase64(value: string): Uint8Array { + const binary = atob(value); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); + return bytes; +} + +function encode(view: Float32Array | Uint32Array, dtype: EncodedArray["dtype"], layout: string): EncodedArray { + return { dtype, length: view.length, layout, data: toBase64(view) }; +} + +function decode(record: EncodedArray, dtype: EncodedArray["dtype"], expectedLength: number): Float32Array | Uint32Array { + if (!record || record.dtype !== dtype || record.length !== expectedLength || typeof record.data !== "string") throw new Error(`invalid ${dtype} experiment field`); + const bytes = fromBase64(record.data); + if (bytes.byteLength !== expectedLength * 4) throw new Error(`invalid ${dtype} experiment byte length`); + return dtype === "f32" ? new Float32Array(bytes.buffer) : new Uint32Array(bytes.buffer); +} + +function canonicalJson(value: unknown): string { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + const record = value as Record; + return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`; +} + +function scientificMetadata(document: Omit, state: ArenaExperimentDocument["state"]): unknown { + const descriptors = Object.fromEntries(Object.entries(state).map(([key, record]) => [key, { dtype: record.dtype, length: record.length, layout: record.layout }])); + return { ...document, state: descriptors }; +} + +async function hashState(state: ArenaExperimentDocument["state"], metadata: unknown): Promise { + const order = [state.mass, state.genome_h, state.genome_q, state.identity, state.environment, state.regions]; + const decoded = order.map((record) => fromBase64(record.data)); + const metadataBytes = new TextEncoder().encode(canonicalJson(metadata)); + const total = metadataBytes.byteLength + decoded.reduce((sum, bytes) => sum + bytes.byteLength, 0); + const all = new Uint8Array(total); + all.set(metadataBytes, 0); + let offset = metadataBytes.byteLength; + for (const bytes of decoded) { all.set(bytes, offset); offset += bytes.byteLength; } + const digest = await crypto.subtle.digest("SHA-256", all); + return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join(""); +} + +export async function buildArenaExperiment(snapshot: ArenaPackedSnapshot, card: ArenaCard): Promise { + const state: ArenaExperimentDocument["state"] = { + mass: encode(snapshot.mass, "f32", "cell-major vec4f; xyz=C0,C1,C2"), + genome_h: encode(snapshot.h, "f32", "cell-major 3xvec4f; first nine lanes are H"), + genome_q: encode(snapshot.q, "f32", "cell-major 3xvec4f; first nine lanes are Q"), + identity: encode(snapshot.identity, "u32", "cell-major vec4u; fingerprint lo/hi,lineage,flags"), + environment: encode(snapshot.environment, "f32", "cell-major vec4f; authored,wall,gate,opacity"), + regions: encode(snapshot.regions, "u32", "cell-major authored region ID"), + }; + const header: Omit = { + schema_version: ARENA_EXPERIMENT_SCHEMA, + model_variant: "flow-lenia-ecosystem-v1", + config: { grid: snapshot.n, seed: snapshot.seed, fixed_step: snapshot.step, mixing_rule: snapshot.mixingRule, channels: 3, kernels: 9, open_system_ledger: { added: 0, removed: 0, converted: 0 } }, + experiment: { id: card.id, title: card.title, provenance: card.provenance }, + ecosystem: { initial_mass: snapshot.initialMass, lineage_ring: snapshot.lineageRing, next_mutation_index: snapshot.nextMutationIndex, extinction_events: snapshot.extinctionEvents }, + environment: { schema_version: "flow-lenia-arena-environment-v1", dynamics: snapshot.dynamics }, + }; + return { + ...header, + generated_utc: new Date().toISOString(), + scientific_sha256: await hashState(state, scientificMetadata(header, state)), + state, + }; +} + +export async function parseArenaExperiment(text: string): Promise<{ document: ArenaExperimentDocument; snapshot: ArenaPackedSnapshot }> { + let value: unknown; + try { value = JSON.parse(text); } catch { throw new Error("Arena import is not valid JSON"); } + const document = value as ArenaExperimentDocument; + if (document?.schema_version !== ARENA_EXPERIMENT_SCHEMA || document.model_variant !== "flow-lenia-ecosystem-v1") throw new Error("unsupported Arena experiment schema or model variant"); + const n = document.config?.grid; + if (!Number.isInteger(n) || (n !== 128 && n !== 256)) throw new Error("Arena import grid must be 128 or 256"); + if (document.config.channels !== 3 || document.config.kernels !== 9) throw new Error("Arena import channel/kernel count is incompatible"); + if (!(["average", "whole", "gene-wise", "best", "negotiation"] as string[]).includes(document.config.mixing_rule)) throw new Error("Arena import mixing rule is invalid"); + const { generated_utc: _generated, scientific_sha256: _hash, state: _state, ...header } = document; + if (await hashState(document.state, scientificMetadata(header, document.state)) !== document.scientific_sha256) throw new Error("Arena import scientific SHA-256 mismatch"); + const n2 = n * n; + const snapshot: ArenaPackedSnapshot = { + schemaVersion: "flow-lenia-arena-snapshot-v1", + n, + seed: document.config.seed >>> 0, + step: document.config.fixed_step, + mixingRule: document.config.mixing_rule, + initialMass: document.ecosystem.initial_mass, + mass: decode(document.state.mass, "f32", n2 * 4) as Float32Array, + h: decode(document.state.genome_h, "f32", n2 * 12) as Float32Array, + q: decode(document.state.genome_q, "f32", n2 * 12) as Float32Array, + identity: decode(document.state.identity, "u32", n2 * 4) as Uint32Array, + environment: decode(document.state.environment, "f32", n2 * 4) as Float32Array, + regions: decode(document.state.regions, "u32", n2) as Uint32Array, + dynamics: document.environment.dynamics, + lineageRing: document.ecosystem.lineage_ring, + nextMutationIndex: document.ecosystem.next_mutation_index, + extinctionEvents: document.ecosystem.extinction_events, + }; + return { document, snapshot }; +} + +export function downloadArenaExperiment(experiment: ArenaExperimentDocument): void { + const blob = new Blob([JSON.stringify(experiment)], { type: "application/json" }); + const link = globalThis.document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `flow-lenia-arena-${experiment.experiment.id}-seed${experiment.config.seed}-step${experiment.config.fixed_step}.json`; + link.click(); + setTimeout(() => URL.revokeObjectURL(link.href), 0); +} diff --git a/packages/flow-lenia/web/src/model/kernels.ts b/packages/flow-lenia/web/src/model/kernels.ts new file mode 100644 index 00000000..208f28d4 --- /dev/null +++ b/packages/flow-lenia/web/src/model/kernels.ts @@ -0,0 +1,43 @@ +import type { OrganismConfig } from "./config.js"; + +/** Build ifftshifted, discretely normalized radial kernels in plane-major layout. */ +export function buildSpatialKernels(config: OrganismConfig): Float32Array { + const { n } = config; + const n2 = n * n; + const output = new Float32Array(config.kernels.length * n2 * 2); + config.kernels.forEach((spec, kernelIndex) => { + const radius = (config.baseRadius + config.radiusOffset) * spec.relativeRadius; + const values = new Float64Array(n2); + let sum = 0; + for (let i = 0; i < n; i += 1) { + const di = i < n / 2 ? i : i - n; + for (let j = 0; j < n; j += 1) { + const dj = j < n / 2 ? j : j - n; + const distance = Math.hypot(di, dj) / radius; + const cutoff = 0.5 * (Math.tanh((-config.cutoffSharpness * (distance - 1)) / 2) + 1); + let rings = 0; + for (let ring = 0; ring < 3; ring += 1) { + const delta = distance - (spec.ringCenters[ring] as number); + rings += (spec.ringAmplitudes[ring] as number) * Math.exp(-(delta * delta) / (spec.ringWidths[ring] as number)); + } + const value = cutoff * rings; + values[i * n + j] = value; + sum += value; + } + } + if (!(sum > 0) || !Number.isFinite(sum)) throw new Error(`kernel ${kernelIndex} failed normalization`); + for (let cell = 0; cell < n2; cell += 1) { + output[(kernelIndex * n2 + cell) * 2] = (values[cell] as number) / sum; + } + }); + return output; +} + +export function kernelSums(spatial: Float32Array, n: number, count: number): number[] { + const n2 = n * n; + return Array.from({ length: count }, (_, kernel) => { + let sum = 0; + for (let cell = 0; cell < n2; cell += 1) sum += spatial[(kernel * n2 + cell) * 2] as number; + return sum; + }); +} diff --git a/packages/flow-lenia/web/src/model/seed.ts b/packages/flow-lenia/web/src/model/seed.ts new file mode 100644 index 00000000..9d1fdf5a --- /dev/null +++ b/packages/flow-lenia/web/src/model/seed.ts @@ -0,0 +1,60 @@ +import { CHANNELS } from "./config.js"; + +function mix32(value: number): number { + let x = value >>> 0; + x = Math.imul(x ^ (x >>> 16), 0x7feb352d); + x = Math.imul(x ^ (x >>> 15), 0x846ca68b); + return (x ^ (x >>> 16)) >>> 0; +} + +function unitHash(seed: number, index: number): number { + return mix32(seed ^ Math.imul(index + 1, 0x9e3779b9)) / 0x1_0000_0000; +} + +/** A deterministic, localized multi-channel seed; no browser RNG is consulted. */ +export function makeSeededOrganismMass(n: number, seed: number): Float32Array { + const mass = new Float32Array(n * n * 4); + const angle = unitHash(seed, 0) * Math.PI * 2; + const cx = n * (0.5 + 0.025 * Math.cos(angle)); + const cy = n * (0.5 + 0.025 * Math.sin(angle)); + const scale = n / 256; + for (let i = 0; i < n; i += 1) { + const di0 = ((i - cx + n / 2) % n) - n / 2; + for (let j = 0; j < n; j += 1) { + const dj0 = ((j - cy + n / 2) % n) - n / 2; + const cell = i * n + j; + for (let channel = 0; channel < CHANNELS; channel += 1) { + const rotation = angle + channel * 2.0943951023931953; + const ca = Math.cos(rotation); + const sa = Math.sin(rotation); + const u = ca * di0 + sa * dj0; + const v = -sa * di0 + ca * dj0; + const offset = (channel - 1) * 9 * scale; + const broad = Math.exp(-0.5 * (((u - offset) / (29 * scale)) ** 2 + (v / (20 * scale)) ** 2)); + const ringRadius = Math.hypot(u - offset, v); + const ring = Math.exp(-0.5 * ((ringRadius - 18 * scale) / (5.5 * scale)) ** 2); + const texture = 0.88 + 0.12 * Math.cos((u + 0.7 * v) / Math.max(1, 4.5 * scale) + unitHash(seed, channel + 2) * 6.283185307179586); + mass[cell * 4 + channel] = Math.max(0, (0.72 * broad + 0.24 * ring) * texture); + } + } + } + return mass; +} + +/** Smooth periodic field shared byte-for-byte in intent with the f64 fixture generator. */ +export function makeConformanceMass(n: number, variant = 0): Float32Array { + const mass = new Float32Array(n * n * 4); + for (let i = 0; i < n; i += 1) { + for (let j = 0; j < n; j += 1) { + for (let channel = 0; channel < CHANNELS; channel += 1) { + const p = 2 * Math.PI * ((channel + 1) * i + (channel + 2) * j) / n; + const q = 2 * Math.PI * ((channel + 3) * i - (channel + 1) * j) / n; + let value = 0.16 + channel * 0.035 + 0.055 * Math.sin(p) + 0.025 * Math.cos(q); + if (variant === 1) value += (i < 2 || j >= n - 2) ? 0.24 / (channel + 1) : 0; + if (variant === 2) value += (i >= n / 2 - 2 && i <= n / 2 + 1 && j >= n / 2 - 2 && j <= n / 2 + 1) ? 2.4 - channel * 0.25 : 0; + mass[(i * n + j) * 4 + channel] = value; + } + } + } + return mass; +} diff --git a/packages/flow-lenia/web/src/model/shader-provenance.ts b/packages/flow-lenia/web/src/model/shader-provenance.ts new file mode 100644 index 00000000..568bb837 --- /dev/null +++ b/packages/flow-lenia/web/src/model/shader-provenance.ts @@ -0,0 +1,14 @@ +import arenaEvents from "../shaders/arena_events.wgsl?raw"; +import arenaPerceive from "../shaders/arena_perceive.wgsl?raw"; +import ecosystemFlow from "../shaders/ecosystem_flow.wgsl?raw"; +import ecosystemMutation from "../shaders/ecosystem_mutation.wgsl?raw"; +import organismPack from "../shaders/organism_pack.wgsl?raw"; +import organismSpectral from "../shaders/organism_spectral.wgsl?raw"; +import ecosystemGather from "../shaders/reintegrate_ecosystem.wgsl?raw"; +import arenaRender from "../shaders/render_arena.wgsl?raw"; + +export async function arenaShaderSha256(): Promise { + const source = [organismPack, organismSpectral, arenaPerceive, ecosystemFlow, ecosystemGather, ecosystemMutation, arenaEvents, arenaRender].join("\n// --- flow-lenia shader boundary ---\n"); + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source)); + return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join(""); +} diff --git a/packages/flow-lenia/web/src/model/solver.ts b/packages/flow-lenia/web/src/model/solver.ts new file mode 100644 index 00000000..f1a2a26e --- /dev/null +++ b/packages/flow-lenia/web/src/model/solver.ts @@ -0,0 +1,608 @@ +import { BatchedFft2d } from "../fft-batch.js"; +import eventWgsl from "../shaders/organism_events.wgsl?raw"; +import flowWgsl from "../shaders/organism_flow.wgsl?raw"; +import packWgsl from "../shaders/organism_pack.wgsl?raw"; +import perceiveWgsl from "../shaders/organism_perceive.wgsl?raw"; +import gatherWgsl from "../shaders/reintegrate_organism.wgsl?raw"; +import spectralWgsl from "../shaders/organism_spectral.wgsl?raw"; +import { + CHANNELS, + GATHER_RADIUS, + KERNELS, + kernelParameterRecords, +} from "./config.js"; +import type { OrganismConfig } from "./config.js"; +import { buildSpatialKernels, kernelSums } from "./kernels.js"; +import { EVENT_LEDGER_SCALE, MAX_STEP_EVENTS, packBrushEvents } from "./events.js"; +import type { BrushEventInput, ScheduledBrushEvent } from "./events.js"; + +const LINEAR_WORKGROUP = 128; +const TILE = 8; + +export interface SolverReadback { + mass: Float32Array; + perception: Float32Array; + growth: Float32Array; + affinity: Float32Array; + alpha: Float32Array; + flow: Float32Array; + displacement: Float32Array; + clampMask: Float32Array; + ledgerAdded: number; + ledgerRemoved: number; +} + +export interface SolverMetrics { + step: number; + totalMass: number; + channelMass: [number, number, number]; + relativeMassDrift: number; + minDensity: number; + maxDensity: number; + occupiedFraction: number; + nonFinite: number; + negative: number; + maxFlow: number; + maxDisplacement: number; + clampFraction: number; + ledgerAdded: number; + ledgerRemoved: number; + expectedMass: number; + ledgerError: number; +} + +export interface CellInspection { + cell: readonly [number, number]; + mass: readonly [number, number, number]; + density: number; + affinity: readonly [number, number, number]; + alpha: readonly [number, number, number]; + flow: readonly [readonly [number, number], readonly [number, number], readonly [number, number]]; + displacement: readonly [readonly [number, number], readonly [number, number], readonly [number, number]]; + clamp: readonly [number, number, number]; + perception: readonly number[]; + growth: readonly number[]; +} + +function storageEntry(binding: number, readOnly: boolean): GPUBindGroupLayoutEntry { + return { + binding, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: readOnly ? "read-only-storage" : "storage" }, + }; +} + +function uniformEntry(binding = 0): GPUBindGroupLayoutEntry { + return { binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }; +} + +export class FlowLeniaOrganismSolver { + readonly n: number; + readonly n2: number; + readonly config: OrganismConfig; + readonly mass: [GPUBuffer, GPUBuffer]; + readonly affinity: GPUBuffer; + readonly transport: GPUBuffer; + readonly diagnostic: GPUBuffer; + readonly allocatedBytes: number; + readonly dispatchesPerStep: number; + readonly spatialKernelSums: number[]; + stepCount = 0; + + private readonly device: GPUDevice; + private readonly fft: BatchedFft2d; + private readonly complex: [GPUBuffer, GPUBuffer]; + private readonly kernelSpectrum: GPUBuffer; + private readonly kernelFields: GPUBuffer; + private readonly eventBuffer: GPUBuffer; + private readonly eventLedger: GPUBuffer; + private readonly kernelParams: GPUBuffer; + private readonly gridUniform: GPUBuffer; + private readonly flowUniform: GPUBuffer; + private readonly gatherUniform: GPUBuffer; + private readonly eventUniform: GPUBuffer; + private readonly packPipeline: GPUComputePipeline; + private readonly spectralPipeline: GPUComputePipeline; + private readonly perceivePipeline: GPUComputePipeline; + private readonly flowPipeline: GPUComputePipeline; + private readonly gatherPipeline: GPUComputePipeline; + private readonly openEventPipeline: GPUComputePipeline; + private readonly impulseEventPipeline: GPUComputePipeline; + private readonly packGroups: [GPUBindGroup, GPUBindGroup]; + private readonly spectralGroup: GPUBindGroup; + private readonly perceiveGroup: GPUBindGroup; + private readonly flowGroups: [GPUBindGroup, GPUBindGroup]; + private readonly gatherGroups: [GPUBindGroup, GPUBindGroup]; + private readonly openEventGroups: [GPUBindGroup, GPUBindGroup]; + private readonly impulseEventGroup: GPUBindGroup; + private readonly ownedBuffers: GPUBuffer[]; + private ping = 0; + private initialMass = new Float64Array(CHANNELS); + private pressureEnabled = true; + private squareHalfWidth: number; + private nextEventId = 1; + private eventQueue: ScheduledBrushEvent[] = []; + private appliedEvents: ScheduledBrushEvent[] = []; + + private constructor(device: GPUDevice, config: OrganismConfig) { + this.device = device; + this.config = config; + this.n = config.n; + this.n2 = config.n * config.n; + this.squareHalfWidth = config.squareHalfWidth; + const storageUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC; + const makeBuffer = (label: string, bytes: number, usage = storageUsage): GPUBuffer => + device.createBuffer({ label: `flow-lenia-m2-${label}`, size: bytes, usage }); + + const massBytes = this.n2 * 16; + const complexBytes = KERNELS * this.n2 * 8; + this.mass = [makeBuffer("mass-a", massBytes), makeBuffer("mass-b", massBytes)]; + this.complex = [makeBuffer("complex-a", complexBytes), makeBuffer("complex-b", complexBytes)]; + this.kernelSpectrum = makeBuffer("kernel-spectra", complexBytes); + this.kernelFields = makeBuffer("kernel-fields", complexBytes); + this.affinity = makeBuffer("affinity", massBytes); + this.transport = makeBuffer("transport", this.n2 * 48); + this.diagnostic = makeBuffer("flow-diagnostic", this.n2 * 32); + this.eventBuffer = makeBuffer("event-records", MAX_STEP_EVENTS * 48); + this.eventLedger = makeBuffer("event-ledger", 8); + this.kernelParams = makeBuffer("kernel-params", KERNELS * 32); + this.gridUniform = makeBuffer("grid-uniform", 16, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.flowUniform = makeBuffer("flow-uniform", 32, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.gatherUniform = makeBuffer("gather-uniform", 32, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.eventUniform = makeBuffer("event-uniform", 16, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST); + this.ownedBuffers = [ + ...this.mass, + ...this.complex, + this.kernelSpectrum, + this.kernelFields, + this.affinity, + this.transport, + this.diagnostic, + this.eventBuffer, + this.eventLedger, + this.kernelParams, + this.gridUniform, + this.flowUniform, + this.gatherUniform, + this.eventUniform, + ]; + this.dispatchesPerStep = 4 * Math.log2(this.n) + 5; + + device.queue.writeBuffer(this.gridUniform, 0, new Uint32Array([this.n, this.n2, CHANNELS, KERNELS])); + device.queue.writeBuffer(this.kernelParams, 0, kernelParameterRecords(config)); + this.writeModelUniforms(); + + this.fft = new BatchedFft2d(device, this.n, this.complex, [CHANNELS, KERNELS]); + this.allocatedBytes = this.ownedBuffers.reduce((sum, buffer) => sum + buffer.size, 0) + this.fft.allocatedBytes; + + const packLayout = device.createBindGroupLayout({ + label: "flow-lenia-m2-pack-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false)], + }); + const spectralLayout = device.createBindGroupLayout({ + label: "flow-lenia-m2-spectral-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, true), storageEntry(3, true), storageEntry(4, false)], + }); + const perceiveLayout = device.createBindGroupLayout({ + label: "flow-lenia-m2-perceive-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, true), storageEntry(3, false), storageEntry(4, false)], + }); + const flowLayout = device.createBindGroupLayout({ + label: "flow-lenia-m2-flow-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, true), storageEntry(3, false), storageEntry(4, false)], + }); + const gatherLayout = device.createBindGroupLayout({ + label: "flow-lenia-m2-organism-gather-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false)], + }); + const openEventLayout = device.createBindGroupLayout({ + label: "flow-lenia-m3-open-event-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false), storageEntry(3, false)], + }); + const emptyLayout = device.createBindGroupLayout({ label: "flow-lenia-m3-empty-layout", entries: [] }); + const impulseEventLayout = device.createBindGroupLayout({ + label: "flow-lenia-m3-impulse-event-layout", + entries: [uniformEntry(), storageEntry(1, true), storageEntry(2, false)], + }); + + const modules = { + pack: device.createShaderModule({ label: "flow-lenia-m2-pack", code: packWgsl }), + spectral: device.createShaderModule({ label: "flow-lenia-m2-spectral", code: spectralWgsl }), + perceive: device.createShaderModule({ label: "flow-lenia-m2-perceive", code: perceiveWgsl }), + flow: device.createShaderModule({ label: "flow-lenia-m2-flow", code: flowWgsl }), + gather: device.createShaderModule({ label: "flow-lenia-m2-gather", code: gatherWgsl }), + events: device.createShaderModule({ label: "flow-lenia-m3-events", code: eventWgsl }), + }; + this.shaderModules = Object.values(modules); + this.packPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [packLayout] }), compute: { module: modules.pack, entryPoint: "pack_mass" }, + }); + this.spectralPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [spectralLayout] }), compute: { module: modules.spectral, entryPoint: "expand_spectra" }, + }); + this.perceivePipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [perceiveLayout] }), compute: { module: modules.perceive, entryPoint: "perceive_growth" }, + }); + this.flowPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [flowLayout] }), compute: { module: modules.flow, entryPoint: "compute_flow" }, + }); + this.gatherPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [gatherLayout] }), compute: { module: modules.gather, entryPoint: "gather_organism" }, + }); + this.openEventPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [openEventLayout] }), compute: { module: modules.events, entryPoint: "apply_open_events" }, + }); + this.impulseEventPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [emptyLayout, impulseEventLayout] }), compute: { module: modules.events, entryPoint: "apply_closed_impulses" }, + }); + + const entries = (layout: GPUBindGroupLayout, resources: GPUBuffer[]): GPUBindGroup => device.createBindGroup({ + layout, + entries: resources.map((buffer, binding) => ({ binding, resource: { buffer } })), + }); + this.packGroups = this.mass.map((mass) => entries(packLayout, [this.gridUniform, mass, this.complex[0]])) as [GPUBindGroup, GPUBindGroup]; + this.spectralGroup = entries(spectralLayout, [this.gridUniform, this.complex[0], this.kernelSpectrum, this.kernelParams, this.complex[1]]); + this.perceiveGroup = entries(perceiveLayout, [this.gridUniform, this.complex[1], this.kernelParams, this.kernelFields, this.affinity]); + this.flowGroups = this.mass.map((mass) => entries(flowLayout, [this.flowUniform, mass, this.affinity, this.transport, this.diagnostic])) as [GPUBindGroup, GPUBindGroup]; + this.gatherGroups = [ + entries(gatherLayout, [this.gatherUniform, this.transport, this.mass[1]]), + entries(gatherLayout, [this.gatherUniform, this.transport, this.mass[0]]), + ]; + this.openEventGroups = this.mass.map((mass) => entries(openEventLayout, [this.eventUniform, this.eventBuffer, mass, this.eventLedger])) as [GPUBindGroup, GPUBindGroup]; + this.impulseEventGroup = entries(impulseEventLayout, [this.eventUniform, this.eventBuffer, this.transport]); + + const spatial = buildSpatialKernels(config); + this.spatialKernelSums = kernelSums(spatial, this.n, KERNELS); + device.queue.writeBuffer(this.complex[0], 0, spatial as unknown as BufferSource); + } + + private readonly shaderModules: GPUShaderModule[]; + + static async create(device: GPUDevice, config: OrganismConfig): Promise { + device.pushErrorScope("validation"); + const solver = new FlowLeniaOrganismSolver(device, config); + const encoder = device.createCommandEncoder({ label: "flow-lenia-m2-kernel-spectrum-build" }); + const pass = encoder.beginComputePass(); + solver.fft.resetPing(0); + solver.fft.encode2d(pass, KERNELS, -1); + pass.end(); + encoder.copyBufferToBuffer(solver.fft.currentBuffer, 0, solver.kernelSpectrum, 0, solver.kernelSpectrum.size); + device.queue.submit([encoder.finish()]); + await device.queue.onSubmittedWorkDone(); + const reports = await Promise.all([solver.fft.module, ...solver.shaderModules].map((module) => module.getCompilationInfo())); + const errors = reports.flatMap((report) => report.messages).filter((message) => message.type === "error"); + const validation = await device.popErrorScope(); + if (validation || errors.length > 0) { + solver.destroy(); + const detail = errors.map((message) => `${message.lineNum}:${message.linePos} ${message.message}`).join("\n"); + throw new Error(`M2 shader validation failed: ${validation?.message ?? detail}`); + } + return solver; + } + + private writeModelUniforms(): void { + const maxDisplacement = GATHER_RADIUS - this.squareHalfWidth; + const flowRaw = new ArrayBuffer(32); + const flowU32 = new Uint32Array(flowRaw); + const flowF32 = new Float32Array(flowRaw); + flowU32.set([this.n, this.n2, CHANNELS, 0]); + flowF32.set([ + this.config.dt, + this.pressureEnabled ? this.config.densityThreshold : 1e9, + this.config.densityExponent, + maxDisplacement, + ], 4); + this.device.queue.writeBuffer(this.flowUniform, 0, flowRaw); + + const gatherRaw = new ArrayBuffer(32); + const gatherU32 = new Uint32Array(gatherRaw); + const gatherF32 = new Float32Array(gatherRaw); + gatherU32.set([this.n, CHANNELS, GATHER_RADIUS, 0]); + gatherF32[4] = this.squareHalfWidth; + this.device.queue.writeBuffer(this.gatherUniform, 0, gatherRaw); + } + + setPressureEnabled(enabled: boolean): void { + this.pressureEnabled = enabled; + this.writeModelUniforms(); + } + + isPressureEnabled(): boolean { return this.pressureEnabled; } + + setSquareHalfWidth(value: number): void { + if (!Number.isFinite(value) || value < 0.3 || value > 1.25) { + throw new Error("square half-width must be within the verified interactive range [0.3, 1.25]"); + } + this.squareHalfWidth = value; + this.writeModelUniforms(); + } + + getSquareHalfWidth(): number { return this.squareHalfWidth; } + + reset(mass: Float32Array): void { + if (mass.length !== this.n2 * 4) throw new Error(`mass must contain ${this.n2 * 4} packed values`); + this.initialMass = new Float64Array(CHANNELS); + for (let cell = 0; cell < this.n2; cell += 1) { + for (let channel = 0; channel < CHANNELS; channel += 1) { + this.initialMass[channel] += mass[cell * 4 + channel] as number; + } + } + this.device.queue.writeBuffer(this.mass[0], 0, mass as unknown as BufferSource); + this.device.queue.writeBuffer(this.mass[1], 0, mass as unknown as BufferSource); + this.ping = 0; + this.stepCount = 0; + this.eventQueue = []; + this.appliedEvents = []; + this.nextEventId = 1; + this.device.queue.writeBuffer(this.eventLedger, 0, new Uint32Array([0, 0])); + } + + queueEvent(input: BrushEventInput): ScheduledBrushEvent { + const scheduled: ScheduledBrushEvent = { + ...input, + x: ((input.x % this.n) + this.n) % this.n, + y: ((input.y % this.n) + this.n) % this.n, + radius: Math.max(1, Math.min(this.n / 3, input.radius)), + strength: Math.max(0, input.strength), + channel: Math.max(0, Math.min(3, input.channel | 0)), + directionX: input.directionX ?? 0, + directionY: input.directionY ?? 0, + polarity: input.polarity ?? 1, + atStep: Math.max(this.stepCount, input.atStep ?? this.stepCount), + id: this.nextEventId, + }; + this.nextEventId += 1; + this.eventQueue.push(scheduled); + this.eventQueue.sort((a, b) => a.atStep - b.atStep || a.id - b.id); + return scheduled; + } + + getPendingEvents(): readonly ScheduledBrushEvent[] { return this.eventQueue; } + getAppliedEvents(): readonly ScheduledBrushEvent[] { return this.appliedEvents; } + + private uploadEvents(events: readonly ScheduledBrushEvent[]): void { + this.device.queue.writeBuffer(this.eventBuffer, 0, packBrushEvents(events)); + const uniformRaw = new ArrayBuffer(16); + const u32 = new Uint32Array(uniformRaw); + const f32 = new Float32Array(uniformRaw); + u32[0] = this.n; + u32[1] = events.length; + f32[2] = GATHER_RADIUS - this.squareHalfWidth; + f32[3] = EVENT_LEDGER_SCALE; + this.device.queue.writeBuffer(this.eventUniform, 0, uniformRaw); + } + + private encodeOne(pass: GPUComputePassEncoder, events: readonly ScheduledBrushEvent[]): void { + if (events.length > 0) { + pass.setPipeline(this.openEventPipeline); + pass.setBindGroup(0, this.openEventGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil(this.n2 / LINEAR_WORKGROUP)); + } + this.fft.resetPing(0); + pass.setPipeline(this.packPipeline); + pass.setBindGroup(0, this.packGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil((CHANNELS * this.n2) / LINEAR_WORKGROUP)); + this.fft.encode2d(pass, CHANNELS, -1); + pass.setPipeline(this.spectralPipeline); + pass.setBindGroup(0, this.spectralGroup); + pass.dispatchWorkgroups(Math.ceil((KERNELS * this.n2) / LINEAR_WORKGROUP)); + this.fft.swapAfterExternalWrite(); + this.fft.encode2d(pass, KERNELS, 1); + pass.setPipeline(this.perceivePipeline); + pass.setBindGroup(0, this.perceiveGroup); + pass.dispatchWorkgroups(Math.ceil(this.n2 / LINEAR_WORKGROUP)); + pass.setPipeline(this.flowPipeline); + pass.setBindGroup(0, this.flowGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil(this.n / TILE), Math.ceil(this.n / TILE)); + if (events.length > 0) { + pass.setPipeline(this.impulseEventPipeline); + pass.setBindGroup(1, this.impulseEventGroup); + pass.dispatchWorkgroups(Math.ceil(this.n2 / LINEAR_WORKGROUP)); + } + pass.setPipeline(this.gatherPipeline); + pass.setBindGroup(0, this.gatherGroups[this.ping]); + pass.dispatchWorkgroups(Math.ceil(this.n / TILE), Math.ceil(this.n / TILE)); + this.ping = 1 - this.ping; + this.stepCount += 1; + } + + step(count = 1): void { + if (!Number.isInteger(count) || count < 1) throw new Error("step count must be a positive integer"); + let remaining = count; + while (remaining > 0) { + const nextEventStep = this.eventQueue[0]?.atStep ?? Number.POSITIVE_INFINITY; + const batch = nextEventStep > this.stepCount + ? Math.min(remaining, nextEventStep - this.stepCount) + : 1; + const events = nextEventStep <= this.stepCount + ? this.eventQueue.filter((event) => event.atStep === this.stepCount).slice(0, MAX_STEP_EVENTS) + : []; + if (events.length > 0) this.uploadEvents(events); + const encoder = this.device.createCommandEncoder({ label: `flow-lenia-m3-step-${this.stepCount}` }); + const pass = encoder.beginComputePass(); + for (let iteration = 0; iteration < batch; iteration += 1) this.encodeOne(pass, iteration === 0 ? events : []); + pass.end(); + this.device.queue.submit([encoder.finish()]); + if (events.length > 0) { + const ids = new Set(events.map((event) => event.id)); + this.eventQueue = this.eventQueue.filter((event) => !ids.has(event.id)); + this.appliedEvents.push(...events); + } + remaining -= batch; + } + } + + get currentMassBuffer(): GPUBuffer { return this.mass[this.ping]; } + + private async readBuffers(requests: Array<[GPUBuffer, number]>): Promise { + const staging = requests.map(([, bytes]) => this.device.createBuffer({ + size: bytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + })); + const encoder = this.device.createCommandEncoder({ label: "flow-lenia-m2-readback" }); + requests.forEach(([source, bytes], index) => encoder.copyBufferToBuffer(source, 0, staging[index] as GPUBuffer, 0, bytes)); + this.device.queue.submit([encoder.finish()]); + await Promise.all(staging.map((buffer) => buffer.mapAsync(GPUMapMode.READ))); + const output = staging.map((buffer) => buffer.getMappedRange().slice(0)); + staging.forEach((buffer) => { buffer.unmap(); buffer.destroy(); }); + return output; + } + + async readback(): Promise { + const [massRaw, kernelRaw, affinityRaw, transportRaw, diagnosticRaw, ledgerRaw] = await this.readBuffers([ + [this.currentMassBuffer, this.n2 * 16], + [this.kernelFields, KERNELS * this.n2 * 8], + [this.affinity, this.n2 * 16], + [this.transport, this.n2 * 48], + [this.diagnostic, this.n2 * 32], + [this.eventLedger, 8], + ]); + const massPacked = new Float32Array(massRaw as ArrayBuffer); + const kernelPacked = new Float32Array(kernelRaw as ArrayBuffer); + const affinityPacked = new Float32Array(affinityRaw as ArrayBuffer); + const transportPacked = new Float32Array(transportRaw as ArrayBuffer); + const diagnosticPacked = new Float32Array(diagnosticRaw as ArrayBuffer); + const mass = new Float32Array(CHANNELS * this.n2); + const perception = new Float32Array(KERNELS * this.n2); + const growth = new Float32Array(KERNELS * this.n2); + const affinity = new Float32Array(CHANNELS * this.n2); + const alpha = new Float32Array(CHANNELS * this.n2); + const flow = new Float32Array(CHANNELS * 2 * this.n2); + const displacement = new Float32Array(CHANNELS * 2 * this.n2); + const clampMask = new Float32Array(CHANNELS * this.n2); + for (let cell = 0; cell < this.n2; cell += 1) { + for (let channel = 0; channel < CHANNELS; channel += 1) { + const planeIndex = channel * this.n2 + cell; + mass[planeIndex] = massPacked[cell * 4 + channel] as number; + affinity[planeIndex] = affinityPacked[cell * 4 + channel] as number; + alpha[planeIndex] = diagnosticPacked[cell * 8 + channel] as number; + clampMask[planeIndex] = diagnosticPacked[cell * 8 + 4 + channel] as number; + for (let axis = 0; axis < 2; axis += 1) { + const vectorIndex = (channel * 2 + axis) * this.n2 + cell; + const value = transportPacked[cell * 12 + (axis === 0 ? 4 : 8) + channel] as number; + displacement[vectorIndex] = value; + flow[vectorIndex] = value / this.config.dt; + } + } + } + for (let index = 0; index < KERNELS * this.n2; index += 1) { + perception[index] = kernelPacked[index * 2] as number; + growth[index] = kernelPacked[index * 2 + 1] as number; + } + const ledger = new Uint32Array(ledgerRaw as ArrayBuffer); + return { + mass, + perception, + growth, + affinity, + alpha, + flow, + displacement, + clampMask, + ledgerAdded: (ledger[0] as number) / EVENT_LEDGER_SCALE, + ledgerRemoved: (ledger[1] as number) / EVENT_LEDGER_SCALE, + }; + } + + async inspect(i: number, j: number): Promise { + const row = ((Math.floor(i) % this.n) + this.n) % this.n; + const column = ((Math.floor(j) % this.n) + this.n) % this.n; + const cell = row * this.n + column; + const staging = this.device.createBuffer({ size: 192, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); + const encoder = this.device.createCommandEncoder({ label: "flow-lenia-m3-cell-inspection" }); + encoder.copyBufferToBuffer(this.currentMassBuffer, cell * 16, staging, 0, 16); + encoder.copyBufferToBuffer(this.affinity, cell * 16, staging, 16, 16); + encoder.copyBufferToBuffer(this.transport, cell * 48, staging, 32, 48); + encoder.copyBufferToBuffer(this.diagnostic, cell * 32, staging, 80, 32); + for (let kernel = 0; kernel < KERNELS; kernel += 1) { + encoder.copyBufferToBuffer(this.kernelFields, (kernel * this.n2 + cell) * 8, staging, 112 + kernel * 8, 8); + } + this.device.queue.submit([encoder.finish()]); + await staging.mapAsync(GPUMapMode.READ); + const values = new Float32Array(staging.getMappedRange().slice(0)); + staging.unmap(); + staging.destroy(); + const mass = [values[0], values[1], values[2]] as [number, number, number]; + const affinity = [values[4], values[5], values[6]] as [number, number, number]; + const alpha = [values[20], values[21], values[22]] as [number, number, number]; + const clamp = [values[24], values[25], values[26]] as [number, number, number]; + const displacement = Array.from({ length: CHANNELS }, (_, channel) => [ + values[8 + channel] as number, + values[12 + channel] as number, + ] as const) as unknown as CellInspection["displacement"]; + const flow = displacement.map(([x, y]) => [x / this.config.dt, y / this.config.dt] as const) as unknown as CellInspection["flow"]; + const perception = Array.from({ length: KERNELS }, (_, kernel) => values[28 + kernel * 2] as number); + const growth = Array.from({ length: KERNELS }, (_, kernel) => values[29 + kernel * 2] as number); + return { + cell: [row, column], + mass, + density: mass[0] + mass[1] + mass[2], + affinity, + alpha, + flow, + displacement, + clamp, + perception, + growth, + }; + } + + async metrics(): Promise { + const state = await this.readback(); + const channelMass: [number, number, number] = [0, 0, 0]; + let minDensity = Number.POSITIVE_INFINITY; + let maxDensity = 0; + let occupied = 0; + let nonFinite = 0; + let negative = 0; + let maxFlow = 0; + let maxDisplacement = 0; + let clamped = 0; + for (let cell = 0; cell < this.n2; cell += 1) { + let density = 0; + for (let channel = 0; channel < CHANNELS; channel += 1) { + const index = channel * this.n2 + cell; + const value = state.mass[index] as number; + if (!Number.isFinite(value)) nonFinite += 1; + if (value < 0) negative += 1; + channelMass[channel] += value; + density += value; + clamped += state.clampMask[index] as number; + for (let axis = 0; axis < 2; axis += 1) { + const vector = (channel * 2 + axis) * this.n2 + cell; + maxFlow = Math.max(maxFlow, Math.abs(state.flow[vector] as number)); + maxDisplacement = Math.max(maxDisplacement, Math.abs(state.displacement[vector] as number)); + } + } + minDensity = Math.min(minDensity, density); + maxDensity = Math.max(maxDensity, density); + if (density > 1e-3) occupied += 1; + } + const totalMass = channelMass[0] + channelMass[1] + channelMass[2]; + const initial = this.initialMass[0] + this.initialMass[1] + this.initialMass[2]; + const expectedMass = initial + state.ledgerAdded - state.ledgerRemoved; + const ledgerError = totalMass - expectedMass; + return { + step: this.stepCount, + totalMass, + channelMass, + relativeMassDrift: Math.abs(ledgerError) / Math.max(expectedMass, 1e-30), + minDensity, + maxDensity, + occupiedFraction: occupied / this.n2, + nonFinite, + negative, + maxFlow, + maxDisplacement, + clampFraction: clamped / (CHANNELS * this.n2), + ledgerAdded: state.ledgerAdded, + ledgerRemoved: state.ledgerRemoved, + expectedMass, + ledgerError, + }; + } + + destroy(): void { + this.fft.destroy(); + for (const buffer of this.ownedBuffers) buffer.destroy(); + } +} diff --git a/packages/flow-lenia/web/src/probe.ts b/packages/flow-lenia/web/src/probe.ts new file mode 100644 index 00000000..256096d5 --- /dev/null +++ b/packages/flow-lenia/web/src/probe.ts @@ -0,0 +1,544 @@ +import { BatchedFft2d } from "./fft-batch.js"; +import { + M0_CHANNELS, + M0_DD, + M0_KERNELS, + M0_SIGMA, + completeEcosystemInventory, +} from "./inventory.js"; +import expandWgsl from "./shaders/spectral_expand.wgsl?raw"; +import gatherWgsl from "./shaders/reintegrate.wgsl?raw"; + +const FFT_WORKGROUP = 128; +const GATHER_TILE = 8; + +export type BenchmarkMode = "fft" | "gather-mass" | "gather-full" | "step-mass" | "step-full"; + +export interface BenchmarkResult { + n: number; + mode: BenchmarkMode; + timing: "timestamp-query" | "queue-completion"; + samples: number; + dispatchesPerStep: number; + meanMs: number; + p50Ms: number; + p95Ms: number; + minMs: number; + maxMs: number; +} + +export interface VerificationResult { + fftMaxAbs: number; + gatherMassRelativeResidual: number; + fullMassRelativeResidual: number; + uniformGenomeMaxAbs: number; + uniformIdentityExact: boolean; +} + +function percentile(sorted: number[], fraction: number): number { + if (sorted.length === 0) return Number.NaN; + const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1); + return sorted[index] as number; +} + +function summarize( + n: number, + mode: BenchmarkMode, + timing: BenchmarkResult["timing"], + dispatchesPerStep: number, + values: number[], +): BenchmarkResult { + const sorted = [...values].sort((a, b) => a - b); + return { + n, + mode, + timing, + samples: values.length, + dispatchesPerStep, + meanMs: values.reduce((sum, value) => sum + value, 0) / values.length, + p50Ms: percentile(sorted, 0.5), + p95Ms: percentile(sorted, 0.95), + minMs: sorted[0] as number, + maxMs: sorted[sorted.length - 1] as number, + }; +} + +function sourceValue(channel: number, x: number, y: number, n: number): number { + const phase = (2 * Math.PI * ((channel + 1) * x + (channel + 2) * y)) / n; + return 0.35 + channel * 0.07 + 0.12 * Math.sin(phase) + 0.04 * Math.cos(phase * 0.5); +} + +function massValue(channel: number, x: number, y: number): number { + return 0.2 + channel * 0.05 + ((x * 17 + y * 29 + channel * 11) % 31) / 155; +} + +export class FlowLeniaM0Probe { + readonly n: number; + readonly allocatedBytes: number; + readonly fft: BatchedFft2d; + readonly expandModule: GPUShaderModule; + readonly gatherModule: GPUShaderModule; + + private readonly device: GPUDevice; + private readonly n2: number; + private readonly complexBuffers: [GPUBuffer, GPUBuffer]; + private readonly kernelSpectrum: GPUBuffer; + private readonly transportIn: GPUBuffer; + private readonly massOut: GPUBuffer; + private readonly hIn: GPUBuffer; + private readonly hOut: GPUBuffer; + private readonly qIn: GPUBuffer; + private readonly qOut: GPUBuffer; + private readonly identityIn: GPUBuffer; + private readonly identityOut: GPUBuffer; + private readonly expandUniform: GPUBuffer; + private readonly gatherUniform: GPUBuffer; + private readonly expandPipeline: GPUComputePipeline; + private readonly gatherMassPipeline: GPUComputePipeline; + private readonly gatherFullPipeline: GPUComputePipeline; + private readonly expandGroups: [GPUBindGroup, GPUBindGroup]; + private readonly gatherMassGroup: GPUBindGroup; + private readonly gatherFullGroup: GPUBindGroup; + private readonly ownedBuffers: GPUBuffer[]; + private readonly inputMassSums = new Float64Array(M0_CHANNELS); + + private constructor(device: GPUDevice, n: number) { + this.device = device; + this.n = n; + this.n2 = n * n; + const log2n = Math.log2(n); + if (!Number.isInteger(log2n)) throw new Error("M0 grid dimension must be a power of two"); + + const storageUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC; + const makeBuffer = (label: string, bytes: number, usage = storageUsage): GPUBuffer => + device.createBuffer({ label, size: bytes, usage }); + + const complexBytes = M0_KERNELS * this.n2 * 8; + this.complexBuffers = [ + makeBuffer("flow-lenia-m0-complex-a", complexBytes), + makeBuffer("flow-lenia-m0-complex-b", complexBytes), + ]; + this.kernelSpectrum = makeBuffer("flow-lenia-m0-kernel-spectra", complexBytes); + this.transportIn = makeBuffer("flow-lenia-m0-transport-in", this.n2 * 48); + this.massOut = makeBuffer("flow-lenia-m0-mass-out", this.n2 * 16); + this.hIn = makeBuffer("flow-lenia-m0-h-in", this.n2 * 48); + this.hOut = makeBuffer("flow-lenia-m0-h-out", this.n2 * 48); + this.qIn = makeBuffer("flow-lenia-m0-q-in", this.n2 * 48); + this.qOut = makeBuffer("flow-lenia-m0-q-out", this.n2 * 48); + this.identityIn = makeBuffer("flow-lenia-m0-identity-in", this.n2 * 16); + this.identityOut = makeBuffer("flow-lenia-m0-identity-out", this.n2 * 16); + this.expandUniform = makeBuffer( + "flow-lenia-m0-expand-uniform", + 16, + GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + ); + this.gatherUniform = makeBuffer( + "flow-lenia-m0-gather-uniform", + 32, + GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + ); + this.ownedBuffers = [ + ...this.complexBuffers, + this.kernelSpectrum, + this.transportIn, + this.massOut, + this.hIn, + this.hOut, + this.qIn, + this.qOut, + this.identityIn, + this.identityOut, + this.expandUniform, + this.gatherUniform, + ]; + this.allocatedBytes = + complexBytes * 3 + this.n2 * (48 + 16 + 48 * 4 + 16 * 2) + 16 + 32; + + this.fft = new BatchedFft2d(device, n, this.complexBuffers, [M0_CHANNELS, M0_KERNELS]); + + const expandLayout = device.createBindGroupLayout({ + label: "flow-lenia-m0-expand-layout", + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + ], + }); + this.expandModule = device.createShaderModule({ + label: "flow-lenia-m0-spectral-expand", + code: expandWgsl, + }); + this.expandPipeline = device.createComputePipeline({ + label: "flow-lenia-m0-spectral-expand", + layout: device.createPipelineLayout({ bindGroupLayouts: [expandLayout] }), + compute: { module: this.expandModule, entryPoint: "spectral_expand" }, + }); + this.expandGroups = [0, 1].map((source) => + device.createBindGroup({ + layout: expandLayout, + entries: [ + { binding: 0, resource: { buffer: this.expandUniform } }, + { binding: 1, resource: { buffer: this.complexBuffers[source] } }, + { binding: 2, resource: { buffer: this.kernelSpectrum } }, + { binding: 3, resource: { buffer: this.complexBuffers[1 - source] } }, + ], + }), + ) as [GPUBindGroup, GPUBindGroup]; + + const massLayout = device.createBindGroupLayout({ + label: "flow-lenia-m0-gather-mass-layout", + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + ], + }); + const fullLayout = device.createBindGroupLayout({ + label: "flow-lenia-m0-gather-full-layout", + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + { binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 8, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + ], + }); + this.gatherModule = device.createShaderModule({ + label: "flow-lenia-m0-reintegration", + code: gatherWgsl, + }); + this.gatherMassPipeline = device.createComputePipeline({ + label: "flow-lenia-m0-gather-mass", + layout: device.createPipelineLayout({ bindGroupLayouts: [massLayout] }), + compute: { module: this.gatherModule, entryPoint: "gather_mass" }, + }); + this.gatherFullPipeline = device.createComputePipeline({ + label: "flow-lenia-m0-gather-full", + layout: device.createPipelineLayout({ bindGroupLayouts: [fullLayout] }), + compute: { module: this.gatherModule, entryPoint: "gather_full" }, + }); + this.gatherMassGroup = device.createBindGroup({ + layout: massLayout, + entries: [ + { binding: 0, resource: { buffer: this.gatherUniform } }, + { binding: 1, resource: { buffer: this.transportIn } }, + { binding: 2, resource: { buffer: this.massOut } }, + ], + }); + this.gatherFullGroup = device.createBindGroup({ + layout: fullLayout, + entries: [ + { binding: 0, resource: { buffer: this.gatherUniform } }, + { binding: 1, resource: { buffer: this.transportIn } }, + { binding: 2, resource: { buffer: this.massOut } }, + { binding: 3, resource: { buffer: this.hIn } }, + { binding: 4, resource: { buffer: this.hOut } }, + { binding: 5, resource: { buffer: this.qIn } }, + { binding: 6, resource: { buffer: this.qOut } }, + { binding: 7, resource: { buffer: this.identityIn } }, + { binding: 8, resource: { buffer: this.identityOut } }, + ], + }); + + device.queue.writeBuffer( + this.expandUniform, + 0, + new Uint32Array([this.n2, M0_CHANNELS, M0_KERNELS, 0]), + ); + const gatherRaw = new ArrayBuffer(32); + const gatherU32 = new Uint32Array(gatherRaw); + const gatherF32 = new Float32Array(gatherRaw); + gatherU32[0] = n; + gatherU32[1] = M0_CHANNELS; + gatherU32[2] = M0_DD; + gatherF32[4] = M0_SIGMA; + device.queue.writeBuffer(this.gatherUniform, 0, gatherRaw); + this.uploadInitialState(); + } + + static async create(device: GPUDevice, n: number): Promise { + device.pushErrorScope("validation"); + const probe = new FlowLeniaM0Probe(device, n); + const modules = [probe.fft.module, probe.expandModule, probe.gatherModule]; + const reports = await Promise.all(modules.map((module) => module.getCompilationInfo())); + const messages = reports.flatMap((report) => report.messages).filter((message) => message.type === "error"); + const validation = await device.popErrorScope(); + if (messages.length > 0 || validation) { + probe.destroy(); + const detail = messages.map((message) => `${message.lineNum}:${message.linePos} ${message.message}`).join("\n"); + throw new Error(`M0 shader validation failed: ${validation?.message ?? detail}`); + } + return probe; + } + + private uploadInitialState(): void { + this.inputMassSums.fill(0); + const complex = new Float32Array(M0_KERNELS * this.n2 * 2); + for (let channel = 0; channel < M0_CHANNELS; channel += 1) { + for (let y = 0; y < this.n; y += 1) { + for (let x = 0; x < this.n; x += 1) { + const element = channel * this.n2 + y * this.n + x; + complex[element * 2] = sourceValue(channel, x, y, this.n); + } + } + } + this.device.queue.writeBuffer(this.complexBuffers[0], 0, complex as unknown as BufferSource); + this.device.queue.writeBuffer(this.complexBuffers[1], 0, complex as unknown as BufferSource); + + const kernels = new Float32Array(M0_KERNELS * this.n2 * 2); + const inverseScale = 1 / this.n2; + for (let element = 0; element < M0_KERNELS * this.n2; element += 1) { + kernels[element * 2] = inverseScale; + } + this.device.queue.writeBuffer(this.kernelSpectrum, 0, kernels as unknown as BufferSource); + + const transport = new Float32Array(this.n2 * 12); + for (let y = 0; y < this.n; y += 1) { + for (let x = 0; x < this.n; x += 1) { + const cell = y * this.n + x; + const base = cell * 12; + for (let channel = 0; channel < M0_CHANNELS; channel += 1) { + const mass = massValue(channel, x, y); + transport[base + channel] = mass; + transport[base + 4 + channel] = 1.75 * Math.sin((2 * Math.PI * (y + channel * 7)) / this.n); + transport[base + 8 + channel] = 1.75 * Math.cos((2 * Math.PI * (x + channel * 5)) / this.n); + this.inputMassSums[channel] += mass; + } + } + } + this.device.queue.writeBuffer(this.transportIn, 0, transport as unknown as BufferSource); + + const h = new Float32Array(this.n2 * 12); + const q = new Float32Array(this.n2 * 12); + for (let cell = 0; cell < this.n2; cell += 1) { + for (let gene = 0; gene < 12; gene += 1) { + h[cell * 12 + gene] = (gene + 1) * 0.05; + q[cell * 12 + gene] = (gene + 1) * 0.025 - 0.1; + } + } + this.device.queue.writeBuffer(this.hIn, 0, h as unknown as BufferSource); + this.device.queue.writeBuffer(this.qIn, 0, q as unknown as BufferSource); + const identity = new Uint32Array(this.n2 * 4); + for (let cell = 0; cell < this.n2; cell += 1) { + identity.set([0x1a2b3c4d, 0x55667788, 7, 0], cell * 4); + } + this.device.queue.writeBuffer(this.identityIn, 0, identity as unknown as BufferSource); + this.fft.resetPing(0); + } + + private encodeFftPrototype(pass: GPUComputePassEncoder): void { + this.fft.encode2d(pass, M0_CHANNELS, -1); + pass.setPipeline(this.expandPipeline); + pass.setBindGroup(0, this.expandGroups[this.fft.currentPing]); + pass.dispatchWorkgroups(Math.ceil((M0_KERNELS * this.n2) / FFT_WORKGROUP)); + this.fft.swapAfterExternalWrite(); + this.fft.encode2d(pass, M0_KERNELS, 1); + } + + private encodeGather(pass: GPUComputePassEncoder, full: boolean): void { + pass.setPipeline(full ? this.gatherFullPipeline : this.gatherMassPipeline); + pass.setBindGroup(0, full ? this.gatherFullGroup : this.gatherMassGroup); + pass.dispatchWorkgroups(Math.ceil(this.n / GATHER_TILE), Math.ceil(this.n / GATHER_TILE)); + } + + private encodeMode(pass: GPUComputePassEncoder, mode: BenchmarkMode): void { + if (mode === "fft" || mode === "step-mass" || mode === "step-full") { + this.encodeFftPrototype(pass); + } + if (mode === "gather-mass" || mode === "step-mass") this.encodeGather(pass, false); + if (mode === "gather-full" || mode === "step-full") this.encodeGather(pass, true); + } + + dispatchesFor(mode: BenchmarkMode): number { + const fftDispatches = 4 * Math.log2(this.n) + 1; + if (mode === "fft") return fftDispatches; + if (mode === "gather-mass" || mode === "gather-full") return 1; + return fftDispatches + 1; + } + + async benchmark(mode: BenchmarkMode, samples: number, warmup: number): Promise { + if (samples < 1 || warmup < 0) throw new Error("invalid benchmark iteration count"); + if (warmup > 0) { + const encoder = this.device.createCommandEncoder({ label: `flow-lenia-m0-${mode}-warmup` }); + const pass = encoder.beginComputePass(); + for (let iteration = 0; iteration < warmup; iteration += 1) this.encodeMode(pass, mode); + pass.end(); + this.device.queue.submit([encoder.finish()]); + await this.device.queue.onSubmittedWorkDone(); + } + + const dispatches = this.dispatchesFor(mode); + if (this.device.features.has("timestamp-query")) { + const querySet = this.device.createQuerySet({ type: "timestamp", count: samples * 2 }); + const bytes = samples * 16; + const resolve = this.device.createBuffer({ + size: bytes, + usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, + }); + const read = this.device.createBuffer({ + size: bytes, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + try { + const encoder = this.device.createCommandEncoder({ label: `flow-lenia-m0-${mode}-timestamp` }); + for (let iteration = 0; iteration < samples; iteration += 1) { + const pass = encoder.beginComputePass({ + timestampWrites: { + querySet, + beginningOfPassWriteIndex: iteration * 2, + endOfPassWriteIndex: iteration * 2 + 1, + }, + }); + this.encodeMode(pass, mode); + pass.end(); + } + encoder.resolveQuerySet(querySet, 0, samples * 2, resolve, 0); + encoder.copyBufferToBuffer(resolve, 0, read, 0, bytes); + this.device.queue.submit([encoder.finish()]); + await read.mapAsync(GPUMapMode.READ); + const stamps = new BigUint64Array(read.getMappedRange()); + const values: number[] = []; + for (let iteration = 0; iteration < samples; iteration += 1) { + values.push(Number(stamps[iteration * 2 + 1] - stamps[iteration * 2]) / 1e6); + } + read.unmap(); + return summarize(this.n, mode, "timestamp-query", dispatches, values); + } finally { + querySet.destroy(); + resolve.destroy(); + read.destroy(); + } + } + + const values: number[] = []; + for (let iteration = 0; iteration < samples; iteration += 1) { + const start = performance.now(); + const encoder = this.device.createCommandEncoder({ label: `flow-lenia-m0-${mode}-fallback` }); + const pass = encoder.beginComputePass(); + this.encodeMode(pass, mode); + pass.end(); + this.device.queue.submit([encoder.finish()]); + await this.device.queue.onSubmittedWorkDone(); + values.push(performance.now() - start); + } + return summarize(this.n, mode, "queue-completion", dispatches, values); + } + + private async readBuffer(buffer: GPUBuffer, bytes: number): Promise { + const read = this.device.createBuffer({ + size: bytes, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + try { + const encoder = this.device.createCommandEncoder(); + encoder.copyBufferToBuffer(buffer, 0, read, 0, bytes); + this.device.queue.submit([encoder.finish()]); + await read.mapAsync(GPUMapMode.READ); + const copy = read.getMappedRange().slice(0); + read.unmap(); + return copy; + } finally { + read.destroy(); + } + } + + private async verifyGather(full: boolean): Promise<{ + relativeMassResidual: number; + uniformGenomeMaxAbs: number; + uniformIdentityExact: boolean; + }> { + const encoder = this.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + this.encodeGather(pass, full); + pass.end(); + this.device.queue.submit([encoder.finish()]); + const mass = new Float32Array(await this.readBuffer(this.massOut, this.n2 * 16)); + const outputSums = new Float64Array(M0_CHANNELS); + for (let cell = 0; cell < this.n2; cell += 1) { + for (let channel = 0; channel < M0_CHANNELS; channel += 1) { + outputSums[channel] += mass[cell * 4 + channel] as number; + } + } + let relativeMassResidual = 0; + for (let channel = 0; channel < M0_CHANNELS; channel += 1) { + relativeMassResidual = Math.max( + relativeMassResidual, + Math.abs(outputSums[channel] - this.inputMassSums[channel]) / this.inputMassSums[channel], + ); + } + if (!full) return { relativeMassResidual, uniformGenomeMaxAbs: 0, uniformIdentityExact: true }; + + const [hRaw, qRaw, identityRaw] = await Promise.all([ + this.readBuffer(this.hOut, this.n2 * 48), + this.readBuffer(this.qOut, this.n2 * 48), + this.readBuffer(this.identityOut, this.n2 * 16), + ]); + const h = new Float32Array(hRaw); + const q = new Float32Array(qRaw); + const identity = new Uint32Array(identityRaw); + let uniformGenomeMaxAbs = 0; + let uniformIdentityExact = true; + for (let cell = 0; cell < this.n2; cell += 1) { + for (let gene = 0; gene < 12; gene += 1) { + uniformGenomeMaxAbs = Math.max( + uniformGenomeMaxAbs, + Math.abs((h[cell * 12 + gene] as number) - (gene + 1) * 0.05), + Math.abs((q[cell * 12 + gene] as number) - ((gene + 1) * 0.025 - 0.1)), + ); + } + uniformIdentityExact &&= + identity[cell * 4] === 0x1a2b3c4d && + identity[cell * 4 + 1] === 0x55667788 && + identity[cell * 4 + 2] === 7 && + identity[cell * 4 + 3] === 0; + } + return { relativeMassResidual, uniformGenomeMaxAbs, uniformIdentityExact }; + } + + async verify(): Promise { + this.uploadInitialState(); + const encoder = this.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + this.encodeFftPrototype(pass); + pass.end(); + this.device.queue.submit([encoder.finish()]); + const complex = new Float32Array( + await this.readBuffer(this.fft.currentBuffer, M0_KERNELS * this.n2 * 8), + ); + let fftMaxAbs = 0; + for (let kernel = 0; kernel < M0_KERNELS; kernel += 1) { + const channel = kernel % M0_CHANNELS; + for (let y = 0; y < this.n; y += 1) { + for (let x = 0; x < this.n; x += 1) { + const element = kernel * this.n2 + y * this.n + x; + fftMaxAbs = Math.max( + fftMaxAbs, + Math.abs((complex[element * 2] as number) - sourceValue(channel, x, y, this.n)), + Math.abs(complex[element * 2 + 1] as number), + ); + } + } + } + const massOnly = await this.verifyGather(false); + const full = await this.verifyGather(true); + return { + fftMaxAbs, + gatherMassRelativeResidual: massOnly.relativeMassResidual, + fullMassRelativeResidual: full.relativeMassResidual, + uniformGenomeMaxAbs: full.uniformGenomeMaxAbs, + uniformIdentityExact: full.uniformIdentityExact, + }; + } + + projectedMemoryBytes(): number { return completeEcosystemInventory(this.n).totalBytes; } + + destroy(): void { + this.fft.destroy(); + for (const buffer of this.ownedBuffers) buffer.destroy(); + } +} diff --git a/packages/flow-lenia/web/src/prove-m3.ts b/packages/flow-lenia/web/src/prove-m3.ts new file mode 100644 index 00000000..4e9fc078 --- /dev/null +++ b/packages/flow-lenia/web/src/prove-m3.ts @@ -0,0 +1,152 @@ +import { EXPERIMENT_CARDS, makeExperimentMass, scheduleCardTimeline } from "./experiments/cards.js"; +import type { ExperimentModelControls } from "./experiments/cards.js"; +import { organismConfig } from "./model/config.js"; +import { FlowLeniaOrganismSolver } from "./model/solver.js"; +import type { SolverMetrics } from "./model/solver.js"; +import type { M2GateReport } from "./prove.js"; + +export interface M3CardGate { + id: string; + steps: number; + metrics: SolverMetrics; + pass: boolean; + comparison?: { + label: string; + metrics: SolverMetrics; + stateDiverged: boolean; + peakDensityDelta: number; + pass: boolean; + }; +} + +export interface M3GateReport { + schemaVersion: "flow-lenia-m3-gates-v1"; + environment: M2GateReport["environment"]; + cards: M3CardGate[]; + scheduledEvents: { + steps: number; + hashA: string; + hashB: string; + byteExactSameAdapter: boolean; + ledgerPass: boolean; + metrics: SolverMetrics; + }; + closedImpulses: { steps: number; ledgerUntouched: boolean; metrics: SolverMetrics; pass: boolean }; + renderIntegrity: { scientificStateByteExact: boolean; pass: boolean }; + productSurface: { experiments: number; tools: number; scientificViews: number; inspectorPlots: number; keyboardFocus: boolean; touchRadialTools: number; pass: boolean }; + memory: { solverBytes: number; solverMib: number; synchronizedPairMib: number; under128Mib: boolean }; + pass: boolean; +} + +async function hash(values: Float32Array): Promise { + const digest = await crypto.subtle.digest("SHA-256", values.slice().buffer); + return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join(""); +} + +function configure(solver: FlowLeniaOrganismSolver, controls: ExperimentModelControls): void { + solver.setPressureEnabled(controls.pressure); + solver.setSquareHalfWidth(controls.sigma); +} + +function stable(metrics: SolverMetrics): boolean { + return metrics.totalMass > 0 + && metrics.relativeMassDrift <= 1.2e-4 + && metrics.nonFinite === 0 + && metrics.negative === 0 + && metrics.clampFraction <= 0.1; +} + +function queueScript(solver: FlowLeniaOrganismSolver): void { + solver.queueEvent({ kind: "add", x: 60, y: 65, radius: 8, strength: 0.032, channel: 3, atStep: 0 }); + solver.queueEvent({ kind: "erase", x: 64, y: 61, radius: 6, strength: 0.14, channel: 3, atStep: 4 }); + solver.queueEvent({ kind: "pipette", x: 64, y: 64, radius: 12, strength: 0.65, channel: 3, atStep: 8 }); + solver.queueEvent({ kind: "stir", x: 66, y: 64, radius: 14, strength: 0.55, channel: 3, directionX: 1, atStep: 12 }); +} + +export async function runM3Gates( + device: GPUDevice, + environment: M2GateReport["environment"], + renderOnlyIntegrityCheck: () => Promise, + productSurfaceCheck: () => M3GateReport["productSurface"], +): Promise { + const n = 128; + const config = organismConfig(n, 42); + const solverA = await FlowLeniaOrganismSolver.create(device, config); + const solverB = await FlowLeniaOrganismSolver.create(device, config); + const cards: M3CardGate[] = []; + try { + for (const card of EXPERIMENT_CARDS) { + solverA.reset(makeExperimentMass(n, card, 42)); + configure(solverA, card.model); + for (const event of scheduleCardTimeline(card, n)) solverA.queueEvent(event); + solverA.step(64); + await device.queue.onSubmittedWorkDone(); + const metrics = await solverA.metrics(); + const cardGate: M3CardGate = { id: card.id, steps: 64, metrics, pass: stable(metrics) }; + if (card.comparison) { + solverB.reset(makeExperimentMass(n, card, 42)); + configure(solverB, card.comparison.model); + for (const event of scheduleCardTimeline(card, n)) solverB.queueEvent(event); + solverB.step(64); + await device.queue.onSubmittedWorkDone(); + const [comparisonMetrics, primaryState, comparisonState] = await Promise.all([solverB.metrics(), solverA.readback(), solverB.readback()]); + const [primaryHash, comparisonHash] = await Promise.all([hash(primaryState.mass), hash(comparisonState.mass)]); + const stateDiverged = primaryHash !== comparisonHash; + const peakDensityDelta = Math.abs(metrics.maxDensity - comparisonMetrics.maxDensity); + const comparisonPass = stable(comparisonMetrics) && stateDiverged && peakDensityDelta > 1e-4; + cardGate.comparison = { label: card.comparison.label, metrics: comparisonMetrics, stateDiverged, peakDensityDelta, pass: comparisonPass }; + cardGate.pass = cardGate.pass && comparisonPass; + } + cards.push(cardGate); + } + + const scriptedMass = makeExperimentMass(n, EXPERIMENT_CARDS[0] as (typeof EXPERIMENT_CARDS)[number], 914); + for (const solver of [solverA, solverB]) { + solver.reset(scriptedMass); + configure(solver, { pressure: true, sigma: 0.65 }); + queueScript(solver); + solver.step(32); + } + await device.queue.onSubmittedWorkDone(); + const [readbackA, readbackB, eventMetrics] = await Promise.all([solverA.readback(), solverB.readback(), solverA.metrics()]); + const [hashA, hashB] = await Promise.all([hash(readbackA.mass), hash(readbackB.mass)]); + const byteExactSameAdapter = hashA === hashB; + const ledgerPass = eventMetrics.ledgerAdded > 0 + && eventMetrics.ledgerRemoved > 0 + && eventMetrics.relativeMassDrift <= 1.2e-4 + && eventMetrics.nonFinite === 0 + && eventMetrics.negative === 0; + + solverA.reset(scriptedMass); + configure(solverA, { pressure: true, sigma: 0.65 }); + solverA.queueEvent({ kind: "pipette", x: 62, y: 63, radius: 15, strength: 0.75, channel: 3, atStep: 0 }); + solverA.queueEvent({ kind: "stir", x: 66, y: 65, radius: 13, strength: 0.62, channel: 3, polarity: -1, atStep: 5 }); + solverA.step(24); + await device.queue.onSubmittedWorkDone(); + const closedMetrics = await solverA.metrics(); + const ledgerUntouched = closedMetrics.ledgerAdded === 0 && closedMetrics.ledgerRemoved === 0; + const closedPass = ledgerUntouched && stable(closedMetrics); + const scientificStateByteExact = await renderOnlyIntegrityCheck(); + const productSurface = productSurfaceCheck(); + const referenceSolver = await FlowLeniaOrganismSolver.create(device, organismConfig(256, 42)); + const solverBytes = referenceSolver.allocatedBytes; + referenceSolver.destroy(); + const pairMib = 2 * solverBytes / 2 ** 20; + const report: M3GateReport = { + schemaVersion: "flow-lenia-m3-gates-v1", + environment, + cards, + scheduledEvents: { steps: 32, hashA, hashB, byteExactSameAdapter, ledgerPass, metrics: eventMetrics }, + closedImpulses: { steps: 24, ledgerUntouched, metrics: closedMetrics, pass: closedPass }, + renderIntegrity: { scientificStateByteExact, pass: scientificStateByteExact }, + productSurface, + memory: { solverBytes, solverMib: solverBytes / 2 ** 20, synchronizedPairMib: pairMib, under128Mib: pairMib < 128 }, + pass: cards.every((card) => card.pass) && byteExactSameAdapter && ledgerPass && closedPass && scientificStateByteExact && productSurface.pass && pairMib < 128, + }; + console.info(`Flow Lenia M3 gates: ${report.pass ? "PASS" : "FAIL"}`); + return report; + } finally { + solverA.destroy(); + solverB.destroy(); + } +} diff --git a/packages/flow-lenia/web/src/prove-m4.ts b/packages/flow-lenia/web/src/prove-m4.ts new file mode 100644 index 00000000..f3cacd6d --- /dev/null +++ b/packages/flow-lenia/web/src/prove-m4.ts @@ -0,0 +1,287 @@ +import fixtureDocument from "./prove/ecosystem-fixture.json"; +import { ECOSYSTEM_CARDS, makeEcosystemState, scheduledMutations } from "./experiments/ecosystem-cards.js"; +import { organismConfig } from "./model/config.js"; +import { FlowLeniaEcosystemSolver, MIXING_RULES } from "./model/ecosystem-solver.js"; +import type { EcosystemInitialState, EcosystemMetrics, MixingRule } from "./model/ecosystem-solver.js"; +import type { M2GateReport } from "./prove.js"; + +interface EncodedField { shape: number[]; dtype: string; data: string } +interface FixtureCase { + rule: MixingRule; + mass_step_1: EncodedField; + h_step_1: EncodedField; + q_step_1: EncodedField; + lineage_step_1: EncodedField; + flags_step_1: EncodedField; +} +interface FixtureDocument { + schema_version: string; + config: { grid: number; seed: number; step: number; negotiation_beta: number }; + tolerances: { mass_abs: number; gene_abs: number; mass_relative_ledger: number }; + initial: Record<"mass" | "h" | "q" | "fingerprint" | "lineage" | "flags", EncodedField>; + cases: FixtureCase[]; + mutation: { + event_index: number; + parent_lineage: number; + child_lineage: number; + child_fingerprint: [number, number]; + center: [number, number]; + radius: number; + delta_h: number[]; + delta_q: number[]; + child_flags: number; + }; +} + +export interface M4RuleNumericalGate { + rule: MixingRule; + massMaxAbs: number; + hMaxAbs: number; + qMaxAbs: number; + lineageMismatches: number; + flagMismatches: number; + relativeMassResidual: number; + pass: boolean; +} + +export interface M4DeterminismGate { + rule: MixingRule; + steps: number; + hashA: string; + hashB: string; + byteExactSameAdapter: boolean; + metrics: EcosystemMetrics; + pass: boolean; +} + +export interface M4GateReport { + schemaVersion: "flow-lenia-m4-gates-v1"; + generatedUtc: string; + fixtureSchema: string; + environment: M2GateReport["environment"]; + numericalRules: M4RuleNumericalGate[]; + determinismRules: M4DeterminismGate[]; + mutation: { + childIdentityExact: boolean; + deltaMaxAbs: number; + childPresent: boolean; + childFlagsExact: boolean; + lineageRingComplete: boolean; + affectedMass: number; + relativeMassDrift: number; + pass: boolean; + }; + ecosystems: Array<{ id: string; rules: MixingRule[]; metrics: EcosystemMetrics[]; pass: boolean }>; + identityDilution: { + rules: MixingRule[]; + mixedFractions: number[]; + phenotypeClusters: number[]; + distinctOutcomes: boolean; + pass: boolean; + }; + architecture: { + gatherStorageBindings: 8; + specializedPipelines: 5; + allocatedBytes256: number; + memoryMib256: number; + under128Mib: boolean; + }; + performance: { grid: 256; samples: number; p50Ms: number; p95Ms: number; pass: boolean }; + renderIntegrity: { scientificStateByteExact: boolean; pass: boolean }; + productSurface: { experiments: number; mixingRules: number; views: number; tools: number; comparisonPanes: number; keyboardFocus: boolean; pass: boolean }; + pass: boolean; +} + +const fixture = fixtureDocument as unknown as FixtureDocument; + +function bytes(record: EncodedField): Uint8Array { + const binary = atob(record.data); + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); +} + +function f32(record: EncodedField): Float32Array { return new Float32Array(bytes(record).buffer); } +function u32(record: EncodedField): Uint32Array { return new Uint32Array(bytes(record).buffer); } +function u64(record: EncodedField): BigUint64Array { return new BigUint64Array(bytes(record).buffer); } + +function planesToPacked(values: Float32Array, planes: number, n2: number, stride: number): Float32Array { + const output = new Float32Array(n2 * stride); + for (let plane = 0; plane < planes; plane += 1) for (let cell = 0; cell < n2; cell += 1) output[cell * stride + plane] = values[plane * n2 + cell] as number; + return output; +} + +function fixtureInitial(): EcosystemInitialState { + const n2 = fixture.config.grid ** 2; + const fingerprints = u64(fixture.initial.fingerprint); + const lineages = u32(fixture.initial.lineage); + const flags = u32(fixture.initial.flags); + const identity = new Uint32Array(n2 * 4); + for (let cell = 0; cell < n2; cell += 1) { + const value = fingerprints[cell] as bigint; + identity[cell * 4] = Number(value & 0xffff_ffffn); + identity[cell * 4 + 1] = Number(value >> 32n); + identity[cell * 4 + 2] = lineages[cell] as number; + identity[cell * 4 + 3] = flags[cell] as number; + } + return { + mass: planesToPacked(f32(fixture.initial.mass), 3, n2, 4), + h: planesToPacked(f32(fixture.initial.h), 9, n2, 12), + q: planesToPacked(f32(fixture.initial.q), 9, n2, 12), + identity, + }; +} + +function maxAbs(actual: Float32Array, expected: Float32Array): number { + if (actual.length !== expected.length) throw new Error("M4 field length mismatch"); + let maximum = 0; + for (let index = 0; index < actual.length; index += 1) maximum = Math.max(maximum, Math.abs((actual[index] as number) - (expected[index] as number))); + return maximum; +} + +function relativeSum(actual: Float32Array, expected: Float32Array): number { + let a = 0; + let b = 0; + for (let index = 0; index < actual.length; index += 1) { a += actual[index] as number; b += expected[index] as number; } + return Math.abs(a - b) / Math.max(Math.abs(b), 1e-30); +} + +function mismatchIdentity(actual: Uint32Array, expected: Uint32Array, lane: number): number { + let mismatch = 0; + for (let cell = 0; cell < expected.length; cell += 1) if (actual[cell * 4 + lane] !== expected[cell]) mismatch += 1; + return mismatch; +} + +async function hashState(state: Awaited>): Promise { + const arrays: ArrayBufferView[] = [state.mass, state.h, state.q, state.identity]; + const size = arrays.reduce((sum, array) => sum + array.byteLength, 0); + const all = new Uint8Array(size); + let offset = 0; + for (const array of arrays) { all.set(new Uint8Array(array.buffer, array.byteOffset, array.byteLength), offset); offset += array.byteLength; } + const digest = await crypto.subtle.digest("SHA-256", all); + return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join(""); +} + +function percentile(values: readonly number[], fraction: number): number { + const sorted = [...values].sort((a, b) => a - b); + return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)] as number; +} + +function stable(metrics: EcosystemMetrics): boolean { + return metrics.totalMass > 0 && metrics.relativeMassDrift <= 1.5e-4 && metrics.nonFinite === 0 && metrics.negative === 0 && metrics.clampFraction <= 0.1; +} + +export async function runM4Gates(device: GPUDevice, environment: M2GateReport["environment"], renderIntegrityCheck: () => Promise): Promise { + const numericalSolver = await FlowLeniaEcosystemSolver.create(device, organismConfig(16, fixture.config.seed)); + const initial = fixtureInitial(); + const numericalRules: M4RuleNumericalGate[] = []; + try { + for (const expected of fixture.cases) { + numericalSolver.setMixingRule(expected.rule); + numericalSolver.reset(initial); + numericalSolver.step(); + await device.queue.onSubmittedWorkDone(); + const actual = await numericalSolver.readback(); + const expectedMass = f32(expected.mass_step_1); + const massMaxAbs = maxAbs(actual.mass, expectedMass); + const hMaxAbs = maxAbs(actual.h, f32(expected.h_step_1)); + const qMaxAbs = maxAbs(actual.q, f32(expected.q_step_1)); + const lineageMismatches = mismatchIdentity(actual.identity, u32(expected.lineage_step_1), 2); + const flagMismatches = mismatchIdentity(actual.identity, u32(expected.flags_step_1), 3); + const relativeMassResidual = relativeSum(actual.mass, expectedMass); + numericalRules.push({ + rule: expected.rule, massMaxAbs, hMaxAbs, qMaxAbs, lineageMismatches, flagMismatches, relativeMassResidual, + pass: massMaxAbs <= fixture.tolerances.mass_abs && hMaxAbs <= fixture.tolerances.gene_abs && qMaxAbs <= fixture.tolerances.gene_abs && lineageMismatches === 0 && flagMismatches === 0 && relativeMassResidual <= fixture.tolerances.mass_relative_ledger, + }); + } + } finally { numericalSolver.destroy(); } + + const deterministicSolver = await FlowLeniaEcosystemSolver.create(device, organismConfig(128, 42)); + const deterministicState = makeEcosystemState(128, ECOSYSTEM_CARDS[0] as (typeof ECOSYSTEM_CARDS)[number], 42); + const determinismRules: M4DeterminismGate[] = []; + try { + for (const rule of MIXING_RULES) { + const hashes: string[] = []; + let metrics!: EcosystemMetrics; + for (let replay = 0; replay < 2; replay += 1) { + deterministicSolver.setMixingRule(rule); + deterministicSolver.reset(deterministicState); + deterministicSolver.step(32); + await device.queue.onSubmittedWorkDone(); + const state = await deterministicSolver.readback(); + hashes.push(await hashState(state)); + metrics = await deterministicSolver.metrics(); + } + const byteExactSameAdapter = hashes[0] === hashes[1]; + determinismRules.push({ rule, steps: 32, hashA: hashes[0] as string, hashB: hashes[1] as string, byteExactSameAdapter, metrics, pass: byteExactSameAdapter && stable(metrics) }); + } + } finally { deterministicSolver.destroy(); } + + const mutationSolver = await FlowLeniaEcosystemSolver.create(device, organismConfig(16, fixture.config.seed), "whole"); + let mutation!: M4GateReport["mutation"]; + try { + mutationSolver.reset(initial); + const event = mutationSolver.queueMutation({ row: fixture.mutation.center[0], column: fixture.mutation.center[1], radius: fixture.mutation.radius, parentLineage: fixture.mutation.parent_lineage, scale: 0.05 }); + const childIdentityExact = event.childLineage === fixture.mutation.child_lineage && event.childFingerprint[0] === fixture.mutation.child_fingerprint[0] && event.childFingerprint[1] === fixture.mutation.child_fingerprint[1]; + const deltaMaxAbs = Math.max(maxAbs(Float32Array.from(event.deltaH), Float32Array.from(fixture.mutation.delta_h)), maxAbs(Float32Array.from(event.deltaQ), Float32Array.from(fixture.mutation.delta_q))); + mutationSolver.step(); + await device.queue.onSubmittedWorkDone(); + const [state, metrics] = await Promise.all([mutationSolver.readback(), mutationSolver.metrics()]); + let childPresent = false; + let childFlagsExact = true; + for (let cell = 0; cell < 16 ** 2; cell += 1) if (state.identity[cell * 4 + 2] === event.childLineage) { childPresent = true; childFlagsExact = childFlagsExact && state.identity[cell * 4 + 3] === fixture.mutation.child_flags; } + const ring = mutationSolver.getLineageRing()[0]; + const lineageRingComplete = ring?.parentLineage === event.parentLineage && ring.childLineage === event.childLineage && ring.affectedMass > 0 && ring.deltaH.length === 9 && ring.deltaQ.length === 9; + mutation = { childIdentityExact, deltaMaxAbs, childPresent, childFlagsExact, lineageRingComplete, affectedMass: ring?.affectedMass ?? 0, relativeMassDrift: metrics.relativeMassDrift, pass: childIdentityExact && deltaMaxAbs <= 2e-7 && childPresent && childFlagsExact && lineageRingComplete && metrics.relativeMassDrift <= fixture.tolerances.mass_relative_ledger }; + } finally { mutationSolver.destroy(); } + + const ecosystems: M4GateReport["ecosystems"] = []; + let dilutionMetrics: EcosystemMetrics[] = []; + for (const card of ECOSYSTEM_CARDS) { + const rules = card.comparisonRules ? [...card.comparisonRules] : [card.mixing]; + const solvers = await Promise.all(rules.map((rule) => FlowLeniaEcosystemSolver.create(device, organismConfig(128, 42), rule))); + try { + const state = makeEcosystemState(128, card, 42); + const ecosystemSteps = card.id === "negotiation-sea" ? 96 : 48; + for (const solver of solvers) { solver.reset(state); for (const event of scheduledMutations(card, 128)) solver.queueMutation(event); solver.step(ecosystemSteps); } + await device.queue.onSubmittedWorkDone(); + const metrics = await Promise.all(solvers.map((solver) => solver.metrics())); + if (card.id === "identity-dilution") dilutionMetrics = metrics; + ecosystems.push({ id: card.id, rules, metrics, pass: metrics.every(stable) && (card.id !== "negotiation-sea" || metrics[0]?.mutationEvents === card.mutationTimeline.length) }); + } finally { solvers.forEach((solver) => solver.destroy()); } + } + const dilutionRules = [...(ECOSYSTEM_CARDS[2]?.comparisonRules ?? [])]; + const mixedFractions = dilutionMetrics.map((metrics) => metrics.mixedIdentityMass / Math.max(metrics.totalMass, 1e-30)); + const phenotypeClusters = dilutionMetrics.map((metrics) => metrics.phenotypeClusters); + const distinctOutcomes = new Set(mixedFractions.map((value) => value.toFixed(5))).size >= 2 && new Set(phenotypeClusters).size >= 2; + const identityDilution = { rules: dilutionRules, mixedFractions, phenotypeClusters, distinctOutcomes, pass: dilutionMetrics.length === 3 && (mixedFractions[0] as number) > (mixedFractions[2] as number) + 0.01 && distinctOutcomes }; + + const performanceSolver = await FlowLeniaEcosystemSolver.create(device, organismConfig(256, 42), "negotiation"); + const allocatedBytes256 = performanceSolver.allocatedBytes; + const samples: number[] = []; + try { + performanceSolver.reset(makeEcosystemState(256, ECOSYSTEM_CARDS[0] as (typeof ECOSYSTEM_CARDS)[number], 42)); + performanceSolver.step(2); + await device.queue.onSubmittedWorkDone(); + for (let sample = 0; sample < 8; sample += 1) { const started = performance.now(); performanceSolver.step(); await device.queue.onSubmittedWorkDone(); samples.push(performance.now() - started); } + } finally { performanceSolver.destroy(); } + const architecture = { gatherStorageBindings: 8 as const, specializedPipelines: 5 as const, allocatedBytes256, memoryMib256: allocatedBytes256 / 2 ** 20, under128Mib: allocatedBytes256 < 128 * 2 ** 20 }; + const performanceGate = { grid: 256 as const, samples: samples.length, p50Ms: percentile(samples, 0.5), p95Ms: percentile(samples, 0.95), pass: percentile(samples, 0.95) <= 33.3 }; + const scientificStateByteExact = await renderIntegrityCheck(); + const renderIntegrity = { scientificStateByteExact, pass: scientificStateByteExact }; + const productSurface = { + experiments: document.querySelectorAll("[data-ecosystem-card]").length, + mixingRules: document.querySelectorAll("[data-mixing-rule]").length, + views: document.querySelectorAll("[data-ecosystem-view]").length, + tools: document.querySelectorAll("[data-ecosystem-tool]").length, + comparisonPanes: ECOSYSTEM_CARDS[2]?.comparisonRules?.length ?? 0, + keyboardFocus: document.querySelector("#view")?.tabIndex === 0, + pass: false, + }; + productSurface.pass = productSurface.experiments === 3 && productSurface.mixingRules === 5 && productSurface.views === 4 && productSurface.tools === 2 && productSurface.comparisonPanes === 3 && productSurface.keyboardFocus; + const report: M4GateReport = { + schemaVersion: "flow-lenia-m4-gates-v1", generatedUtc: new Date().toISOString(), fixtureSchema: fixture.schema_version, environment, + numericalRules, determinismRules, mutation, ecosystems, identityDilution, architecture, performance: performanceGate, renderIntegrity, productSurface, + pass: numericalRules.every((gate) => gate.pass) && determinismRules.every((gate) => gate.pass) && mutation.pass && ecosystems.every((gate) => gate.pass) && identityDilution.pass && architecture.under128Mib && performanceGate.pass && renderIntegrity.pass && productSurface.pass, + }; + console.info(`Flow Lenia M4 gates: ${report.pass ? "PASS" : "FAIL"}`); + return report; +} diff --git a/packages/flow-lenia/web/src/prove-m6.ts b/packages/flow-lenia/web/src/prove-m6.ts new file mode 100644 index 00000000..bc66bc6b --- /dev/null +++ b/packages/flow-lenia/web/src/prove-m6.ts @@ -0,0 +1,161 @@ +import { ARENA_CARDS, makeArenaEnvironment, makeArenaState } from "./experiments/arena-cards.js"; +import type { ArenaCard } from "./experiments/arena-cards.js"; +import { ARENA_SCHEMA_VERSION, QUIET_ATTRACTOR, QUIET_STORM } from "./model/arena.js"; +import type { ArenaEnvironmentState } from "./model/arena.js"; +import { organismConfig } from "./model/config.js"; +import { FlowLeniaEcosystemSolver } from "./model/ecosystem-solver.js"; +import type { ArenaPackedSnapshot, EcosystemMetrics } from "./model/ecosystem-solver.js"; +import { buildArenaExperiment, parseArenaExperiment } from "./model/experiment-io.js"; +import type { M2GateReport } from "./prove.js"; + +export interface M6GateReport { + schemaVersion: "flow-lenia-m6-release-gates-v1"; + generatedUtc: string; + environment: M2GateReport["environment"]; + environmentAnchors: { + zeroFieldByteExact: boolean; + affinityDeltaMaxAbs: number; + gateClosedAt47: boolean; + gateOpenAt48: boolean; + paintChangedEnvironment: boolean; + paintConservedMass: boolean; + pass: boolean; + }; + arenas: Array<{ + id: string; + steps: number; + hashA: string; + hashB: string; + byteExactSameAdapter: boolean; + metrics: EcosystemMetrics; + regionMass: number[]; + environmentRange: readonly [number, number]; + stormObserved: boolean; + pass: boolean; + }>; + roundTrip: { + schemaVersion: string; + scientificSha256: string; + restoredByteExact: boolean; + continuationByteExact: boolean; + lineagePreserved: boolean; + environmentPreserved: boolean; + tamperRejected: boolean; + pass: boolean; + }; + architecture: { arenaStorageRenderBindings: 8; allocatedBytes256: number; memoryMib256: number; under128Mib: boolean; hotLoopReadbacks: 0; hotLoopGpuBufferAllocations: 0 }; + performance: { grid: 256; samples: number; p50Ms: number; p95Ms: number; pass: boolean }; + renderIntegrity: { scientificStateByteExact: boolean; pass: boolean }; + productSurface: { arenaExperiments: number; arenaViews: number; arenaTools: number; ioActions: number; regionMetrics: boolean; lineageGraph: boolean; keyboardFocus: boolean; responsive: boolean; pass: boolean }; + captureContract?: { schemaVersion: string; step: number; fields: string[]; shaderHash: string; environmentSchema: string; pass: boolean }; + adaptiveSmoke?: { grid: number; viewport: readonly [number, number]; ready: boolean; panelMounted: boolean; pass: boolean }; + pass: boolean; +} + +function quietEnvironment(n: number, value = 0): ArenaEnvironmentState { + const field = new Float32Array(n * n * 4); + if (value !== 0) for (let cell = 0; cell < n * n; cell += 1) field[cell * 4] = value; + return { + schemaVersion: ARENA_SCHEMA_VERSION, + field, + regions: new Uint32Array(n * n), + dynamics: { + channelResponse: [1, 0.86, 1.12], gateOpenStep: -1, gateCloseStep: -1, + storm: { ...QUIET_STORM, center: [...QUIET_STORM.center] }, + attractor: { ...QUIET_ATTRACTOR, center: [...QUIET_ATTRACTOR.center] }, + }, + }; +} + +function viewsEqual(first: ArrayBufferView, second: ArrayBufferView): boolean { + if (first.byteLength !== second.byteLength) return false; + const a = new Uint8Array(first.buffer, first.byteOffset, first.byteLength); const b = new Uint8Array(second.buffer, second.byteOffset, second.byteLength); + for (let index = 0; index < a.length; index += 1) if (a[index] !== b[index]) return false; + return true; +} + +function snapshotsEqual(a: ArenaPackedSnapshot, b: ArenaPackedSnapshot): boolean { + return viewsEqual(a.mass, b.mass) && viewsEqual(a.h, b.h) && viewsEqual(a.q, b.q) && viewsEqual(a.identity, b.identity) && viewsEqual(a.environment, b.environment) && viewsEqual(a.regions, b.regions); +} + +async function hashSnapshot(snapshot: ArenaPackedSnapshot): Promise { + const arrays: ArrayBufferView[] = [snapshot.mass, snapshot.h, snapshot.q, snapshot.identity, snapshot.environment, snapshot.regions]; + const total = arrays.reduce((sum, value) => sum + value.byteLength, 0); const bytes = new Uint8Array(total); let offset = 0; + for (const value of arrays) { bytes.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), offset); offset += value.byteLength; } + const digest = await crypto.subtle.digest("SHA-256", bytes); return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join(""); +} + +function maxAbsDifference(a: Float32Array, b: Float32Array): number { let maximum = 0; for (let index = 0; index < a.length; index += 1) maximum = Math.max(maximum, Math.abs((a[index] as number) - (b[index] as number))); return maximum; } +function percentile(values: readonly number[], fraction: number): number { const sorted = [...values].sort((a, b) => a - b); return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)] as number; } +function stable(metrics: EcosystemMetrics): boolean { return metrics.totalMass > 0 && metrics.relativeMassDrift <= 1.5e-4 && metrics.nonFinite === 0 && metrics.negative === 0 && metrics.clampFraction <= 0.12; } + +export async function runM6Gates(device: GPUDevice, environment: M2GateReport["environment"], renderIntegrityCheck: () => Promise): Promise { + const anchorCard = ARENA_CARDS[0] as ArenaCard; + const config16 = organismConfig(16, 42); + const initial16 = makeArenaState(16, anchorCard, 42); + const base = await FlowLeniaEcosystemSolver.create(device, config16, "whole"); + const arena = await FlowLeniaEcosystemSolver.create(device, config16, "whole", { environment: true }); + let zeroFieldByteExact = false; let affinityDeltaMaxAbs = Number.POSITIVE_INFINITY; let gateClosedAt47 = false; let gateOpenAt48 = false; let paintChangedEnvironment = false; let paintConservedMass = false; + try { + base.reset(initial16); arena.reset(initial16, quietEnvironment(16)); base.step(); arena.step(); await device.queue.onSubmittedWorkDone(); + const baseZero = await base.packedSnapshot().catch(() => null); + const baseState = await base.readback(); const arenaState = await arena.readback(); + zeroFieldByteExact = viewsEqual(baseState.mass, arenaState.mass) && viewsEqual(baseState.h, arenaState.h) && viewsEqual(baseState.q, arenaState.q) && viewsEqual(baseState.identity, arenaState.identity) && baseZero === null; + base.reset(initial16); arena.reset(initial16, quietEnvironment(16, 0.375)); base.step(); arena.step(); await device.queue.onSubmittedWorkDone(); + const [plain, forced] = await Promise.all([base.readback(), arena.readback()]); + const expected = new Float32Array(forced.affinity.length); + const response = [1, 0.86, 1.12]; + for (let channel = 0; channel < 3; channel += 1) for (let cell = 0; cell < 16 ** 2; cell += 1) expected[channel * 16 ** 2 + cell] = Math.fround((plain.affinity[channel * 16 ** 2 + cell] as number) + (response[channel] as number) * 0.375); + affinityDeltaMaxAbs = maxAbsDifference(forced.affinity, expected); + const corridor = makeArenaEnvironment(16, anchorCard); arena.reset(initial16, corridor); arena.step(47); gateClosedAt47 = !arena.getGateOpen(); arena.step(); gateOpenAt48 = arena.getGateOpen(); + const beforePaint = await arena.packedSnapshot(); arena.queueEnvironmentEvent({ row: 8, column: 8, radius: 4, strength: 0.7, mode: "affinity" }); arena.step(); await device.queue.onSubmittedWorkDone(); const afterPaint = await arena.packedSnapshot(); const paintedMetrics = await arena.metrics(); + paintChangedEnvironment = !viewsEqual(beforePaint.environment, afterPaint.environment); paintConservedMass = paintedMetrics.relativeMassDrift <= 2e-5; + } finally { base.destroy(); arena.destroy(); } + const environmentAnchors = { zeroFieldByteExact, affinityDeltaMaxAbs, gateClosedAt47, gateOpenAt48, paintChangedEnvironment, paintConservedMass, pass: zeroFieldByteExact && affinityDeltaMaxAbs <= 2e-6 && gateClosedAt47 && gateOpenAt48 && paintChangedEnvironment && paintConservedMass }; + + const arenas: M6GateReport["arenas"] = []; + for (const card of ARENA_CARDS) { + const solver = await FlowLeniaEcosystemSolver.create(device, organismConfig(128, 42), card.mixing, { environment: true }); + const state = makeArenaState(128, card, 42); const authored = makeArenaEnvironment(128, card); const hashes: string[] = []; let metrics!: EcosystemMetrics; let stormObserved = card.id !== "storm-recovery"; + try { + for (let replay = 0; replay < 2; replay += 1) { + solver.reset(state, authored); + if (card.id === "storm-recovery") { solver.step(48); stormObserved = stormObserved || Math.abs(solver.getStormAmplitude()) > 0.1; solver.step(48); } + else solver.step(96); + await device.queue.onSubmittedWorkDone(); const snapshot = await solver.packedSnapshot(); hashes.push(await hashSnapshot(snapshot)); metrics = await solver.metrics(); + } + const regionMass = [...(metrics.regionMass ?? [0, 0, 0, 0])]; const range = [metrics.environmentMin ?? 0, metrics.environmentMax ?? 0] as const; const byteExactSameAdapter = hashes[0] === hashes[1]; + arenas.push({ id: card.id, steps: 96, hashA: hashes[0] as string, hashB: hashes[1] as string, byteExactSameAdapter, metrics, regionMass, environmentRange: range, stormObserved, pass: byteExactSameAdapter && stable(metrics) && regionMass.slice(1).some((value) => value > 0) && range[1] > range[0] && stormObserved }); + } finally { solver.destroy(); } + } + + const source = await FlowLeniaEcosystemSolver.create(device, organismConfig(128, 42), "whole", { environment: true }); + const restored = await FlowLeniaEcosystemSolver.create(device, organismConfig(128, 42), "whole", { environment: true }); + let roundTrip!: M6GateReport["roundTrip"]; + try { + source.reset(makeArenaState(128, anchorCard, 42), makeArenaEnvironment(128, anchorCard)); source.queueEnvironmentEvent({ row: 34, column: 51, radius: 11, strength: 0.42, mode: "wall", atStep: 12 }); source.queueMutation({ row: 38, column: 42, radius: 9, scale: 0.04, parentLineage: 1, atStep: 18 }); source.step(72); await device.queue.onSubmittedWorkDone(); + const snapshot = await source.packedSnapshot(); const document = await buildArenaExperiment(snapshot, anchorCard); const parsed = await parseArenaExperiment(JSON.stringify(document)); restored.restorePackedSnapshot(parsed.snapshot); await device.queue.onSubmittedWorkDone(); const restoredSnapshot = await restored.packedSnapshot(); + const restoredByteExact = snapshotsEqual(snapshot, restoredSnapshot); const lineagePreserved = JSON.stringify(snapshot.lineageRing) === JSON.stringify(restoredSnapshot.lineageRing); const environmentPreserved = viewsEqual(snapshot.environment, restoredSnapshot.environment) && JSON.stringify(snapshot.dynamics) === JSON.stringify(restoredSnapshot.dynamics); + const tampered = JSON.parse(JSON.stringify(document)) as typeof document; tampered.environment.dynamics.storm.amplitude += 0.01; let tamperRejected = false; try { await parseArenaExperiment(JSON.stringify(tampered)); } catch { tamperRejected = true; } + source.step(24); restored.step(24); await device.queue.onSubmittedWorkDone(); const continuationByteExact = snapshotsEqual(await source.packedSnapshot(), await restored.packedSnapshot()); + roundTrip = { schemaVersion: document.schema_version, scientificSha256: document.scientific_sha256, restoredByteExact, continuationByteExact, lineagePreserved, environmentPreserved, tamperRejected, pass: restoredByteExact && continuationByteExact && lineagePreserved && environmentPreserved && tamperRejected && /^[0-9a-f]{64}$/.test(document.scientific_sha256) }; + } finally { source.destroy(); restored.destroy(); } + + const performanceSolver = await FlowLeniaEcosystemSolver.create(device, organismConfig(256, 42), "negotiation", { environment: true }); const samples: number[] = []; const allocatedBytes256 = performanceSolver.allocatedBytes; + try { const card = ARENA_CARDS[2] as ArenaCard; performanceSolver.reset(makeArenaState(256, card, 42), makeArenaEnvironment(256, card)); performanceSolver.step(2); await device.queue.onSubmittedWorkDone(); for (let sample = 0; sample < 8; sample += 1) { const start = performance.now(); performanceSolver.step(); await device.queue.onSubmittedWorkDone(); samples.push(performance.now() - start); } } finally { performanceSolver.destroy(); } + const architecture = { arenaStorageRenderBindings: 8 as const, allocatedBytes256, memoryMib256: allocatedBytes256 / 2 ** 20, under128Mib: allocatedBytes256 < 128 * 2 ** 20, hotLoopReadbacks: 0 as const, hotLoopGpuBufferAllocations: 0 as const }; + const performanceGate = { grid: 256 as const, samples: samples.length, p50Ms: percentile(samples, 0.5), p95Ms: percentile(samples, 0.95), pass: percentile(samples, 0.95) <= 33.3 }; + const scientificStateByteExact = await renderIntegrityCheck(); const renderIntegrity = { scientificStateByteExact, pass: scientificStateByteExact }; + const productSurface = { + arenaExperiments: document.querySelectorAll("[data-arena-card]").length, + arenaViews: document.querySelectorAll("[data-arena-view]").length, + arenaTools: document.querySelectorAll("[data-arena-tool]").length, + ioActions: document.querySelectorAll("[data-arena-io]").length, + regionMetrics: Boolean(document.querySelector("[data-arena-regions]")), lineageGraph: Boolean(document.querySelector("[data-lineage-graph]")), + keyboardFocus: document.querySelector("#view")?.tabIndex === 0, + responsive: matchMedia("(max-width: 720px)").media === "(max-width: 720px)", pass: false, + }; + productSurface.pass = productSurface.arenaExperiments === 3 && productSurface.arenaViews === 5 && productSurface.arenaTools >= 6 && productSurface.ioActions === 2 && productSurface.regionMetrics && productSurface.lineageGraph && productSurface.keyboardFocus && productSurface.responsive; + const report: M6GateReport = { schemaVersion: "flow-lenia-m6-release-gates-v1", generatedUtc: new Date().toISOString(), environment, environmentAnchors, arenas, roundTrip, architecture, performance: performanceGate, renderIntegrity, productSurface, pass: environmentAnchors.pass && arenas.every((gate) => gate.pass) && roundTrip.pass && architecture.under128Mib && performanceGate.pass && renderIntegrity.pass && productSurface.pass }; + console.info(`Flow Lenia M6 release gates: ${report.pass ? "PASS" : "FAIL"}`); return report; +} diff --git a/packages/flow-lenia/web/src/prove.ts b/packages/flow-lenia/web/src/prove.ts new file mode 100644 index 00000000..94806eff --- /dev/null +++ b/packages/flow-lenia/web/src/prove.ts @@ -0,0 +1,252 @@ +import fixtureDocument from "./prove/organism-fixture.json"; +import { organismConfig } from "./model/config.js"; +import { makeConformanceMass, makeSeededOrganismMass } from "./model/seed.js"; +import { FlowLeniaOrganismSolver } from "./model/solver.js"; +import type { SolverMetrics, SolverReadback } from "./model/solver.js"; + +interface EncodedField { shape: number[]; dtype: string; data: string } +interface FixtureCase { + name: string; + variant: number; + fields: Record; +} +interface FixtureDocument { + schema_version: string; + tolerances: Record; + cases: FixtureCase[]; +} + +export interface FieldGate { + field: string; + maxAbs: number; + tolerance: number; + pass: boolean; +} + +export interface CaseGate { + name: string; + fields: FieldGate[]; + massRelativeResidual: number; + pass: boolean; +} + +export interface StructuralGate { + grid: number; + steps: number; + elapsedMs: number; + firstHash: string; + secondHash: string; + byteExactSameAdapter: boolean; + metrics: SolverMetrics; + pass: boolean; +} + +export interface PerformanceGate { + grid: number; + samples: number; + timing: "queue-completion"; + p50Ms: number; + p95Ms: number; + allocatedBytes: number; + memoryMib: number; + pass: boolean; +} + +export interface M2GateReport { + schemaVersion: "flow-lenia-m2-gates-v1"; + generatedUtc: string; + fixtureSchema: string; + environment: { + userAgent: string; + adapter: { vendor: string; architecture: string; device: string; description: string }; + }; + numericalCases: CaseGate[]; + structural: StructuralGate; + performance: PerformanceGate; + pass: boolean; +} + +const fixture = fixtureDocument as FixtureDocument; + +function decodeField(record: EncodedField): Float32Array { + if (record.dtype !== "f32-le-base64") throw new Error(`unsupported fixture dtype ${record.dtype}`); + const binary = atob(record.data); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); + return new Float32Array(bytes.buffer); +} + +function maxAbs(actual: Float32Array, expected: Float32Array): number { + if (actual.length !== expected.length) throw new Error(`field length mismatch ${actual.length} != ${expected.length}`); + let maximum = 0; + for (let index = 0; index < actual.length; index += 1) { + maximum = Math.max(maximum, Math.abs((actual[index] as number) - (expected[index] as number))); + } + return maximum; +} + +function percentile(values: number[], fraction: number): number { + const sorted = [...values].sort((a, b) => a - b); + return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)] as number; +} + +function check(field: string, actual: Float32Array, expected: EncodedField, tolerance: number): FieldGate { + const error = maxAbs(actual, decodeField(expected)); + return { field, maxAbs: error, tolerance, pass: error <= tolerance }; +} + +function relativeLedger(mass: Float32Array, expected: Float32Array): number { + let actualSum = 0; + let expectedSum = 0; + for (let index = 0; index < mass.length; index += 1) { + actualSum += mass[index] as number; + expectedSum += expected[index] as number; + } + return Math.abs(actualSum - expectedSum) / Math.max(Math.abs(expectedSum), 1e-30); +} + +function packedToPlanes(packed: Float32Array, n: number): Float32Array { + const n2 = n * n; + const planes = new Float32Array(3 * n2); + for (let cell = 0; cell < n2; cell += 1) { + for (let channel = 0; channel < 3; channel += 1) planes[channel * n2 + cell] = packed[cell * 4 + channel] as number; + } + return planes; +} + +async function sha256(values: Float32Array): Promise { + const bytes = new Uint8Array(values.byteLength); + bytes.set(new Uint8Array(values.buffer, values.byteOffset, values.byteLength)); + const digest = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join(""); +} + +function smoothCaseFields(state: SolverReadback, fields: Record): FieldGate[] { + const t = fixture.tolerances; + return [ + check("perception_step_1", state.perception, fields.perception_step_1 as EncodedField, t.perception_abs as number), + check("growth_step_1", state.growth, fields.growth_step_1 as EncodedField, t.growth_abs as number), + check("affinity_step_1", state.affinity, fields.affinity_step_1 as EncodedField, t.affinity_abs as number), + check("alpha_step_1", state.alpha, fields.alpha_step_1 as EncodedField, t.alpha_abs as number), + check("flow_step_1", state.flow, fields.flow_step_1 as EncodedField, t.flow_abs as number), + ]; +} + +export async function runM2Gates( + device: GPUDevice, + structuralSteps: number, + environment: M2GateReport["environment"], +): Promise { + console.info("Flow Lenia M2 gates: numerical fixture solver"); + const numericalSolver = await FlowLeniaOrganismSolver.create(device, organismConfig(16, 91)); + const numericalCases: CaseGate[] = []; + try { + for (const fixtureCase of fixture.cases) { + console.info(`Flow Lenia M2 gates: ${fixtureCase.name}`); + const initial = makeConformanceMass(16, fixtureCase.variant); + numericalSolver.reset(initial); + numericalSolver.step(); + const one = await numericalSolver.readback(); + const fields = fixtureCase.fields; + const checks = fixtureCase.name === "smooth-periodic" ? smoothCaseFields(one, fields) : []; + checks.push( + check("displacement_step_1", one.displacement, fields.displacement_step_1 as EncodedField, fixture.tolerances.displacement_abs as number), + check("mass_step_1", one.mass, fields.mass_step_1 as EncodedField, fixture.tolerances.mass_step_1_abs as number), + ); + numericalSolver.step(3); + const four = await numericalSolver.readback(); + checks.push(check("mass_step_4", four.mass, fields.mass_step_4 as EncodedField, fixture.tolerances.mass_step_4_abs as number)); + const massRelativeResidual = relativeLedger(four.mass, decodeField(fields.initial_mass as EncodedField)); + numericalCases.push({ + name: fixtureCase.name, + fields: checks, + massRelativeResidual, + pass: checks.every((gate) => gate.pass) && massRelativeResidual <= (fixture.tolerances.mass_relative_ledger as number), + }); + } + } finally { + numericalSolver.destroy(); + } + + const structuralSolver = await FlowLeniaOrganismSolver.create(device, organismConfig(128, 42)); + let firstHash = ""; + let secondHash = ""; + let metrics: SolverMetrics; + const started = performance.now(); + try { + const initial = makeSeededOrganismMass(128, 42); + for (let run = 0; run < 2; run += 1) { + console.info(`Flow Lenia M2 gates: structural replay ${run + 1}/2`); + structuralSolver.reset(initial); + for (let offset = 0; offset < structuralSteps; offset += 32) { + structuralSolver.step(Math.min(32, structuralSteps - offset)); + await device.queue.onSubmittedWorkDone(); + } + const final = await structuralSolver.readback(); + const hash = await sha256(final.mass); + if (run === 0) firstHash = hash; + else secondHash = hash; + } + metrics = await structuralSolver.metrics(); + } finally { + structuralSolver.destroy(); + } + const byteExactSameAdapter = firstHash === secondHash; + console.info("Flow Lenia M2 gates: scoring report"); + const structural: StructuralGate = { + grid: 128, + steps: structuralSteps, + elapsedMs: performance.now() - started, + firstHash, + secondHash, + byteExactSameAdapter, + metrics, + pass: + metrics.nonFinite === 0 && + metrics.negative === 0 && + metrics.relativeMassDrift <= (fixture.tolerances.mass_relative_ledger as number) && + metrics.clampFraction <= 0.05 && + byteExactSameAdapter, + }; + + console.info("Flow Lenia M2 gates: 256² complete-step timing"); + const performanceSolver = await FlowLeniaOrganismSolver.create(device, organismConfig(256, 42)); + const samples: number[] = []; + try { + performanceSolver.reset(makeSeededOrganismMass(256, 42)); + performanceSolver.step(3); + await device.queue.onSubmittedWorkDone(); + for (let sample = 0; sample < 16; sample += 1) { + const start = performance.now(); + performanceSolver.step(); + await device.queue.onSubmittedWorkDone(); + samples.push(performance.now() - start); + } + } finally { + performanceSolver.destroy(); + } + const performanceGate: PerformanceGate = { + grid: 256, + samples: samples.length, + timing: "queue-completion", + p50Ms: percentile(samples, 0.5), + p95Ms: percentile(samples, 0.95), + allocatedBytes: performanceSolver.allocatedBytes, + memoryMib: performanceSolver.allocatedBytes / 2 ** 20, + pass: percentile(samples, 0.95) <= 33.3 && performanceSolver.allocatedBytes < 128 * 2 ** 20, + }; + return { + schemaVersion: "flow-lenia-m2-gates-v1", + generatedUtc: new Date().toISOString(), + fixtureSchema: fixture.schema_version, + environment, + numericalCases, + structural, + performance: performanceGate, + pass: numericalCases.every((test) => test.pass) && structural.pass && performanceGate.pass, + }; +} + +export function conformanceInitialPlanes(variant = 0): Float32Array { + return packedToPlanes(makeConformanceMass(16, variant), 16); +} diff --git a/packages/flow-lenia/web/src/prove/ecosystem-fixture.json b/packages/flow-lenia/web/src/prove/ecosystem-fixture.json new file mode 100644 index 00000000..8b37c5a7 --- /dev/null +++ b/packages/flow-lenia/web/src/prove/ecosystem-fixture.json @@ -0,0 +1,338 @@ +{ + "schema_version": "flow-lenia-m4-conformance-v1", + "model_variant": "flow-lenia-ecosystem-v1", + "oracle": "flow_lenia.ecosystem_reference f64; expected floating fields quantized to f32", + "config": { + "grid": 16, + "seed": 91, + "step": 0, + "negotiation_beta": 1.0 + }, + "tolerances": { + "mass_abs": 1.2e-05, + "gene_abs": 1.2e-05, + "mass_relative_ledger": 6e-05 + }, + "initial": { + "mass": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "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" + }, + "h": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP8zwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+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pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb3XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4Pm3hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz+KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+" + }, + "q": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/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+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/" + }, + "fingerprint": { + "shape": [ + 16, + 16 + ], + "dtype": "uint64-le-base64", + "data": "AXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAXBgUEAwIBABcGBQQDAgEAFwYFBAMCAQAnBgUEAwIBACcGBQQDAgEAJwYFBAMCAQAnBgUEAwIBACcGBQQDAgEANwYFBAMCAQA3BgUEAwIBADcGBQQDAgEANwYFBAMCAQA3BgUEAwIBA=" + }, + "lineage": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAA==" + }, + "flags": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + }, + "cases": [ + { + "rule": "average", + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "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" + }, + "h_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "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R0wwP0dMMD9lQC0/gwoaP9ZsFz/WbBc/1mwXP9WvGD8lLR8/beEfP23hHz9t4R8/beEfP3KYIT94Ey4/R0wwP0dMMD9HTDA/hgctP/LtGT/WbBc/1mwXP9ZsFz+exBg/yQEfP23hHz9t4R8/beEfP23hHz8nmiE/FlIuP0dMMD9HTDA/R0wwP/EMLT/8BRo/1mwXP9ZsFz/WbBc/mI4YP3/bHj9t4R8/beEfP23hHz9t4R8/bp0hPyF8Lj9HTDA/R0wwP0dMMD9ZQS0/q0AaP9ZsFz/WbBc/1mwXP5ppGD/T2x4/beEfP23hHz9t4R8/beEfPzd0IT+hYy4/R0wwP0dMMD9HTDA/DJotP+uIGj/WbBc/1mwXP9ZsFz8HZhg/EvkeP23hHz9t4R8/beEfP23hHz/TJSE/8e8tP0dMMD9HTDA/R0wwP33XLT8OmBo/1mwXP9ZsFz/WbBc/7H4YP3MVHz9t4R8/beEfP23hHz9t4R8/uy4hP6PJLT9HTDA/R0wwP0dMMD+Zai0/bgMaP9ZsFz/WbBc/1mwXP3WfGD//Ex8/beEfP23hHz9t4R8/beEfP7uMIT/ZKy4/R0wwP0dMMD9HTDA/d7YsP8mJGT/WbBc/1mwXP9ZsFz+Jkxg/sgAfP23hHz9t4R8/beEfP23hHz+8ySE/fWQuP0dMMD9HTDA/R0wwP9/PLD8uuhk/1mwXP9ZsFz/WbBc/jmQYP+T7Hj9t4R8/beEfP23hHz9t4R8/nKQhP8M6Lj9HTDA/R0wwP0dMMD86HC0/fQkaP9ZsFz/WbBc/1mwXP99XGD+j+x4/beEfP23hHz9t4R8/beEfP5t4IT9aAS4/R0wwP0dMMD9HTDA/PS4tP7YYGj/WbBc/1mwXP9ZsFz8+bBg/eeseP23hHz9t4R8/beEfP23hHz+lUCE/EsAtP0dMMD9HTDA/R0wwPzRGLT/jJho/1mwXP9ZsFz/WbBc/FHQYP0bjHj9t4R8/beEfP23hHz9t4R8/SSEhP3yXLT9HTDA/R0wwP0dMMD+jMi0/EScaP9ZsFz/WbBc/1mwXPx50GD/A5B4/beEfP23hHz9t4R8/beEfPzQmIT8q7S0/R0wwP0dMMD9HTDA/4/QsP4r4GT/WbBc/1mwXP9ZsFz92bBg/lOEeP23hHz9t4R8/beEfP23hHz+wcyE/HUcuP0dMMD9HTDA/R0wwP+4HLT+V7Bk/1mwXP9ZsFz/WbBc/slQYPwr+Hj9t4R8/beEfP23hHz9t4R8/ZaQhPx4nLj9HTDA/R0wwP0dMMD/jTS0/0Q8aP9ZsFz/WbBc/1mwXPzNpGD870la9irAbvYqwG72KsBu9irAbvT9SK70a4Yy9blKXvW5Sl71uUpe9u++4vf9zRr6e4lS+nuJUvp7iVL5F7Tq+HaVVvYqwG72KsBu9irAbvYqwG71ECiu9sWCNvW5Sl71uUpe9blKXvTlju72YEUe+nuJUvp7iVL6e4lS+p0E5vmeVY72KsBu9irAbvYqwG72KsBu9jRkrvfF4jr1uUpe9blKXvW5Sl71wJ7u9+IxGvp7iVL6e4lS+nuJUviWZPb6B5G+9irAbvYqwG72KsBu9irAbveU2K70cNY+9blKXvW5Sl71uUpe9LuW4vTZJRb6e4lS+nuJUvp7iVL4tkkC+RslvvYqwG72KsBu9irAbvYqwG736xSm9dMeOvW5Sl71uUpe9blKXvXoStb2YukO+nuJUvp7iVL6e4lS+sttAvpFiZr2KsBu9irAbvYqwG72KsBu9SAgnva3BjL1uUpe9blKXvW5Sl72EbLK9GmdDvp7iVL6e4lS+nuJUvo3bPr71Ql29irAbvYqwG72KsBu9irAbvf5XJ71AFoy9blKXvW5Sl71uUpe9Ch63vRCbRr6e4lS+nuJUvp7iVL40Pjy+y7pdvYqwG72KsBu9irAbvYqwG71moSq9y82NvW5Sl71uUpe9blKXvaXhvr0vOkm+nuJUvp7iVL6e4lS+bjM9viXvY72KsBu9irAbvYqwG72KsBu9ccMsvUvLjr1uUpe9blKXvW5Sl71Ryb29Li9Ivp7iVL6e4lS+nuJUvgr6QL5aemW9irAbvYqwG72KsBu9irAbvSN3K72LEI69blKXvW5Sl71uUpe9yH66vaV5Rr6e4lS+nuJUvp7iVL7z/kG+b49lvYqwG72KsBu9irAbvYqwG71D7Sm9nA+NvW5Sl71uUpe9blKXvRG4ub2lJUa+nuJUvp7iVL6e4lS+3VtAvpbBar2KsBu9irAbvYqwG72KsBu9jocovW7ri71uUpe9blKXvW5Sl72cr7i9ctdFvp7iVL6e4lS+nuJUvqm6P74/ZG29irAbvYqwG72KsBu9irAbvaTfJr3JNYu9blKXvW5Sl71uUpe9hYe5vXPWRb6e4lS+nuJUvp7iVL7duT++yupsvYqwG72KsBu9irAbvYqwG72qCye9QrWMvW5Sl71uUpe9blKXvd8wvL0j10a+nuJUvp7iVL6e4lS+W1dAvurvbb2KsBu9irAbvYqwG72KsBu9PsEpvdFHjr1uUpe9blKXvW5Sl73BXru9HRlHvp7iVL6e4lS+nuJUvlJAQr6pyWS9irAbvYqwG72KsBu9irAbvTZ1K72fuI29blKXvW5Sl71uUpe91Fq4vbhWRr6e4lS+nuJUvp7iVL59mkC+" + }, + "q_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "w1xUP656Wz+uels/rnpbP656Wz/xalU/9ZcqP7F+Ij+xfiI/sX4iP3M3Hz9Diwo/nroHP566Bz+eugc/3TkUPwCBVD+uels/rnpbP656Wz+uels/24ZVPwI1Kj+xfiI/sX4iP7F+Ij9B+h4/hmwKP566Bz+eugc/nroHP7oHFT+J01I/rnpbP656Wz+uels/rnpbP+2AVT+qWyk/sX4iP7F+Ij+xfiI/FQAfP2SGCj+eugc/nroHP566Bz+g8BI/R1hRP656Wz+uels/rnpbP656Wz+MdVU/vMkoP7F+Ij+xfiI/sX4iP3o4Hz+KxQo/nroHP566Bz+eugc/P4IRP45bUT+uels/rnpbP656Wz+uels/mgRWP8YeKT+xfiI/sX4iP7F+Ij/tlx8/SxMLP566Bz+eugc/nroHP9peET84fVI/rnpbP656Wz+uels/rnpbP7MUVz9VsCo/sX4iP7F+Ij+xfiI/DNofP5QjCz+eugc/nroHP566Bz9pVRI/U5ZTP656Wz+uels/rnpbP656Wz/K9VY/RzUrP7F+Ij+xfiI/sX4iP95kHz+kgwo/nroHP566Bz+eugc/p5cTP+eHUz+uels/rnpbP656Wz+uels/ha9VP2XgKT+xfiI/sX4iP7F+Ij8Gox4/vQAKP566Bz+eugc/nroHP5ghEz+8yFI/rnpbP656Wz+uels/rnpbP8fbVD/MGyk/sX4iP7F+Ij+xfiI/Xb4eP9E0Cj+eugc/nroHP566Bz8+UBE/K5lSP656Wz+uels/rnpbP656Wz+jXFU/oawpP7F+Ij+xfiI/sX4iP4gQHz8pigo/nroHP566Bz+eugc/otIQP6GWUj+uels/rnpbP656Wz+uels/X/VVP+RzKj+xfiI/sX4iP7F+Ij/pIx8/i5oKP566Bz+eugc/nroHP2WcET+K9lE/rnpbP656Wz+uels/rnpbPxSAVj98Vis/sX4iP7F+Ij+xfiI/tD0fP8ypCj+eugc/nroHP566Bz8A6hE/VqVRP656Wz+uels/rnpbP656Wz91JFc/XOMrP7F+Ij+xfiI/sX4iP6UoHz/+qQo/nroHP566Bz+eugc/YuoRP/WzUT+uels/rnpbP656Wz+uels/YxNXP/a5Kj+xfiI/sX4iP7F+Ij8y5h4/7XcKP566Bz+eugc/nroHP5CeET+HlFE/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RiR3P20ecj8vvlQ/muZQP5rmUD+a5lA/OQtTP13OYj9n6GQ/Z+hkP2foZD9n6GQ/RN1mP1rCdD9GJHc/RiR3P0Ykdz/7iXI/W/RUP5rmUD+a5lA/muZQP7w7Uz+mpXA/CM52PwjOdj8IznY/CM52PzwGdT8Gcmg/CRFmPwkRZj8JEWY/yD1fP8AzND9pVy4/aVcuP2lXLj9/Jzk/AcVwPwjOdj8IznY/CM52PwjOdj9uDnU/9lRoPwkRZj8JEWY/CRFmP1++Xj/B8zM/aVcuP2lXLj9pVy4/ntk5P2tRbz8IznY/CM52PwjOdj8IznY/sQx1Px4VaD8JEWY/CRFmPwkRZj+Dyl4/nCk0P2lXLj9pVy4/aVcuP6IKOD9GCW4/CM52PwjOdj8IznY/CM52P1kJdT9B6mc/CRFmPwkRZj8JEWY/7D9fPxatND9pVy4/aVcuP2lXLj+hzTY/HAxuPwjOdj8IznY/CM52PwjOdj9eM3U/PANoPwkRZj8JEWY/CRFmP6UGYD/1TjU/aVcuP2lXLj9pVy4/Aa82P7wGbz8IznY/CM52PwjOdj8IznY/S4N1Py95aD8JEWY/CRFmPwkRZj9NkGA/3XA1P2lXLj9pVy4/aVcuP1aENz/1+W8/CM52PwjOdj8IznY/CM52PzZ6dT88oGg/CRFmPwkRZj8JEWY/VpxfP+MjND9pVy4/aVcuP2lXLj8mmzg/eu1vPwjOdj8IznY/CM52PwjOdj9gGnU/GzxoPwkRZj8JEWY/CRFmP8QIXj9aEzM/aVcuP2lXLj9pVy4/ATU4PxJIbz8IznY/CM52PwjOdj8IznY/Ltx0P1wCaD8JEWY/CRFmPwkRZj+vQV4/yH8zP2lXLj9pVy4/aVcuP12iNj/rHm8/CM52PwjOdj8IznY/CM52PwgCdT/nLGg/CRFmPwkRZj8JEWY/wexeP3UxND9pVy4/aVcuP2lXLj+vNTY/uRxvPwjOdj8IznY/CM52PwjOdj/lLnU/bmdoPwkRZj8JEWY/CRFmPxsVXz+SUzQ/aVcuP2lXLj9pVy4/QeQ2PzSSbj8IznY/CM52PwjOdj8IznY/o1d1P/2paD8JEWY/CRFmPwkRZj/NSl8/U3M0P2lXLj9pVy4/aVcuP2cnNz/yS24/CM52PwjOdj8IznY/CM52P+yHdT9e02g/CRFmPwkRZj8JEWY/9h5fP7tzND9pVy4/aVcuP2lXLj+7Jzc/mFhuPwjOdj8IznY/CM52PwjOdj/ognU/A3xoPwkRZj8JEWY/CRFmP56UXj9+CzQ/aVcuP2lXLj9pVy4/IeY2P2c9bj8IznY/CM52PwjOdj8IznY/6DN1P08gaD8JEWY/CRFmPwkRZj9Iv14/s/AzP2lXLj9pVy4/aVcuP3QaNj9RMW8/CM52PwjOdj8IznY/CM52P0ACdT/uQGg/CRFmPwkRZj8JEWY/BFxfP6Q/ND9pVy4/aVcuP2lXLj8qyjY/" + }, + "lineage_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "/////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA/////w==" + }, + "flags_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAA==" + } + }, + { + "rule": "whole", + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "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" + }, + "h_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND+UbRI/WW40P5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+UbRI/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP8zwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+xnmUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPukfrj6mGKc+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+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pVaHvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb3XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPte5+j4zBc0+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPte5+j7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j4hBfg+cdX6PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4Pm3hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/1mwXP0dMMD/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz+KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvZ7iVL5uUpe9nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvZ7iVL6KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+" + }, + "q_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+eugc/sX4iP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+eugc/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/7LETPygIDD/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPyFYMD88Iwk/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/AekIPwHpCD8B6Qg/AekIPwHpCD8B6Qg/7OYaP+zmGj/s5ho/7OYaP4TjUj+E41I/hONSP4TjUj+E41I/hONSPwHpCD8B6Qg/AekIPwHpCD8B6Qg/AekIP+zmGj/s5ho/7OYaP+zmGj/s5ho/hONSP4TjUj+E41I/hONSP4TjUj8B6Qg/AekIPwHpCD8B6Qg/AekIPwHpCD/s5ho/7OYaP+zmGj/s5ho/hONSP4TjUj+E41I/hONSP4TjUj+E41I/AekIPwHpCD8B6Qg/AekIPwHpCD8B6Qg/7OYaP+zmGj/s5ho/7OYaP+zmGj/s5ho/hONSP4TjUj+E41I/AekIPwHpCD8B6Qg/AekIPwHpCD8B6Qg/AekIP+zmGj/s5ho/7OYaP+zmGj+E41I/7OYaP4TjUj+E41I/hONSP4TjUj8B6Qg/AekIPwHpCD8B6Qg/AekIPwHpCD/s5ho/7OYaP+zmGj/s5ho/hONSP4TjUj+E41I/hONSP4TjUj+E41I/AekIPwHpCD8B6Qg/AekIPwHpCD8B6Qg/7OYaP+zmGj/s5ho/7OYaP4TjUj+E41I/hONSP4TjUj+E41I/hONSPwHpCD8B6Qg/AekIPwHpCD8B6Qg/7OYaP+zmGj/s5ho/7OYaP+zmGj+E41I/hONSP4TjUj+E41I/hONSPwHpCD+E41I/AekIPwHpCD8B6Qg/AekIPwHpCD/s5ho/7OYaP+zmGj/s5ho/hONSP4TjUj+E41I/hONSP4TjUj+E41I/AekIPwHpCD8B6Qg/AekIPwHpCD8B6Qg/7OYaP+zmGj/s5ho/7OYaP+zmGj+E41I/hONSP4TjUj+E41I/hONSPwHpCD8B6Qg/AekIPwHpCD8B6Qg/AekIP+zmGj/s5ho/7OYaP+zmGj/s5ho/hONSP4TjUj+E41I/hONSP4TjUj8B6Qg/AekIPwHpCD8B6Qg/AekIPwHpCD/s5ho/7OYaP+zmGj/s5ho/7OYaP4TjUj+E41I/hONSP4TjUj+E41I/AekIPwHpCD8B6Qg/AekIPwHpCD8B6Qg/7OYaP+zmGj/s5ho/7OYaP+zmGj+E41I/hONSP4TjUj+E41I/hONSPwHpCD8B6Qg/AekIPwHpCD8B6Qg/AekIP+zmGj/s5ho/7OYaP+zmGj/s5ho/hONSP4TjUj+E41I/hONSP4TjUj8B6Qg/AekIPwHpCD8B6Qg/AekIPwHpCD/s5ho/7OYaP+zmGj/s5ho/7OYaP4TjUj+E41I/hONSP4TjUj+E41I/AekIPwHpCD8B6Qg/AekIPwHpCD8B6Qg/7OYaP+zmGj/s5ho/7OYaP+zmGj+E41I/hONSP4TjUj+E41I/hONSP+dxDD/ncQw/53EMP+dxDD/ncQw/53EMP8aiOj/Gojo/xqI6P8aiOj8SUm4/ElJuPxJSbj8SUm4/ElJuPxJSbj/ncQw/53EMP+dxDD/ncQw/53EMP+dxDD/Gojo/xqI6P8aiOj/Gojo/xqI6PxJSbj8SUm4/ElJuPxJSbj8SUm4/53EMP+dxDD/ncQw/53EMP+dxDD/ncQw/xqI6P8aiOj/Gojo/xqI6PxJSbj8SUm4/ElJuPxJSbj8SUm4/ElJuP+dxDD/ncQw/53EMP+dxDD/ncQw/53EMP8aiOj/Gojo/xqI6P8aiOj/Gojo/xqI6PxJSbj8SUm4/ElJuP+dxDD/ncQw/53EMP+dxDD/ncQw/53EMP+dxDD/Gojo/xqI6P8aiOj/Gojo/ElJuP8aiOj8SUm4/ElJuPxJSbj8SUm4/53EMP+dxDD/ncQw/53EMP+dxDD/ncQw/xqI6P8aiOj/Gojo/xqI6PxJSbj8SUm4/ElJuPxJSbj8SUm4/ElJuP+dxDD/ncQw/53EMP+dxDD/ncQw/53EMP8aiOj/Gojo/xqI6P8aiOj8SUm4/ElJuPxJSbj8SUm4/ElJuPxJSbj/ncQw/53EMP+dxDD/ncQw/53EMP8aiOj/Gojo/xqI6P8aiOj/Gojo/ElJuPxJSbj8SUm4/ElJuPxJSbj/ncQw/ElJuP+dxDD/ncQw/53EMP+dxDD/ncQw/xqI6P8aiOj/Gojo/xqI6PxJSbj8SUm4/ElJuPxJSbj8SUm4/ElJuP+dxDD/ncQw/53EMP+dxDD/ncQw/53EMP8aiOj/Gojo/xqI6P8aiOj/Gojo/ElJuPxJSbj8SUm4/ElJuPxJSbj/ncQw/53EMP+dxDD/ncQw/53EMP+dxDD/Gojo/xqI6P8aiOj/Gojo/xqI6PxJSbj8SUm4/ElJuPxJSbj8SUm4/53EMP+dxDD/ncQw/53EMP+dxDD/ncQw/xqI6P8aiOj/Gojo/xqI6P8aiOj8SUm4/ElJuPxJSbj8SUm4/ElJuP+dxDD/ncQw/53EMP+dxDD/ncQw/53EMP8aiOj/Gojo/xqI6P8aiOj/Gojo/ElJuPxJSbj8SUm4/ElJuPxJSbj/ncQw/53EMP+dxDD/ncQw/53EMP+dxDD/Gojo/xqI6P8aiOj/Gojo/xqI6PxJSbj8SUm4/ElJuPxJSbj8SUm4/53EMP+dxDD/ncQw/53EMP+dxDD/ncQw/xqI6P8aiOj/Gojo/xqI6P8aiOj8SUm4/ElJuPxJSbj8SUm4/ElJuP+dxDD/ncQw/53EMP+dxDD/ncQw/53EMP8aiOj/Gojo/xqI6P8aiOj/Gojo/ElJuPxJSbj8SUm4/ElJuPxJSbj+YZCM/mGQjP5hkIz+YZCM/mGQjP5hkIz+4Y1w/uGNcP7hjXD+4Y1w/bE94P2xPeD9sT3g/bE94P2xPeD9sT3g/mGQjP5hkIz+YZCM/mGQjP5hkIz+YZCM/uGNcP7hjXD+4Y1w/uGNcP7hjXD9sT3g/bE94P2xPeD9sT3g/bE94P5hkIz+YZCM/mGQjP5hkIz+YZCM/mGQjP7hjXD+4Y1w/uGNcP7hjXD9sT3g/bE94P2xPeD9sT3g/bE94P2xPeD+YZCM/mGQjP5hkIz+YZCM/mGQjP5hkIz+4Y1w/uGNcP7hjXD+4Y1w/uGNcP7hjXD9sT3g/bE94P2xPeD+YZCM/mGQjP5hkIz+YZCM/mGQjP5hkIz+YZCM/uGNcP7hjXD+4Y1w/uGNcP2xPeD+4Y1w/bE94P2xPeD9sT3g/bE94P5hkIz+YZCM/mGQjP5hkIz+YZCM/mGQjP7hjXD+4Y1w/uGNcP7hjXD9sT3g/bE94P2xPeD9sT3g/bE94P2xPeD+YZCM/mGQjP5hkIz+YZCM/mGQjP5hkIz+4Y1w/uGNcP7hjXD+4Y1w/bE94P2xPeD9sT3g/bE94P2xPeD9sT3g/mGQjP5hkIz+YZCM/mGQjP5hkIz+4Y1w/uGNcP7hjXD+4Y1w/uGNcP2xPeD9sT3g/bE94P2xPeD9sT3g/mGQjP2xPeD+YZCM/mGQjP5hkIz+YZCM/mGQjP7hjXD+4Y1w/uGNcP7hjXD9sT3g/bE94P2xPeD9sT3g/bE94P2xPeD+YZCM/mGQjP5hkIz+YZCM/mGQjP5hkIz+4Y1w/uGNcP7hjXD+4Y1w/uGNcP2xPeD9sT3g/bE94P2xPeD9sT3g/mGQjP5hkIz+YZCM/mGQjP5hkIz+YZCM/uGNcP7hjXD+4Y1w/uGNcP7hjXD9sT3g/bE94P2xPeD9sT3g/bE94P5hkIz+YZCM/mGQjP5hkIz+YZCM/mGQjP7hjXD+4Y1w/uGNcP7hjXD+4Y1w/bE94P2xPeD9sT3g/bE94P2xPeD+YZCM/mGQjP5hkIz+YZCM/mGQjP5hkIz+4Y1w/uGNcP7hjXD+4Y1w/uGNcP2xPeD9sT3g/bE94P2xPeD9sT3g/mGQjP5hkIz+YZCM/mGQjP5hkIz+YZCM/uGNcP7hjXD+4Y1w/uGNcP7hjXD9sT3g/bE94P2xPeD9sT3g/bE94P5hkIz+YZCM/mGQjP5hkIz+YZCM/mGQjP7hjXD+4Y1w/uGNcP7hjXD+4Y1w/bE94P2xPeD9sT3g/bE94P2xPeD+YZCM/mGQjP5hkIz+YZCM/mGQjP5hkIz+4Y1w/uGNcP7hjXD+4Y1w/uGNcP2xPeD9sT3g/bE94P2xPeD9sT3g/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz+XGG0/+nNzP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/muZQP0Ykdz+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmP2lXLj8JEWY/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52P2lXLj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/" + }, + "lineage_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAADAAAAAgAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAA==" + }, + "flags_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + }, + { + "rule": "gene-wise", + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "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" + }, + "h_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+Wm0E/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP8zwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+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pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaz7sb3XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+MwXNPte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtZsFz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz+e4lS+irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9nuJUvm5Sl72e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+" + }, + "q_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "nroHP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/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+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz+a5lA/RiR3P5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/CM52P2lXLj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/" + }, + "lineage_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "/////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA//////////8BAAAAAQAAAAEAAAABAAAA//////////8CAAAAAgAAAAIAAAD//////////wMAAAADAAAAAwAAAP//////////AQAAAAEAAAABAAAAAQAAAP//////////AgAAAAIAAAACAAAA//////////8DAAAAAwAAAAMAAAD//////////wEAAAABAAAAAQAAAAEAAAD//////////wIAAAACAAAAAgAAAP//////////AwAAAAMAAAADAAAA/////w==" + }, + "flags_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAA==" + } + }, + { + "rule": "best", + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "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" + }, + "h_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND+UbRI/WW40P5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+UbRI/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP8zwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+xnmUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPukfrj6mGKc+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+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pVaHvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb3XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPte5+j4zBc0+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPte5+j7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j4hBfg+cdX6PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4Pm3hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/1mwXP0dMMD/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz+KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvZ7iVL5uUpe9nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvZ7iVL6KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+" + }, + "q_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+eugc/sX4iP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+eugc/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/7LETPygIDD/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPyFYMD88Iwk/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/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+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz+XGG0/+nNzP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/muZQP0Ykdz+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmP2lXLj8JEWY/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52P2lXLj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/" + }, + "lineage_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAADAAAAAgAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAA==" + }, + "flags_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + }, + { + "rule": "negotiation", + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "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" + }, + "h_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P5abQT9ZbjQ/WW40P1luND9ZbjQ/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5abQT+Wm0E/lptBP5abQT+Wm0E/lptBP1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP5RtEj+Wm0E/lptBP5abQT+Wm0E/lptBP5abQT9ZbjQ/WW40P1luND9ZbjQ/WW40P5RtEj+UbRI/lG0SP5RtEj+UbRI/lptBP5abQT+Wm0E/lptBP5abQT+Wm0E/WW40P1luND9ZbjQ/WW40P1luND+UbRI/lG0SP5RtEj+UbRI/lG0SP8zwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL7M8Ku9hANUvoQDVL6EA1S+hANUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7M8Ku9zPCrvczwq73M8Ku9zPCrvczwq72EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL7GeZS+zPCrvczwq73M8Ku9zPCrvczwq73M8Ku9hANUvoQDVL6EA1S+hANUvoQDVL7GeZS+xnmUvsZ5lL7GeZS+xnmUvszwq73M8Ku9zPCrvczwq73M8Ku9zPCrvYQDVL6EA1S+hANUvoQDVL6EA1S+xnmUvsZ5lL7GeZS+xnmUvsZ5lL4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+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pVaHvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb3XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+18mPPjMFzT4zBc0+MwXNPjMFzT7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+18mPPtfJjz7XyY8+18mPPtfJjz7XyY8+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+17n6PtfJjz7XyY8+18mPPtfJjz7XyY8+18mPPjMFzT4zBc0+MwXNPjMFzT4zBc0+17n6Pte5+j7Xufo+17n6Pte5+j7XyY8+18mPPtfJjz7XyY8+18mPPtfJjz4zBc0+MwXNPjMFzT4zBc0+MwXNPte5+j7Xufo+17n6Pte5+j7Xufo+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PtBRuT5x1fo+cdX6PnHV+j5x1fo+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PtBRuT7QUbk+0FG5PtBRuT7QUbk+0FG5PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4PiEF+D7QUbk+0FG5PtBRuT7QUbk+0FG5PtBRuT5x1fo+cdX6PnHV+j5x1fo+cdX6PiEF+D4hBfg+IQX4PiEF+D4hBfg+0FG5PtBRuT7QUbk+0FG5PtBRuT7QUbk+cdX6PnHV+j5x1fo+cdX6PnHV+j4hBfg+IQX4PiEF+D4hBfg+IQX4Pm3hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9t4R8/R0wwP0dMMD9HTDA/R0wwP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz9t4R8/beEfP23hHz9t4R8/beEfP23hHz9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz/WbBc/beEfP23hHz9t4R8/beEfP23hHz9t4R8/R0wwP0dMMD9HTDA/R0wwP0dMMD/WbBc/1mwXP9ZsFz/WbBc/1mwXP23hHz9t4R8/beEfP23hHz9t4R8/beEfP0dMMD9HTDA/R0wwP0dMMD9HTDA/1mwXP9ZsFz/WbBc/1mwXP9ZsFz+KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9irAbvW5Sl71uUpe9blKXvW5Sl72e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+irAbvYqwG72KsBu9irAbvYqwG72KsBu9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+nuJUvoqwG72KsBu9irAbvYqwG72KsBu9irAbvW5Sl71uUpe9blKXvW5Sl71uUpe9nuJUvp7iVL6e4lS+nuJUvp7iVL6KsBu9irAbvYqwG72KsBu9irAbvYqwG71uUpe9blKXvW5Sl71uUpe9blKXvZ7iVL6e4lS+nuJUvp7iVL6e4lS+" + }, + "q_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "float32-le-base64", + "data": "rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP656Wz+xfiI/sX4iP7F+Ij+xfiI/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP656Wz+uels/rnpbP656Wz+uels/rnpbP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP566Bz+uels/rnpbP656Wz+uels/rnpbP656Wz+xfiI/sX4iP7F+Ij+xfiI/sX4iP566Bz+eugc/nroHP566Bz+eugc/rnpbP656Wz+uels/rnpbP656Wz+uels/sX4iP7F+Ij+xfiI/sX4iP7F+Ij+eugc/nroHP566Bz+eugc/nroHP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD/dlzk/KAgMPygIDD8oCAw/KAgMP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/dlzk/3Zc5P92XOT/dlzk/3Zc5P92XOT8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz/ssRM/3Zc5P92XOT/dlzk/3Zc5P92XOT/dlzk/KAgMPygIDD8oCAw/KAgMPygIDD/ssRM/7LETP+yxEz/ssRM/7LETP92XOT/dlzk/3Zc5P92XOT/dlzk/3Zc5PygIDD8oCAw/KAgMPygIDD8oCAw/7LETP+yxEz/ssRM/7LETP+yxEz+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/IVgwP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aPzwjCT88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD8hWDA/pR4aP6UeGj+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT8hWDA/IVgwPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/pR4aP6UeGj88Iwk/PCMJPzwjCT88Iwk/PCMJPyFYMD8hWDA/IVgwPyFYMD+lHho/pR4aP6UeGj+lHho/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+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/EcRT/6c3M/+nNzP/pzcz/6c3M/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP/EcRT/xHEU/8RxFP/EcRT/xHEU/8RxFP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP5cYbT/xHEU/8RxFP/EcRT/xHEU/8RxFP/EcRT/6c3M/+nNzP/pzcz/6c3M/+nNzP5cYbT+XGG0/lxhtP5cYbT+XGG0/8RxFP/EcRT/xHEU/8RxFP/EcRT/xHEU/+nNzP/pzcz/6c3M/+nNzP/pzcz+XGG0/lxhtP5cYbT+XGG0/lxhtP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9n6GQ/RiR3P0Ykdz9GJHc/RiR3P5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP2foZD9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD+a5lA/Z+hkP2foZD9n6GQ/Z+hkP2foZD9n6GQ/RiR3P0Ykdz9GJHc/RiR3P0Ykdz+a5lA/muZQP5rmUD+a5lA/muZQP2foZD9n6GQ/Z+hkP2foZD9n6GQ/Z+hkP0Ykdz9GJHc/RiR3P0Ykdz9GJHc/muZQP5rmUD+a5lA/muZQP5rmUD8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CM52PwkRZj8JEWY/CRFmPwkRZj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/CM52PwjOdj8IznY/CM52PwjOdj8IznY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/aVcuPwjOdj8IznY/CM52PwjOdj8IznY/CM52PwkRZj8JEWY/CRFmPwkRZj8JEWY/aVcuP2lXLj9pVy4/aVcuP2lXLj8IznY/CM52PwjOdj8IznY/CM52PwjOdj8JEWY/CRFmPwkRZj8JEWY/CRFmP2lXLj9pVy4/aVcuP2lXLj9pVy4/" + }, + "lineage_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAAAwAAAA==" + }, + "flags_step_1": { + "shape": [ + 16, + 16 + ], + "dtype": "uint32-le-base64", + "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + } + ], + "mutation": { + "event_index": 1, + "parent_lineage": 1, + "child_lineage": 4121745000, + "child_fingerprint": [ + 1576982248, + 2074750725 + ], + "center": [ + 8, + 2 + ], + "radius": 3.0, + "delta_h": [ + -0.039332317245548566, + -0.052471715344177605, + -0.0836553314535377, + -0.021678399269668847, + 0.09304854870192134, + -0.03450831798337091, + -0.03878040283077501, + 0.01609759507539627, + -0.017784268154352398 + ], + "delta_q": [ + 0.033162661154932035, + -0.03005544961268321, + -0.07661100950070321, + -0.026604221891672632, + -0.07732653234410215, + -0.015958175648281586, + -0.026455080350820254, + 0.03871216881876271, + -0.025016230442801157 + ], + "child_flags": 26263554 + } +} diff --git a/packages/flow-lenia/web/src/prove/organism-fixture.json b/packages/flow-lenia/web/src/prove/organism-fixture.json new file mode 100644 index 00000000..29e36f98 --- /dev/null +++ b/packages/flow-lenia/web/src/prove/organism-fixture.json @@ -0,0 +1,204 @@ +{ + "schema_version": "flow-lenia-m2-conformance-v1", + "model_variant": "flow-lenia-ecosystem-v1", + "oracle": "flow_lenia.ecosystem_reference f64; expected arrays quantized to f32", + "config": { + "grid": 16, + "channels": 3, + "kernels": 9, + "seed": 91, + "dt": 0.2, + "dd": 5, + "sigma": 0.65, + "density_threshold": 2.0, + "density_exponent": 2.0 + }, + "tolerances": { + "perception_abs": 1e-06, + "growth_abs": 1e-05, + "affinity_abs": 1e-05, + "alpha_abs": 2e-07, + "flow_abs": 2e-05, + "displacement_abs": 4e-06, + "mass_step_1_abs": 1e-05, + "mass_step_4_abs": 2e-05, + "mass_relative_ledger": 5e-05 + }, + "cases": [ + { + "name": "smooth-periodic", + "variant": 0, + "fields": { + "initial_mass": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "displacement_step_1": { + "shape": [ + 3, + 2, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "mass_step_4": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "perception_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "growth_step_1": { + "shape": [ + 9, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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E0RdP7k7XT81N10/RjldP4o8XT/NP10/3kFdP1s9XT8BPF0/yzVdP/44XT/0PV0/FUBdP3NAXT8TPV0/azZdPzw2XT9zQF0/60ZdP/Q9XT8nMl0/yzVdP9dCXT99RV0/QD9dPx1GXT8nP10/9TJdP5c0XT8KQl0/sEZdP848XT+XNF0/MThdPyc/XT/iQF0/QD9dP0U8XT+eN10/CTddP/02XT/1P10/FUJdP3M+XT8SO10/azhdPz04XT9zPl0/6kRdP/U/XT8oNF0/yjNdP9ZAXT99R10/AT5dP8ozXT89QF0/kzVdP4EzXT9sP10/xUddP2w/XT+BM10/kzVdPz1AXT+AQ10/Kj5dP6c5XT+0OF0/pzldPyo+XT+AQ10/6kRdP3M+XT89OF0/azhdPxI7XT9zPl0/FUJdP/U/XT/9Nl0/yjNdPwE+XT99R10/1kBdP8ozXT8oNF0/9T9dP5c0XT/OPF0/sEZdPwpCXT+XNF0/9TJdPyc/XT8dRl0/QD9dPwk3XT+eN10/RTxdP0A/XT/iQF0/Jz9dPzE4XT88Nl0/azZdPxM9XT9zQF0/FUBdP/Q9XT/+OF0/yzVdPwE8XT99RV0/10JdP8s1XT8nMl0/9D1dP+tGXT9zQF0/E0RdPx9DXT81N10/3zFdP4o8XT8zR10/3kFdP/Q1XT8ANV0/Wz1dP95BXT/NP10/ijxdP0Y5XT81N10/uTtdPxM9XT9IQ10/FUBdPx87XT/+OF0/oDhdPwE8XT+oQl0/10JdP6A4XT8nMl0/HztdP+tGXT9IQ10/PDZdP5UzXT/TOV0/9TJdP+w5XT8dRl0/e0RdPwk3XT9iMl0/RTxdP3tEXT/iQF0/7DldPzE4XT/TOV0/zjxdP3VBXT8KQl0/FUJdPx45XT/9Nl0/oTpdPwE+XT+oQF0/1kBdP6E6XT8oNF0/HjldP+pEXT9JRV0/PThdP5QxXT8SO10/SUVdP9Y4XT+AQ10/kUVdP6c5XT9MMV0/pzldP5FFXT+AQ10/1jhdP5M1XT/pOl0/bD9dP19AXT9sP10/6TpdP5M1XT8oNF0/oTpdP9ZAXT+oQF0/AT5dP6E6XT/9Nl0/HjldPxVCXT9JRV0/EjtdP5QxXT89OF0/SUVdP+pEXT8eOV0/e0RdP0U8XT9iMl0/CTddP3tEXT8dRl0/7DldP/UyXT/TOV0/CkJdP3VBXT/OPF0/0zldPzE4XT/sOV0/4kBdP9dCXT+oQl0/ATxdP6A4XT/+OF0/HztdPxVAXT9IQ10/Ez1dP5UzXT88Nl0/SENdP+tGXT8fO10/JzJdP6A4XT/k9jw+XHsyPuekPj7M80k+yPI9PhrzMT6rQD0+bWtJPq/uPj4PPDM+q0A9PjQiSD7I8j0+U8QzPuekPj6Sqkg+IBQ0PhICOT76SUc+kzFCPmWcND4/Zjo+WNJHPqU1QT5qUzM+P2Y6PpAbST6TMUI+J8syPhICOT4yk0g+gy1DPizNNj7Ps0Y+B8tFPngyNT4NaTU+citGPv3GRj56ezY+DWk1PkDiRD4Gy0U+wwM3PizNNj6bakU+Es9EPr66NT7TSEE+xTJIPticOj7QZTQ+KtFBPsw2Rz7HUzk+0GU0PlUaQz7FMkg+ess4PrsBMz78kUI+vi5JPooUOj68ATM+/wpJPhAKPT6uKTI+LlM+PvoGSj4uUz4+rikyPhAKPT7/Ckk+gts+Pr6NMz5jkj0+BQ9IPmOSPT6+jTM+gts+PlUaQz7QZTQ+x1M5Psw2Rz4q0UE+0GU0PticOj7FMkg+00hBPrwBMz6KFDo+vi5JPvyRQj67ATM+ess4PsUySD4NaTU+ens2Pv3GRj5yK0Y+DWk1PngyNT4Hy0U+z7NGPizNNj6+ujU+Es9EPptqRT4szTY+wwM3PgbLRT5A4kQ+alMzPqU1QT5Y0kc+P2Y6PmWcND6TMUI++klHPhICOT4gFDQ+gy1DPjKTSD4SAjk+J8syPpMxQj6QG0k+P2Y6Pq/uPj5ta0k+q0A9PhrzMT7I8j0+zPNJPuekPj5cezI+5PY8PpKqSD7npD4+U8QzPsjyPT40Ikg+q0A9Pg88Mz5Y0kc+uONCPmWcND4otDk++klHPm9/QT4gFDQ+BLA6PjKTSD5vf0E+J8syPii0OT6QG0k+uONCPmpTMz5PuDg+3hhFPngyNT4cGzY+z7NGPjB9Rj6/ujU+TR81PptqRT4wfUY+wwM3PhwbNj5A4kQ+3hhFPnp7Nj7uFjc+citGPticOj7FszM+00hBPpmARz6KFDo+kK80PvyRQj6ZgEc+ess4PsWzMz5VGkM+8eRIPsdTOT79tzI+KtFBPvHkSD612zI+gts+Piy9ST5jkj0+8N8xPmOSPT4svUk+gts+PrXbMj4QCj0+01hIPi5TPj591zM+LlM+PtNYSD4QCj0+ess4PpmARz78kUI+kK80PooUOj6ZgEc+00hBPsWzMz7YnDo+8eRIPirRQT79tzI+x1M5PvHkSD5VGkM+xbMzPjB9Rj6bakU+TR81Pr+6NT4wfUY+z7NGPhwbNj54MjU+3hhFPnIrRj7uFjc+ens2Pt4YRT5A4kQ+HBs2PsMDNz4yk0g+BLA6PiAUND5vf0E++klHPii0OT5lnDQ+uONCPljSRz5PuDg+alMzPrjjQj6QG0k+KLQ5PifLMj5vf0E+" + }, + "affinity_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "alpha_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "flow_step_1": { + "shape": [ + 3, + 2, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + } + } + }, + { + "name": "seam-loaded", + "variant": 1, + "fields": { + "initial_mass": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "displacement_step_1": { + "shape": [ + 3, + 2, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "seg+vIigjbwwm1i8BpIwvB27C7xnuW67NvZyOZoMy7c2/3S77Uj6u9XyOrwQEXa8Y/RivA9RvrvzDSg7RZYXOn0EODxkYKk8ed2IPPHS7Tsz8nA7/H2uO5r5+zt1vh088uc/PJ1FSzyxFBE87RCeO1jjlzv9V4g7TlVUOvmQZzoeCtq6AYWuu/SULLyZjl28AKdgvGgaTbzceym8/zztuzHElbsW5qe7Qq0EvL99HbyQlv67LVVXu7r8qLlOwYa6DbiTOxTU77r5pLm77AnAuy72nbuaP4+7FlKSu+cke7ujIiu7ZPEcuxMVMbuiwxS772k/OntVwztEWL47daHcO3xVbzyHAz48X5c6PIRQVTwgAmY8sF9YPIjOODxPRh08nkoLPPW9EDyj+R08Zj8hPMFhOjy3g2o8EkxePMdskjyBAmA8ktNmPI2OgzyJw5Q8KeSaPG67kDyjYHk864dKPCKKKTz81ig8ZvgyPMjtPDw550s8BqRePN8pcTywcYI8sGzwO1hx6jt+ugs8zTknPOlxMTyo4SE8EA32O+I6rTuaQaQ74iu4O50L4DuarQ08dDsiPDC9Ljwn3FY8VtgnPEOfGjzI3Og7UKfhO81BAjzYzw88epMEPP/duDu0h6A7tfXiO0rAFjzlz0I8Q3JxPCdrhzwsPYs8qTqCPGkjTzwput07jh2oO051mzuN75w7ldmKO46MHDuTAHU6PB3uOhfYZDsCWMA78LoYPMePTjyIy3A8dp5uPH2/MzydAw48cRsPvGoy27tDGqG7866RuzMPvbvrWQm8R7IhvOUELbxLIja8MHMjvDqW+7uIsLy7BKasu8yi2LvJJA284CQMvMcBa7xxyx68dvC3u/lbX7venFa7CI13u5FYn7uMffW7NXUivEPoM7xI8Te8ODY+vPLeXrz5A4e8ECeSvCB3iLwp2VO8LJgcvBa71bvAWZq7yt0/u2Bs2LmV5tc2Hr28upweULuwGq67W8/2u3KiG7xA4D28Nd1ivP7hY7wLOoC8lQ6BvHSDd7wNZX+8zq6EvP2JbLyYukK89k4tvKVaG7wIaBW8ECEjvFsnOrxhclK8F2FlvBHpfLz34WO8zzl3vNC2RLzUPDO8+LZXvM7/g7zqjYm8CH+HvET0drwXWU28OtEsvLSbGbzgfhC8uTYUvNNXLby47Fe8mklyvGogQLw9sjA5XfKBO0uTfDvHIXk6Y48Xu7wIZ7tvKQ67I1douc7QmzrqjRA73QJcO+e5ZDttGzg5BfuMuyuxBbwo1ou7xT3EO23WDjwBIi48IN0mPCtFEDwxcRs8IS83PFPKQjw6LjI87m4KPKNtyjst6oo7493sOkX42znCXBQ4gZgRusSuCrp5DN871auiPJCKbzybHKk7TgImPP99GTynGd67p0E8vD519Lv8/D+8YQmIvCVeKLxJC/W62+rCOjbL/Tp8sPI59yggPNKmjjwS8C08lIPTO8d6YTxNOjQ82FL1uyZiRrzl3Q68JN2BvK1kl7zYDPa7L1O9OqbnyjpeW4q5b87ZOofsiDtP8BY8KYUOPGg0AjxVMX08ffRcPKo2nLuuzkG8LWZEvCrGk7xloZC8jEjjuxfQdzu7FIi7YAbXO02ER7vS3wy7EZDbO7WDHjw78xU8LAmEPP6AbTywZD276C80vLkMXbwqy528GLeDvPmS+7r2bw08orsXvGsSRjyFxOK71TzQuycm0Tul/jc8KQkgPKM1eDxiOVs8CH1Hu+q3L7y5k1W868ievFn8eLyNBQA7HUREPNrmH7xdo4s84W0KvNwC3LtDkwU8JRBVPHRgHzwQllg8jZIwPJ1GsLsUhTe8eDZGvHpHn7xlh3S8gTeBO02lZTyREdi7TBGhPBc4KLyRk7K7fY0qPJOEbTwKlRs8VL0+PFvHBDydoAC8jEU7vGytN7zDFZe8ZXxhvITKqjtuxX48u4CUu2tAbzyhXGi8Cqi+uxhJQjyUyYE8SosaPHsUHzzIbaw78DEIvDsFIry+ZRi8c2OCvN1yP7zXSc07Iq2APNoewrvdMQI8FtGTvPf05Lta9Uo8dBKHPJDvEDysz/A7bDB9O3M63rsWXfq77FbWuyH1TrxR4hW8LJPxO8qrYTzE0iy8oyWcO09+l7yc4827bAtWPAhAhTyHBuc7zIirO1taeDvgnc67zzjcu724g7ut0R28Qlreu1S2ATwjECo8zN1qvEdqmzt5Jnm8uwMxu9ePfDwXjIc8R/axO09WgDv3bRs7lPsOvJZ09LtcWjy7LaoJvB1m2rs+rsk70zX9O1IGXbykNLQ7pj88vPeewTo/dpY8bJ2OPJXIwDsxl507Qr+aOqX2N7zv+Ri8ZQqIu5WKHLxJTgu8Ij5jO67X1Ds1okq8fXyFO0sxI7x+xk079P6aPJJbkzx0HQ08iG3uO5Ia2TpSvj68QustvD4awbsskze8o2EnvMsm5zp6K787mSlDvCisxTq93A28sHI5Ow6tiTxHZJA8f7YmPGw1CzwMm2A7Y/8ivKEjJrzPyMm7s+Y/vMoINLzvW/23LZN/O3ZSJbyqhSU7jR2Tu8Onajt2VmY8Q59zPBq+CTz0ZhI8VkXAO/oZ/7vYwhG8mNavu0BhNbxEpEK8aCF5uwr+5zjsfwi8Y6sJPPWhmjogycs7S7haPF9DQDyEu8Q7SNMePKg2/TtPo9675uYQvEborbtBUy+8G7hivJbIB7zqCSK7zISau5b9KjyDGRq8ywtmvMu17rsC/SM6W6G6OzeokzvdTmW7lOlGvDJVUrwDNta7PI0+uggMgrqcv427up4Ru/ju7zmufLC6alZ4OzGTKjzXq+g7EpOoOxTC6jsXHwU8JriHOz8NUrskcKC7r8WdOvXzFTxpOEw85R8pPF/dRzvBpPW6PUslu22yuLsqZta79AbGu47sBbu/1UK5yuWTu5QbVbyZXY+8NMlivJOXt7sJ1p45ErvFOocucbqWh2K7xgmQu14C0LtSZxu8jKMLvMjRhLtpase6CJqwuxXeebyCgrm8qE6wvDG/ZrxNYNC7q1lPuzDchbvhVMC7MKG8u+zABrx8GxS8thQxvBRp0bvgEAG79imNu0EjZ7wt68u8dWvwvAqC1ryfhZ68G99HvIbY3LuB2NW74qgAvLa/G7zFwE68MM4wvEYuXbxJRwC8zp/ku75PgbwXRPC8XMkavRzeHL0o8Aq9stTqvD50tbxUSIi8patlvHfmX7yinIC86mSKvJI5hrxxtK+7e2xHu0jy9bvaT5u8Z/z5vAhTDb3iNv68ns/avEOJvby74Z68d8qDvL4HVbwD0kq8C7tKvIhKLrzSNBy8BoJ4POJaWjwyjaA7f3GSuwnhErwy/cy7gv0Wun9fQDtVWKg7bHqhO1OhmTsMA5I7sxNrO7yzgDv9FtA7z5A1PDNtjzxfOVo8nBi0O7q7+TdAJh64E36VO6zaJTzspHA81alzPM7NTDxgziA8dFXXO/aVnDvcd6g7MeURPMWTcjy8XTK7QNP4u1n8RLyNLGe8m8NavOv9PLwEQQu8i5O2u9IroLuDWbW7TH0QvDqLV7yK0X68keNcvIM++Lv1mBS7BYqtu/bgCrxwigy8Cw6ouxgBobonaeA6yHyCO4RCkTv4a5I7+Y/NOnFEhLvpHii842dlvOejMryTqLG7Tec3u2ZZ2zt/s7Q7RbQWPORMizw/7cw84FYEPWfjDj2JzAw9Tt4BPbwVwjyhKmA8+0fYO49xxTvriyc8WfxWPBxhHjxx0BQ8Ld8BPPcxODwy5pM8WLDhPBFsED3kuRs9fxgaPSkJBj05nrs8H49WPJptLTxmWXA8qRSdPL8vojxhF2k8RFShO+7ykjufpKA7WVoQPN1giDzHcLg8qyjRPEd6xDz3JZI8AcQbPHUukjtBxPo74FZkPGhHgDzAsj48KXUSPJ8GczuSh6w7hhq1O0LZATzVJUQ8Nw+HPAjPljwlEnU89Qb5O4WoxzrwVS87lmsaPDXsYDxO0yk8/hCAOwu+QTsKZRU7fg8jOysYhDudTPU7RKE/PCXLbDyQCz48yHCdO5e3DbpRE1g4vhOxO+OrJDylBwM8d855OwIfATuZRFC5bxIjvK/NNbzPfgC8eMMDvFt7RDte66I8GstNPL/9lrxXTc+8gsT8uiMUhzwtY5U8/qxLPPCEqTsR3VW6MnqUu3AfIrzFewW8zqXHu1J3n7uOj6g77UuPPJDetzsFEcS8MHS8vEuMmzsI8KI8shGJPJ+vOzxuxYA7xsvpuvzHxru72/+7UgIWvPmVrruURe26iAOCOxhMNTwsGXO66mrHvLN8jLyZrDw8obe3PKTxiTx6exU8vqcVO8Cg7LvdelS78Cn/u06B+btnf8u5VgzIuXU1Ibt8MYQ6iLuxu4BqqrwdNgu8L7qQPFGNvTw6/XE8SF8APElnMTtjgXy8Co7nOgXvALzQbTS7FGdtO60Zebtwbly8feIHvEws17sAJXi8HhpHuQ+Owjw2Ec48tChOPKWn2zs0ayQ7uzGDvNM5PDuw8r27/Eo/O+0oWTt7tlu85pvavAEDcbyB9pS7kpIgvLMTxTt6zv88gKvuPFy8Vjyslaw7fl/CuhGFa7wGMCS6BBmUuhikqzvH0lK7yx7gvJHQG702Ooa8jkmCOer8wLsfxy88arIVPV+dBT1goHc8C6dwO+5TwruLBWq8EM1pujhLYju5sQ87hR1bvCgiHL1tpy+92VhTvA9Vtzvz4EC7gS1XPKW3Gj0V8A09BiyEPD05Xzur5627ycU8vHffhTvTdNM7B/eSu3SSurz/TTO9eDYrvfcE4bvhfTI8PPkdOQJMTDySdhQ9L58HPVh0aDw97Tg7WsnLuoXqbruyvSc8H2meO2iCOLz1gOi8Gdk5vTThIL0wm0e7cJ2BPCvwFjtJKjc83xELPcUW7DxyLTE8n4HuOmZdUTunNqI7tfRtPJIb7Tk3Nny8nTvxvKW3ML38ihe9l+KNuo/5lzxMnFQ7Vtc0PF71+jx6XL08ObK3O9wWkjoaeQo8xhIgPBl/lDxTezu71b+EvAEZz7zf5xO9jFD6vJHAXzsyy5s8JrphOynRBTzeTbo8565zPArrLTqvY0w7zCJkPK1YFDyopJ88UzjUu5lOdrzXdp68FjvhvCrkqLyCXf870omYPIr/GDv0j906Pi9BPB5IwTu7GgS6EosFPAPNkTyV6po7BkljPAx8NrwGf2u8mqaIvAvupbxDtjS8TtFJPF0RnzzlQlW5BmHBu7+ttzryhp06vg1vO9W7aTzhNIs8FFVyOiahxjusBz+8GcxgvKApgLxuj2+8tKOOu02AjDxI4p48nxWqu/O5abyYSZS7k65/O4GUMzyxIJA8BuYsPO/JObv3cSI7gvINvHndTLxdt1+8KdpLvNiJCbpbQqY8gZGMPIYWQLwNRLG8jwSsu1z5KDwNo4o80XiDPExrxTtKy0y7tjIuOmeGjzltCBe8SQ4TvLAC27vflpO7GCqZu8jvCbve34C7yR4WvOBUHrzD5e66X7IyOyiCWbtgfuC7w8bIusr8gjvlNR48CVfVO9TGCrsvDc861U8ZPNQOFzxNVt46EK+fuhUDSzs/ls07uWLhOxLqzTuw3c079koFOcdt2LqqcZM7Wf+iu22cTbwZZQi8Yb53O51HrjuVKIa7S4M2vNPeFrxq0g67kqRhOvQtkbhsRKO7xSEZvKvKErxn40K7LEQcu1AKkby6a428GcAivPzAgbsEZ9u7HyNuvE3goLx1OIu8oHAZvHfsrbvbzx+8SNl/vNo7iryc1ya85MsHvJAeSrw9tsC8PxWWvM2oULzpzkO8C2WYvHIq0LxIXs28euulvEyWcbwuZHq8926gvPYsrLzbnoW8sndDvKA+erx0Mqi81ZW0vLaxe7xta128ri2gvGQo6rwcUAa9CyXqvLU8uLzmkq68PmLJvMn22bzoybC8QB9avHLbWryQYJq8H3/GvLgCG7xZS7i7sFwZvJLukbzqP8i8bGDFvEFIkLz6oXK8NlGivA0Eu7wJbqS8g3oqvLYssrsqUwu83KljvLwrarxTYyk88jMcPP6pJDtUPJu79cepuy1dpjmdEqw7JG5yOoWwy7ux2/y7LjfKOIoM7jvRTus70QkbO/wLJDorzqQ73hJTPLUBFjxlkxY7UbaOuWaNhzsvlTM8+Dk2PMIxrTvecVm5hS8iO/TwFDzUnh48shewOyvrvDry3Wo7GqMdPPL8XbsfVQi8zvY5vM/oALyjPJ+6umitOSXkqbselS28B4gXvHCWrLqtYYg5CvKdu9qEPLzqNE28rmb8u7IlIrsibqS7vawYvAgX5rvQ4L864WX7O/8KXDvxEFu7VK+lu8yBDjs6BP474V+fOwLIjrs8uxy8NmT9uxR/MbupUYe658ngO/YwwTvOR0k8rleuPJgjuTwV95E845xnPPa/lzw7FME8FbW4PDEyejwrswM8EDUOPNLkYDzxUYM8G0pQPA5DVzxNu3s8bYnAPOJz2zwa68I8uMCqPFkiwTw8Ivs8fZ4APfGWyjweW4U8dsBcPCz/lDzKysQ8lr7GPKdBlTwx4148kTmWPMLHtjw9ypo8H6NuPNZggDxOEL883szhPA00xzzCe4A8ZM0uPJRdRjyRoJk85Ae5POPOljxeOnA8NIdAPFF7kDwDKHI8Q+YIPH8doztRpz08Ap+dPFVRnzzY00o8vrXGO5+/3DvhtlU8wW6OPKvQczzkGtw7jufHO7PyMjw8kRs8eZe4Oo33G7scwRg6bM/FO5ilGjyWrwU8/P/ZOmpLfbvRlbQ5gBctPLIgRDyvonU7/JVeOcYDADu374Y62WwlvCfXwLzc9dm8GsMIvBREbDySWl48/a5AvOLvgbzNl6473jjHPLPW2jxCH2Q8sxq+unpHwLsq93s6y7IOuzX3hrzNfta8JjqlvPNFB7tKi3A82lDoO5AriLyvdVu8CYRPPNbb5Twvibc8glsSPCRHjrrDvXM6vijHO9sOurtEgLi8kPa9vPbZY7x/zGO6e9wLPOKyczpueoS8kszWu/IKmDzvNNk87GZ3PDvxazvJN+06DfdbOwlWKTznY0C85p2lvA8dc7wu2hq8e92xuwHqtDmCfES75y00vEUM1DqJdqs85IigPAvXGjzuDZQ7Jdk4PE397DrK9SQ8F5x/vOkaUrzEFPq7CNcrvOBLWrwNPNi7zSQeu+abbrtQ5f07gpmVPCeEXzx9s+c7MkkyPJhBRzxNwDi78myhugI1LLzwtcG7F+Liu4pdj7yTCbO8t04QvCwwZTu/K1Y7b4L1O6egYDwKekU8LdpLPMswkjykhtc7aisRvO/t2buTS/s5hDkUu92dR7zCO9689mXkvK9Uu7uJri48iaukOzL8Jjv8oTc8ltiMPG/0ujxaU6Q8emyWO1XgK7wlb367enK/O9Gci7vA5Ka8flwOvYGu5LxKASU630BzPAEyKbo0umW73r41PLWU3zx3muw8zxqOPE75WLmiyiq8zS8LO75CWjutAym8EnvnvGn7E70XsrS8QH4EPFNZVjzoqQu8u+IBvMlIjjxXdhE9GOj2PEn5Tzzjj0y7gso3vFQGPjzSmA86rzWJvHAcAb0/RwW93jVyvDpOCTzjxM87LIpuvPaP0Lu0Fcs8vgsRPWDzyTzrYNc7KQDiunNa9LtQpYc85vt0u8WCp7yZ/OS8E3jFvBxEJ7yCWxg7dOY3OQtzZbzH2yA7/IvrPKRJ/jwwE4E8Y4BnO4PxFLn6Uu86ll04POFxEbz5GaO8s7mivL2he7yYP0K8LfyXu7DGobupm/S7u7kPPI5N2DxoxLk845kbPBBxkTv3CT07a6quOxsCzDtHfhq8uUp+vEmOJLyYU068xNSBvA+tDbxxK8a6PkmrOF9ZETyccJY8sbplPOuLCDy5rSo8XH5APAr2hDvIP3E7c0sYvIsjBrzZicK7hA2BvMgBpbyeQN+7UNnnOzEZTzt4ims72U8PPHmRIDxAhUk8ENSPPM37ezxlflq6sV/COfPJ+buwVEW7d6IzvG0Ws7wfQbG8W+/BOnMfYTwSz6E6Mampu0yIKjsJKjs8t2+cPF27rTz6myc87v4nvKi+3bqZJzw5Wkpsu0uhkLxJpuG8p6yEvDmsKzyo5IY8xxqfu/DiTbzFCa46qX6QPAHpzzzdW6Y8FpIVO/BpTLzgfEq6" + }, + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "mass_step_4": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "YROxPpYstD7S+ro+hHm4PqO3pz6Pv5Y+DSySPqIGlz5wBaE+EWmuPvHEtT62764+H8ShPuDznj4ECKw+4dWzPq7ftD5JJ7g+u7K7PqD0tD6gxKQ+B0OaPjW6nT4oC6c+CDCvPuAOtT5eeLE+YHShPv+6kT4zD5M+G2OnPtTatD6lw5Q+9l6JPrSZgj60v20+dKhTPkyvTT4vMmQ+n+KBPrgTjD47UY0+5OKBPoN0Wj4t8T4+jJ5aPuQVlj5s5qU+FceCPi9tTD5YJis+cHEQPlus/j0rsgU+1YUmPlaqTT5n/mM+Lg1hPlfGRD6/Yh4+4NwOPk/SQD6geJU+iiWkPlr5hz7MSD8+rnIRPoaC6j2esNI9H2LsPb3fGT5KZz4+ygNOPpRoRD4Siyc+NcIJPvoWCj4p9E8+sPmjPhltsj4sGpA+KG5EPj08FD5D8fg9OVfvPQplCT4toCo+2f9CPnK6QT7QsCs+y+4MPsHu7z1pXAc+twNePiS6sD4f0L8+NPGKPl4XOj7afBI+xHUIPsqyET5HTis+bfpIPseBUT5nWzw+K+UYPlrT7z0/StM9xXQCPn3pYD6P4bM+toi/PixkeT4hlh4+2EMCPioyCT7nfSM+HphGPsicYT49bV4+l689PvmMFT4aou89h/TgPePEDj6mnm0+szW2Pnt6uD59gWQ+r60GPigZ4z0HEwQ+g5ooPho1Tj6sVWM+zjFZPkztNz5A6Rc+48sHPkkjDT4Q9DI+aUCIPgL/vz75aLY+TmhcPnA4AD5WTOI94q4IPsqiKz6jjUY+4zpNPoWaOj506Bw+7MoLPrGODz60aiU+n1RTPvitlj5T/cY+m8q1PlyoVj5SMwg+xHwEPqHuID5qwzw+KpVFPhUXOD42Qhk+QHT5PcNh7j0vkAk+4EkpPqibVz5CbJQ+iwrAPmtirj4cSko+wF8QPoANHT73v0E+zdxYPm8rUz497TU+eQIOPpZW5D3HsOU9bu8JPv+TJz738Us+rM2HPhDsrz53VKA+j1M3PqTzDT4VViY+F/dOPupKYj4qDFY+62o1Piu7ED5Qpvk9yGkEPjo4Gz5/Ri8+u9lDPvH1dj7gTp8+DKKQPsTYLD4g4Qo+UxwjPiLkQz4moU0+RIU9Pl8uIz50lQ0+oHQKPm5fHT4YcDU+87pAPqOZRj4smWk+OiaUPprjhj4WmUI+LMckPt6LNT5HxUU+bDk+PpBlJj6gERI+C1YMPr7MGj6gWzo+X8RWPgCZXT5qt1g+cLRuPsQ8kz6CuYo+iv2EPuwEez46k4M+bOGDPtlybT57YEs+wOI5PuDyPT6DmlU+il96PvhUiz4RoYs+k9yDPlu4hz6At50+WnqdPmNumD41nJo+GqaSPiGYgT7eg3E+IRqIPjRboz6vCqc+k2qOPiyPdD45wnk+TmONPnYNmj5f2Zo+NCSYPuTElj5+gJs+CQyZPjidjT7VWIE+PEyCPkGAlD6ahqM+FXCXPtCBfT4NHnI+KNyJPgWlmz4yU5w+DI+SPjVkkT6Uq5c+DH6KPl6ucT740lc+csJXPhX3cj41wYk+kkqJPso+Zj5wcEA+yVxLPn7hdT5byYc+yMqCPoxigD5HHJA+97qXPtGofj5FFEQ+h30wPp2IRT5CIWc+3/t0PizmYj6cw0E+4aY0PlGZSD6CN2I+Wa1gPu2fUj7RcG0+h8uWPnF8nD7SDm0+Rm07Pt1FPj4PkVo+VNZjPnbXUD6CeDs+QHE2Phq5RD6ftlw+AgVjPha4Sz6AOD8+UwJsPpEBmT578Zg+tHFPPnSsOj4iiVk+5lRxPikVWj5dUTE+rLUiPpsnLz6TiEQ+bclWPtB8Vj5sHUY+aTlKPr1sdz4K2ZM+JAOJPtwnPT5uCT0+nKthPhSLZj7FBz8+Jx4fPnZoIz7I6zM+bqc5PrZeOD5xSDY+2949PldBWj49T4E+bY6NPvsCdz7keFI+vlFXPioOZD7mg0o+fTUiPl3BGz77njc+p3dJPi3RPD5X+yk+vCMpPqYhQT5YJmQ+JeSAPmmViT79gnk+S+59PiFffz6Vk2w+WDY8PhwtGj76oyU+22VLPq60WD7wjkI+qyoyPsTJQD6zKF4+gl9tPkPicz7Ob4Q+0L+FPnAViD48PoE+tDRgPv6SNj7EHyQ+2WYyPg7SSj54F0c+7+QuPt7vMT6qlFc+f2dzPr5TaT7X2F0+qZl7PliMij5etIM+b4pePobuPD7VkS4+umcwPg8kOz7E/D8+d2ssPmhHGD5yYys+fBpWPolAZD78jU4+ElNLPqoUfD4wMI8+nr2BPtgWRj7F2ys+eKIxPkA5PD6pJ0E+smM9Pqf9LD6hXSc+0ec+PrroVD5Bvkc+fCgwPpBcRj7ZJog+aX6ZPkMWfj5qf0U+8yc9Pm4XST5T3kg+WTg/Pl/XOT6LiT0+l2ZOPqovYj69fls+SVE4PjhrJj7xrFI+ZfaTPp7Hnj6k5mc++slGPqt5VT7ui2A+pIBPPmHwNz5PVzY+441MPiqaZD5yZWg+nXxRPiz1ND6B1zg+3yZwPm7smj55S5c+NKZbPiFVTj4GQWI+wDljPk7/Sj72aTs+uUZNPnqeaT7EWWo+9zdRPsngOD7BwDg+KN9aPsYQiT5qZZ0+UEiOPijhez69hnk+ljB7Pp9YZj5tEU4+KrBYPuTzgz6/eJA+MkyAPg06UT5Q1kA+mhVYPn0xgj6ZipQ+CiecPruojz5t0Jw+EWKiPm9tlz7hVYg+31qIPoEvlD5DdJo+H4eUPtrujT6vvY0+2maPPiaSkz6uyZw+hquhPpBGnD6+GJc++vyjPtwinz5KHZE+x1eKPmLpjD4Vl5E+E/WVPpgqlz7+lpE+OfiJPlzfij43yJc+T1OiPimCnT6d/5Y+IjicPs97mT7Ds4Y+c/l/PsXMgz4xVYM+aj+APvi1hD6wjIg+iXuBPvmudT7oy4E+1IeNPvsQjT4pNYk+KkGWPgQooz4+tIw+dGt1PrTtdT6jLXk+a1tvPn0PcT6oLIA+W5V6PiiFYz42Q2Y+R5aBPqg/hT7bY3U+QtN9Pqu+mT5F2KI+HdOAPtnMdj48enw+DslvPmI/Yj4gQWw+h/93PmSQaz78614+5uVtPuqSfT5YTnE+YM9oPjrYhT6Zdp0+/7CXPpgneD4v5HM+8fNxPvVaZz6qPGM+qv5jPkLbXT4RQVg+Ih5kPm5DdD5wZms+qMRYPuujZz65soo+/RmZPsRLjT4rUX8+/rJvPoUIYT5+Fl4+pjRePlWCVD4xe0w+je1TPrKIYT4hUWA+MwJUPmcfWD7+ynI+4OuFPraejD6N/og+xrSDPtdtcj4xzWg+LhVkPoLoUj5mLEU+LzlTPq6/aD77S2E+Ps5IPkYIST4inWk+9/qBPlDHgz4tZ4k+zHmMPi/shT7yr3c+ZEl7Pntjbz7LAFA+3FpJPsQOaD7ykHc+LfdcPkiRRz5o1lo+Q817Pmswgj5//YQ+dXuTPlt5lj5VIYY+4kx9Ph/veD4v+2A+mr9MPhsOXD5sAHY+9/hoPvLSSD6iYU0+iiZwPsaofj4v4XQ+pQqCPsM6lj51dZc+muuCPl3OfT4rU2g+XzZLPj1kSj5tBGQ+QyZsPlOQUT4HHz8+E2hSPoNCbD4WcG0+VCdrPv7bfz7UHo4+paaMPpdkgj5sCHM+/Q5cPl4oUz4jf10+4bhiPhYlVj4jVEo+g+NRPjxIYT6UXGI+l0lfPunibD6woIE+BnGLPsu+ij7/mIc+PiFoPhc4Wz4VEG0+Hy52PjUjYz6C1FE+FNJYPmU6aD4mt2o+SoBlPtu6aT7xhXQ+o0uBPv4RkD5TT5Y+6XCHPtkgaj7ycW8+qkl/PtFLcj7tml0+AcdkPmm8dT4WqnA+W7RjPm1xaj73q3k+LXV7Pgavgj7T6ZY+QhadPvz0gD5sr3g+0NSGPpHXgj7WlWM+UI5dPn02ez6WOYQ+v9RzPgxtZz5Aa3I+ME16PswufD5fL4s+JyWfPgxRmT4n1Ic+aEGNPuixkD7dUYM+BhtwPmwMfj7Q8oo+enqIPnTVfz4B9oA+D2SEPiRxgj7AZIY+m8qWPsp3oT6EkJU+" + } + } + }, + { + "name": "crowded-pressure", + "variant": 2, + "fields": { + "initial_mass": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "displacement_step_1": { + "shape": [ + 3, + 2, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "mass_step_1": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + }, + "mass_step_4": { + "shape": [ + 3, + 16, + 16 + ], + "dtype": "f32-le-base64", + "data": "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" + } + } + } + ] +} diff --git a/packages/flow-lenia/web/src/render/arena-renderer.ts b/packages/flow-lenia/web/src/render/arena-renderer.ts new file mode 100644 index 00000000..e37d8732 --- /dev/null +++ b/packages/flow-lenia/web/src/render/arena-renderer.ts @@ -0,0 +1,76 @@ +import type { ArenaCard } from "../experiments/arena-cards.js"; +import type { FlowLeniaEcosystemSolver } from "../model/ecosystem-solver.js"; +import renderWgsl from "../shaders/render_arena.wgsl?raw"; + +export type ArenaView = ArenaCard["view"]; +const VIEW_INDEX: Record = { lineage: 0, phenotype: 1, density: 2, flow: 3, environment: 4 }; + +export class ArenaRenderer { + private readonly device: GPUDevice; + private readonly context: GPUCanvasContext; + private readonly canvas: HTMLCanvasElement; + private readonly uniform: GPUBuffer; + private readonly layout: GPUBindGroupLayout; + private readonly pipeline: GPURenderPipeline; + private solver: FlowLeniaEcosystemSolver | null = null; + private groups: [GPUBindGroup, GPUBindGroup] | null = null; + private view: ArenaView = "lineage"; + private center: [number, number] = [0, 0]; + private zoom = 1; + private inspection: [number, number, number] = [0, 0, 8]; + + constructor(device: GPUDevice, context: GPUCanvasContext, canvas: HTMLCanvasElement, format: GPUTextureFormat) { + this.device = device; this.context = context; this.canvas = canvas; + this.uniform = device.createBuffer({ size: 256, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); + this.layout = device.createBindGroupLayout({ + entries: [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 144 } }, + ...Array.from({ length: 8 }, (_, index) => ({ binding: index + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" as const } })), + ], + }); + const module = device.createShaderModule({ label: "flow-lenia-m5-arena-render", code: renderWgsl }); + this.pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [this.layout] }), vertex: { module, entryPoint: "vertex_main" }, fragment: { module, entryPoint: "fragment_main", targets: [{ format }] }, primitive: { topology: "triangle-list" } }); + } + + setSolver(solver: FlowLeniaEcosystemSolver): void { + if (!solver.environment || !solver.environmentRegions) throw new Error("Arena renderer requires environment buffers"); + this.solver = solver; + this.groups = [0, 1].map((ping) => this.device.createBindGroup({ layout: this.layout, entries: [ + { binding: 0, resource: { buffer: this.uniform, size: 144 } }, + { binding: 1, resource: { buffer: solver.mass[ping] as GPUBuffer } }, + { binding: 2, resource: { buffer: solver.genomeH[ping] as GPUBuffer } }, + { binding: 3, resource: { buffer: solver.genomeQ[ping] as GPUBuffer } }, + { binding: 4, resource: { buffer: solver.identity[ping] as GPUBuffer } }, + { binding: 5, resource: { buffer: solver.transport } }, + { binding: 6, resource: { buffer: solver.diagnostic } }, + { binding: 7, resource: { buffer: solver.environment as GPUBuffer } }, + { binding: 8, resource: { buffer: solver.environmentRegions as GPUBuffer } }, + ] })) as [GPUBindGroup, GPUBindGroup]; + this.center = [solver.n / 2, solver.n / 2]; + } + setView(view: ArenaView): void { this.view = view; } + getView(): ArenaView { return this.view; } + setInspection(row: number, column: number, radius: number): void { this.inspection = [row, column, radius]; } + setZoom(value: number): void { this.zoom = Math.max(0.5, Math.min(8, value)); } + getZoom(): number { return this.zoom; } + resetCamera(): void { if (this.solver) this.center = [this.solver.n / 2, this.solver.n / 2]; this.zoom = 1; } + worldFromCanvas(clientX: number, clientY: number): readonly [number, number] { const rect = this.canvas.getBoundingClientRect(); const n = this.solver?.n ?? 256; return [this.center[0] + ((clientY - rect.top) / rect.height - 0.5) * n / this.zoom, this.center[1] + ((clientX - rect.left) / rect.width - 0.5) * n / this.zoom]; } + + render(time: number): void { + if (!this.solver || !this.groups) return; + const width = Math.max(1, Math.floor(this.canvas.clientWidth * devicePixelRatio)); const height = Math.max(1, Math.floor(this.canvas.clientHeight * devicePixelRatio)); + if (this.canvas.width !== width || this.canvas.height !== height) { this.canvas.width = width; this.canvas.height = height; } + const raw = new ArrayBuffer(144); const u32 = new Uint32Array(raw); const f32 = new Float32Array(raw); const dynamics = this.solver.getArenaDynamics(); + u32.set([this.solver.n, VIEW_INDEX[this.view], 1, 0]); + f32.set([width, height, 0, 0, width, height, 1.75, 3.2, this.center[0], this.center[1], this.zoom, time, this.inspection[0], this.inspection[1], this.inspection[2], 0], 4); + f32.set([this.solver.getGateOpen() ? 1 : 0, this.solver.stepCount, dynamics?.storm.startStep ?? 0, dynamics?.storm.duration ?? 0], 20); + f32.set([...(dynamics?.storm.center ?? [0.5, 0.5]), dynamics?.storm.radius ?? 0.2, dynamics?.storm.amplitude ?? 0], 24); + f32.set([...(dynamics?.attractor.center ?? [0.5, 0.5]), dynamics?.attractor.radius ?? 0.2, dynamics?.attractor.amplitude ?? 0], 28); + f32.set([dynamics?.attractor.orbitRadius ?? 0, dynamics?.attractor.angularSpeed ?? 0, dynamics?.attractor.phase ?? 0, 0], 32); + this.device.queue.writeBuffer(this.uniform, 0, raw); + const encoder = this.device.createCommandEncoder({ label: "flow-lenia-m5-arena-render" }); + const pass = encoder.beginRenderPass({ colorAttachments: [{ view: this.context.getCurrentTexture().createView(), clearValue: { r: 0.006, g: 0.01, b: 0.02, a: 1 }, loadOp: "clear", storeOp: "store" }] }); + pass.setPipeline(this.pipeline); pass.setBindGroup(0, this.groups[this.solver.currentPing]); pass.draw(3); pass.end(); this.device.queue.submit([encoder.finish()]); + } + destroy(): void { this.uniform.destroy(); } +} diff --git a/packages/flow-lenia/web/src/render/ecosystem-renderer.ts b/packages/flow-lenia/web/src/render/ecosystem-renderer.ts new file mode 100644 index 00000000..b7f6fc31 --- /dev/null +++ b/packages/flow-lenia/web/src/render/ecosystem-renderer.ts @@ -0,0 +1,105 @@ +import type { EcosystemView } from "../experiments/ecosystem-cards.js"; +import type { FlowLeniaEcosystemSolver } from "../model/ecosystem-solver.js"; +import renderWgsl from "../shaders/render_ecosystem.wgsl?raw"; + +const VIEW_INDEX: Record = { lineage: 0, phenotype: 1, density: 2, flow: 3 }; + +export class EcosystemRenderer { + private readonly device: GPUDevice; + private readonly context: GPUCanvasContext; + private readonly canvas: HTMLCanvasElement; + private readonly uniform: GPUBuffer; + private readonly layout: GPUBindGroupLayout; + private readonly pipeline: GPURenderPipeline; + private solvers: FlowLeniaEcosystemSolver[] = []; + private groups: Array<[GPUBindGroup, GPUBindGroup]> = []; + private view: EcosystemView = "lineage"; + private center: [number, number] = [0, 0]; + private zoom = 1; + private inspection: [number, number, number] = [0, 0, 8]; + + constructor(device: GPUDevice, context: GPUCanvasContext, canvas: HTMLCanvasElement, format: GPUTextureFormat) { + this.device = device; + this.context = context; + this.canvas = canvas; + this.uniform = device.createBuffer({ size: 256 * 3, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); + this.layout = device.createBindGroupLayout({ + entries: [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 80 } }, + ...Array.from({ length: 6 }, (_, index) => ({ binding: index + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" as const } })), + ], + }); + const module = device.createShaderModule({ label: "flow-lenia-m4-ecosystem-render", code: renderWgsl }); + this.pipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [this.layout] }), + vertex: { module, entryPoint: "vertex_main" }, + fragment: { module, entryPoint: "fragment_main", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + } + + setSolvers(solvers: readonly FlowLeniaEcosystemSolver[]): void { + this.solvers = [...solvers].slice(0, 3); + this.groups = this.solvers.map((solver) => [0, 1].map((ping) => this.device.createBindGroup({ + layout: this.layout, + entries: [ + { binding: 0, resource: { buffer: this.uniform, size: 80 } }, + { binding: 1, resource: { buffer: solver.mass[ping] as GPUBuffer } }, + { binding: 2, resource: { buffer: solver.genomeH[ping] as GPUBuffer } }, + { binding: 3, resource: { buffer: solver.genomeQ[ping] as GPUBuffer } }, + { binding: 4, resource: { buffer: solver.identity[ping] as GPUBuffer } }, + { binding: 5, resource: { buffer: solver.transport } }, + { binding: 6, resource: { buffer: solver.diagnostic } }, + ], + })) as [GPUBindGroup, GPUBindGroup]); + const n = this.solvers[0]?.n ?? 256; + this.center = [n / 2, n / 2]; + } + + setView(view: EcosystemView): void { this.view = view; } + getView(): EcosystemView { return this.view; } + setInspection(row: number, column: number, radius: number): void { this.inspection = [row, column, radius]; } + setZoom(zoom: number): void { this.zoom = Math.max(0.5, Math.min(8, zoom)); } + getZoom(): number { return this.zoom; } + pan(deltaRow: number, deltaColumn: number): void { this.center[0] += deltaRow; this.center[1] += deltaColumn; } + resetCamera(): void { const n = this.solvers[0]?.n ?? 256; this.center = [n / 2, n / 2]; this.zoom = 1; } + + worldFromCanvas(clientX: number, clientY: number): readonly [number, number] { + const rect = this.canvas.getBoundingClientRect(); + const panes = Math.max(1, this.solvers.length); + const localX = (clientX - rect.left) / rect.width; + const pane = Math.min(panes - 1, Math.max(0, Math.floor(localX * panes))); + const paneX = localX * panes - pane; + const y = (clientY - rect.top) / rect.height; + const n = this.solvers[0]?.n ?? 256; + return [this.center[0] + (y - 0.5) * n / this.zoom, this.center[1] + (paneX - 0.5) * n / this.zoom]; + } + + render(time: number): void { + if (this.solvers.length === 0) return; + const width = Math.max(1, Math.floor(this.canvas.clientWidth * devicePixelRatio)); + const height = Math.max(1, Math.floor(this.canvas.clientHeight * devicePixelRatio)); + if (this.canvas.width !== width || this.canvas.height !== height) { this.canvas.width = width; this.canvas.height = height; } + const paneWidth = width / this.solvers.length; + const raw = new ArrayBuffer(256 * this.solvers.length); + for (let pane = 0; pane < this.solvers.length; pane += 1) { + const u32 = new Uint32Array(raw, pane * 256, 20); + const f32 = new Float32Array(raw, pane * 256, 20); + u32.set([this.solvers[pane]?.n ?? 256, VIEW_INDEX[this.view], 1, pane]); + f32.set([width, height, pane * paneWidth, 0, paneWidth, height, 1.75, 3.2, this.center[0], this.center[1], this.zoom, time, this.inspection[0], this.inspection[1], this.inspection[2], 0], 4); + } + this.device.queue.writeBuffer(this.uniform, 0, raw); + const encoder = this.device.createCommandEncoder({ label: "flow-lenia-m4-render" }); + const pass = encoder.beginRenderPass({ colorAttachments: [{ view: this.context.getCurrentTexture().createView(), clearValue: { r: 0.006, g: 0.01, b: 0.02, a: 1 }, loadOp: "clear", storeOp: "store" }] }); + pass.setPipeline(this.pipeline); + for (let pane = 0; pane < this.solvers.length; pane += 1) { + pass.setViewport(pane * paneWidth, 0, paneWidth, height, 0, 1); + pass.setBindGroup(0, this.groups[pane]?.[this.solvers[pane]?.currentPing ?? 0] as GPUBindGroup, [pane * 256]); + pass.draw(3); + } + pass.end(); + this.device.queue.submit([encoder.finish()]); + } + + destroy(): void { this.uniform.destroy(); } +} diff --git a/packages/flow-lenia/web/src/render/overlays.ts b/packages/flow-lenia/web/src/render/overlays.ts new file mode 100644 index 00000000..923e5895 --- /dev/null +++ b/packages/flow-lenia/web/src/render/overlays.ts @@ -0,0 +1,161 @@ +import { KERNEL_SPECS } from "../model/config.js"; +import type { CellInspection } from "../model/solver.js"; + +function line(parent: HTMLElement, label: string, value = "—"): HTMLDivElement { + const row = document.createElement("div"); + row.className = "fl-inspect-row"; + const term = document.createElement("span"); + term.textContent = label; + const reading = document.createElement("output"); + reading.dataset.field = label; + reading.textContent = value; + row.append(term, reading); + parent.appendChild(row); + return row; +} + +export class ScientificInspector { + readonly element: HTMLElement; + private readonly kernelCanvas: HTMLCanvasElement; + private readonly responseCanvas: HTMLCanvasElement; + private readonly fields = new Map(); + private kernel = 0; + private latest: CellInspection | null = null; + + constructor(host: HTMLElement) { + this.element = document.createElement("aside"); + this.element.className = "fl-inspector"; + this.element.setAttribute("aria-label", "Scientific cell inspector"); + const heading = document.createElement("div"); + heading.className = "fl-inspector-head"; + heading.innerHTML = "INSPECT LENSread only"; + this.element.appendChild(heading); + const values = document.createElement("div"); + values.className = "fl-inspect-values"; + for (const label of ["cell", "mass C₀/C₁/C₂", "density ρ", "affinity V", "pressure α", "flow F", "displacement dt·F", "clamped"]) { + const row = line(values, label); + this.fields.set(label, row.querySelector("output") as HTMLOutputElement); + } + this.element.appendChild(values); + + const kernelHead = document.createElement("label"); + kernelHead.className = "fl-kernel-label"; + kernelHead.textContent = "kernel rings "; + const select = document.createElement("select"); + select.setAttribute("aria-label", "Inspected kernel"); + KERNEL_SPECS.forEach((kernel, index) => { + const option = document.createElement("option"); + option.value = String(index); + option.textContent = `K${index} · C${kernel.source}→C${kernel.target}`; + select.appendChild(option); + }); + select.addEventListener("change", () => { + this.kernel = Number.parseInt(select.value, 10); + this.draw(); + }); + kernelHead.appendChild(select); + this.kernelCanvas = document.createElement("canvas"); + this.kernelCanvas.width = 240; + this.kernelCanvas.height = 108; + this.kernelCanvas.setAttribute("role", "img"); + this.kernelCanvas.setAttribute("aria-label", "Normalized spatial rings for kernel zero"); + this.responseCanvas = document.createElement("canvas"); + this.responseCanvas.width = 240; + this.responseCanvas.height = 108; + this.responseCanvas.setAttribute("role", "img"); + this.responseCanvas.setAttribute("aria-label", "Bell growth response and inspected perception sample"); + this.element.append(kernelHead, this.kernelCanvas, this.responseCanvas); + const note = document.createElement("p"); + note.className = "fl-inspect-note"; + note.textContent = "Alt samples from any tool · kernel plot is normalized shape; response dot is the selected cell."; + this.element.appendChild(note); + host.appendChild(this.element); + this.draw(); + } + + update(inspection: CellInspection): void { + this.latest = inspection; + const channel = KERNEL_SPECS[this.kernel]?.target ?? 0; + this.set("cell", `${inspection.cell[0]}, ${inspection.cell[1]}`); + this.set("mass C₀/C₁/C₂", inspection.mass.map((value) => value.toFixed(3)).join(" · ")); + this.set("density ρ", inspection.density.toFixed(4)); + this.set("affinity V", inspection.affinity.map((value) => value.toFixed(3)).join(" · ")); + this.set("pressure α", inspection.alpha.map((value) => value.toFixed(3)).join(" · ")); + this.set("flow F", inspection.flow[channel]?.map((value) => value.toFixed(3)).join(", ") ?? "—"); + this.set("displacement dt·F", inspection.displacement[channel]?.map((value) => value.toFixed(3)).join(", ") ?? "—"); + this.set("clamped", inspection.clamp.some((value) => value > 0.5) ? "yes" : "no"); + this.draw(); + } + + private set(label: string, value: string): void { const output = this.fields.get(label); if (output) output.textContent = value; } + + private draw(): void { + const spec = KERNEL_SPECS[this.kernel] as (typeof KERNEL_SPECS)[number]; + const kernelContext = this.kernelCanvas.getContext("2d") as CanvasRenderingContext2D; + kernelContext.clearRect(0, 0, 240, 108); + kernelContext.fillStyle = "#071017"; + kernelContext.fillRect(0, 0, 240, 108); + kernelContext.save(); + kernelContext.translate(58, 54); + kernelContext.strokeStyle = "#31535a"; + kernelContext.lineWidth = 1; + kernelContext.beginPath(); kernelContext.arc(0, 0, 43, 0, Math.PI * 2); kernelContext.stroke(); + const colors = ["#5de1c1", "#d477ff", "#ffb34d"]; + spec.ringCenters.forEach((center, index) => { + kernelContext.strokeStyle = colors[index] as string; + kernelContext.globalAlpha = 0.35 + 0.6 * (spec.ringAmplitudes[index] as number); + kernelContext.lineWidth = Math.max(1.5, 42 * Math.sqrt(spec.ringWidths[index] as number)); + kernelContext.beginPath(); kernelContext.arc(0, 0, center * 42, 0, Math.PI * 2); kernelContext.stroke(); + }); + kernelContext.restore(); + kernelContext.globalAlpha = 1; + kernelContext.strokeStyle = "#75d8c0"; + kernelContext.beginPath(); + for (let pixel = 0; pixel <= 132; pixel += 1) { + const radius = pixel / 132; + let value = 0; + for (let ring = 0; ring < 3; ring += 1) { + const delta = radius - (spec.ringCenters[ring] as number); + value += (spec.ringAmplitudes[ring] as number) * Math.exp(-(delta * delta) / (spec.ringWidths[ring] as number)); + } + const x = 98 + pixel; + const y = 93 - Math.min(1, value) * 72; + if (pixel === 0) kernelContext.moveTo(x, y); else kernelContext.lineTo(x, y); + } + kernelContext.stroke(); + kernelContext.fillStyle = "#8aa7aa"; + kernelContext.font = "10px ui-monospace, monospace"; + kernelContext.fillText(`K${this.kernel} r=${spec.relativeRadius.toFixed(2)} Σ=1`, 98, 104); + this.kernelCanvas.setAttribute("aria-label", `Normalized three-ring spatial kernel ${this.kernel}, channel ${spec.source} to ${spec.target}`); + + const responseContext = this.responseCanvas.getContext("2d") as CanvasRenderingContext2D; + responseContext.clearRect(0, 0, 240, 108); + responseContext.fillStyle = "#071017"; + responseContext.fillRect(0, 0, 240, 108); + responseContext.strokeStyle = "#29464d"; + responseContext.beginPath(); responseContext.moveTo(18, 54); responseContext.lineTo(230, 54); responseContext.stroke(); + responseContext.strokeStyle = "#ffb860"; + responseContext.beginPath(); + const xmax = 0.6; + for (let pixel = 0; pixel <= 212; pixel += 1) { + const perception = pixel / 212 * xmax; + const z = (perception - spec.growthMean) / spec.growthWidth; + const growth = 2 * Math.exp(-0.5 * z * z) - 1; + const x = 18 + pixel; + const y = 54 - growth * 40; + if (pixel === 0) responseContext.moveTo(x, y); else responseContext.lineTo(x, y); + } + responseContext.stroke(); + const sample = this.latest?.perception[this.kernel]; + const sampleGrowth = this.latest?.growth[this.kernel]; + if (sample !== undefined && sampleGrowth !== undefined) { + const x = 18 + Math.max(0, Math.min(1, sample / xmax)) * 212; + const y = 54 - Math.max(-1, Math.min(1, sampleGrowth)) * 40; + responseContext.fillStyle = "#efff91"; + responseContext.beginPath(); responseContext.arc(x, y, 4, 0, Math.PI * 2); responseContext.fill(); + } + responseContext.fillStyle = "#8aa7aa"; + responseContext.font = "10px ui-monospace, monospace"; + responseContext.fillText(`G(U) · m=${spec.growthMean.toFixed(2)} · s=${spec.growthWidth.toFixed(3)} · −1…+1`, 18, 104); + } +} diff --git a/packages/flow-lenia/web/src/render/renderer.ts b/packages/flow-lenia/web/src/render/renderer.ts new file mode 100644 index 00000000..fdc5c8cd --- /dev/null +++ b/packages/flow-lenia/web/src/render/renderer.ts @@ -0,0 +1,304 @@ +import renderWgsl from "../shaders/render_organism.wgsl?raw"; +import trailWgsl from "../shaders/render_trails.wgsl?raw"; +import type { FlowLeniaOrganismSolver } from "../model/solver.js"; + +export type RenderMode = "density" | "channels" | "affinity" | "flow" | "pressure" | "flux"; +const MODE_INDEX: Record = { density: 0, channels: 1, affinity: 2, flow: 3, pressure: 4, flux: 5 }; + +export interface CameraState { centerRow: number; centerColumn: number; zoom: number } +export interface PresentationState { + mode: RenderMode; + channel: number; + exposure: number; + trails: number; + contours: boolean; + glyphs: boolean; + camera: CameraState; +} + +export class OrganismRenderer { + readonly module: GPUShaderModule; + private readonly device: GPUDevice; + private readonly context: GPUCanvasContext; + private readonly canvas: HTMLCanvasElement; + private readonly solver: FlowLeniaOrganismSolver; + private readonly uniform: GPUBuffer; + private readonly trailUniform: GPUBuffer; + private readonly pipeline: GPURenderPipeline; + private readonly trailPipeline: GPURenderPipeline; + private readonly displayPipeline: GPURenderPipeline; + private readonly scientificLayout: GPUBindGroupLayout; + private readonly trailLayout: GPUBindGroupLayout; + private readonly sampler: GPUSampler; + private groups: GPUBindGroup[] = []; + private comparison: FlowLeniaOrganismSolver | null = null; + private mode: RenderMode = "density"; + private channel = 0; + private exposure = 1.65; + private persistence = 0.84; + private contours = false; + private glyphs = false; + private camera: CameraState; + private inspection: { row: number; column: number; radius: number } | null = null; + private sceneTexture: GPUTexture | null = null; + private trailTextures: [GPUTexture, GPUTexture] | null = null; + private trailGroups: [GPUBindGroup, GPUBindGroup] | null = null; + private displayGroups: [GPUBindGroup, GPUBindGroup] | null = null; + private trailPing = 0; + private needsTrailClear = true; + private textureBytes = 0; + + constructor( + device: GPUDevice, + context: GPUCanvasContext, + canvas: HTMLCanvasElement, + format: GPUTextureFormat, + solver: FlowLeniaOrganismSolver, + ) { + this.device = device; + this.context = context; + this.canvas = canvas; + this.solver = solver; + this.camera = { centerRow: solver.n / 2, centerColumn: solver.n / 2, zoom: 1 }; + this.uniform = device.createBuffer({ + label: "flow-lenia-m3-render-uniform", + size: 64, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + this.trailUniform = device.createBuffer({ + label: "flow-lenia-m3-trail-uniform", + size: 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + this.scientificLayout = device.createBindGroupLayout({ + label: "flow-lenia-m3-render-layout", + entries: [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, + ...Array.from({ length: 8 }, (_, index) => ({ + binding: index + 1, + visibility: GPUShaderStage.FRAGMENT, + buffer: { type: "read-only-storage" as const }, + })), + ], + }); + this.module = device.createShaderModule({ label: "flow-lenia-m3-render", code: renderWgsl }); + this.pipeline = device.createRenderPipeline({ + label: "flow-lenia-m3-scientific-render-pipeline", + layout: device.createPipelineLayout({ bindGroupLayouts: [this.scientificLayout] }), + vertex: { module: this.module, entryPoint: "vertex_main" }, + fragment: { module: this.module, entryPoint: "fragment_main", targets: [{ format: "rgba8unorm" }] }, + primitive: { topology: "triangle-list" }, + }); + this.trailLayout = device.createBindGroupLayout({ + label: "flow-lenia-m3-trail-layout", + entries: [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, + { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, + { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } }, + { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } }, + ], + }); + const trailModule = device.createShaderModule({ label: "flow-lenia-m3-trails", code: trailWgsl }); + const trailPipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [this.trailLayout] }); + this.trailPipeline = device.createRenderPipeline({ + label: "flow-lenia-m3-trail-accumulation", + layout: trailPipelineLayout, + vertex: { module: trailModule, entryPoint: "vertex_main" }, + fragment: { module: trailModule, entryPoint: "accumulate", targets: [{ format: "rgba8unorm" }] }, + primitive: { topology: "triangle-list" }, + }); + this.displayPipeline = device.createRenderPipeline({ + label: "flow-lenia-m3-trail-display", + layout: trailPipelineLayout, + vertex: { module: trailModule, entryPoint: "vertex_main" }, + fragment: { module: trailModule, entryPoint: "display", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" }); + this.rebuildScientificGroups(); + } + + private rebuildScientificGroups(): void { + const secondary = this.comparison ?? this.solver; + this.groups = []; + for (let primaryPing = 0; primaryPing < 2; primaryPing += 1) { + for (let secondaryPing = 0; secondaryPing < 2; secondaryPing += 1) { + this.groups.push(this.device.createBindGroup({ + layout: this.scientificLayout, + entries: [ + { binding: 0, resource: { buffer: this.uniform } }, + { binding: 1, resource: { buffer: this.solver.mass[primaryPing] as GPUBuffer } }, + { binding: 2, resource: { buffer: this.solver.affinity } }, + { binding: 3, resource: { buffer: this.solver.transport } }, + { binding: 4, resource: { buffer: this.solver.diagnostic } }, + { binding: 5, resource: { buffer: secondary.mass[secondaryPing] as GPUBuffer } }, + { binding: 6, resource: { buffer: secondary.affinity } }, + { binding: 7, resource: { buffer: secondary.transport } }, + { binding: 8, resource: { buffer: secondary.diagnostic } }, + ], + })); + } + } + this.clearTrails(); + } + + setComparisonSolver(solver: FlowLeniaOrganismSolver | null): void { + this.comparison = solver; + this.rebuildScientificGroups(); + } + + hasComparison(): boolean { return this.comparison !== null; } + setMode(mode: RenderMode): void { this.mode = mode; this.clearTrails(); } + getMode(): RenderMode { return this.mode; } + setChannel(channel: number): void { this.channel = Math.max(0, Math.min(2, channel | 0)); this.clearTrails(); } + setExposure(value: number): void { this.exposure = Math.max(0.2, Math.min(4, value)); } + setTrailPersistence(value: number): void { this.persistence = Math.max(0, Math.min(0.97, value)); if (value === 0) this.clearTrails(); } + setContours(enabled: boolean): void { this.contours = enabled; } + setFlowGlyphs(enabled: boolean): void { this.glyphs = enabled; } + setInspection(row: number, column: number, radius: number): void { this.inspection = { row, column, radius }; } + clearInspection(): void { this.inspection = null; } + + getCamera(): CameraState { return { ...this.camera }; } + setCamera(camera: CameraState): void { + this.camera = { + centerRow: ((camera.centerRow % this.solver.n) + this.solver.n) % this.solver.n, + centerColumn: ((camera.centerColumn % this.solver.n) + this.solver.n) % this.solver.n, + zoom: Math.max(0.45, Math.min(12, camera.zoom)), + }; + this.clearTrails(); + } + pan(deltaRow: number, deltaColumn: number): void { + this.setCamera({ ...this.camera, centerRow: this.camera.centerRow + deltaRow, centerColumn: this.camera.centerColumn + deltaColumn }); + } + zoom(factor: number): void { this.setCamera({ ...this.camera, zoom: this.camera.zoom * factor }); } + + screenToWorld(clientX: number, clientY: number): readonly [number, number] { + const rect = this.canvas.getBoundingClientRect(); + const comparison = this.comparison !== null; + const localX = comparison ? ((clientX - rect.left) % (rect.width * 0.5)) : clientX - rect.left; + const paneWidth = comparison ? rect.width * 0.5 : rect.width; + const screenX = localX / Math.max(1, paneWidth) - 0.5; + const screenY = (clientY - rect.top) / Math.max(1, rect.height) - 0.5; + const verticalScale = comparison ? 0.5 : 1; + const row = this.camera.centerRow + screenY * this.solver.n * verticalScale / this.camera.zoom; + const column = this.camera.centerColumn + screenX * this.solver.n / this.camera.zoom; + return [((row % this.solver.n) + this.solver.n) % this.solver.n, ((column % this.solver.n) + this.solver.n) % this.solver.n]; + } + + getPresentationState(): PresentationState { + return { mode: this.mode, channel: this.channel, exposure: this.exposure, trails: this.persistence, contours: this.contours, glyphs: this.glyphs, camera: this.getCamera() }; + } + + restorePresentationState(state: PresentationState): void { + this.mode = state.mode; + this.channel = state.channel; + this.exposure = state.exposure; + this.persistence = state.trails; + this.contours = state.contours; + this.glyphs = state.glyphs; + this.camera = { ...state.camera }; + this.clearTrails(); + } + + private resize(): void { + const scale = Math.min(devicePixelRatio || 1, 2); + const width = Math.max(1, Math.floor(this.canvas.clientWidth * scale)); + const height = Math.max(1, Math.floor(this.canvas.clientHeight * scale)); + if (this.canvas.width === width && this.canvas.height === height && this.sceneTexture) return; + this.canvas.width = width; + this.canvas.height = height; + this.sceneTexture?.destroy(); + this.trailTextures?.forEach((texture) => texture.destroy()); + const texture = (label: string): GPUTexture => this.device.createTexture({ + label, + size: [width, height], + format: "rgba8unorm", + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + }); + this.sceneTexture = texture("flow-lenia-m3-scene"); + this.trailTextures = [texture("flow-lenia-m3-trail-a"), texture("flow-lenia-m3-trail-b")]; + this.trailGroups = this.trailTextures.map((history) => this.device.createBindGroup({ + layout: this.trailLayout, + entries: [ + { binding: 0, resource: { buffer: this.trailUniform } }, + { binding: 1, resource: this.sampler }, + { binding: 2, resource: this.sceneTexture?.createView() as GPUTextureView }, + { binding: 3, resource: history.createView() }, + ], + })) as [GPUBindGroup, GPUBindGroup]; + this.displayGroups = this.trailTextures.map((current) => this.device.createBindGroup({ + layout: this.trailLayout, + entries: [ + { binding: 0, resource: { buffer: this.trailUniform } }, + { binding: 1, resource: this.sampler }, + { binding: 2, resource: current.createView() }, + { binding: 3, resource: current.createView() }, + ], + })) as [GPUBindGroup, GPUBindGroup]; + this.textureBytes = width * height * 4 * 3; + this.trailPing = 0; + this.needsTrailClear = true; + } + + get allocatedBytes(): number { return this.uniform.size + this.trailUniform.size + this.textureBytes; } + clearTrails(): void { this.needsTrailClear = true; } + + render(): void { + this.resize(); + if (!this.sceneTexture || !this.trailTextures || !this.trailGroups || !this.displayGroups) return; + const raw = new ArrayBuffer(64); + const u32 = new Uint32Array(raw); + const f32 = new Float32Array(raw); + let flags = 0; + if (this.contours) flags |= 1; + if (this.glyphs) flags |= 2; + if (this.inspection) flags |= 4; + u32.set([this.solver.n, MODE_INDEX[this.mode], this.channel, flags]); + f32.set([this.canvas.width, this.canvas.height, this.exposure, 1.25], 4); + f32.set([this.camera.centerRow, this.camera.centerColumn, this.camera.zoom, performance.now() * 0.001], 8); + f32.set([this.inspection?.row ?? 0, this.inspection?.column ?? 0, this.inspection?.radius ?? 0, this.comparison ? 1 : 0], 12); + this.device.queue.writeBuffer(this.uniform, 0, raw); + this.device.queue.writeBuffer(this.trailUniform, 0, new Float32Array([this.persistence, 0, 0, 0])); + const encoder = this.device.createCommandEncoder({ label: "flow-lenia-m3-render-frame" }); + if (this.needsTrailClear) { + for (const texture of this.trailTextures) { + const clear = encoder.beginRenderPass({ colorAttachments: [{ view: texture.createView(), clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" }] }); + clear.end(); + } + this.needsTrailClear = false; + } + const scene = encoder.beginRenderPass({ + colorAttachments: [{ view: this.sceneTexture.createView(), clearValue: { r: 0.01, g: 0.015, b: 0.022, a: 1 }, loadOp: "clear", storeOp: "store" }], + }); + scene.setPipeline(this.pipeline); + const primaryPing = this.solver.currentMassBuffer === this.solver.mass[0] ? 0 : 1; + const secondaryPing = this.comparison?.currentMassBuffer === this.comparison?.mass[1] ? 1 : 0; + scene.setBindGroup(0, this.groups[primaryPing * 2 + secondaryPing] as GPUBindGroup); + scene.draw(3); + scene.end(); + const nextTrail = 1 - this.trailPing; + const accumulate = encoder.beginRenderPass({ + colorAttachments: [{ view: this.trailTextures[nextTrail].createView(), clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" }], + }); + accumulate.setPipeline(this.trailPipeline); + accumulate.setBindGroup(0, this.trailGroups[this.trailPing]); + accumulate.draw(3); + accumulate.end(); + const display = encoder.beginRenderPass({ + colorAttachments: [{ view: this.context.getCurrentTexture().createView(), clearValue: { r: 0.01, g: 0.015, b: 0.022, a: 1 }, loadOp: "clear", storeOp: "store" }], + }); + display.setPipeline(this.displayPipeline); + display.setBindGroup(0, this.displayGroups[nextTrail]); + display.draw(3); + display.end(); + this.trailPing = nextTrail; + this.device.queue.submit([encoder.finish()]); + } + + destroy(): void { + this.uniform.destroy(); + this.trailUniform.destroy(); + this.sceneTexture?.destroy(); + this.trailTextures?.forEach((texture) => texture.destroy()); + } +} diff --git a/packages/flow-lenia/web/src/shaders/arena_events.wgsl b/packages/flow-lenia/web/src/shaders/arena_events.wgsl new file mode 100644 index 00000000..7463a506 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/arena_events.wgsl @@ -0,0 +1,48 @@ +struct EventUniform { n: u32, n2: u32, count: u32, _pad: u32 } +struct EnvironmentEvent { + centerRadius: vec4, + mode: u32, + _pad0: u32, + _pad1: u32, + _pad2: u32, +} + +@group(0) @binding(0) var U: EventUniform; +@group(0) @binding(1) var events: array; +@group(0) @binding(2) var environment: array>; + +fn torus_distance(a: f32, b: f32) -> f32 { + let direct = abs(a - b); + return min(direct, f32(U.n) - direct); +} + +@compute @workgroup_size(128) +fn apply_environment_events(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n2) { return; } + let row = f32(g.x / U.n); + let column = f32(g.x % U.n); + var field = environment[g.x]; + for (var eventIndex = 0u; eventIndex < U.count; eventIndex += 1u) { + let event = events[eventIndex]; + let dr = torus_distance(row, event.centerRadius.x); + let dc = torus_distance(column, event.centerRadius.y); + let radius = max(event.centerRadius.z, 1.0); + let distance = sqrt(dr * dr + dc * dc); + if (distance <= radius) { + let falloff = 0.5 + 0.5 * cos(3.141592653589793 * distance / radius); + let amount = event.centerRadius.w * falloff; + if (event.mode == 0u) { + field.x = clamp(field.x + amount, -4.0, 4.0); + } else if (event.mode == 1u) { + field.y = clamp(field.y - abs(amount), -4.0, 0.0); + field.w = clamp(field.w + abs(amount), 0.0, 4.0); + } else { + let removal = clamp(abs(amount), 0.0, 1.0); + field.x *= 1.0 - removal; + field.y *= 1.0 - removal; + field.w *= 1.0 - removal; + } + } + } + environment[g.x] = field; +} diff --git a/packages/flow-lenia/web/src/shaders/arena_perceive.wgsl b/packages/flow-lenia/web/src/shaders/arena_perceive.wgsl new file mode 100644 index 00000000..ecc3dfd9 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/arena_perceive.wgsl @@ -0,0 +1,82 @@ +struct ArenaUniform { + n: u32, + n2: u32, + channels: u32, + kernels: u32, + channelResponse: vec4, + gateOpen: f32, + step: f32, + stormStart: f32, + stormDuration: f32, + stormCenterRadius: vec4, + attractorCenterRadius: vec4, + attractorMotion: vec4, +} +struct KernelParam { + source: u32, + target_channel: u32, + mean: f32, + width: f32, + weight: f32, + _pad0: f32, + _pad1: f32, + _pad2: f32, +} + +@group(0) @binding(0) var U: ArenaUniform; +@group(0) @binding(1) var response: array>; +@group(0) @binding(2) var params: array; +@group(0) @binding(3) var genomeH: array>; +@group(0) @binding(4) var kernelFields: array>; +@group(0) @binding(5) var affinityOut: array>; +@group(0) @binding(6) var environment: array>; + +fn gene_h(cell: u32, kernel: u32) -> f32 { + return genomeH[cell * 3u + kernel / 4u][kernel % 4u]; +} + +fn torus_delta(value: f32) -> f32 { return value - round(value); } + +fn external_affinity(cell: u32) -> f32 { + let row = cell / U.n; + let column = cell % U.n; + let point = (vec2(f32(row), f32(column)) + 0.5) / f32(U.n); + let authored = environment[cell]; + var value = authored.x + authored.y + (1.0 - U.gateOpen) * authored.z; + if (U.stormDuration > 0.0 && U.step >= U.stormStart && U.step < U.stormStart + U.stormDuration) { + let phase = (U.step - U.stormStart + 0.5) / U.stormDuration; + let envelope = sin(3.141592653589793 * phase); + let delta = vec2(torus_delta(point.x - U.stormCenterRadius.x), torus_delta(point.y - U.stormCenterRadius.y)); + let radius = max(U.stormCenterRadius.z, 1e-6); + let radial = exp(-0.5 * dot(delta, delta) / (radius * radius)); + let angular = cos(3.0 * atan2(delta.y, delta.x) + 6.283185307179586 * envelope); + value += U.stormCenterRadius.w * envelope * radial * angular; + } + if (U.attractorCenterRadius.w != 0.0) { + let angle = U.attractorMotion.z + U.attractorMotion.y * U.step; + let center = U.attractorCenterRadius.xy + U.attractorMotion.x * vec2(cos(angle), sin(angle)); + let delta = vec2(torus_delta(point.x - center.x), torus_delta(point.y - center.y)); + let radius = max(U.attractorCenterRadius.z, 1e-6); + value += U.attractorCenterRadius.w * exp(-0.5 * dot(delta, delta) / (radius * radius)); + } + return value; +} + +@compute @workgroup_size(128) +fn perceive_arena(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n2) { return; } + let inverse_n2 = 1.0 / f32(U.n2); + var affinity = vec4(0.0); + for (var kernel = 0u; kernel < 9u; kernel += 1u) { + let index = kernel * U.n2 + g.x; + let perception = response[index].x * inverse_n2; + let parameter = params[kernel]; + let z = (perception - parameter.mean) / parameter.width; + let growth = 2.0 * exp(-0.5 * z * z) - 1.0; + kernelFields[index] = vec2(perception, growth); + affinity[parameter.target_channel] += gene_h(g.x, kernel) * growth; + } + let environmentValue = external_affinity(g.x); + affinity.xyz += U.channelResponse.xyz * environmentValue; + affinityOut[g.x] = affinity; +} diff --git a/packages/flow-lenia/web/src/shaders/batch_fft.wgsl b/packages/flow-lenia/web/src/shaders/batch_fft.wgsl new file mode 100644 index 00000000..edf4659e --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/batch_fft.wgsl @@ -0,0 +1,47 @@ +// Flow Lenia M0 — batched plane-aware 2D Stockham coordinate surface. +// The numerical butterfly and precision trig are injected verbatim from +// common/common-web/src/fft-wgsl.ts. This file owns only plane/axis addressing. + +struct FftUniform { + n: u32, + planes: u32, + half_n2: u32, + _pad0: u32, +} + +struct FftPass { + axis: u32, + stage: u32, + dir: f32, + _pad0: u32, +} + +@group(0) @binding(0) var U: FftUniform; +@group(0) @binding(1) var cIn: array>; +@group(0) @binding(2) var cOut: array>; +@group(1) @binding(0) var P: FftPass; + +//__COMMON_FFT__ + +fn idx2(x: u32, y: u32) -> u32 { return y * U.n + x; } + +fn coord_of(plane: u32, line_id: u32, e: u32) -> u32 { + let in_plane = select(idx2(line_id, e), idx2(e, line_id), P.axis == 0u); + return plane * U.n * U.n + in_plane; +} + +@compute @workgroup_size(128) +fn fft_pass(@builtin(global_invocation_id) g: vec3) { + let total = U.planes * U.half_n2; + if (g.x >= total) { return; } + let plane = g.x / U.half_n2; + let local = g.x % U.half_n2; + let half_line = U.n / 2u; + let line_id = local / half_line; + let t = local % half_line; + let fb = fft_butterfly(t, P.stage, half_line, P.dir); + let va = cIn[coord_of(plane, line_id, fb.ea)]; + let vb = cmul(fb.w, cIn[coord_of(plane, line_id, fb.eb)]); + cOut[coord_of(plane, line_id, fb.ec)] = va + vb; + cOut[coord_of(plane, line_id, fb.ed)] = va - vb; +} diff --git a/packages/flow-lenia/web/src/shaders/ecosystem_flow.wgsl b/packages/flow-lenia/web/src/shaders/ecosystem_flow.wgsl new file mode 100644 index 00000000..83105109 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/ecosystem_flow.wgsl @@ -0,0 +1,82 @@ +struct FlowUniform { + n: u32, + n2: u32, + channels: u32, + _pad0: u32, + dt: f32, + density_threshold: f32, + density_exponent: f32, + max_displacement: f32, +} +struct EcosystemTransportCell { + mass: vec4, + displacement_x: vec4, + displacement_y: vec4, + growth_0: vec4, + growth_1: vec4, + growth_2: vec4, +} +struct FlowDiagnostic { alpha: vec4, clamp_mask: vec4 } + +@group(0) @binding(0) var U: FlowUniform; +@group(0) @binding(1) var massIn: array>; +@group(0) @binding(2) var affinityIn: array>; +@group(0) @binding(3) var kernelFields: array>; +@group(0) @binding(4) var transportOut: array; +@group(0) @binding(5) var diagnosticOut: array; + +fn wrap(value: i32) -> u32 { return u32((value + i32(U.n)) % i32(U.n)); } +fn index(i: i32, j: i32) -> u32 { return wrap(i) * U.n + wrap(j); } +fn density(cell: u32) -> f32 { return dot(massIn[cell], vec4(1.0, 1.0, 1.0, 0.0)); } + +fn sobel_density(i: i32, j: i32) -> vec2 { + let di = density(index(i + 1, j - 1)) + 2.0 * density(index(i + 1, j)) + density(index(i + 1, j + 1)) + - density(index(i - 1, j - 1)) - 2.0 * density(index(i - 1, j)) - density(index(i - 1, j + 1)); + let dj = density(index(i - 1, j + 1)) + 2.0 * density(index(i, j + 1)) + density(index(i + 1, j + 1)) + - density(index(i - 1, j - 1)) - 2.0 * density(index(i, j - 1)) - density(index(i + 1, j - 1)); + return vec2(di, dj); +} + +fn sobel_affinity(i: i32, j: i32, channel: u32) -> vec2 { + let di = affinityIn[index(i + 1, j - 1)][channel] + 2.0 * affinityIn[index(i + 1, j)][channel] + affinityIn[index(i + 1, j + 1)][channel] + - affinityIn[index(i - 1, j - 1)][channel] - 2.0 * affinityIn[index(i - 1, j)][channel] - affinityIn[index(i - 1, j + 1)][channel]; + let dj = affinityIn[index(i - 1, j + 1)][channel] + 2.0 * affinityIn[index(i, j + 1)][channel] + affinityIn[index(i + 1, j + 1)][channel] + - affinityIn[index(i - 1, j - 1)][channel] - 2.0 * affinityIn[index(i, j - 1)][channel] - affinityIn[index(i + 1, j - 1)][channel]; + return vec2(di, dj); +} + +@compute @workgroup_size(8, 8) +fn compute_ecosystem_flow(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n || g.y >= U.n) { return; } + let i = i32(g.x); + let j = i32(g.y); + let cell = g.x * U.n + g.y; + let mass = massIn[cell]; + let pressure_gradient = sobel_density(i, j); + var dx = vec4(0.0); + var dy = vec4(0.0); + var alpha = vec4(0.0); + var clamped = vec4(0.0); + for (var channel = 0u; channel < 3u; channel += 1u) { + let gate = clamp(pow(mass[channel] / U.density_threshold, U.density_exponent), 0.0, 1.0); + let gradient = sobel_affinity(i, j, channel); + let flow = (1.0 - gate) * gradient - gate * pressure_gradient; + let raw = U.dt * flow; + let bounded = clamp(raw, vec2(-U.max_displacement), vec2(U.max_displacement)); + dx[channel] = bounded.x; + dy[channel] = bounded.y; + alpha[channel] = gate; + clamped[channel] = select(0.0, 1.0, any(bounded != raw)); + } + var growth0 = vec4(0.0); + var growth1 = vec4(0.0); + var growth2 = vec4(0.0); + for (var kernel = 0u; kernel < 9u; kernel += 1u) { + let value = kernelFields[kernel * U.n2 + cell].y; + if (kernel < 4u) { growth0[kernel] = value; } + else if (kernel < 8u) { growth1[kernel - 4u] = value; } + else { growth2[kernel - 8u] = value; } + } + transportOut[cell] = EcosystemTransportCell(mass, dx, dy, growth0, growth1, growth2); + diagnosticOut[cell] = FlowDiagnostic(alpha, clamped); +} diff --git a/packages/flow-lenia/web/src/shaders/ecosystem_mutation.wgsl b/packages/flow-lenia/web/src/shaders/ecosystem_mutation.wgsl new file mode 100644 index 00000000..73929332 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/ecosystem_mutation.wgsl @@ -0,0 +1,46 @@ +struct MutationUniform { n: u32, count: u32, _pad0: u32, _pad1: u32 } + +@group(0) @binding(0) var U: MutationUniform; +@group(0) @binding(1) var records: array>; +@group(0) @binding(2) var genomeH: array>; +@group(0) @binding(3) var genomeQ: array>; +@group(0) @binding(4) var identity: array>; +@group(0) @binding(5) var massIn: array>; +@group(0) @binding(6) var affectedMass: array>; + +fn torus_distance(a: u32, b: u32) -> f32 { + let raw = abs(i32(a) - i32(b)); + return f32(min(raw, i32(U.n) - raw)); +} +fn hash32(input: u32) -> u32 { + var value = input; + value = (value ^ (value >> 16u)) * 0x7feb352du; + value = (value ^ (value >> 15u)) * 0x846ca68bu; + return value ^ (value >> 16u); +} + +@compute @workgroup_size(128) +fn apply_mutation_patches(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n * U.n) { return; } + let row = g.x / U.n; + let column = g.x % U.n; + for (var event = 0u; event < U.count; event += 1u) { + let base = event * 8u; + let header = records[base]; + let child = records[base + 1u]; + let dr = torus_distance(row, header.x); + let dc = torus_distance(column, header.y); + let radius = bitcast(header.z); + if (dr * dr + dc * dc <= radius * radius && identity[g.x].z == header.w) { + for (var bank = 0u; bank < 3u; bank += 1u) { + let gene = g.x * 3u + bank; + genomeH[gene] = clamp(genomeH[gene] + bitcast>(records[base + 2u + bank]), vec4(-2.0), vec4(2.0)); + genomeQ[gene] = clamp(genomeQ[gene] + bitcast>(records[base + 5u + bank]), vec4(-2.0), vec4(2.0)); + } + let parent_hue_key = (hash32(header.w) & 0xffffu) | ((header.w & 7u) << 16u); + identity[g.x] = vec4(child.y, child.z, child.x, identity[g.x].w | 2u | (parent_hue_key << 8u)); + let density = massIn[g.x].x + massIn[g.x].y + massIn[g.x].z; + atomicAdd(&affectedMass[child.w], u32(round(max(density, 0.0) * 65536.0))); + } + } +} diff --git a/packages/flow-lenia/web/src/shaders/ecosystem_perceive.wgsl b/packages/flow-lenia/web/src/shaders/ecosystem_perceive.wgsl new file mode 100644 index 00000000..23550f2c --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/ecosystem_perceive.wgsl @@ -0,0 +1,39 @@ +struct GridUniform { n: u32, n2: u32, channels: u32, kernels: u32 } +struct KernelParam { + source: u32, + target_channel: u32, + mean: f32, + width: f32, + weight: f32, + _pad0: f32, + _pad1: f32, + _pad2: f32, +} + +@group(0) @binding(0) var U: GridUniform; +@group(0) @binding(1) var response: array>; +@group(0) @binding(2) var params: array; +@group(0) @binding(3) var genomeH: array>; +@group(0) @binding(4) var kernelFields: array>; +@group(0) @binding(5) var affinityOut: array>; + +fn gene_h(cell: u32, kernel: u32) -> f32 { + return genomeH[cell * 3u + kernel / 4u][kernel % 4u]; +} + +@compute @workgroup_size(128) +fn perceive_ecosystem(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n2) { return; } + let inverse_n2 = 1.0 / f32(U.n2); + var affinity = vec4(0.0); + for (var kernel = 0u; kernel < 9u; kernel += 1u) { + let index = kernel * U.n2 + g.x; + let perception = response[index].x * inverse_n2; + let parameter = params[kernel]; + let z = (perception - parameter.mean) / parameter.width; + let growth = 2.0 * exp(-0.5 * z * z) - 1.0; + kernelFields[index] = vec2(perception, growth); + affinity[parameter.target_channel] += gene_h(g.x, kernel) * growth; + } + affinityOut[g.x] = affinity; +} diff --git a/packages/flow-lenia/web/src/shaders/organism_events.wgsl b/packages/flow-lenia/web/src/shaders/organism_events.wgsl new file mode 100644 index 00000000..01c1fc93 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/organism_events.wgsl @@ -0,0 +1,112 @@ +struct EventUniform { + n: u32, + count: u32, + max_displacement: f32, + ledger_scale: f32, +} +struct EventRecord { + kind: u32, + channel: u32, + _pad0: vec2, + brush: vec4, + direction: vec4, +} +struct TransportCell { + mass: vec4, + displacement_x: vec4, + displacement_y: vec4, +} + +@group(0) @binding(0) var U: EventUniform; +@group(0) @binding(1) var events: array; +@group(0) @binding(2) var massState: array>; +@group(0) @binding(3) var eventLedger: array>; + +@group(1) @binding(0) var UI: EventUniform; +@group(1) @binding(1) var impulseEvents: array; +@group(1) @binding(2) var transportState: array; + +fn torus_delta(value: f32, center: f32, n: f32) -> f32 { + let raw = value - center; + return raw - round(raw / n) * n; +} + +fn brush_weight(delta: vec2, radius: f32) -> f32 { + let q = dot(delta, delta) / max(radius * radius, 1.0); + let compact = max(0.0, 1.0 - q); + return compact * compact; +} + +@compute @workgroup_size(128) +fn apply_open_events(@builtin(global_invocation_id) g: vec3) { + let n2 = U.n * U.n; + if (g.x >= n2) { return; } + let position = vec2(f32(g.x / U.n) + 0.5, f32(g.x % U.n) + 0.5); + var mass = massState[g.x]; + for (var event_index = 0u; event_index < U.count; event_index += 1u) { + let event = events[event_index]; + if (event.kind != 1u && event.kind != 2u) { continue; } + let delta = vec2( + torus_delta(position.x, event.brush.x, f32(U.n)), + torus_delta(position.y, event.brush.y, f32(U.n)), + ); + let weight = brush_weight(delta, event.brush.z); + if (weight <= 0.0) { continue; } + for (var channel = 0u; channel < 3u; channel += 1u) { + if (event.channel < 3u && event.channel != channel) { continue; } + let before = mass[channel]; + if (event.kind == 1u) { + let mixture = select(1.0, array(0.45, 0.32, 0.23)[channel], event.channel == 3u); + mass[channel] = before + event.brush.w * weight * mixture; + let credited = u32(round(max(0.0, mass[channel] - before) * U.ledger_scale)); + atomicAdd(&eventLedger[0], credited); + } else { + mass[channel] = before * (1.0 - clamp(event.brush.w * weight, 0.0, 0.95)); + let debited = u32(round(max(0.0, before - mass[channel]) * U.ledger_scale)); + atomicAdd(&eventLedger[1], debited); + } + } + } + massState[g.x] = mass; +} + +@compute @workgroup_size(128) +fn apply_closed_impulses(@builtin(global_invocation_id) g: vec3) { + let n2 = UI.n * UI.n; + if (g.x >= n2) { return; } + let position = vec2(f32(g.x / UI.n) + 0.5, f32(g.x % UI.n) + 0.5); + var transport = transportState[g.x]; + for (var event_index = 0u; event_index < UI.count; event_index += 1u) { + let event = impulseEvents[event_index]; + if (event.kind != 3u && event.kind != 4u) { continue; } + let delta = vec2( + torus_delta(position.x, event.brush.x, f32(UI.n)), + torus_delta(position.y, event.brush.y, f32(UI.n)), + ); + let distance = length(delta); + let weight = brush_weight(delta, event.brush.z); + if (weight <= 0.0 || distance < 0.25) { continue; } + let radial = delta / distance; + let polarity = select(-1.0, 1.0, event.direction.z >= 0.0); + var direction = -radial * polarity; + if (event.kind == 4u) { + let drag = event.direction.xy; + direction = select(vec2(-radial.y, radial.x), normalize(drag), length(drag) > 0.01) * polarity; + } + let impulse = event.brush.w * weight * direction; + for (var channel = 0u; channel < 3u; channel += 1u) { + if (event.channel < 3u && event.channel != channel) { continue; } + transport.displacement_x[channel] = clamp( + transport.displacement_x[channel] + impulse.x, + -UI.max_displacement, + UI.max_displacement, + ); + transport.displacement_y[channel] = clamp( + transport.displacement_y[channel] + impulse.y, + -UI.max_displacement, + UI.max_displacement, + ); + } + } + transportState[g.x] = transport; +} diff --git a/packages/flow-lenia/web/src/shaders/organism_flow.wgsl b/packages/flow-lenia/web/src/shaders/organism_flow.wgsl new file mode 100644 index 00000000..313f9707 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/organism_flow.wgsl @@ -0,0 +1,69 @@ +struct FlowUniform { + n: u32, + n2: u32, + channels: u32, + _pad0: u32, + dt: f32, + density_threshold: f32, + density_exponent: f32, + max_displacement: f32, +} +struct TransportCell { + mass: vec4, + displacement_x: vec4, + displacement_y: vec4, +} +struct FlowDiagnostic { alpha: vec4, clamp_mask: vec4 } + +@group(0) @binding(0) var U: FlowUniform; +@group(0) @binding(1) var massIn: array>; +@group(0) @binding(2) var affinityIn: array>; +@group(0) @binding(3) var transportOut: array; +@group(0) @binding(4) var diagnosticOut: array; + +fn wrap(value: i32) -> u32 { return u32((value + i32(U.n)) % i32(U.n)); } +fn index(i: i32, j: i32) -> u32 { return wrap(i) * U.n + wrap(j); } +fn density(cell: u32) -> f32 { return dot(massIn[cell], vec4(1.0, 1.0, 1.0, 0.0)); } + +fn sobel_density(i: i32, j: i32) -> vec2 { + let di = density(index(i + 1, j - 1)) + 2.0 * density(index(i + 1, j)) + density(index(i + 1, j + 1)) + - density(index(i - 1, j - 1)) - 2.0 * density(index(i - 1, j)) - density(index(i - 1, j + 1)); + let dj = density(index(i - 1, j + 1)) + 2.0 * density(index(i, j + 1)) + density(index(i + 1, j + 1)) + - density(index(i - 1, j - 1)) - 2.0 * density(index(i, j - 1)) - density(index(i + 1, j - 1)); + return vec2(di, dj); +} + +fn sobel_affinity(i: i32, j: i32, channel: u32) -> vec2 { + let di = affinityIn[index(i + 1, j - 1)][channel] + 2.0 * affinityIn[index(i + 1, j)][channel] + affinityIn[index(i + 1, j + 1)][channel] + - affinityIn[index(i - 1, j - 1)][channel] - 2.0 * affinityIn[index(i - 1, j)][channel] - affinityIn[index(i - 1, j + 1)][channel]; + let dj = affinityIn[index(i - 1, j + 1)][channel] + 2.0 * affinityIn[index(i, j + 1)][channel] + affinityIn[index(i + 1, j + 1)][channel] + - affinityIn[index(i - 1, j - 1)][channel] - 2.0 * affinityIn[index(i, j - 1)][channel] - affinityIn[index(i + 1, j - 1)][channel]; + return vec2(di, dj); +} + +@compute @workgroup_size(8, 8) +fn compute_flow(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n || g.y >= U.n) { return; } + let i = i32(g.x); + let j = i32(g.y); + let cell = g.x * U.n + g.y; + let mass = massIn[cell]; + let pressure_gradient = sobel_density(i, j); + var dx = vec4(0.0); + var dy = vec4(0.0); + var alpha = vec4(0.0); + var clamped = vec4(0.0); + for (var channel = 0u; channel < 3u; channel += 1u) { + let gate = clamp(pow(mass[channel] / U.density_threshold, U.density_exponent), 0.0, 1.0); + let gradient = sobel_affinity(i, j, channel); + let flow = (1.0 - gate) * gradient - gate * pressure_gradient; + let raw = U.dt * flow; + let bounded = clamp(raw, vec2(-U.max_displacement), vec2(U.max_displacement)); + dx[channel] = bounded.x; + dy[channel] = bounded.y; + alpha[channel] = gate; + clamped[channel] = select(0.0, 1.0, any(bounded != raw)); + } + transportOut[cell] = TransportCell(mass, dx, dy); + diagnosticOut[cell] = FlowDiagnostic(alpha, clamped); +} diff --git a/packages/flow-lenia/web/src/shaders/organism_pack.wgsl b/packages/flow-lenia/web/src/shaders/organism_pack.wgsl new file mode 100644 index 00000000..14355a67 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/organism_pack.wgsl @@ -0,0 +1,14 @@ +struct GridUniform { n: u32, n2: u32, channels: u32, kernels: u32 } + +@group(0) @binding(0) var U: GridUniform; +@group(0) @binding(1) var massIn: array>; +@group(0) @binding(2) var complexOut: array>; + +@compute @workgroup_size(128) +fn pack_mass(@builtin(global_invocation_id) g: vec3) { + let total = U.channels * U.n2; + if (g.x >= total) { return; } + let channel = g.x / U.n2; + let cell = g.x % U.n2; + complexOut[g.x] = vec2(massIn[cell][channel], 0.0); +} diff --git a/packages/flow-lenia/web/src/shaders/organism_perceive.wgsl b/packages/flow-lenia/web/src/shaders/organism_perceive.wgsl new file mode 100644 index 00000000..c8c1cec1 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/organism_perceive.wgsl @@ -0,0 +1,34 @@ +struct GridUniform { n: u32, n2: u32, channels: u32, kernels: u32 } +struct KernelParam { + source: u32, + target_channel: u32, + mean: f32, + width: f32, + weight: f32, + _pad0: f32, + _pad1: f32, + _pad2: f32, +} + +@group(0) @binding(0) var U: GridUniform; +@group(0) @binding(1) var response: array>; +@group(0) @binding(2) var params: array; +@group(0) @binding(3) var kernelFields: array>; +@group(0) @binding(4) var affinityOut: array>; + +@compute @workgroup_size(128) +fn perceive_growth(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n2) { return; } + let inverse_n2 = 1.0 / f32(U.n2); + var affinity = vec4(0.0); + for (var kernel = 0u; kernel < 9u; kernel += 1u) { + let index = kernel * U.n2 + g.x; + let perception = response[index].x * inverse_n2; + let parameter = params[kernel]; + let z = (perception - parameter.mean) / parameter.width; + let growth = 2.0 * exp(-0.5 * z * z) - 1.0; + kernelFields[index] = vec2(perception, growth); + affinity[parameter.target_channel] += parameter.weight * growth; + } + affinityOut[g.x] = affinity; +} diff --git a/packages/flow-lenia/web/src/shaders/organism_spectral.wgsl b/packages/flow-lenia/web/src/shaders/organism_spectral.wgsl new file mode 100644 index 00000000..012a9569 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/organism_spectral.wgsl @@ -0,0 +1,33 @@ +struct GridUniform { n: u32, n2: u32, channels: u32, kernels: u32 } +struct KernelParam { + source: u32, + target_channel: u32, + mean: f32, + width: f32, + weight: f32, + _pad0: f32, + _pad1: f32, + _pad2: f32, +} + +@group(0) @binding(0) var U: GridUniform; +@group(0) @binding(1) var sourceSpectrum: array>; +@group(0) @binding(2) var kernelSpectrum: array>; +@group(0) @binding(3) var params: array; +@group(0) @binding(4) var responseSpectrum: array>; + +fn cmul(a: vec2, b: vec2) -> vec2 { + return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +} + +@compute @workgroup_size(128) +fn expand_spectra(@builtin(global_invocation_id) g: vec3) { + let total = U.kernels * U.n2; + if (g.x >= total) { return; } + let kernel = g.x / U.n2; + let mode = g.x % U.n2; + responseSpectrum[g.x] = cmul( + sourceSpectrum[params[kernel].source * U.n2 + mode], + kernelSpectrum[g.x], + ); +} diff --git a/packages/flow-lenia/web/src/shaders/reintegrate.wgsl b/packages/flow-lenia/web/src/shaders/reintegrate.wgsl new file mode 100644 index 00000000..67b42a9f --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/reintegrate.wgsl @@ -0,0 +1,151 @@ +// Flow Lenia M0 — faithful dd=5 destination gather for a finite square cell. +// No float atomics or source scatter. Candidate order is lexicographically fixed. + +struct GatherUniform { + n: u32, + channels: u32, + dd: u32, + _pad0: u32, + sigma: f32, + _pad1: f32, + _pad2: f32, + _pad3: f32, +} + +struct TransportCell { + mass: vec4, + displacement_x: vec4, + displacement_y: vec4, +} + +struct GenomeCell { + a: vec4, + b: vec4, + c: vec4, +} + +@group(0) @binding(0) var U: GatherUniform; +@group(0) @binding(1) var transportIn: array; +@group(0) @binding(2) var massOut: array>; +@group(0) @binding(3) var hIn: array; +@group(0) @binding(4) var hOut: array; +@group(0) @binding(5) var qIn: array; +@group(0) @binding(6) var qOut: array; +@group(0) @binding(7) var identityIn: array>; +@group(0) @binding(8) var identityOut: array>; + +fn wrap_offset(x: u32, offset: i32) -> u32 { + return u32((i32(x) + offset + i32(U.n)) % i32(U.n)); +} + +fn periodic_delta(destination: u32, source: u32, displacement: f32) -> f32 { + let raw = f32(destination) - (f32(source) + displacement); + return raw - round(raw / f32(U.n)) * f32(U.n); +} + +fn overlap_1d(delta: f32) -> f32 { + return clamp(U.sigma + 0.5 - abs(delta), 0.0, min(1.0, 2.0 * U.sigma)); +} + +fn overlap_weight( + destination_x: u32, + destination_y: u32, + source_x: u32, + source_y: u32, + displacement_x: f32, + displacement_y: f32, +) -> f32 { + let dx = periodic_delta(destination_x, source_x, displacement_x); + let dy = periodic_delta(destination_y, source_y, displacement_y); + return overlap_1d(dx) * overlap_1d(dy) / (4.0 * U.sigma * U.sigma); +} + +fn candidate_mass(destination_x: u32, destination_y: u32) -> vec4 { + var result = vec4(0.0); + for (var oy = -5; oy <= 5; oy += 1) { + let sy = wrap_offset(destination_y, oy); + for (var ox = -5; ox <= 5; ox += 1) { + let sx = wrap_offset(destination_x, ox); + let source_index = sy * U.n + sx; + let source = transportIn[source_index]; + for (var channel = 0u; channel < 3u; channel += 1u) { + let weight = overlap_weight( + destination_x, + destination_y, + sx, + sy, + source.displacement_x[channel], + source.displacement_y[channel], + ); + result[channel] += source.mass[channel] * weight; + } + } + } + return result; +} + +@compute @workgroup_size(8, 8) +fn gather_mass(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n || g.y >= U.n) { return; } + massOut[g.y * U.n + g.x] = candidate_mass(g.x, g.y); +} + +@compute @workgroup_size(8, 8) +fn gather_full(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n || g.y >= U.n) { return; } + let destination_index = g.y * U.n + g.x; + var result = vec4(0.0); + var ha = vec4(0.0); + var hb = vec4(0.0); + var hc = vec4(0.0); + var qa = vec4(0.0); + var qb = vec4(0.0); + var qc = vec4(0.0); + var genome_weight = 0.0; + var best_weight = -1.0; + var chosen_identity = vec4(0u); + + for (var oy = -5; oy <= 5; oy += 1) { + let sy = wrap_offset(g.y, oy); + for (var ox = -5; ox <= 5; ox += 1) { + let sx = wrap_offset(g.x, ox); + let source_index = sy * U.n + sx; + let source = transportIn[source_index]; + var contribution = 0.0; + for (var channel = 0u; channel < 3u; channel += 1u) { + let weight = overlap_weight( + g.x, + g.y, + sx, + sy, + source.displacement_x[channel], + source.displacement_y[channel], + ); + let incoming = source.mass[channel] * weight; + result[channel] += incoming; + contribution += incoming; + } + if (contribution > 0.0) { + let h = hIn[source_index]; + let q = qIn[source_index]; + ha += contribution * h.a; + hb += contribution * h.b; + hc += contribution * h.c; + qa += contribution * q.a; + qb += contribution * q.b; + qc += contribution * q.c; + genome_weight += contribution; + if (contribution > best_weight) { + best_weight = contribution; + chosen_identity = identityIn[source_index]; + } + } + } + } + + let inv_weight = select(0.0, 1.0 / genome_weight, genome_weight > 0.0); + massOut[destination_index] = result; + hOut[destination_index] = GenomeCell(ha * inv_weight, hb * inv_weight, hc * inv_weight); + qOut[destination_index] = GenomeCell(qa * inv_weight, qb * inv_weight, qc * inv_weight); + identityOut[destination_index] = chosen_identity; +} diff --git a/packages/flow-lenia/web/src/shaders/reintegrate_ecosystem.wgsl b/packages/flow-lenia/web/src/shaders/reintegrate_ecosystem.wgsl new file mode 100644 index 00000000..a90ba7a3 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/reintegrate_ecosystem.wgsl @@ -0,0 +1,257 @@ +// Five pipelines are compiled from this module by specializing MIXING_RULE: +// 0 average, 1 whole-genome, 2 gene-wise, 3 best-affinity, 4 negotiation. +override MIXING_RULE: u32 = 1u; + +struct GatherUniform { + n: u32, + channels: u32, + dd: u32, + step: u32, + sigma: f32, + negotiation_beta: f32, + seed: u32, + _pad0: u32, +} +struct EcosystemTransportCell { + mass: vec4, + displacement_x: vec4, + displacement_y: vec4, + growth_0: vec4, + growth_1: vec4, + growth_2: vec4, +} +struct U64 { lo: u32, hi: u32 } + +@group(0) @binding(0) var U: GatherUniform; +@group(0) @binding(1) var transportIn: array; +@group(0) @binding(2) var massOut: array>; +@group(0) @binding(3) var hIn: array>; +@group(0) @binding(4) var hOut: array>; +@group(0) @binding(5) var qIn: array>; +@group(0) @binding(6) var qOut: array>; +@group(0) @binding(7) var identityIn: array>; +@group(0) @binding(8) var identityOut: array>; + +fn wrap_offset(x: u32, offset: i32) -> u32 { return u32((i32(x) + offset + i32(U.n)) % i32(U.n)); } +fn periodic_delta(destination: u32, source: u32, displacement: f32) -> f32 { + let raw = f32(destination) - (f32(source) + displacement); + return raw - round(raw / f32(U.n)) * f32(U.n); +} +fn overlap_1d(delta: f32) -> f32 { + return clamp(U.sigma + 0.5 - abs(delta), 0.0, min(1.0, 2.0 * U.sigma)); +} +fn overlap_weight(di: u32, dj: u32, si: u32, sj: u32, displacement: vec2) -> f32 { + let delta_i = periodic_delta(di, si, displacement.x); + let delta_j = periodic_delta(dj, sj, displacement.y); + return overlap_1d(delta_i) * overlap_1d(delta_j) / (4.0 * U.sigma * U.sigma); +} + +fn mul_hi_u32(a: u32, b: u32) -> u32 { + let a0 = a & 0xffffu; + let a1 = a >> 16u; + let b0 = b & 0xffffu; + let b1 = b >> 16u; + let w0 = a0 * b0; + let t = a1 * b0 + (w0 >> 16u); + var w1 = t & 0xffffu; + let w2 = t >> 16u; + w1 += a0 * b1; + return a1 * b1 + w2 + (w1 >> 16u); +} +fn add64(a: U64, b: U64) -> U64 { + let lo = a.lo + b.lo; + return U64(lo, a.hi + b.hi + select(0u, 1u, lo < a.lo)); +} +fn xor64(a: U64, b: U64) -> U64 { return U64(a.lo ^ b.lo, a.hi ^ b.hi); } +fn shr64(a: U64, shift: u32) -> U64 { + return U64((a.lo >> shift) | (a.hi << (32u - shift)), a.hi >> shift); +} +fn mul64(a: U64, b: U64) -> U64 { + return U64(a.lo * b.lo, mul_hi_u32(a.lo, b.lo) + a.lo * b.hi + a.hi * b.lo); +} +fn splitmix64(input: U64) -> U64 { + var value = add64(input, U64(0x7f4a7c15u, 0x9e3779b9u)); + value = mul64(xor64(value, shr64(value, 30u)), U64(0x1ce4e5b9u, 0xbf58476du)); + value = mul64(xor64(value, shr64(value, 27u)), U64(0x133111ebu, 0x94d049bbu)); + return xor64(value, shr64(value, 31u)); +} +fn counter_hash(destination: u32, candidate: u32, gene: u32, negative_gene: bool) -> U64 { + var value = U64(U.seed, 0u); + value = splitmix64(xor64(value, U64(U.step, 0u))); + value = splitmix64(xor64(value, U64(destination, 0u))); + value = splitmix64(xor64(value, U64(candidate, 0u))); + var gene_value = U64(gene, 0u); + if (negative_gene) { gene_value = U64(0xffffffffu, 0xffffffffu); } + value = splitmix64(xor64(value, gene_value)); + return value; +} +fn unit_float(value: U64) -> f32 { + return clamp((f32(value.hi) + 0.5) * 2.3283064365386963e-10, 1e-7, 0.9999999); +} +fn genome_hash(h: array, q: array) -> vec2 { + var first = 0x811c9dc5u; + var second = 0x9e3779b9u; + for (var gene = 0u; gene < 9u; gene += 1u) { + first = (first ^ bitcast(h[gene])) * 0x01000193u; + second = (second ^ bitcast(q[gene]) ^ gene) * 0x85ebca6bu; + } + return vec2(first, second); +} +fn same_genome(first: u32, candidate: u32) -> bool { + if (any(identityIn[first] != identityIn[candidate])) { return false; } + for (var bank = 0u; bank < 3u; bank += 1u) { + if (any(hIn[first * 3u + bank] != hIn[candidate * 3u + bank])) { return false; } + if (any(qIn[first * 3u + bank] != qIn[candidate * 3u + bank])) { return false; } + } + return true; +} +fn growth_gene(context: EcosystemTransportCell, kernel: u32) -> f32 { + if (kernel < 4u) { return context.growth_0[kernel]; } + if (kernel < 8u) { return context.growth_1[kernel - 4u]; } + return context.growth_2[kernel - 8u]; +} +fn source_gene(buffer: ptr>, read>, source: u32, gene: u32) -> f32 { + return (*buffer)[source * 3u + gene / 4u][gene % 4u]; +} + +@compute @workgroup_size(8, 8) +fn gather_ecosystem(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n || g.y >= U.n) { return; } + let destination = g.x * U.n + g.y; + let context = transportIn[destination]; + var result = vec4(0.0); + var total = 0.0; + var first_source = 0u; + var selected_source = 0u; + var has_source = false; + var all_same = true; + var best_score = -1.0e30; + var negotiation_score = -1.0e30; + var average_h: array; + var average_q: array; + var gene_sources: array; + var gene_has_source: array; + for (var gene = 0u; gene < 9u; gene += 1u) { + average_h[gene] = 0.0; + average_q[gene] = 0.0; + } + for (var gene = 0u; gene < 18u; gene += 1u) { gene_has_source[gene] = false; } + + var candidate = 0u; + for (var oi = -5; oi <= 5; oi += 1) { + let si = wrap_offset(g.x, oi); + for (var oj = -5; oj <= 5; oj += 1) { + let sj = wrap_offset(g.y, oj); + let source_index = si * U.n + sj; + let source = transportIn[source_index]; + var arrivals = vec4(0.0); + for (var channel = 0u; channel < 3u; channel += 1u) { + let weight = overlap_weight(g.x, g.y, si, sj, vec2(source.displacement_x[channel], source.displacement_y[channel])); + arrivals[channel] = source.mass[channel] * weight; + } + result += arrivals; + let incoming = arrivals.x + arrivals.y + arrivals.z; + if (incoming > 0.0) { + total += incoming; + if (!has_source) { + first_source = source_index; + selected_source = source_index; + has_source = true; + } else if (all_same && !same_genome(first_source, source_index)) { + all_same = false; + } + var affinity_score = 0.0; + var negotiation_affinity = 0.0; + for (var gene = 0u; gene < 9u; gene += 1u) { + let h = source_gene(&hIn, source_index, gene); + let q = source_gene(&qIn, source_index, gene); + average_h[gene] += incoming * h; + average_q[gene] += incoming * q; + affinity_score += growth_gene(context, gene) * h; + negotiation_affinity += growth_gene(context, gene) * q; + } + let reservoir_u = unit_float(counter_hash(destination, candidate, 0xffffffffu, true)); + if (reservoir_u < incoming / total) { selected_source = source_index; } + for (var gene = 0u; gene < 18u; gene += 1u) { + let gene_u = unit_float(counter_hash(destination, candidate, gene, false)); + if (!gene_has_source[gene] || gene_u < incoming / total) { + gene_sources[gene] = source_index; + gene_has_source[gene] = true; + } + } + if (affinity_score > best_score) { + best_score = affinity_score; + if (MIXING_RULE == 3u) { selected_source = source_index; } + } + let gumbel_u = unit_float(counter_hash(destination, candidate, 18u, false)); + let score = U.negotiation_beta * incoming * negotiation_affinity - log(-log(gumbel_u)); + if (score > negotiation_score) { + negotiation_score = score; + if (MIXING_RULE == 4u) { selected_source = source_index; } + } + } + candidate += 1u; + } + } + massOut[destination] = result; + if (!has_source || total <= 0.0) { + for (var bank = 0u; bank < 3u; bank += 1u) { + hOut[destination * 3u + bank] = vec4(0.0); + qOut[destination * 3u + bank] = vec4(0.0); + } + identityOut[destination] = vec4(0u); + return; + } + if (all_same) { + selected_source = first_source; + for (var bank = 0u; bank < 3u; bank += 1u) { + hOut[destination * 3u + bank] = hIn[selected_source * 3u + bank]; + qOut[destination * 3u + bank] = qIn[selected_source * 3u + bank]; + } + identityOut[destination] = identityIn[selected_source]; + return; + } + if (MIXING_RULE == 0u) { + var mixed_h: array; + var mixed_q: array; + for (var gene = 0u; gene < 9u; gene += 1u) { + mixed_h[gene] = average_h[gene] / total; + mixed_q[gene] = average_q[gene] / total; + } + hOut[destination * 3u] = vec4(mixed_h[0], mixed_h[1], mixed_h[2], mixed_h[3]); + hOut[destination * 3u + 1u] = vec4(mixed_h[4], mixed_h[5], mixed_h[6], mixed_h[7]); + hOut[destination * 3u + 2u] = vec4(mixed_h[8], 0.0, 0.0, 0.0); + qOut[destination * 3u] = vec4(mixed_q[0], mixed_q[1], mixed_q[2], mixed_q[3]); + qOut[destination * 3u + 1u] = vec4(mixed_q[4], mixed_q[5], mixed_q[6], mixed_q[7]); + qOut[destination * 3u + 2u] = vec4(mixed_q[8], 0.0, 0.0, 0.0); + let fingerprint = genome_hash(mixed_h, mixed_q); + identityOut[destination] = vec4(fingerprint, 0xffffffffu, 1u); + } else if (MIXING_RULE == 2u) { + var mixed_h: array; + var mixed_q: array; + var parent_h: array; + var parent_q: array; + for (var gene = 0u; gene < 9u; gene += 1u) { + let h_parent = gene_sources[gene]; + let q_parent = gene_sources[gene + 9u]; + mixed_h[gene] = source_gene(&hIn, h_parent, gene); + mixed_q[gene] = source_gene(&qIn, q_parent, gene); + parent_h[gene] = bitcast(identityIn[h_parent].x ^ identityIn[h_parent].y); + parent_q[gene] = bitcast(identityIn[q_parent].x ^ identityIn[q_parent].y); + } + hOut[destination * 3u] = vec4(mixed_h[0], mixed_h[1], mixed_h[2], mixed_h[3]); + hOut[destination * 3u + 1u] = vec4(mixed_h[4], mixed_h[5], mixed_h[6], mixed_h[7]); + hOut[destination * 3u + 2u] = vec4(mixed_h[8], 0.0, 0.0, 0.0); + qOut[destination * 3u] = vec4(mixed_q[0], mixed_q[1], mixed_q[2], mixed_q[3]); + qOut[destination * 3u + 1u] = vec4(mixed_q[4], mixed_q[5], mixed_q[6], mixed_q[7]); + qOut[destination * 3u + 2u] = vec4(mixed_q[8], 0.0, 0.0, 0.0); + let fingerprint = genome_hash(parent_h, parent_q); + identityOut[destination] = vec4(fingerprint, 0xffffffffu, 1u); + } else { + for (var bank = 0u; bank < 3u; bank += 1u) { + hOut[destination * 3u + bank] = hIn[selected_source * 3u + bank]; + qOut[destination * 3u + bank] = qIn[selected_source * 3u + bank]; + } + identityOut[destination] = identityIn[selected_source]; + } +} diff --git a/packages/flow-lenia/web/src/shaders/reintegrate_organism.wgsl b/packages/flow-lenia/web/src/shaders/reintegrate_organism.wgsl new file mode 100644 index 00000000..9c24bc36 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/reintegrate_organism.wgsl @@ -0,0 +1,51 @@ +struct GatherUniform { + n: u32, + channels: u32, + dd: u32, + _pad0: u32, + sigma: f32, + _pad1: f32, + _pad2: f32, + _pad3: f32, +} +struct TransportCell { + mass: vec4, + displacement_x: vec4, + displacement_y: vec4, +} + +@group(0) @binding(0) var U: GatherUniform; +@group(0) @binding(1) var transportIn: array; +@group(0) @binding(2) var massOut: array>; + +fn wrap_offset(x: u32, offset: i32) -> u32 { return u32((i32(x) + offset + i32(U.n)) % i32(U.n)); } +fn periodic_delta(destination: u32, source: u32, displacement: f32) -> f32 { + let raw = f32(destination) - (f32(source) + displacement); + return raw - round(raw / f32(U.n)) * f32(U.n); +} +fn overlap_1d(delta: f32) -> f32 { + return clamp(U.sigma + 0.5 - abs(delta), 0.0, min(1.0, 2.0 * U.sigma)); +} +fn overlap_weight(di: u32, dj: u32, si: u32, sj: u32, displacement: vec2) -> f32 { + let delta_i = periodic_delta(di, si, displacement.x); + let delta_j = periodic_delta(dj, sj, displacement.y); + return overlap_1d(delta_i) * overlap_1d(delta_j) / (4.0 * U.sigma * U.sigma); +} + +@compute @workgroup_size(8, 8) +fn gather_organism(@builtin(global_invocation_id) g: vec3) { + if (g.x >= U.n || g.y >= U.n) { return; } + var result = vec4(0.0); + for (var oi = -5; oi <= 5; oi += 1) { + let si = wrap_offset(g.x, oi); + for (var oj = -5; oj <= 5; oj += 1) { + let sj = wrap_offset(g.y, oj); + let source = transportIn[si * U.n + sj]; + for (var channel = 0u; channel < 3u; channel += 1u) { + let weight = overlap_weight(g.x, g.y, si, sj, vec2(source.displacement_x[channel], source.displacement_y[channel])); + result[channel] += source.mass[channel] * weight; + } + } + } + massOut[g.x * U.n + g.y] = result; +} diff --git a/packages/flow-lenia/web/src/shaders/render_arena.wgsl b/packages/flow-lenia/web/src/shaders/render_arena.wgsl new file mode 100644 index 00000000..b46725cf --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/render_arena.wgsl @@ -0,0 +1,70 @@ +struct RenderUniform { + n: u32, mode: u32, flags: u32, pane_index: u32, + canvas_width: f32, canvas_height: f32, origin_x: f32, origin_y: f32, + pane_width: f32, pane_height: f32, exposure: f32, flow_scale: f32, + center_row: f32, center_column: f32, zoom: f32, time: f32, + inspect_row: f32, inspect_column: f32, brush_radius: f32, _pad0: f32, + gate_open: f32, step: f32, storm_start: f32, storm_duration: f32, + storm_center_radius: vec4, + attractor_center_radius: vec4, + attractor_motion: vec4, +} +struct EcosystemTransportCell { + mass: vec4, displacement_x: vec4, displacement_y: vec4, + growth_0: vec4, growth_1: vec4, growth_2: vec4, +} +struct FlowDiagnostic { alpha: vec4, clamp_mask: vec4 } + +@group(0) @binding(0) var U: RenderUniform; +@group(0) @binding(1) var massIn: array>; +@group(0) @binding(2) var hIn: array>; +@group(0) @binding(3) var qIn: array>; +@group(0) @binding(4) var identityIn: array>; +@group(0) @binding(5) var transportIn: array; +@group(0) @binding(6) var diagnosticIn: array; +@group(0) @binding(7) var environmentIn: array>; +@group(0) @binding(8) var regionIn: array; + +struct VertexOut { @builtin(position) position: vec4 } +@vertex fn vertex_main(@builtin(vertex_index) index: u32) -> VertexOut { + var points = array, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + var output: VertexOut; output.position = vec4(points[index], 0.0, 1.0); return output; +} + +fn wrap_index(value: i32) -> u32 { return u32((value + i32(U.n)) % i32(U.n)); } +fn cell_index(row: f32, column: f32) -> u32 { return wrap_index(i32(floor(row))) * U.n + wrap_index(i32(floor(column))); } +fn density(cell: u32) -> f32 { return massIn[cell].x + massIn[cell].y + massIn[cell].z; } +fn hash32(input: u32) -> u32 { var value = input; value = (value ^ (value >> 16u)) * 0x7feb352du; value = (value ^ (value >> 15u)) * 0x846ca68bu; return value ^ (value >> 16u); } +fn hsv(h: f32, s: f32, v: f32) -> vec3 { let p = abs(fract(h + vec3(0.0, 0.6666667, 0.3333333)) * 6.0 - 3.0); return v * mix(vec3(1.0), clamp(p - 1.0, vec3(0.0), vec3(1.0)), s); } +fn lineage_color(identity: vec4) -> vec3 { + if (identity.z == 0xffffffffu) { return mix(vec3(0.36, 0.39, 0.46), vec3(0.82, 0.77, 0.90), f32(hash32(identity.x ^ identity.y) & 1023u) / 1023.0); } + var hue = fract(f32(hash32(identity.z) & 0xffffu) / 65535.0 + 0.61803398875 * f32(identity.z & 7u)); + if ((identity.w & 2u) != 0u) { let parent_key = identity.w >> 8u; let parent_hue = fract(f32(parent_key & 0xffffu) / 65535.0 + 0.61803398875 * f32((parent_key >> 16u) & 7u)); hue = fract(parent_hue + (f32(hash32(identity.z) & 1023u) / 1023.0 - 0.5) * 0.05); } + return hsv(hue, 0.72, 1.0); +} +fn phenotype_color(cell: u32) -> vec3 { let h0 = hIn[cell * 3u]; let h1 = hIn[cell * 3u + 1u]; let h2 = hIn[cell * 3u + 2u]; let q0 = qIn[cell * 3u]; let angle = 2.2 * h0.x - 1.7 * h1.x + 1.3 * h2.x + 0.6 * q0.x; return hsv(fract(0.5 + angle * 0.15915494), clamp(0.48 + 0.18 * abs(h0.z - h1.z) + 0.12 * abs(q0.y), 0.38, 0.88), 1.0); } +fn torus_delta(value: f32) -> f32 { return value - round(value); } +fn external_affinity(cell: u32) -> f32 { + let row = cell / U.n; let column = cell % U.n; let point = (vec2(f32(row), f32(column)) + 0.5) / f32(U.n); let authored = environmentIn[cell]; + var value = authored.x + authored.y + (1.0 - U.gate_open) * authored.z; + if (U.storm_duration > 0.0 && U.step >= U.storm_start && U.step < U.storm_start + U.storm_duration) { let phase = (U.step - U.storm_start + 0.5) / U.storm_duration; let envelope = sin(3.141592653589793 * phase); let delta = vec2(torus_delta(point.x - U.storm_center_radius.x), torus_delta(point.y - U.storm_center_radius.y)); let radius = max(U.storm_center_radius.z, 1e-6); value += U.storm_center_radius.w * envelope * exp(-0.5 * dot(delta, delta) / (radius * radius)) * cos(3.0 * atan2(delta.y, delta.x) + 6.283185307179586 * envelope); } + if (U.attractor_center_radius.w != 0.0) { let angle = U.attractor_motion.z + U.attractor_motion.y * U.step; let center = U.attractor_center_radius.xy + U.attractor_motion.x * vec2(cos(angle), sin(angle)); let delta = vec2(torus_delta(point.x - center.x), torus_delta(point.y - center.y)); let radius = max(U.attractor_center_radius.z, 1e-6); value += U.attractor_center_radius.w * exp(-0.5 * dot(delta, delta) / (radius * radius)); } + return value; +} + +@fragment fn fragment_main(@builtin(position) p: vec4) -> @location(0) vec4 { + let screen = vec2((p.x - U.origin_x) / U.pane_width - 0.5, (p.y - U.origin_y) / U.pane_height - 0.5); + let world = vec2(U.center_row + screen.y * f32(U.n) / U.zoom, U.center_column + screen.x * f32(U.n) / U.zoom); + let cell = cell_index(world.x, world.y); let rho = density(cell); let glow = 1.0 - exp(-U.exposure * rho); let identity = identityIn[cell]; let transport = transportIn[cell]; let environment = external_affinity(cell); let wall = clamp(environmentIn[cell].w + (1.0 - U.gate_open) * max(-environmentIn[cell].z, 0.0), 0.0, 1.0); + var color = vec3(0.0); + if (U.mode == 1u) { color = phenotype_color(cell) * glow; } + else if (U.mode == 2u) { let channels = massIn[cell].xyz / max(rho, 1e-7); color = (mat3x3(vec3(0.15, 0.92, 0.77), vec3(0.82, 0.31, 1.0), vec3(1.0, 0.67, 0.16)) * channels) * glow; } + else if (U.mode == 3u) { let displacement = vec2(transport.displacement_x.x, transport.displacement_y.x); let magnitude = clamp(length(displacement) * U.flow_scale, 0.0, 1.0); let direction = 0.5 + 0.5 * normalize(displacement + vec2(1e-7)); color = mix(vec3(0.015, 0.025, 0.045), vec3(direction.x, 0.32 + 0.68 * magnitude, direction.y), magnitude) + glow * 0.12; } + else if (U.mode == 4u) { let signed = tanh(environment * 0.8); let attractive = vec3(0.08, 0.88, 0.79); let repulsive = vec3(1.0, 0.42, 0.11); color = mix(vec3(0.018, 0.028, 0.045), select(repulsive, attractive, signed >= 0.0), abs(signed)); let region = regionIn[cell]; color += 0.035 * hsv(f32(region) * 0.27, 0.65, 1.0); color = mix(color, vec3(1.0, 0.58, 0.13), 0.52 * wall); } + else { color = lineage_color(identity) * glow; if ((identity.w & 1u) != 0u) { color *= 0.72 + 0.28 * (0.5 + 0.5 * sin((world.x + world.y) * 2.1)); } if ((identity.w & 2u) != 0u) { color += (0.10 + 0.08 * sin(U.time * 5.0 + f32(identity.z & 255u))) * vec3(0.95, 0.88, 0.50) * glow; } } + if (U.mode != 4u) { color = mix(color, vec3(0.98, 0.43, 0.08), 0.30 * wall); color += max(environment, 0.0) * 0.035 * vec3(0.18, 0.95, 0.82); } + let gradient = vec2(density(cell_index(world.x + 1.0, world.y)) - density(cell_index(world.x - 1.0, world.y)), density(cell_index(world.x, world.y + 1.0)) - density(cell_index(world.x, world.y - 1.0))); let relief = clamp(0.5 + dot(normalize(vec3(-gradient * 0.7, 1.0)), normalize(vec3(-0.45, -0.32, 0.84))), 0.0, 1.0); color *= 0.76 + 0.34 * relief; + let clamp_hot = max(diagnosticIn[cell].clamp_mask.x, max(diagnosticIn[cell].clamp_mask.y, diagnosticIn[cell].clamp_mask.z)); color += clamp_hot * vec3(0.8, 0.06, 0.01); + if ((U.flags & 1u) != 0u) { let raw = world - vec2(U.inspect_row, U.inspect_column); let delta = raw - round(raw / f32(U.n)) * f32(U.n); let pixel_world = f32(U.n) / max(U.pane_width * U.zoom, 1.0); let ring = select(0.0, 1.0, abs(length(delta) - U.brush_radius) < 1.5 * pixel_world); color = mix(color, vec3(0.98, 0.91, 0.56), ring * 0.9); } + return vec4(color, 1.0); +} diff --git a/packages/flow-lenia/web/src/shaders/render_ecosystem.wgsl b/packages/flow-lenia/web/src/shaders/render_ecosystem.wgsl new file mode 100644 index 00000000..a216e921 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/render_ecosystem.wgsl @@ -0,0 +1,133 @@ +struct RenderUniform { + n: u32, + mode: u32, + flags: u32, + pane_index: u32, + canvas_width: f32, + canvas_height: f32, + origin_x: f32, + origin_y: f32, + pane_width: f32, + pane_height: f32, + exposure: f32, + flow_scale: f32, + center_row: f32, + center_column: f32, + zoom: f32, + time: f32, + inspect_row: f32, + inspect_column: f32, + brush_radius: f32, + _pad0: f32, +} +struct EcosystemTransportCell { + mass: vec4, + displacement_x: vec4, + displacement_y: vec4, + growth_0: vec4, + growth_1: vec4, + growth_2: vec4, +} +struct FlowDiagnostic { alpha: vec4, clamp_mask: vec4 } + +@group(0) @binding(0) var U: RenderUniform; +@group(0) @binding(1) var massIn: array>; +@group(0) @binding(2) var hIn: array>; +@group(0) @binding(3) var qIn: array>; +@group(0) @binding(4) var identityIn: array>; +@group(0) @binding(5) var transportIn: array; +@group(0) @binding(6) var diagnosticIn: array; + +struct VertexOut { @builtin(position) position: vec4 } +@vertex fn vertex_main(@builtin(vertex_index) index: u32) -> VertexOut { + var points = array, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + var output: VertexOut; + output.position = vec4(points[index], 0.0, 1.0); + return output; +} + +fn wrap_index(value: i32) -> u32 { return u32((value + i32(U.n)) % i32(U.n)); } +fn cell_index(row: f32, column: f32) -> u32 { return wrap_index(i32(floor(row))) * U.n + wrap_index(i32(floor(column))); } +fn density(cell: u32) -> f32 { return massIn[cell].x + massIn[cell].y + massIn[cell].z; } +fn hash32(input: u32) -> u32 { + var value = input; + value = (value ^ (value >> 16u)) * 0x7feb352du; + value = (value ^ (value >> 15u)) * 0x846ca68bu; + return value ^ (value >> 16u); +} +fn hsv(h: f32, s: f32, v: f32) -> vec3 { + let p = abs(fract(h + vec3(0.0, 0.6666667, 0.3333333)) * 6.0 - 3.0); + return v * mix(vec3(1.0), clamp(p - 1.0, vec3(0.0), vec3(1.0)), s); +} +fn lineage_color(identity: vec4) -> vec3 { + if (identity.z == 0xffffffffu) { + let mixed = f32(hash32(identity.x ^ identity.y) & 1023u) / 1023.0; + return mix(vec3(0.36, 0.39, 0.46), vec3(0.82, 0.77, 0.90), mixed); + } + var hue = fract(f32(hash32(identity.z) & 0xffffu) / 65535.0 + 0.61803398875 * f32(identity.z & 7u)); + if ((identity.w & 2u) != 0u) { + let parent_key = identity.w >> 8u; + let parent_hue = fract(f32(parent_key & 0xffffu) / 65535.0 + 0.61803398875 * f32((parent_key >> 16u) & 7u)); + let child_offset = (f32(hash32(identity.z) & 1023u) / 1023.0 - 0.5) * 0.05; + hue = fract(parent_hue + child_offset); + } + return hsv(hue, 0.72, 1.0); +} +fn phenotype_color(cell: u32) -> vec3 { + let h0 = hIn[cell * 3u]; + let h1 = hIn[cell * 3u + 1u]; + let h2 = hIn[cell * 3u + 2u]; + let q0 = qIn[cell * 3u]; + let angle = 2.2 * h0.x - 1.7 * h1.x + 1.3 * h2.x + 0.6 * q0.x; + let hue = fract(0.5 + angle * 0.15915494); + let saturation = clamp(0.48 + 0.18 * abs(h0.z - h1.z) + 0.12 * abs(q0.y), 0.38, 0.88); + return hsv(hue, saturation, 1.0); +} + +@fragment fn fragment_main(@builtin(position) p: vec4) -> @location(0) vec4 { + let screen = vec2((p.x - U.origin_x) / U.pane_width - 0.5, (p.y - U.origin_y) / U.pane_height - 0.5); + let world = vec2( + U.center_row + screen.y * f32(U.n) / U.zoom, + U.center_column + screen.x * f32(U.n) / U.zoom, + ); + let cell = cell_index(world.x, world.y); + let rho = density(cell); + let glow = 1.0 - exp(-U.exposure * rho); + let identity = identityIn[cell]; + let transport = transportIn[cell]; + var color = vec3(0.0); + if (U.mode == 1u) { + color = phenotype_color(cell) * glow; + } else if (U.mode == 2u) { + let channels = massIn[cell].xyz / max(rho, 1e-7); + color = (mat3x3(vec3(0.15, 0.92, 0.77), vec3(0.82, 0.31, 1.0), vec3(1.0, 0.67, 0.16)) * channels) * glow; + } else if (U.mode == 3u) { + let displacement = vec2(transport.displacement_x.x, transport.displacement_y.x); + let magnitude = clamp(length(displacement) * U.flow_scale, 0.0, 1.0); + let direction = 0.5 + 0.5 * normalize(displacement + vec2(1e-7)); + color = mix(vec3(0.015, 0.025, 0.045), vec3(direction.x, 0.32 + 0.68 * magnitude, direction.y), magnitude) + glow * 0.12; + } else { + color = lineage_color(identity) * glow; + if ((identity.w & 1u) != 0u) { + let hatch = 0.5 + 0.5 * sin((world.x + world.y) * 2.1); + color *= 0.72 + 0.28 * hatch; + } + if ((identity.w & 2u) != 0u) { + let pulse = 0.10 + 0.08 * sin(U.time * 5.0 + f32(identity.z & 255u)); + color += pulse * vec3(0.95, 0.88, 0.50) * glow; + } + } + let gradient = vec2(density(cell_index(world.x + 1.0, world.y)) - density(cell_index(world.x - 1.0, world.y)), density(cell_index(world.x, world.y + 1.0)) - density(cell_index(world.x, world.y - 1.0))); + let relief = clamp(0.5 + dot(normalize(vec3(-gradient * 0.7, 1.0)), normalize(vec3(-0.45, -0.32, 0.84))), 0.0, 1.0); + color *= 0.76 + 0.34 * relief; + let clamp_hot = max(diagnosticIn[cell].clamp_mask.x, max(diagnosticIn[cell].clamp_mask.y, diagnosticIn[cell].clamp_mask.z)); + color += clamp_hot * vec3(0.8, 0.06, 0.01); + if ((U.flags & 1u) != 0u) { + let raw = world - vec2(U.inspect_row, U.inspect_column); + let delta = raw - round(raw / f32(U.n)) * f32(U.n); + let pixel_world = f32(U.n) / max(U.pane_width * U.zoom, 1.0); + let ring = select(0.0, 1.0, abs(length(delta) - U.brush_radius) < 1.5 * pixel_world); + color = mix(color, vec3(0.98, 0.91, 0.56), ring * 0.9); + } + return vec4(color, 1.0); +} diff --git a/packages/flow-lenia/web/src/shaders/render_organism.wgsl b/packages/flow-lenia/web/src/shaders/render_organism.wgsl new file mode 100644 index 00000000..b00efdc4 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/render_organism.wgsl @@ -0,0 +1,147 @@ +struct RenderUniform { + n: u32, + mode: u32, + channel: u32, + flags: u32, + width: f32, + height: f32, + exposure: f32, + flow_scale: f32, + center_row: f32, + center_column: f32, + zoom: f32, + time: f32, + inspect_row: f32, + inspect_column: f32, + brush_radius: f32, + compare_enabled: f32, +} +struct TransportCell { mass: vec4, displacement_x: vec4, displacement_y: vec4 } +struct FlowDiagnostic { alpha: vec4, clamp_mask: vec4 } + +@group(0) @binding(0) var U: RenderUniform; +@group(0) @binding(1) var massA: array>; +@group(0) @binding(2) var affinityA: array>; +@group(0) @binding(3) var transportA: array; +@group(0) @binding(4) var diagnosticA: array; +@group(0) @binding(5) var massB: array>; +@group(0) @binding(6) var affinityB: array>; +@group(0) @binding(7) var transportB: array; +@group(0) @binding(8) var diagnosticB: array; + +struct VertexOut { @builtin(position) position: vec4 } +@vertex fn vertex_main(@builtin(vertex_index) index: u32) -> VertexOut { + var points = array, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + var output: VertexOut; + output.position = vec4(points[index], 0.0, 1.0); + return output; +} + +fn wrap_coordinate(value: f32) -> f32 { return value - floor(value / f32(U.n)) * f32(U.n); } +fn wrap_index(value: i32) -> u32 { return u32((value + i32(U.n)) % i32(U.n)); } +fn cell_index(row: f32, column: f32) -> u32 { + return wrap_index(i32(floor(row))) * U.n + wrap_index(i32(floor(column))); +} +fn mass_at(cell: u32, second: bool) -> vec4 { return select(massA[cell], massB[cell], second); } +fn affinity_at(cell: u32, second: bool) -> vec4 { return select(affinityA[cell], affinityB[cell], second); } +fn transport_at(cell: u32, second: bool) -> TransportCell { + if (second) { return transportB[cell]; } + return transportA[cell]; +} +fn diagnostic_at(cell: u32, second: bool) -> FlowDiagnostic { + if (second) { return diagnosticB[cell]; } + return diagnosticA[cell]; +} +fn density_at(row: f32, column: f32, second: bool) -> f32 { + return dot(mass_at(cell_index(row, column), second), vec4(1.0, 1.0, 1.0, 0.0)); +} +fn torus_delta(value: f32, center: f32) -> f32 { + let raw = value - center; + return raw - round(raw / f32(U.n)) * f32(U.n); +} +fn diverging(value: f32) -> vec3 { + let t = clamp(abs(value) / 1.5, 0.0, 1.0); + let neutral = vec3(0.045, 0.065, 0.085); + return select(mix(neutral, vec3(0.12, 0.72, 0.94), t), mix(neutral, vec3(1.0, 0.31, 0.24), t), value >= 0.0); +} + +@fragment fn fragment_main(@builtin(position) p: vec4) -> @location(0) vec4 { + let comparison = U.compare_enabled > 0.5; + let second = comparison && p.x >= U.width * 0.5; + let pane_width = select(U.width, U.width * 0.5, comparison); + let pane_x = select(p.x, p.x - select(0.0, U.width * 0.5, second), comparison); + let screen = vec2(pane_x / pane_width - 0.5, p.y / U.height - 0.5); + let vertical_scale = select(1.0, 0.5, comparison); + let world = vec2( + wrap_coordinate(U.center_row + screen.y * f32(U.n) * vertical_scale / U.zoom), + wrap_coordinate(U.center_column + screen.x * f32(U.n) / U.zoom), + ); + let cell = cell_index(world.x, world.y); + let mass = mass_at(cell, second); + let affinity = affinity_at(cell, second); + let transport = transport_at(cell, second); + let flow_diag = diagnostic_at(cell, second); + let rho = mass.x + mass.y + mass.z; + let displacement = vec2(transport.displacement_x[U.channel], transport.displacement_y[U.channel]); + var color = vec3(0.0); + if (U.mode == 1u) { + color = 1.0 - exp(-U.exposure * mass.xyz); + } else if (U.mode == 2u) { + color = diverging(affinity[U.channel]); + } else if (U.mode == 3u) { + let magnitude = clamp(length(displacement) * U.flow_scale, 0.0, 1.0); + let direction = 0.5 + 0.5 * normalize(displacement + vec2(1e-7)); + color = mix(vec3(0.02, 0.03, 0.045), vec3(direction.x, 0.28 + 0.72 * magnitude, direction.y), magnitude) + 0.10 * min(rho, 1.0); + } else if (U.mode == 4u) { + let alpha = flow_diag.alpha[U.channel]; + let body = 1.0 - exp(-U.exposure * rho); + let gradient = vec2( + density_at(world.x + 1.0, world.y, second) - density_at(world.x - 1.0, world.y, second), + density_at(world.x, world.y + 1.0, second) - density_at(world.x, world.y - 1.0, second), + ); + let pressure = clamp(length(gradient) * alpha * 0.32, 0.0, 1.0); + color = body * vec3(0.08, 0.25, 0.29) + alpha * vec3(0.92, 0.34, 0.08) + pressure * vec3(0.85, 0.18, 0.08); + } else if (U.mode == 5u) { + let flux = clamp(dot(mass.xyz, vec3(length(vec2(transport.displacement_x.x, transport.displacement_y.x)), length(vec2(transport.displacement_x.y, transport.displacement_y.y)), length(vec2(transport.displacement_x.z, transport.displacement_y.z)))) * 1.5, 0.0, 1.0); + let hot = max(flow_diag.clamp_mask.x, max(flow_diag.clamp_mask.y, flow_diag.clamp_mask.z)); + color = mix(vec3(0.015, 0.03, 0.055), vec3(0.20, 0.82, 0.93), flux) + hot * vec3(1.0, 0.12, 0.04); + } else { + let channels = mass.xyz / max(rho, 1e-6); + let palette = mat3x3(vec3(0.19, 0.93, 0.74), vec3(0.83, 0.28, 1.0), vec3(1.0, 0.67, 0.18)); + let hue = palette * channels; + let glow = 1.0 - exp(-U.exposure * rho); + color = hue * glow + vec3(0.03, 0.07, 0.08) * glow * glow; + } + + if ((U.flags & 1u) != 0u) { + let spacing = 0.25; + let phase = abs(fract((affinity[U.channel] + 1.5) / spacing) - 0.5) * spacing; + let contour = 1.0 - smoothstep(0.008, 0.022, phase); + color += contour * vec3(0.25, 0.32, 0.34); + } + if ((U.flags & 2u) != 0u) { + let spacing = max(5.0, 18.0 / U.zoom); + let glyph_center = floor(world / spacing + vec2(0.5)) * spacing; + let glyph_transport = transport_at(cell_index(glyph_center.x, glyph_center.y), second); + let glyph_vector = vec2(glyph_transport.displacement_x[U.channel], glyph_transport.displacement_y[U.channel]); + let magnitude = length(glyph_vector); + let direction = glyph_vector / max(magnitude, 1e-6); + let relative = vec2(torus_delta(world.x, glyph_center.x), torus_delta(world.y, glyph_center.y)); + let along = dot(relative, direction); + let across = abs(relative.x * direction.y - relative.y * direction.x); + let length_world = min(spacing * 0.38, 1.5 + magnitude * 7.0); + let shaft = select(0.0, 1.0, abs(along) < length_world && across < 0.42 / U.zoom && magnitude > 0.01); + let head = select(0.0, 1.0, abs(along - length_world) + across * 0.8 < 1.4 / U.zoom && magnitude > 0.01); + color = mix(color, vec3(0.82, 0.98, 0.92), 0.78 * max(shaft, head)); + } + if ((U.flags & 4u) != 0u) { + let inspect_delta = vec2(torus_delta(world.x, U.inspect_row), torus_delta(world.y, U.inspect_column)); + let inspect_distance = length(inspect_delta); + let pixel_world = f32(U.n) / max(pane_width * U.zoom, 1.0); + let ring = select(0.0, 1.0, abs(inspect_distance - U.brush_radius) < 1.4 * pixel_world); + let cross = select(0.0, 1.0, inspect_distance < 2.5 * pixel_world); + color = mix(color, vec3(0.96, 0.91, 0.56), 0.85 * max(ring, cross)); + } + if (comparison && abs(p.x - U.width * 0.5) < 1.5) { color = vec3(0.72, 0.88, 0.83); } + return vec4(color, 1.0); +} diff --git a/packages/flow-lenia/web/src/shaders/render_trails.wgsl b/packages/flow-lenia/web/src/shaders/render_trails.wgsl new file mode 100644 index 00000000..36d29346 --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/render_trails.wgsl @@ -0,0 +1,25 @@ +struct TrailUniform { persistence: f32, _pad0: f32, _pad1: f32, _pad2: f32 } +@group(0) @binding(0) var U: TrailUniform; +@group(0) @binding(1) var linearSampler: sampler; +@group(0) @binding(2) var currentFrame: texture_2d; +@group(0) @binding(3) var historyFrame: texture_2d; + +struct VertexOut { @builtin(position) position: vec4, @location(0) uv: vec2 } +@vertex fn vertex_main(@builtin(vertex_index) index: u32) -> VertexOut { + var points = array, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + var output: VertexOut; + output.position = vec4(points[index], 0.0, 1.0); + output.uv = 0.5 * (points[index] + vec2(1.0)); + output.uv.y = 1.0 - output.uv.y; + return output; +} + +@fragment fn accumulate(@location(0) uv: vec2) -> @location(0) vec4 { + let current = textureSample(currentFrame, linearSampler, uv).rgb; + let history = textureSample(historyFrame, linearSampler, uv).rgb * U.persistence; + return vec4(max(current, history), 1.0); +} + +@fragment fn display(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(textureSample(currentFrame, linearSampler, uv).rgb, 1.0); +} diff --git a/packages/flow-lenia/web/src/shaders/spectral_expand.wgsl b/packages/flow-lenia/web/src/shaders/spectral_expand.wgsl new file mode 100644 index 00000000..2e0bec7f --- /dev/null +++ b/packages/flow-lenia/web/src/shaders/spectral_expand.wgsl @@ -0,0 +1,30 @@ +// M0 spectrum fan-out: C source planes -> K kernel-response planes. + +struct ExpandUniform { + n2: u32, + channels: u32, + kernels: u32, + _pad0: u32, +} + +@group(0) @binding(0) var U: ExpandUniform; +@group(0) @binding(1) var sourceSpectrum: array>; +@group(0) @binding(2) var kernelSpectrum: array>; +@group(0) @binding(3) var responseSpectrum: array>; + +fn cmul_expand(a: vec2, b: vec2) -> vec2 { + return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +} + +@compute @workgroup_size(128) +fn spectral_expand(@builtin(global_invocation_id) g: vec3) { + let total = U.kernels * U.n2; + if (g.x >= total) { return; } + let kernel = g.x / U.n2; + let mode = g.x % U.n2; + let source = kernel % U.channels; + responseSpectrum[g.x] = cmul_expand( + sourceSpectrum[source * U.n2 + mode], + kernelSpectrum[g.x], + ); +} diff --git a/packages/flow-lenia/web/src/style.css b/packages/flow-lenia/web/src/style.css new file mode 100644 index 00000000..d82dcb26 --- /dev/null +++ b/packages/flow-lenia/web/src/style.css @@ -0,0 +1,110 @@ +html, body { margin: 0; min-height: 100%; background: #05080c; } +#organism-lab { position: relative; inset: auto; width: min(94vmin, 940px); height: min(94vmin, 940px); overflow: hidden; background: #05090e; border: 1px solid rgb(94 215 188 / 12%); border-radius: 10px; box-shadow: 0 20px 80px rgb(0 0 0 / 45%); } +#view { display: block; width: 100%; height: 100%; image-rendering: auto; touch-action: none; cursor: crosshair; outline: none; } +#view:focus-visible { box-shadow: inset 0 0 0 2px #67d8bd; } +#view[data-tool="inspect"] { cursor: cell; } +#view[data-tool="mutate"] { cursor: copy; } +#view[data-tool="attract"], #view[data-tool="repel"], #view[data-tool="wall"], #view[data-tool="erase"] { cursor: crosshair; } +.fl-lab-switch { position: absolute; z-index: 8; top: 14px; right: 14px; padding: 7px 10px; border: 1px solid rgb(94 215 188 / 32%); border-radius: 999px; color: #8ce2cc; background: rgb(4 10 15 / 78%); text-decoration: none; font: 10px/1.3 ui-monospace, monospace; letter-spacing: .06em; text-transform: uppercase; backdrop-filter: blur(8px); } +.fl-lab-switch:hover, .fl-lab-switch:focus-visible { color: #06110f; background: #67d8bd; outline: 2px solid #f1d373; outline-offset: 2px; } +.fl-hud { position: absolute; z-index: 2; left: 18px; bottom: 16px; padding: 9px 12px; color: #b9d4d2; background: rgb(4 10 15 / 72%); border: 1px solid rgb(94 215 188 / 22%); border-radius: 8px; backdrop-filter: blur(8px); font: 11px/1.45 ui-monospace, monospace; pointer-events: none; } +.fl-hud p { margin: 0 0 2px; } +.fl-hud #ledger { color: #8dd9b8; } +.fl-hud #perf { color: #829ca4; } +.fl-hud #compare-label { margin-top: 5px; padding-top: 4px; border-top: 1px solid rgb(94 215 188 / 18%); color: #f1c47d; } +.fl-kicker { color: #5ed7bc; font: 10px/1.4 ui-monospace, monospace; letter-spacing: .13em; text-transform: uppercase; } +.fl-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; } +.fl-controls button { padding: 7px 8px; border: 1px solid #29444a; border-radius: 6px; color: #b9d4d2; background: #0a151b; cursor: pointer; } +.fl-controls button[aria-pressed="true"] { color: #07110f; background: #67d8bd; border-color: #67d8bd; } +.fl-controls button:focus-visible, .fl-experiment-card:focus-visible, .fl-channel-row button:focus-visible, .fl-prove-button:focus-visible { outline: 2px solid #f1d373; outline-offset: 2px; } +.fl-controls-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.fl-tool-controls { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.fl-tool-controls button:last-child { grid-column: span 2; } +.fl-prove-button { width: 100%; padding: 8px; border: 1px solid #40636a; border-radius: 6px; color: #d5e9e7; background: #102229; cursor: pointer; } +.fl-prove-button + .fl-prove-button { margin-top: 6px; } +.fl-prove-button.fl-secondary { color: #8da8b5; background: #0a151b; } +.fl-range { display: grid; grid-template-columns: 94px 1fr 38px; gap: 7px; align-items: center; margin: 7px 0; color: #82959c; font-size: 11px; } +.fl-range input { width: 100%; accent-color: #67d8bd; } +.fl-range output { color: #c8dedb; text-align: right; font-variant-numeric: tabular-nums; } +.fl-select-row { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; color: #82959c; } +.fl-select-row select, .fl-kernel-label select { padding: 4px 6px; border: 1px solid #29444a; border-radius: 5px; color: #c8dedb; background: #071118; font: inherit; } +.fl-channel-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-top: 7px; } +.fl-channel-row button { padding: 5px; border: 1px solid #29444a; border-radius: 5px; color: #97aaa9; background: #091319; cursor: pointer; } +.fl-channel-row button[aria-pressed="true"] { color: #07110f; background: #f0b965; border-color: #f0b965; } + +.fl-experiment-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; } +.fl-experiment-card { min-height: 52px; padding: 7px; border: 1px solid #263e44; border-radius: 7px; color: #c8d9d7; background: linear-gradient(145deg, #0b171d, #081116); text-align: left; cursor: pointer; font: 600 10.5px/1.25 ui-monospace, monospace; } +.fl-experiment-card small { display: block; margin-top: 4px; color: #71888e; font: 9.5px/1.25 ui-monospace, monospace; } +.fl-experiment-card[aria-pressed="true"] { color: #07110f; background: #67d8bd; border-color: #67d8bd; } +.fl-experiment-card[aria-pressed="true"] small { color: #17433c; } +.fl-experiment-note { position: absolute; z-index: 3; top: 15px; left: 60%; width: min(390px, 44%); padding: 10px 13px; transform: translateX(-50%); color: #9fb6b5; background: rgb(4 10 15 / 68%); border: 1px solid rgb(94 215 188 / 18%); border-radius: 8px; backdrop-filter: blur(7px); pointer-events: none; font: 10.5px/1.4 ui-monospace, monospace; } +.fl-experiment-note b, .fl-experiment-note span, .fl-experiment-note small { display: block; } +.fl-experiment-note:empty { display: none; } +.fl-experiment-note b { color: #70ddc3; text-transform: uppercase; letter-spacing: .08em; } +.fl-experiment-note small { margin-top: 4px; color: #718b91; } + +.fl-inspector { position: absolute; z-index: 4; top: 14px; left: 14px; width: 240px; padding: 10px; color: #9eb3b5; background: rgb(4 10 15 / 76%); border: 1px solid rgb(94 215 188 / 20%); border-radius: 8px; backdrop-filter: blur(8px); font: 10px/1.35 ui-monospace, monospace; pointer-events: auto; } +.fl-inspector-head { display: flex; justify-content: space-between; color: #63d7bb; letter-spacing: .11em; } +.fl-inspector-head b { color: #6e898b; font-weight: 400; letter-spacing: 0; } +.fl-inspect-values { margin: 7px 0; padding: 5px 0; border-block: 1px solid rgb(94 215 188 / 14%); } +.fl-inspect-row { display: flex; justify-content: space-between; gap: 8px; } +.fl-inspect-row span { color: #70888e; } +.fl-inspect-row output { color: #c0d4d2; text-align: right; font-variant-numeric: tabular-nums; } +.fl-kernel-label { display: flex; justify-content: space-between; align-items: center; color: #70888e; } +.fl-kernel-label select { max-width: 152px; padding: 2px 4px; font-size: 9px; } +.fl-inspector canvas { display: block; width: 240px; height: 108px; margin-top: 5px; border: 1px solid rgb(94 215 188 / 10%); border-radius: 4px; } +.fl-inspect-note { margin: 6px 0 0; color: #61777d; } +.fl-ecosystem-inspector { width: 268px; } +.fl-ecosystem-inspector .fl-inspect-row output { max-width: 170px; overflow: hidden; text-overflow: ellipsis; } + +.fl-arena-tools button { min-width: 0; } +.fl-arena-metrics { position: absolute; z-index: 4; right: 14px; bottom: 14px; width: 268px; padding: 10px; color: #8ea8a8; background: rgb(4 10 15 / 76%); border: 1px solid rgb(94 215 188 / 20%); border-radius: 8px; backdrop-filter: blur(8px); font: 9.5px/1.35 ui-monospace, monospace; pointer-events: none; } +.fl-arena-metric-head { margin-bottom: 6px; color: #63d7bb; letter-spacing: .11em; } +.fl-region-bars > div { display: grid; grid-template-columns: 22px 1fr 48px; gap: 6px; align-items: center; margin: 3px 0; } +.fl-region-bars i { height: 5px; border-radius: 3px; background: linear-gradient(90deg, #67d8bd calc(var(--share) * 100%), #173038 0); } +.fl-region-bars b { color: #bfd4d1; font-weight: 400; text-align: right; } +.fl-arena-metrics > canvas { display: block; width: 100%; height: 70px; margin: 7px 0; border-block: 1px solid rgb(94 215 188 / 12%); } +.fl-lineage-graph { padding-top: 5px; border-top: 1px solid rgb(94 215 188 / 13%); } +.fl-lineage-nodes { display: flex; flex-wrap: wrap; gap: 4px; } +.fl-lineage-node { display: flex; gap: 5px; padding: 3px 5px; border: 1px solid #233a40; border-radius: 4px; opacity: calc(.35 + .65 * var(--mass)); } +.fl-lineage-node.is-active { color: #bce8dc; border-color: #3a746b; } +.fl-lineage-node b { color: #758e91; font-weight: 400; } +.fl-lineage-edges { margin: 6px 0 0; padding: 0; list-style: none; color: #8fa5a5; } +.fl-lineage-edges li { display: flex; justify-content: space-between; } +.fl-lineage-edges span { color: #5f757a; } + +.fl-touch-tools { position: absolute; z-index: 20; width: 168px; height: 168px; transform: translate(-50%, -50%); border: 1px solid #4dbba5; border-radius: 50%; background: rgb(5 13 18 / 92%); box-shadow: 0 8px 36px rgb(0 0 0 / 55%); } +.fl-touch-tools button { position: absolute; width: 66px; padding: 7px 3px; border: 1px solid #345e5d; border-radius: 999px; color: #d0e5e2; background: #102329; font: 10px ui-monospace, monospace; } +.fl-touch-tools button:nth-child(1) { left: 51px; top: 8px; } +.fl-touch-tools button:nth-child(2) { right: 3px; top: 58px; } +.fl-touch-tools button:nth-child(3) { right: 20px; bottom: 10px; } +.fl-touch-tools button:nth-child(4) { left: 20px; bottom: 10px; } +.fl-touch-tools button:nth-child(5) { left: 3px; top: 58px; } +.fl-sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } + +.fl-probe { box-sizing: border-box; width: min(90vmin, 760px); min-height: min(80vmin, 620px); margin: auto; padding: clamp(24px, 5vw, 56px); color: #c8d8e7; background: radial-gradient(circle at 20% 15%, rgb(49 160 146 / 18%), transparent 35%), linear-gradient(145deg, #0b1219, #071017 62%, #0b1517); border: 1px solid #1d343a; border-radius: 18px; box-shadow: 0 20px 80px rgb(0 0 0 / 35%); font: 14px/1.55 system-ui, sans-serif; } +.fl-probe h1 { margin: 4px 0 10px; color: #ecfbf6; font-size: clamp(28px, 5vw, 52px); } +.fl-table { width: 100%; margin-top: 24px; border-collapse: collapse; font: 12px/1.45 ui-monospace, monospace; } +.fl-table th, .fl-table td { padding: 8px 7px; border-bottom: 1px solid #183039; text-align: right; } +.fl-table th:first-child, .fl-table td:first-child { text-align: left; } +.fl-table th { color: #77cdbb; font-weight: 600; } +.fl-pass { color: #7fe0a2; } +.fl-warn { color: #e3bd71; } +.fl-note { margin-top: 22px; color: #8da8b5; } + +@media (max-width: 720px) { + #organism-lab { width: 100vw; height: 100svh; border: 0; border-radius: 0; } + .fl-hud { left: 10px; bottom: 10px; } + .fl-experiment-note { top: 8px; width: calc(100% - 92px); max-width: 320px; } + .fl-experiment-note small { display: none; } + .fl-inspector { top: auto; right: 8px; bottom: 82px; left: auto; width: 202px; } + .fl-inspector canvas, .fl-inspect-note, .fl-kernel-label { display: none; } + .fl-inspect-values { margin-bottom: 0; } + .bps { max-height: min(52svh, 460px); } + .fl-arena-stage .fl-inspector { display: none; } + .fl-arena-metrics { right: 8px; bottom: 76px; width: min(230px, calc(100vw - 16px)); } + .fl-arena-metrics > canvas, .fl-lineage-edges { display: none; } +} + +@media (pointer: fine) { .fl-touch-tools { display: none; } } +@media (prefers-reduced-motion: reduce) { #view { scroll-behavior: auto; } } diff --git a/packages/flow-lenia/web/tsconfig.json b/packages/flow-lenia/web/tsconfig.json new file mode 100644 index 00000000..cfcfcf94 --- /dev/null +++ b/packages/flow-lenia/web/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["@webgpu/types", "vite/client"], + "strict": true, + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "isolatedModules": true + }, + "include": ["src", "../../../../common/common-web/src"] +} diff --git a/packages/flow-lenia/web/vite.config.ts b/packages/flow-lenia/web/vite.config.ts new file mode 100644 index 00000000..e1e53632 --- /dev/null +++ b/packages/flow-lenia/web/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + base: "./", + build: { + outDir: "dist", + target: "es2022", + sourcemap: true, + }, +}); diff --git a/tools/productization/web-deploy/pipeline.py b/tools/productization/web-deploy/pipeline.py index 81c2438a..5f77c00c 100644 --- a/tools/productization/web-deploy/pipeline.py +++ b/tools/productization/web-deploy/pipeline.py @@ -144,6 +144,12 @@ # _gate_lbm_multiphase; spec-ref § 6.2 — the analytic-gates conjunction # is the sim's moat claim, so it is CI-held). "lbm-multiphase": "new_canonical", + # Flow Lenia release lab: the default full affinity/pressure/finite-square + # Organism capture is replayed twice byte-exactly and its closed mass, + # non-negativity, finite-state, and displacement-clamp invariants are held + # by verify.py. The deeper M2–M6 CPU/GPU, inheritance, environment, reload, + # and render-integrity artifacts remain package-local release gates. + "flow-lenia": "new_canonical", } WEB_DIR = {s: f"packages/{s}/web" for s in GATE_KIND} diff --git a/tools/productization/web-deploy/smoke/test_pipeline.py b/tools/productization/web-deploy/smoke/test_pipeline.py index 35d6f41c..511890ff 100644 --- a/tools/productization/web-deploy/smoke/test_pipeline.py +++ b/tools/productization/web-deploy/smoke/test_pipeline.py @@ -52,6 +52,7 @@ "schrodinger-smoke", "sph-water", "sph-multiphase", + "flow-lenia", } diff --git a/tools/productization/web-deploy/verify.py b/tools/productization/web-deploy/verify.py index ec3ea44d..d7f1919c 100644 --- a/tools/productization/web-deploy/verify.py +++ b/tools/productization/web-deploy/verify.py @@ -312,6 +312,7 @@ "sph-water": "particle-fluids", "mpm-multimaterial": "hybrid-pg", "pic-flip": "particle-fluids", + "flow-lenia": "continuous-ca", } GATE_KIND = { @@ -328,6 +329,7 @@ "sph-multiphase": "new_canonical", "mpm-multimaterial": "new_canonical", "pic-flip": "new_canonical", + "flow-lenia": "new_canonical", } @@ -3015,6 +3017,89 @@ def _gate_lbm_multiphase(bundles: list[dict]) -> VerifyResult: ) +def _gate_flow_lenia(bundles: list[dict]) -> VerifyResult: + """Flow Lenia release gate for the public default Organism Lab. + + The package's committed M2–M6 artifacts hold the f64/f32, localized + inheritance, environment, import/export and render-integrity claims. The + deployment gate independently drives the shipped default twice and holds + the properties visible in its canonical browser capture: exact same-adapter + replay, closed finite non-negative matter, an explicit zero open-system + ledger, and bounded Reintegration displacement/clamping. + """ + steps0 = _bundle_steps(bundles[0]) + if len(steps0) != 1 or int(steps0[0].get("step", -1)) != 32: + return VerifyResult( + sim="flow-lenia", + kind="new_canonical", + passed=False, + run_twice_identical=False, + detail={"error": "default capture must contain exactly step 32"}, + ) + mass0 = _field(steps0[0], "mass") + twice = False + if len(bundles) > 1: + steps1 = _bundle_steps(bundles[1]) + twice = bool( + len(steps1) == 1 + and int(steps1[0].get("step", -1)) == 32 + and np.array_equal(mass0, _field(steps1[0], "mass")) + ) + manifest = bundles[0].get("manifest", {}) + sim = manifest.get("sim", {}) + params = manifest.get("config", {}).get("params", {}) + diagnostics = steps0[0].get("diagnostics", {}) + finite = bool(np.isfinite(mass0).all()) + non_negative = bool((mass0 >= 0).all()) + measured_sum = float(np.asarray(mass0, dtype=np.float64).sum()) + declared_sum = float(diagnostics.get("total_mass", np.nan)) + sum_residual = abs(measured_sum - declared_sum) / max(abs(declared_sum), 1e-30) + drift = float(diagnostics.get("mass_relative_drift", np.inf)) + ledger = float(diagnostics.get("mass_ledger_error", np.inf)) + ledger_relative = ledger / max(abs(declared_sum), 1e-30) + clamp = float(diagnostics.get("clamp_fraction", np.inf)) + displacement = float(diagnostics.get("max_displacement", np.inf)) + schema_ok = bool( + sim.get("name") == "flow-lenia" + and sim.get("variant") == "flow-lenia-ecosystem-v1" + and params.get("channels") == 3 + and params.get("kernels") == 9 + and list(mass0.shape)[0] == 3 + ) + invariants = bool( + finite + and non_negative + and int(diagnostics.get("non_finite", -1)) == 0 + and int(diagnostics.get("negative", -1)) == 0 + and drift <= 8e-5 + and ledger_relative <= 8e-5 + and sum_residual <= 2e-7 + and clamp <= 0.12 + and displacement <= 0.8 + and float(diagnostics.get("open_added", np.inf)) == 0 + and float(diagnostics.get("open_removed", np.inf)) == 0 + ) + return VerifyResult( + sim="flow-lenia", + kind="new_canonical", + passed=bool(schema_ok and twice and invariants), + run_twice_identical=twice, + detail={ + "model_schema": schema_ok, + "run_twice_mass_byte_exact": twice, + "finite": finite, + "non_negative": non_negative, + "mass_relative_drift": drift, + "mass_ledger_error": ledger, + "mass_ledger_relative_error": ledger_relative, + "capture_sum_relative_residual": sum_residual, + "clamp_fraction": clamp, + "max_displacement": displacement, + "note": "Public default full model. M2–M6 package artifacts separately hold f64/f32, localized ecosystem, Arena, reload, performance, and integrity gates.", + }, + ) + + _GATES = { "reaction-diffusion-2d": _gate_rd2d, "neural-ca": _gate_neural_ca, @@ -3036,6 +3121,7 @@ def _gate_lbm_multiphase(bundles: list[dict]) -> VerifyResult: "phase-field-fracture": _gate_phase_field_fracture, "fdtd-optics": _gate_fdtd_optics, "lbm-multiphase": _gate_lbm_multiphase, + "flow-lenia": _gate_flow_lenia, } # Opt-in observable/structural BROWSER gates, activated per-sim ONLY via diff --git a/tools/productization/web-deploy/web/pages/assets/flow-lenia.png b/tools/productization/web-deploy/web/pages/assets/flow-lenia.png new file mode 100644 index 00000000..a650ad09 Binary files /dev/null and b/tools/productization/web-deploy/web/pages/assets/flow-lenia.png differ diff --git a/tools/productization/web-deploy/web/pages/assets/flow-lenia.webm b/tools/productization/web-deploy/web/pages/assets/flow-lenia.webm new file mode 100644 index 00000000..6862f544 Binary files /dev/null and b/tools/productization/web-deploy/web/pages/assets/flow-lenia.webm differ diff --git a/tools/productization/web-deploy/web/pages/assets/make-loops.mjs b/tools/productization/web-deploy/web/pages/assets/make-loops.mjs index aeba017d..9aa67743 100644 --- a/tools/productization/web-deploy/web/pages/assets/make-loops.mjs +++ b/tools/productization/web-deploy/web/pages/assets/make-loops.mjs @@ -152,6 +152,9 @@ const SIMS = { // drops. Start past the boot so the first frame already has droplets // (cold-start black-flash lesson). "lbm-multiphase": { start: 40, shots: 280, gap: 2, fps: 30, px: 512, crf: 46, query: "?preset=droplet-rain", hide: [".lm-explain", ".lm-verify"] }, + // Flow Lenia maze: motion is the mass-conservative flow responding to a + // slowly orbiting affinity beacon. The loop wraps with a declared cut. + "flow-lenia": { start: 40, shots: 280, gap: 2, fps: 30, px: 512, crf: 48, query: "?arena=1&preset=maze-navigation&grid=256", hide: [".fl-hud", ".fl-experiment-note", ".fl-inspector", ".fl-arena-metrics", ".fl-lab-switch"] }, }; const MIME = { diff --git a/tools/productization/web-deploy/web/pages/assets/make-posters.mjs b/tools/productization/web-deploy/web/pages/assets/make-posters.mjs index 1e1a05ef..f98b5240 100644 --- a/tools/productization/web-deploy/web/pages/assets/make-posters.mjs +++ b/tools/productization/web-deploy/web/pages/assets/make-posters.mjs @@ -126,6 +126,10 @@ const SIMS = { // over the wetting floor; by frame ~340 several have coalesced and the // film has formed on the floor (the multiphase money shot). Hide EXPLAIN/PROVE bars. "lbm-multiphase": { frames: 340, px: 512, query: "?preset=droplet-rain", hide: [".lm-explain", ".lm-verify"] }, + // Flow Lenia release: the authored maze in Arena Lab, after the orbiting + // affinity beacon has pulled lineages into the corridor network. Hide lab + // chrome so the signed field, soft walls, and localized matter own the card. + "flow-lenia": { frames: 180, px: 512, query: "?arena=1&preset=maze-navigation&grid=256", hide: [".fl-hud", ".fl-experiment-note", ".fl-inspector", ".fl-arena-metrics", ".fl-lab-switch"] }, }; // Experiment overrides: POSTER_FRAMES= POSTER_SUFFIX=-x node make-posters.mjs const FRAMES_OVERRIDE = process.env.POSTER_FRAMES ? Number(process.env.POSTER_FRAMES) : null; diff --git a/tools/productization/web-deploy/web/pages/check-links.mjs b/tools/productization/web-deploy/web/pages/check-links.mjs index bd580dd4..0b12901b 100644 --- a/tools/productization/web-deploy/web/pages/check-links.mjs +++ b/tools/productization/web-deploy/web/pages/check-links.mjs @@ -30,6 +30,7 @@ const SIMS = [ "curl-noise", "eulerian-smoke", "mpm-multimaterial", "physarum", "pic-flip", "schrodinger-smoke", "sph-water", "sph-multiphase", "heat-equation", "signal-workbench", "phase-field-fracture", "fdtd-optics", "lbm-multiphase", + "flow-lenia", ]; // The confirmed GitHub Pages base (P-8) — observed live (HTTP 200) in the diff --git a/tools/productization/web-deploy/web/pages/index.html b/tools/productization/web-deploy/web/pages/index.html index 584b098d..1020ecf2 100644 --- a/tools/productization/web-deploy/web/pages/index.html +++ b/tools/productization/web-deploy/web/pages/index.html @@ -422,9 +422,29 @@

    Smoke & flow

    -

    Heat, patterns & fracture

    -

    Continuum fields — diffusing, patterning, and breaking.

    +

    Life, heat, patterns & fracture

    +

    Conservative artificial life and continuum fields — flowing, diffusing, patterning, and breaking.