From 2207550a8e041c5dbd120bc305ac48feddbdafff Mon Sep 17 00:00:00 2001 From: StevenFAU Date: Sat, 18 Jul 2026 17:55:12 -0400 Subject: [PATCH] feat(flow-lenia): add verified ecosystem arena --- .../lenia/implementation-plan.md | 288 +++++ .../lenia/m0-webgpu-benchmark.md | 148 +++ .../lenia/m2-organism-validation.md | 91 ++ .../lenia/m3-visual-laboratory-validation.md | 106 ++ .../m4-localized-ecosystem-validation.md | 154 +++ .../lenia/m5-m6-arena-release-validation.md | 140 ++ .../continuous-ca/lenia/spec-web-ecosystem.md | 968 ++++++++++++++ packages/flow-lenia/README.md | 71 ++ packages/flow-lenia/flow_lenia/__init__.py | 37 + .../flow-lenia/flow_lenia/ecosystem_config.py | 228 ++++ .../flow_lenia/ecosystem_reference.py | 819 ++++++++++++ .../flow-lenia/scripts/generate_m2_fixture.py | 114 ++ .../flow-lenia/scripts/generate_m4_fixture.py | 155 +++ .../fixtures/flow-lenia-ecosystem-v1.json | 70 + .../flow-lenia/tests/test_ecosystem_golden.py | 101 ++ .../tests/test_ecosystem_properties.py | 196 +++ .../tests/test_ecosystem_reference.py | 261 ++++ .../tests/test_m2_browser_fixture.py | 79 ++ .../web/artifacts/m0-browser-benchmark.json | 165 +++ .../web/artifacts/m2-browser-gates.json | 159 +++ .../web/artifacts/m3-browser-gates.json | 308 +++++ .../web/artifacts/m4-browser-gates.json | 467 +++++++ .../artifacts/m6-browser-release-gates.json | 328 +++++ .../artifacts/m6-canonical-capture-index.json | 37 + packages/flow-lenia/web/index.html | 41 + packages/flow-lenia/web/package-lock.json | 1127 +++++++++++++++++ packages/flow-lenia/web/package.json | 27 + packages/flow-lenia/web/public/favicon.svg | 5 + packages/flow-lenia/web/scripts/check-m0.mjs | 55 + packages/flow-lenia/web/scripts/check-m2.mjs | 57 + packages/flow-lenia/web/scripts/check-m3.mjs | 51 + packages/flow-lenia/web/scripts/check-m4.mjs | 60 + packages/flow-lenia/web/scripts/check-m6.mjs | 33 + .../web/scripts/run-browser-benchmark.mjs | 95 ++ .../web/scripts/run-browser-gates.mjs | 84 ++ .../web/scripts/run-browser-m3-gates.mjs | 85 ++ .../web/scripts/run-browser-m4-gates.mjs | 92 ++ .../web/scripts/run-browser-m6-gates.mjs | 59 + packages/flow-lenia/web/src/app.ts | 745 +++++++++++ packages/flow-lenia/web/src/arena-app.ts | 187 +++ packages/flow-lenia/web/src/ecosystem-app.ts | 305 +++++ .../web/src/experiments/arena-cards.ts | 147 +++ .../flow-lenia/web/src/experiments/cards.ts | 207 +++ .../web/src/experiments/ecosystem-cards.ts | 169 +++ packages/flow-lenia/web/src/fft-batch.ts | 132 ++ packages/flow-lenia/web/src/inventory.ts | 136 ++ packages/flow-lenia/web/src/m0-app.ts | 318 +++++ packages/flow-lenia/web/src/main.ts | 12 + packages/flow-lenia/web/src/model/arena.ts | 137 ++ packages/flow-lenia/web/src/model/config.ts | 90 ++ .../web/src/model/ecosystem-solver.ts | 888 +++++++++++++ packages/flow-lenia/web/src/model/events.ts | 45 + .../flow-lenia/web/src/model/experiment-io.ts | 170 +++ packages/flow-lenia/web/src/model/kernels.ts | 43 + packages/flow-lenia/web/src/model/seed.ts | 60 + .../web/src/model/shader-provenance.ts | 14 + packages/flow-lenia/web/src/model/solver.ts | 608 +++++++++ packages/flow-lenia/web/src/probe.ts | 544 ++++++++ packages/flow-lenia/web/src/prove-m3.ts | 152 +++ packages/flow-lenia/web/src/prove-m4.ts | 287 +++++ packages/flow-lenia/web/src/prove-m6.ts | 161 +++ packages/flow-lenia/web/src/prove.ts | 252 ++++ .../web/src/prove/ecosystem-fixture.json | 338 +++++ .../web/src/prove/organism-fixture.json | 204 +++ .../web/src/render/arena-renderer.ts | 76 ++ .../web/src/render/ecosystem-renderer.ts | 105 ++ .../flow-lenia/web/src/render/overlays.ts | 161 +++ .../flow-lenia/web/src/render/renderer.ts | 304 +++++ .../web/src/shaders/arena_events.wgsl | 48 + .../web/src/shaders/arena_perceive.wgsl | 82 ++ .../flow-lenia/web/src/shaders/batch_fft.wgsl | 47 + .../web/src/shaders/ecosystem_flow.wgsl | 82 ++ .../web/src/shaders/ecosystem_mutation.wgsl | 46 + .../web/src/shaders/ecosystem_perceive.wgsl | 39 + .../web/src/shaders/organism_events.wgsl | 112 ++ .../web/src/shaders/organism_flow.wgsl | 69 + .../web/src/shaders/organism_pack.wgsl | 14 + .../web/src/shaders/organism_perceive.wgsl | 34 + .../web/src/shaders/organism_spectral.wgsl | 33 + .../web/src/shaders/reintegrate.wgsl | 151 +++ .../src/shaders/reintegrate_ecosystem.wgsl | 257 ++++ .../web/src/shaders/reintegrate_organism.wgsl | 51 + .../web/src/shaders/render_arena.wgsl | 70 + .../web/src/shaders/render_ecosystem.wgsl | 133 ++ .../web/src/shaders/render_organism.wgsl | 147 +++ .../web/src/shaders/render_trails.wgsl | 25 + .../web/src/shaders/spectral_expand.wgsl | 30 + packages/flow-lenia/web/src/style.css | 110 ++ packages/flow-lenia/web/tsconfig.json | 19 + packages/flow-lenia/web/vite.config.ts | 10 + tools/productization/web-deploy/pipeline.py | 6 + .../web-deploy/smoke/test_pipeline.py | 1 + tools/productization/web-deploy/verify.py | 86 ++ .../web/pages/assets/flow-lenia.png | Bin 0 -> 48933 bytes .../web/pages/assets/flow-lenia.webm | Bin 0 -> 405926 bytes .../web/pages/assets/make-loops.mjs | 3 + .../web/pages/assets/make-posters.mjs | 4 + .../web-deploy/web/pages/check-links.mjs | 1 + .../web-deploy/web/pages/index.html | 24 +- 99 files changed, 16090 insertions(+), 2 deletions(-) create mode 100644 docs/sim-specs/continuous-ca/lenia/implementation-plan.md create mode 100644 docs/sim-specs/continuous-ca/lenia/m0-webgpu-benchmark.md create mode 100644 docs/sim-specs/continuous-ca/lenia/m2-organism-validation.md create mode 100644 docs/sim-specs/continuous-ca/lenia/m3-visual-laboratory-validation.md create mode 100644 docs/sim-specs/continuous-ca/lenia/m4-localized-ecosystem-validation.md create mode 100644 docs/sim-specs/continuous-ca/lenia/m5-m6-arena-release-validation.md create mode 100644 docs/sim-specs/continuous-ca/lenia/spec-web-ecosystem.md create mode 100644 packages/flow-lenia/flow_lenia/ecosystem_config.py create mode 100644 packages/flow-lenia/flow_lenia/ecosystem_reference.py create mode 100644 packages/flow-lenia/scripts/generate_m2_fixture.py create mode 100644 packages/flow-lenia/scripts/generate_m4_fixture.py create mode 100644 packages/flow-lenia/tests/fixtures/flow-lenia-ecosystem-v1.json create mode 100644 packages/flow-lenia/tests/test_ecosystem_golden.py create mode 100644 packages/flow-lenia/tests/test_ecosystem_properties.py create mode 100644 packages/flow-lenia/tests/test_ecosystem_reference.py create mode 100644 packages/flow-lenia/tests/test_m2_browser_fixture.py create mode 100644 packages/flow-lenia/web/artifacts/m0-browser-benchmark.json create mode 100644 packages/flow-lenia/web/artifacts/m2-browser-gates.json create mode 100644 packages/flow-lenia/web/artifacts/m3-browser-gates.json create mode 100644 packages/flow-lenia/web/artifacts/m4-browser-gates.json create mode 100644 packages/flow-lenia/web/artifacts/m6-browser-release-gates.json create mode 100644 packages/flow-lenia/web/artifacts/m6-canonical-capture-index.json create mode 100644 packages/flow-lenia/web/index.html create mode 100644 packages/flow-lenia/web/package-lock.json create mode 100644 packages/flow-lenia/web/package.json create mode 100644 packages/flow-lenia/web/public/favicon.svg create mode 100644 packages/flow-lenia/web/scripts/check-m0.mjs create mode 100644 packages/flow-lenia/web/scripts/check-m2.mjs create mode 100644 packages/flow-lenia/web/scripts/check-m3.mjs create mode 100644 packages/flow-lenia/web/scripts/check-m4.mjs create mode 100644 packages/flow-lenia/web/scripts/check-m6.mjs create mode 100644 packages/flow-lenia/web/scripts/run-browser-benchmark.mjs create mode 100644 packages/flow-lenia/web/scripts/run-browser-gates.mjs create mode 100644 packages/flow-lenia/web/scripts/run-browser-m3-gates.mjs create mode 100644 packages/flow-lenia/web/scripts/run-browser-m4-gates.mjs create mode 100644 packages/flow-lenia/web/scripts/run-browser-m6-gates.mjs create mode 100644 packages/flow-lenia/web/src/app.ts create mode 100644 packages/flow-lenia/web/src/arena-app.ts create mode 100644 packages/flow-lenia/web/src/ecosystem-app.ts create mode 100644 packages/flow-lenia/web/src/experiments/arena-cards.ts create mode 100644 packages/flow-lenia/web/src/experiments/cards.ts create mode 100644 packages/flow-lenia/web/src/experiments/ecosystem-cards.ts create mode 100644 packages/flow-lenia/web/src/fft-batch.ts create mode 100644 packages/flow-lenia/web/src/inventory.ts create mode 100644 packages/flow-lenia/web/src/m0-app.ts create mode 100644 packages/flow-lenia/web/src/main.ts create mode 100644 packages/flow-lenia/web/src/model/arena.ts create mode 100644 packages/flow-lenia/web/src/model/config.ts create mode 100644 packages/flow-lenia/web/src/model/ecosystem-solver.ts create mode 100644 packages/flow-lenia/web/src/model/events.ts create mode 100644 packages/flow-lenia/web/src/model/experiment-io.ts create mode 100644 packages/flow-lenia/web/src/model/kernels.ts create mode 100644 packages/flow-lenia/web/src/model/seed.ts create mode 100644 packages/flow-lenia/web/src/model/shader-provenance.ts create mode 100644 packages/flow-lenia/web/src/model/solver.ts create mode 100644 packages/flow-lenia/web/src/probe.ts create mode 100644 packages/flow-lenia/web/src/prove-m3.ts create mode 100644 packages/flow-lenia/web/src/prove-m4.ts create mode 100644 packages/flow-lenia/web/src/prove-m6.ts create mode 100644 packages/flow-lenia/web/src/prove.ts create mode 100644 packages/flow-lenia/web/src/prove/ecosystem-fixture.json create mode 100644 packages/flow-lenia/web/src/prove/organism-fixture.json create mode 100644 packages/flow-lenia/web/src/render/arena-renderer.ts create mode 100644 packages/flow-lenia/web/src/render/ecosystem-renderer.ts create mode 100644 packages/flow-lenia/web/src/render/overlays.ts create mode 100644 packages/flow-lenia/web/src/render/renderer.ts create mode 100644 packages/flow-lenia/web/src/shaders/arena_events.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/arena_perceive.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/batch_fft.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/ecosystem_flow.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/ecosystem_mutation.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/ecosystem_perceive.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/organism_events.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/organism_flow.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/organism_pack.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/organism_perceive.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/organism_spectral.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/reintegrate.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/reintegrate_ecosystem.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/reintegrate_organism.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/render_arena.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/render_ecosystem.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/render_organism.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/render_trails.wgsl create mode 100644 packages/flow-lenia/web/src/shaders/spectral_expand.wgsl create mode 100644 packages/flow-lenia/web/src/style.css create mode 100644 packages/flow-lenia/web/tsconfig.json create mode 100644 packages/flow-lenia/web/vite.config.ts create mode 100644 tools/productization/web-deploy/web/pages/assets/flow-lenia.png create mode 100644 tools/productization/web-deploy/web/pages/assets/flow-lenia.webm 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+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+NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD+98Ro/BKoAP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaP6VWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+rPuxvYXeYb6s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9rPuxvaVWh76lVoe+pVaHvqVWh76lVoe+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/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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/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": "Z/syPgWKTz50Kmc+Q4RVPtaqIj6Gm/k9cmb+PYafBz5oQR4+XnJGPuTgVz5GNEU+UUEpPhNjEj40cxQ+xVEoPu5/Oz6OGlY+HOJmPjgFTz5joB0+uHcBPpe+FD76xSc+VGA6PmplVD4nP1A+UGArPjs4CD5haew9T0QGPjx7KT6bXDo+Q+dKPrXZTz7Q2zM+xQoLPgeO/j0GfCg+YSRKPi3JWj7T22g+2sVTPqneIj66Wvk99gPePSFzBj5Fhyw+RNo/PvugPT4mrDI+mkoRPhBC4j1k7uc9+hwtPlaBWT4yi2g+GjVuPlA2Uj72XiI+0o8EPtz6AT68oSE+cMVCPrT9Tj7EmDo+3m8jPsHf/D0BUMg99RzfPWULLj6nlFg+agVePiC2WT4rfzw+siAXPj1ZCz63Vhs+XBRIPlcwZT5ug1c+SjM6PpXLIT4elAI+u0nfPTLtAD6OID8+NkBbPvT1Sz5TjTg+7rwYPgc8/T1GPgU+OQknPi16Xz7LXno+9FlKPimSLD5TBRw+nzQNPmIgBz54xyA+aEdgPv05az7M0kQ+hHEiPkvs/T0fhNg9b6L7PQp9Jz5o818+CmdzPsIcLD4UVQ8+NY0IPpd9DT4W+BY+s9M5PkS6ej4dpHk+XPFFPqTyHj55vv0929vmPWV3CT625C8+Gt1ZPsDnWz7YYhE+72fnPfLS5z3t5wY+DrUaPipJQD50pXs+7Y5yPmxePT7nTx4+u0QNPkQsDz7+cSo+wPlJPovjXz74sUs+o9wIPrDP2j3uL+g98sIMPnaPHz5kCDw+ZCZnPj/PUz4uCiI+T7oQPhmiFD4YMSg+4E9LPm1LZj7IsG0+wSVKPqmzCj6ybvU9a+IMPv6pKD67RDI+8PA9PvVIUz6ASjE+QVABPtNd9z3nYxA+cqwwPvMnVz6VJ20+msdsPgugRj6KgwU+3WMIPnFBKj5G3ko+zh5LPt+ESD72c04+DtEhPly35j0Xpuo9HyMRPl67MT4/L1A+C4haPvd+UT70TzA+tSbsPfa3Cj4zADk+f7VcPq28Vj53RUw+UZVNPv2EIz5UG/09GfoJPjorJz7Cfz0+JNhJPiI6QT4I5io+VvoNPmxB2T1/Kwo+MJA6Pv9FWT6+lUw+Hc07PtMGPT4RbSE+5LkQPoI8Kz5AEUs+IeRVPguYTz7f+TQ+T4ITPuHc8T1TUPQ9ARcYPsB8QD7wck8+ejI2PlY+HD4L0hs+jiMRPhuKFz5hO0E+/4tkPvHZZz4/N1Y+hQI0Pje1Ez5lpgA+Ow0ZPl6bNT7j6lM+BXZPPiVMJj5GngI+R/P/PXjbAD5qvRU+jkVFPoSMZT68DGE+TjRKPmesKz7JPRo+jncYPv3kaD77cIg+dJtsPuV+Mz6PJSY+dZlJPqmyVj5V+2Q+LiNcPoI1Lj4Fliw+aCtXPpk3az4db1A+AR4gPlqsHT7VQnk+7ciJPlQsXT4xvzA+2O9DPuTeaD4QyFA+A8Y5PkjULz5ToS0+VlJYPmpAgT4A5W0+Z44xPvhsBT5M/B0+6VV7Pjr6aD4wWDA+uTcwPmxSdD69MIo+M8BOPjnTGT5Q9BQ+rR8vPoZjZj72xIA+cVRfPtqIMD5dEB0+ruE9Pgc4gT74yUg+k/oUPvH5MT6X24E+WqCJPuU2QD40Mxo+QlIwPnYaTz4Pk2M+M5xVPizAMj4lgDA+co1EPjoSaT5zh3M+PJ1EPplSLz4Rr1E+jf98Pi2mYz7hfBk+VcgfPuo5XT7fEno+5wlmPjg0Nj65Yxc+T3gxPpjUUj6BXGs+NqFCPvaFPz5owlo+m0l8PtdBez7k7EA+WtICPg1AJT4pOmw+EpB5PpEnVz7WwzQ+lzQyPpuoUD4HA1U+giVFPr8jJD6mND4+oCppPpKiez6Wo2s+wk9CPgBpHj4jdEI+Xz9pPtWNTj538So+Q9gzPurrXT4FVXw+vFRcPnt0KD4Mtz4+SVtgPphWZj5+WFA+dzZAPib4Rz5U2ko+xfhnPpSBaj6Oxy4+f9oPPmhcND6eX2w+X7F9PmBnTz7BsyQ+VvlqPgrlhz7cjGg+C/UwPgidJj66hUw+LIBZPk9GZT7n2Vo+W/wrPoR6KT5R/FM+OiNrPsOTUz4sTiM+GVkgPuA4eT5IKog+eJ9ZPnbeLz6HUEY+FUZtPlMyUz5j1jk+JZguPj/aKj7g/VM+5Oh/PtqTbz7LZjQ+zbUHPmHTHz40NHg+c7pjPkiULT7zazA+c4x4PqarjD5VNVA+5UcZPhSZEz6uViw+rXNjPkTcgD5t6mE+/vUyPsU3Hz6d3z4+FXh9PioIRD5fEBM+Z48yPnK1gz4JbIs+wMdAPgvsGD7CAC4+rDdMPrBjYz60Vlg+Bbw1Pp3/Mj7n0UY+2rdoPheNbj4bXEE+MLkuPgZcUz4BtX8+aRBlPna0GD4Whh0+R45ZPgerdz6cXWc+fzg5Pk92Gj6ehDQ+0N1UPo9FaT6NQD8+ffY9PpcyXD7xBIA+43J+PiEwQT6cDQE+b+QhPsR/aD6zrng+9rpYPvA8Nz7tNDU+sm1TPi9SVT4CUUI+M9IhPufiPT6HE2w+UE2APor9bj5+GkI+XwwcPjivPj6L1mY+fetOPsmdLD6qQzY+dpZgPhSWfD6solk+EIElPq9vPD7P5GA+tENqPtGMVD5UokE+FKFGPl8iSD5g+mU+J2dqPrEMMD5+4BE+zjc3Pjgzbj4xLXs+eyRLPuGWIT451Gw+4mqDPsIxZD5u/z0+AhlSPjPkgT73yoY+w+tbPuYhYD7cZIY+WFR3PiaGTj78a2k+y1SCPuFZaj6UOVc+Lwh9PidzdD5K+VY+CVdcPkxkaj7MBmE+O3RnPnJqfD6qi4Y+kDVyPkhoRz77hVw+EYmJPhvRfz7sVk4+3PdePoWsbz4Z9js+QipSPpX5hT4hlXs+SQdCPj4rWj5Er4Y+7CmEPsP8Yj5W5lw+m5B2PkhHdD6MPlQ+N+FqPgJ+iz4thlw+xkQ/PjeBaz656IA+XE9UPgY3Rj7R/4Y+WOKJPok9UT6Ug1Q+F2SHPm/Dhj59Mk8+Lks8PkqAgT7Jg5E+hd09PtITaj69C4o+k3poPgE7Lj7TxFE+4w6TPu0mhT4lYUk+bqVqPoAChz7wvWY+VY9KPtl3bj6k1Yk+LXJtPg6ERz7QRXU+sf18PrWGWj69I1A+WbpuPi/ifz6nIl4+V3ZrPoaWjj4e2ng+XXc1Ptb3UT7npYo+tpyIPu5mVz5mw30+YkZwPvuNST60RlE+Ms11Pvxvez5tlGc+LtFZPtwkfj5G3Ic+UjVjPtNMTj4+X3U+QXd8Pix3YD6LtGo+GJmAPiTBXT4VXlM+fiRxPp6TbD7/gUs+NUBoPjjSiT6KIIQ+cRhXPtYyTD7OmXo+0tuIPjepYT4S6Uw+vAp7PglSUj46Rzw+Y9R1PnvCiz4e/Vo+PMMrPmYscz4zLpM+WQp3PpKnUT56h2g+iR9+PvKjZz6IeFU+CTF/Pg6xiD50Z0U+SClOPsbofz7cbnY+XWZDPu0cUj6qto0+b76BPveQRT77Bmw+km+OPnGZcz7T+zs+TYVUPuTDjD7FBIc+YytUPoMdgj6t6IA+NNJGPoxONT4dXHM+EnmTPgK4bT5yZ0g++Qd/PjQKhj4twFk+E29VPixPez64sX8+g4NbPmdqZT5ePH0+U3xnPn3uUz4pF2A+IGhvPiW8bj4VHWM+ZKWAPpwyiT5GN1w+7Ss/PsRfdT6fqok+YOlsPijGUj6SNoA+UOdPPpz3Pz60nnA+QtuDPhgSYj7Nd1I+AzBsPu2qhj7oXH8+ggtbPo1VYD5zync+qu9lPqJJYT4V/oM+471zPvscRT6J8lk+GimAPm/QZD4baEE+8+x2Pm/rjT6u2G8+uBhNPrCZaz5GhIY+iGFyPm/+RD4DZWU+lk2NPmIwPD4dv00+5EqIPnC6hD5Q9Ds+Q3UxPvdOiD4qdpI+DVJfPja7Vz6Rtnw+rht0PlESVD6Gh2E+B7+JPiDxgD5Tbjo+dO5jPoEJhD62sGc+z9RBPgtBYT4cEIo+ezRuPhVgUz4XtoU+UtqIPkuETD7dFz0+DSl8PqDGkD6mUG0+" + }, + "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+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz7pH64+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+EzvnPhM75z4TO+c+EzvnPhM75z4TO+c+phinPqYYpz6mGKc+phinPukfrj6mGKc+6R+uPukfrj7pH64+6R+uPhM75z4TO+c+EzvnPhM75z4TO+c+EzvnPqYYpz6mGKc+phinPqYYpz6mGKc+6R+uPukfrj7pH64+6R+uPukfrj4TO+c+EzvnPhM75z4TO+c+EzvnPhM75z6mGKc+phinPqYYpz6mGKc+phinPukfrj7pH64+6R+uPukfrj7pH64+NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPwSqAD8EqgA/BKoAPwSqAD8EqgA/vfEaP73xGj+98Ro/vfEaP73xGj80Mg8/NDIPPzQyDz80Mg8/NDIPPzQyDz8EqgA/BKoAPwSqAD8EqgA/BKoAP73xGj+98Ro/vfEaP73xGj+98Ro/NDIPPzQyDz80Mg8/NDIPPzQyDz80Mg8/BKoAPwSqAD8EqgA/BKoAPwSqAD+98Ro/vfEaP73xGj+98Ro/vfEaP6VWh76lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvoXeYb6F3mG+hd5hvoXeYb6F3mG+rPuxvaz7sb2s+7G9rPuxvaz7sb2lVoe+pVaHvqVWh76lVoe+pVaHvqVWh76F3mG+hd5hvoXeYb6F3mG+hd5hvqz7sb2s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+hd5hvoXeYb6F3mG+hd5hvoXeYb6s+7G9rPuxvaz7sb2s+7G9pVaHvqVWh76lVoe+pVaHvqVWh76lVoe+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": "Z/syPgWKTz50Kmc+Q4RVPtaqIj6Gm/k9cmb+PYafBz5oQR4+XnJGPuTgVz5GNEU+UUEpPhNjEj40cxQ+xVEoPu5/Oz6OGlY+HOJmPjgFTz5joB0+uHcBPpe+FD76xSc+VGA6PmplVD4nP1A+UGArPjs4CD5haew9T0QGPjx7KT6bXDo+Q+dKPrXZTz7Q2zM+xQoLPgeO/j0GfCg+YSRKPi3JWj7T22g+2sVTPqneIj66Wvk99gPePSFzBj5Fhyw+RNo/PvugPT4mrDI+mkoRPhBC4j1k7uc9+hwtPlaBWT4yi2g+GjVuPlA2Uj72XiI+0o8EPtz6AT68oSE+cMVCPrT9Tj7EmDo+3m8jPsHf/D0BUMg99RzfPWULLj6nlFg+agVePiC2WT4rfzw+siAXPj1ZCz63Vhs+XBRIPlcwZT5ug1c+SjM6PpXLIT4elAI+u0nfPTLtAD6OID8+NkBbPvT1Sz5TjTg+7rwYPgc8/T1GPgU+OQknPi16Xz7LXno+9FlKPimSLD5TBRw+nzQNPmIgBz54xyA+aEdgPv05az7M0kQ+hHEiPkvs/T0fhNg9b6L7PQp9Jz5o818+CmdzPsIcLD4UVQ8+NY0IPpd9DT4W+BY+s9M5PkS6ej4dpHk+XPFFPqTyHj55vv0929vmPWV3CT625C8+Gt1ZPsDnWz7YYhE+72fnPfLS5z3t5wY+DrUaPipJQD50pXs+7Y5yPmxePT7nTx4+u0QNPkQsDz7+cSo+wPlJPovjXz74sUs+o9wIPrDP2j3uL+g98sIMPnaPHz5kCDw+ZCZnPj/PUz4uCiI+T7oQPhmiFD4YMSg+4E9LPm1LZj7IsG0+wSVKPqmzCj6ybvU9a+IMPv6pKD67RDI+8PA9PvVIUz6ASjE+QVABPtNd9z3nYxA+cqwwPvMnVz6VJ20+msdsPgugRj6KgwU+3WMIPnFBKj5G3ko+zh5LPt+ESD72c04+DtEhPly35j0Xpuo9HyMRPl67MT4/L1A+C4haPvd+UT70TzA+tSbsPfa3Cj4zADk+f7VcPq28Vj53RUw+UZVNPv2EIz5UG/09GfoJPjorJz7Cfz0+JNhJPiI6QT4I5io+VvoNPmxB2T1/Kwo+MJA6Pv9FWT6+lUw+Hc07PtMGPT4RbSE+5LkQPoI8Kz5AEUs+IeRVPguYTz7f+TQ+T4ITPuHc8T1TUPQ9ARcYPsB8QD7wck8+ejI2PlY+HD4L0hs+jiMRPhuKFz5hO0E+/4tkPvHZZz4/N1Y+hQI0Pje1Ez5lpgA+Ow0ZPl6bNT7j6lM+BXZPPiVMJj5GngI+R/P/PXjbAD5qvRU+jkVFPoSMZT68DGE+TjRKPmesKz7JPRo+jncYPv3kaD77cIg+dJtsPuV+Mz6PJSY+dZlJPqmyVj5V+2Q+LiNcPoI1Lj4Fliw+aCtXPpk3az4db1A+AR4gPlqsHT7VQnk+7ciJPlQsXT4xvzA+2O9DPuTeaD4QyFA+A8Y5PkjULz5ToS0+VlJYPmpAgT4A5W0+Z44xPvhsBT5M/B0+6VV7Pjr6aD4wWDA+uTcwPmxSdD69MIo+M8BOPjnTGT5Q9BQ+rR8vPoZjZj72xIA+cVRfPtqIMD5dEB0+ruE9Pgc4gT74yUg+k/oUPvH5MT6X24E+WqCJPuU2QD40Mxo+QlIwPnYaTz4Pk2M+M5xVPizAMj4lgDA+co1EPjoSaT5zh3M+PJ1EPplSLz4Rr1E+jf98Pi2mYz7hfBk+VcgfPuo5XT7fEno+5wlmPjg0Nj65Yxc+T3gxPpjUUj6BXGs+NqFCPvaFPz5owlo+m0l8PtdBez7k7EA+WtICPg1AJT4pOmw+EpB5PpEnVz7WwzQ+lzQyPpuoUD4HA1U+giVFPr8jJD6mND4+oCppPpKiez6Wo2s+wk9CPgBpHj4jdEI+Xz9pPtWNTj538So+Q9gzPurrXT4FVXw+vFRcPnt0KD4Mtz4+SVtgPphWZj5+WFA+dzZAPib4Rz5U2ko+xfhnPpSBaj6Oxy4+f9oPPmhcND6eX2w+X7F9PmBnTz7BsyQ+VvlqPgrlhz7cjGg+C/UwPgidJj66hUw+LIBZPk9GZT7n2Vo+W/wrPoR6KT5R/FM+OiNrPsOTUz4sTiM+GVkgPuA4eT5IKog+eJ9ZPnbeLz6HUEY+FUZtPlMyUz5j1jk+JZguPj/aKj7g/VM+5Oh/PtqTbz7LZjQ+zbUHPmHTHz40NHg+c7pjPkiULT7zazA+c4x4PqarjD5VNVA+5UcZPhSZEz6uViw+rXNjPkTcgD5t6mE+/vUyPsU3Hz6d3z4+FXh9PioIRD5fEBM+Z48yPnK1gz4JbIs+wMdAPgvsGD7CAC4+rDdMPrBjYz60Vlg+Bbw1Pp3/Mj7n0UY+2rdoPheNbj4bXEE+MLkuPgZcUz4BtX8+aRBlPna0GD4Whh0+R45ZPgerdz6cXWc+fzg5Pk92Gj6ehDQ+0N1UPo9FaT6NQD8+ffY9PpcyXD7xBIA+43J+PiEwQT6cDQE+b+QhPsR/aD6zrng+9rpYPvA8Nz7tNDU+sm1TPi9SVT4CUUI+M9IhPufiPT6HE2w+UE2APor9bj5+GkI+XwwcPjivPj6L1mY+fetOPsmdLD6qQzY+dpZgPhSWfD6solk+EIElPq9vPD7P5GA+tENqPtGMVD5UokE+FKFGPl8iSD5g+mU+J2dqPrEMMD5+4BE+zjc3Pjgzbj4xLXs+eyRLPuGWIT451Gw+4mqDPsIxZD5u/z0+AhlSPjPkgT73yoY+w+tbPuYhYD7cZIY+WFR3PiaGTj78a2k+y1SCPuFZaj6UOVc+Lwh9PidzdD5K+VY+CVdcPkxkaj7MBmE+O3RnPnJqfD6qi4Y+kDVyPkhoRz77hVw+EYmJPhvRfz7sVk4+3PdePoWsbz4Z9js+QipSPpX5hT4hlXs+SQdCPj4rWj5Er4Y+7CmEPsP8Yj5W5lw+m5B2PkhHdD6MPlQ+N+FqPgJ+iz4thlw+xkQ/PjeBaz656IA+XE9UPgY3Rj7R/4Y+WOKJPok9UT6Ug1Q+F2SHPm/Dhj59Mk8+Lks8PkqAgT7Jg5E+hd09PtITaj69C4o+k3poPgE7Lj7TxFE+4w6TPu0mhT4lYUk+bqVqPoAChz7wvWY+VY9KPtl3bj6k1Yk+LXJtPg6ERz7QRXU+sf18PrWGWj69I1A+WbpuPi/ifz6nIl4+V3ZrPoaWjj4e2ng+XXc1Ptb3UT7npYo+tpyIPu5mVz5mw30+YkZwPvuNST60RlE+Ms11Pvxvez5tlGc+LtFZPtwkfj5G3Ic+UjVjPtNMTj4+X3U+QXd8Pix3YD6LtGo+GJmAPiTBXT4VXlM+fiRxPp6TbD7/gUs+NUBoPjjSiT6KIIQ+cRhXPtYyTD7OmXo+0tuIPjepYT4S6Uw+vAp7PglSUj46Rzw+Y9R1PnvCiz4e/Vo+PMMrPmYscz4zLpM+WQp3PpKnUT56h2g+iR9+PvKjZz6IeFU+CTF/Pg6xiD50Z0U+SClOPsbofz7cbnY+XWZDPu0cUj6qto0+b76BPveQRT77Bmw+km+OPnGZcz7T+zs+TYVUPuTDjD7FBIc+YytUPoMdgj6t6IA+NNJGPoxONT4dXHM+EnmTPgK4bT5yZ0g++Qd/PjQKhj4twFk+E29VPixPez64sX8+g4NbPmdqZT5ePH0+U3xnPn3uUz4pF2A+IGhvPiW8bj4VHWM+ZKWAPpwyiT5GN1w+7Ss/PsRfdT6fqok+YOlsPijGUj6SNoA+UOdPPpz3Pz60nnA+QtuDPhgSYj7Nd1I+AzBsPu2qhj7oXH8+ggtbPo1VYD5zync+qu9lPqJJYT4V/oM+471zPvscRT6J8lk+GimAPm/QZD4baEE+8+x2Pm/rjT6u2G8+uBhNPrCZaz5GhIY+iGFyPm/+RD4DZWU+lk2NPmIwPD4dv00+5EqIPnC6hD5Q9Ds+Q3UxPvdOiD4qdpI+DVJfPja7Vz6Rtnw+rht0PlESVD6Gh2E+B7+JPiDxgD5Tbjo+dO5jPoEJhD62sGc+z9RBPgtBYT4cEIo+ezRuPhVgUz4XtoU+UtqIPkuETD7dFz0+DSl8PqDGkD6mUG0+" + }, + "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": "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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": "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 0000000000000000000000000000000000000000..a650ad09b5b135aa6c2c558ad87a1c80e4c4526d GIT binary patch literal 48933 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 MjghgHAo$@Ml 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ÐFJ(8b_Az;dw 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ξ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 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.