Freeze SmoothLife model contract and fixtures

This commit is contained in:
2026-07-14 18:26:55 +02:00
parent 51cff7b0f3
commit d587f542eb
25 changed files with 30612 additions and 38 deletions

18
.pi/APPEND_SYSTEM.md Normal file
View File

@@ -0,0 +1,18 @@
# Plan Macrostep Workflow
Continue implementing the plans under `plans/` using this strict macrostep workflow:
1. Work only on the macrostep explicitly selected by the developer.
2. Read the complete macrostep and gather all required context from the plans, codebase, tests, and related documentation.
3. Identify ambiguities, missing decisions, or conflicting requirements. Ask the developer for immediate clarification when needed.
4. Once the macrostep is understood and confirmed, prepare a concrete plan to submit the developer.
5. Once the developer confirms the concrete plan, implement it completely.
6. Run appropriate diagnostics and tests, and fix issues caused by the implementation.
7. Summarize the changes, affected files, and verification results.
8. Stop and wait for developer feedback.
Do not begin, investigate, plan, or implement the next macrostep unless the developer explicitly tells you which macrostep to work on next. Developer feedback about the completed macrostep is not authorization to advance unless it explicitly selects the next one.
Preserve existing repository conventions and avoid unrelated changes. If no macrostep has been selected at the beginning of the session, inspect only enough of `plans/` to identify the available macrosteps, then ask the developer which one to start.
If you identify certain simple and verbose subtasks, run subagents for them.

293
docs/historical-options.md Normal file
View File

@@ -0,0 +1,293 @@
# Retained historical options
## 1. Policy and scope
There is no legacy engine, compatibility profile, legacy preset format, or migration mode. Exactly three localized choices are retained:
```text
Rk4RelaxationReference::StepOrigin
FftAlgorithm::LegacyPackedUnitary
SphereModel::Legacy
```
Their counterparts are the new defaults:
```text
Rk4RelaxationReference::StageState
FftAlgorithm::Standard
SphereModel::Corrected
```
The three settings are independent and affect only the subsystem named by their type. Selecting one must not change seeds, initializers, curves, kernels, scheduling, another option, or unrelated defaults. A configuration must not offer a global `legacy`, `compatibility`, or version-emulation switch.
Only newly authored presets are supported. The project does **not** support, capture, bundle, import, deduplicate, or migrate rows from any legacy preset catalogue. A new preset may explicitly select one of the three options, but that does not make the preset a migrated legacy preset.
Safe deterministic infrastructure always applies: all storage is initialized, updates ping-pong, dot products are clamped before `acos`, kernels have complete contract-defined support except for the explicitly retained Legacy sphere stencil, and invalid/unsupported requests fail rather than falling back.
`docs/model-contract.md` is the mathematical authority. This document defines the historical deltas and their provenance.
## 2. Applicability matrix
| Type | Value | Default | Applicable only to | Invalid elsewhere |
| --- | --- | --- | --- | --- |
| `Rk4RelaxationReference` | `StageState` | yes | planar Relaxation+RK4 | field must be absent |
| | `StepOrigin` | no | planar Relaxation+RK4 | field must be absent |
| `FftAlgorithm` | `Standard` | yes | planar and multiscale convolution | variant must reject unsupported value |
| | `LegacyPackedUnitary` | no | supported GPU planar/multiscale FFT | CPU/unsupported GPU request fails |
| `SphereModel` | `Corrected` | yes | sphere variant | field absent outside sphere |
| | `Legacy` | no | sphere variant | field absent outside sphere |
An option is structurally absent when inapplicable, not accepted and ignored. Any option change invalidates AB derivative history. Exact checkpoints include every applicable choice in their validated arithmetic descriptor.
## 3. `Rk4RelaxationReference::StepOrigin`
### 3.1 Exact semantic delta
Let `A0` be the state at the beginning of one RK4 step. Intermediate states are still formed and clamped exactly as in the ordinary RK4 contract, and every `S(Yj)` is recomputed from that clamped stage:
```text
k1 = S(A0)-A0
Y2 = clamp01(A0 + dt*k1/2)
k2 = S(Y2)-A0
Y3 = clamp01(A0 + dt*k2/2)
k3 = S(Y3)-A0
Y4 = clamp01(A0 + dt*k3)
k4 = S(Y4)-A0
next = clamp01(A0 + dt*(k1+2*k2+2*k3+k4)/6)
```
Only the subtraction reference differs. Neighborhoods and targets are never evaluated from `A0` in place of the stage. The default `StageState` instead subtracts `Y2`, `Y3`, and `Y4` from the corresponding targets.
### 3.2 Constraints
- Valid only for Relaxation dynamics with RK4 integration.
- It has no meaning for Discrete, Growth, Euler, AB3, multiscale composition, sphere, or delayed time.
- Every stage and final clamp remains mandatory.
- Curves and targets may overshoot; this option introduces no additional clamp.
- Switching the value invalidates AB history even though a valid configuration cannot simultaneously use AB3. This keeps the global invalidation policy uniform.
### 3.3 Retained versus rejected behavior
Retained: the historical use of the step-origin state as the relaxation subtraction reference at stages 24. Source evidence: `SmoothLife/main.cpp:2603-2611,2968-2976` (SHA-256 `26b21bfa7fb262f02f5eec4c2615aa501639471ccebce33b2abeb4fb6ee02e58`).
Rejected: stale neighborhoods, unclamped RK stages, implicit timestep changes, numeric mode aliases, and carrying the selector in configurations where it is ignored.
## 4. `FftAlgorithm::LegacyPackedUnitary`
### 4.1 Exact semantic delta
This option selects the complete operation-ordered binary32 algorithm in `model-contract.md` section 6.2:
- adjacent x samples are packed as real/imaginary pairs;
- x spectra contain `Nx/2+1` complex values;
- integer bit reversal and radix-2 stage order are fixed;
- each butterfly is scaled by binary32 `1/sqrt(2)`;
- forward/inverse real-complex conversions are fixed;
- remaining dimensions use full unitary complex transforms;
- spectral multiplication applies `sqrt(sample_count)/kernel_sum` before inverse stages;
- fixtures freeze every plan, stage, conversion, spectrum, and convolution with the stated bit-exact/absolute-relative tolerances.
The option changes the convolution algorithm and its explicitly retained f32 arithmetic. It does not change sampled kernel values, kernel support, normalization intent, rule evaluation, dynamics, integration, state clamp points, or initializer.
### 4.2 Constraints and failure behavior
- Valid only for planar and multiscale convolution.
- Every active extent is a power of two; `Nx` is even and at least 2.
- It is a GPU application backend. The scalar CPU implementation exists only as a stage oracle and is not a selectable simulation backend.
- The GPU must provide the required binary32 operations, deterministic ping-pong resources, half-spectrum storage, and validated plan limits.
- A CPU request, unsupported shape, unsupported GPU, failed shader/resource creation, or failed algorithm validation is a configuration/runtime error. It must never silently select `Standard`.
- Kernel sums are computed from the same complete sampled kernels as Standard. A zero or non-finite sum is an error.
- The semantic oracle compares results in f64. A production GPU may retain f32 values through the common rule/integration pipeline and must satisfy the documented f32 backend tolerances; ordinary execution requires no readback or widening.
### 4.3 Retained versus rejected behavior
Retained: packed adjacent-real storage, half-width x spectra, historical bit-reversal/twiddle stages, unitary butterflies, real/complex conversion, and matching `sqrt(V)/kernel_sum` correction. Primary evidence is `SmoothLife/main.cpp:1442-1967` (SHA-256 `26b21bfa7fb262f02f5eec4c2615aa501639471ccebce33b2abeb4fb6ee02e58`) and the packed FFT shaders listed in section 7.
Rejected:
- treating this as a whole-application compatibility mode;
- incomplete or connected-component-cutoff kernels;
- old OpenGL compatibility syntax;
- shader lookup relative to the process working directory;
- framebuffer/texture feedback;
- uninitialized plans or textures;
- resource leaks or allocation during steady-state passes;
- silent CPU or Standard fallback;
- permitting compiler FMA/reassociation to redefine the stage oracle.
## 5. `SphereModel::Legacy`
### 5.1 Shared safe geometry
Legacy uses the six face order, face frames, `K×K` active cells, `R=K/2`, center directions, exact cell areas, clamped-dot geodesic distance, and common rule curves from `model-contract.md` section 8. `K` must be even. Active arrays are always initialized, and commits ping-pong.
The input radius is `ra_planar`; validate
```text
ra_planar > 0
ceil(2*ra_planar) <= K
```
The second condition is mandatory and is intentionally specific to Legacy. It ensures its doubled-planar-radius face-local search and side gutters fit one face. Convert radii exactly as
```text
ri_planar = ra_planar/3
ri = R*acos(1-ri_planar²/(2R²))
ra = R*acos(1-ra_planar²/(2R²))
```
Both transition widths are `1`. The `ceil(2*ra_planar)<=K` constraint implies valid conversion arguments; they must still be validated as finite.
### 5.2 Face-local stencil and side gutters
Legacy does not perform corrected all-face enumeration. Its conceptual atlas is `18K×3K`: face `f` owns a `3K×3K` tile, and its active rectangle is `[3fK+K,3fK+2K) × [K,2K)`. The four adjacent `K×K` rectangles are side gutters copied from edge-neighbor active faces with rotation/reflection; the four corner `K×K` rectangles remain deterministically masked zero. Persistent atlas storage is not required, but candidate resolution must be equivalent to this geometry.
For each center cell `(face,x,y)`, let
```text
h = ceil(2*ra_planar)
dx,dy ∈ {-h,...,+h}
x' = x+dx
y' = y+dy
```
Enumerate candidates in `dy` then `dx` order. Resolve each candidate as follows:
1. If `0≤x'<K` and `0≤y'<K`, sample `(face,x',y')`.
2. If both x and y are outside, the candidate belongs to a diagonal/corner sector and is **masked**: it contributes neither numerator nor any sampled weight. This deterministic zero mask replaces undefined corner atlas contents.
3. If exactly one coordinate is outside, resolve it through that edge's side gutter.
The side-gutter transform is normative. Define extended face coordinates
```text
s = 2*(x'+0.5)/K - 1
t = 2*(y'+0.5)/K - 1
```
For one crossed edge, form a cube-surface vector `q`:
```text
s > 1: δ=s-1; q=+u_axis + (1-δ)*normal + t*v_axis
s < -1: δ=-1-s; q=-u_axis + (1-δ)*normal + t*v_axis
t > 1: δ=t-1; q=+v_axis + (1-δ)*normal + s*u_axis
t < -1: δ=-1-t; q=-v_axis + (1-δ)*normal + s*u_axis
```
The destination face is the one whose normal is the first signed axis term (`±u_axis` or `±v_axis`). With that destination's fixed frame, compute
```text
sd = q·destination_u_axis
td = q·destination_v_axis
xd = clamp(floor((sd+1)*K/2), 0, K-1)
yd = clamp(floor((td+1)*K/2), 0, K-1)
```
and sample destination active cell `(xd,yd)`. The clamp resolves exact edge arithmetic only; it does not fill a corner sector. Each stencil offset contributes independently even if finite discretization maps two offsets to the same destination cell. Side gutters are derived from initialized active arrays on every evaluation; they are not persistent undefined storage.
This local square is the entire Legacy search stencil. It is not expanded to capture transition weights beyond `h`, and it never follows a second edge into a masked diagonal sector. These omissions are retained characteristics of this option.
### 5.3 Accumulation and analytic normalization
For each resolved candidate `j`, compute the geodesic distance from the center direction to the resolved active-cell direction and evaluate
```text
KD(r)=1-L(r;ri,1)
KR(r)=L(r;ri,1)*(1-L(r;ra,1))
```
Accumulate `cell_area(j)*weight*A(j)` in stencil order. Masked candidates contribute nothing. Normalize with analytic spherical cap areas, not actual sampled denominators:
```text
cap(r) = 2πR²*(1-cos(r/R))
M = Σ_j cell_area(j)*KD(distance)*A(j) / cap(ri)
N = Σ_j cell_area(j)*KR(distance)*A(j) / (cap(ra)-cap(ri))
```
The denominators must be finite and strictly positive. They intentionally ignore transition-profile area, cell sampling, duplicate gutter mappings, and masked sectors. Consequently Legacy is not required to preserve a constant field, and center/edge/corner results may differ. Do not “correct” those differences while this option is selected.
Both update modes remain exact:
```text
Direct: next = clamp01(S)
Smooth: next = clamp01(A + 0.1*(2*S-1))
```
The geodesic sphere-overlay initializer is shared with Corrected. It paints all six active arrays globally across seams and corners before the Legacy gutter view is derived; no historical atlas garbage is reproduced.
### 5.4 Corrected comparison
`SphereModel::Corrected` differs in all of the following as one coherent model:
- globally enumerates all six active arrays for every center;
- includes every nonzero geodesic support sample, including seams and corners;
- has no side-gutter search limit or corner mask;
- uses actual per-center `Σ(cell_area*kernel_weight)` for disk and ring;
- preserves every finite constant field (up to specified arithmetic tolerance);
- does not require `ceil(2*ra_planar)<=K`, although its general radius/domain validation still applies.
Mixing corrected enumeration with analytic normalization, or Legacy masking with sampled normalization, is forbidden. There are exactly two coherent sphere models, not independent toggles for geometry and normalization.
### 5.5 Retained versus rejected behavior
Retained: face-local direct stencil, four side gutters, deterministic masked diagonal/corner sectors, planar-to-geodesic radius conversion, analytic cap normalization, Direct replacement, and fixed-`0.1` Smooth update. Source evidence: `SmoothLifeSphere/main.cpp:351-858,1368-1392` (SHA-256 `c770fb36c562135d4bc65aeae36d71ae725286b05aa0ee37f92b6e6f0ddf8b1e`) and `SmoothLifeSphere/program.frag:110-162` (SHA-256 `037da34db6a050422c372bb458c78553df5ca00e22a26e1ced455f0007c6ce21`).
Rejected:
- uninitialized gutter or corner texels;
- nondeterministic diagonal ownership;
- sampling from a texture while rendering into it;
- unclamped `acos` arguments;
- out-of-range stencils or accepting `ceil(2*ra_planar)>K`;
- preserving atlas memory layout as public state;
- using atlas behavior in Corrected;
- sphere-specific random-number streams or seam-clipped initializers.
## 6. Behaviors with no historical option
The following are permanently rejected rather than configurable:
| Rejected behavior | Contract replacement |
| --- | --- |
| historical kernel connected-component cutoff | complete sampled nonzero support and diagnostics |
| undefined AB derivative buffers | Euler, then AB2, then AB3 startup; authoritative invalidation |
| experimental numeric dynamics modes 3/4 | named Discrete, Growth, Relaxation only |
| stale chained multiscale fields | recompute from the required stage/snapshot |
| additive mode-0 multiscale response | Discrete rejected until a target aggregator is specified |
| undefined multiscale relaxation source | sequential stage or shared-snapshot `A0` reference |
| multiscale texture feedback | safe snapshot/ping-pong evaluation |
| delayed-time head anomaly | `head=next overwrite`, `latest=head-1`, causal nearest layer |
| gradual or undefined delayed-history initialization | one periodic box field replicated to all 16 layers |
| platform `rand()` behavior | exact seeded ChaCha12 stream |
| seam-clipped planar/sphere initialization | periodic splats and global geodesic overlays |
| undefined state, atlas, texture, or framebuffer contents | explicit initialization everywhere |
| legacy scheduling quirks | fixed deterministic simulation scheduling |
| legacy presets/catalogues/importers | newly authored presets only; no capture or migration |
Discovering that a retired implementation behaved differently does not create a fourth option. A change requires an explicit contract revision, rationale, fixtures, and provenance update.
## 7. Provenance and authority trail
The checked-in planning record froze which source-observed behaviors remain selectable. The references below are provenance evidence, not runtime inputs and not competing implementation specifications.
Legacy source-relative paths below are rooted at `/home/fpasqua/Nextcloud/VecchiProgetti/SmoothLifeAll/`; hashes cover complete file bytes. They are evidence only, and implementation never opens that root.
| Topic | Provenance source | Complete-file hash | Relevant range |
| --- | --- | --- | --- |
| Step-origin subtraction | `SmoothLife/main.cpp` | SHA-256 `26b21bfa7fb262f02f5eec4c2615aa501639471ccebce33b2abeb4fb6ee02e58` | 26032611, 29682976 |
| Packed plans/dispatch/correction | `SmoothLife/main.cpp` | SHA-256 `26b21bfa7fb262f02f5eec4c2615aa501639471ccebce33b2abeb4fb6ee02e58` | 14421967 |
| Packed butterfly/tangle operations | `SmoothLife/shaders/fft2D.frag` | SHA-256 `da1d4af18e4b6340a936a036ed010e7e03b3d548d4925f386c26f3fa310af99f` | 153 |
| Packed real/complex copies | `SmoothLife/shaders/copybufferrc2D.frag`; `copybuffercr2D.frag` | SHA-256 `5c325aaf44b5dfb6dd2f2cc1f831596cae1d61f1259bbafbdc5ddb55050e6914`; `fb26244b12c6bad90fa84c9e784e72d409d024d1e66360fa7d0209c99059ab00` | 113; 122 |
| Sphere directions/areas/side table | `SmoothLifeSphere/main.cpp` | SHA-256 `c770fb36c562135d4bc65aeae36d71ae725286b05aa0ee37f92b6e6f0ddf8b1e` | 351858 |
| Sphere radius setup/frame loop | `SmoothLifeSphere/main.cpp` | same SHA-256 | 12201506 |
| Sphere stencil/rule/update | `SmoothLifeSphere/program.frag` | SHA-256 `037da34db6a050422c372bb458c78553df5ca00e22a26e1ced455f0007c6ce21` | 1162 |
| Contract decisions | `plans/00_model_contract_and_historical_options.md` | Git blob SHA-1 `d277a98d3c34799903bc5036c147df0e17b7fc0d` | 109170, 206236 |
| Exactly-three policy | `plans/README.md` | Git blob SHA-1 `6e7d79c55989f82ca29673f695e427085e521d9e` | 112137 |
No retired source tree is needed to interpret these options, and no old executable or preset catalogue is an oracle. Normative precedence is:
1. `docs/model-contract.md` for shared mathematics and arithmetic;
2. this document for the three historical deltas and their constraints;
3. checked-in fixtures for concrete values, with discrepancies resolved by updating documentation and fixtures together;
4. plans as provenance and delivery history only.
Future consultation of retired source is permitted only to investigate a concrete documented discrepancy. Any accepted finding must be restated locally as deterministic semantics, accompanied by fixtures and a provenance entry; merely citing external code is insufficient.

218
docs/legacy-source-map.md Normal file
View File

@@ -0,0 +1,218 @@
# Legacy source map
This document is the static provenance record for Macrostep 00. It maps the decisions in the repository-local model contract and retained-options specification to the legacy tree without creating a runtime or build dependency on that tree.
## Scope, authority, and notation
Legacy root used for every source-relative path below:
```text
/home/fpasqua/Nextcloud/VecchiProgetti/SmoothLifeAll/
```
Line ranges are 1-based, inclusive physical lines. Every SHA-256 is for the complete file bytes, not only the cited range.
Decision labels used below:
- **SOURCE FACT** — behavior or data directly evidenced by a cited legacy file.
- **CORRECTED DEFAULT** — deterministic behavior selected by the rewrite instead of accidental or defective legacy behavior.
- **RETAINED HISTORICAL OPTION** — one of the three intentionally selectable historical behaviors.
- **REJECTED DEFECT** — observed legacy behavior that must not be reproduced.
- **PROVENANCE ONLY** — historical evidence that cannot control implementation or product behavior.
The implementation authority is the **local contract+spec set**: `docs/model-contract.md`, `docs/historical-options.md`, and this source map. The external `SMOOTHLIFE_SPECIFICATION.md` is a reviewed legacy specification and provenance source, identified by hash below; it is not an application, test, build, importer, or runtime dependency. If source and local authority differ, the explicit local decision wins.
This review was static. No legacy `.exe`, simulator, reference program, shader, build, or test executable was run. Hashing, line counting, and text inspection did not execute legacy code.
## Shared/base model
### Frozen interpretation
- **SOURCE FACT:** State is scalar; the FFT family supplies periodic 1-D, 2-D, and 3-D domains. The real and half-width complex buffers and default shapes are declared in `SmoothLife/main.cpp:52-79,2839-2902`.
- **SOURCE FACT:** The legacy constant named `PI`/`pi` is `6.283185...`, i.e. conventional `TAU = 2π`, not π (`SmoothLife/main.cpp:52`; `SmoothLife/shaders/snm2D.frag:6`).
- **SOURCE FACT:** `ri=ra/rr`, transition width `w=ra/rb`, Euclidean sampled disk/ring weights, periodic signed coordinates, and normalization sums are in `SmoothLife/main.cpp:1279-1427`. The log records the check `sum(KR)=279.216312`, `sum(KD)=35.524035` for `ra=10, rr=3, rb=10` at `SmoothLife/SmoothLifeLog.txt:303`.
- **SOURCE FACT:** Rising curves 07, complete windows 89, mixer curves 07, and the four rule constructions are in `SmoothLife/shaders/snm2D.frag:16-104`. Modes 0/1/2 produce replacement, `2S-1`, and `S-A` respectively at lines 105-119.
- **SOURCE FACT:** Euler replacement/update, AB3 coefficients, RK4 coefficients, final clamping, and the frame pipeline are in `SmoothLife/shaders/inteuler2D.frag:14-25`, `intab2D.frag:16-29`, `intrk2D.frag:18-31`, and `SmoothLife/main.cpp:2601-2991`.
- **SOURCE FACT:** The SDL branch is the same model with a one-pass clamped Euler update and no separate integration buffers (`SmoothLifeSDL/main.cpp:61-82,2534-2550`; `SmoothLifeSDL/shaders/snm2D.frag:108-130`). It is subsumed by the shared rewrite, not retained as a backend.
- **CORRECTED DEFAULT:** Generate complete sampled kernel support and warn when geometry is unsuitable. Do not retain the legacy `Ra=(int)(ra*2)` component cutoff (`SmoothLife/main.cpp:1317-1323`).
- **CORRECTED DEFAULT:** Discrete ignores `dt` and integrator; Euler, deterministic Euler→AB2→AB3 startup, and RK4 clamp at every intermediate and final commit are authoritative. Undefined legacy AB buffers are never used.
- **CORRECTED DEFAULT:** For relaxation+RK4, stages subtract their own stage state (`Rk4RelaxationReference::StageState`).
- **RETAINED HISTORICAL OPTION:** `Rk4RelaxationReference::StepOrigin` preserves the main program's stage-neighborhood/origin-subtraction mismatch: `deriv(aa,de)` computes from `aa` but passes global `AA` to `snm` (`SmoothLife/main.cpp:2603-2611`), while RK4 supplies `AA1` for stages 24 (`2968-2976`). This option exists only for relaxation+RK4.
- **REJECTED DEFECT:** Modes 3/4 in shader/keyboard remnants are not supported (`SmoothLife/shaders/snm2D.frag:111-117`; the SDL catalogue's mode-4 row is `SmoothLifeSDL/SmoothLifeConfig.txt:174`).
- **REJECTED DEFECT:** Uninitialized AB history, stale history after model changes, framebuffer feedback, working-directory shader loading, old OpenGL compatibility syntax, platform `rand()`, and implicit backend substitution are not compatibility requirements.
### Consulted shared/base files
| Source-relative path | Relevant lines | SHA-256 | Evidence |
| --- | ---: | --- | --- |
| `SMOOTHLIFE_SPECIFICATION.md` | 1-307 | `9f651e2697cf44a68cd29e835cb02b7c2e52e8f1de4353ae136254e760ef3d54` | Complete reviewed legacy specification. |
| `readme.txt` | 1-42 | `1194e6a53f8ae46131a60243cef296eef7ccd57bc832b0a88d1a147dbd5acc73` | Variant catalogue, config convention, dependencies, controls. |
| `SmoothLife/main.cpp` | 1-220, 1279-2041, 2601-3000 | `26b21bfa7fb262f02f5eec4c2615aa501639471ccebce33b2abeb4fb6ee02e58` | Controls/parser, buffers, kernels, packed FFT driver, convolution/rule pass, dynamics/integration/defaults. |
| `SmoothLife/shaders/snm1D.frag` | 1-119 | `35a0ba9092b48bcf56be03f392f528a78e0dc72112c5ad7e0c8914b47b272f36` | 1-D shared rule. |
| `SmoothLife/shaders/snm2D.frag` | 1-119 | `747c1d6ed3c0218af54479f685774fe3ab8586101bfaa89985c5f683d68c09a3` | Canonical reviewed rule curves, constructions, and dynamics. |
| `SmoothLife/shaders/snm3D.frag` | 1-119 | `b059270be6aab015a091152c77ec7f3ee4fa19d87981512b73f31f4ca1b11b37` | 3-D shared rule. |
| `SmoothLife/shaders/inteuler1D.frag` | 1-25 | `1496e49426db4a911bd704a431348512d0ddaf1f2aee7e012ea2e11cf7e808e9` | 1-D Euler/replacement and clamp. |
| `SmoothLife/shaders/inteuler2D.frag` | 1-25 | `c5c64d4bb44ae15c1681fa4109ed66e93429b73c0df922f6597023838868c8d9` | 2-D Euler/replacement and clamp. |
| `SmoothLife/shaders/inteuler3D.frag` | 1-25 | `dcb889521b935dd57f44f89df866484a922c1f314fb294385e90ddb5342276db` | 3-D Euler/replacement and clamp. |
| `SmoothLife/shaders/intab1D.frag` | 1-29 | `938854c65c8f2133fd5bba2ae2256282defe23e8a0de3b888ffc3477927ca978` | 1-D AB3 formula and clamp. |
| `SmoothLife/shaders/intab2D.frag` | 1-29 | `75c51d618061bbe453162ed62b6d9b86745bf689443044bec7343071c354ed1b` | 2-D AB3 formula and clamp. |
| `SmoothLife/shaders/intab3D.frag` | 1-29 | `68241b04768a22a28185dbaee994ffc5efac8b80110faeb9d793041e036bc915` | 3-D AB3 formula and clamp. |
| `SmoothLife/shaders/intrk1D.frag` | 1-31 | `9da9e4a012731ca6c2ce4106c1fdd973b4ed2e6cea918fc4f04fbb9f409001ef` | 1-D RK4 formula and final clamp. |
| `SmoothLife/shaders/intrk2D.frag` | 1-31 | `e8a46cf4e6f92597facbbb2d10172eadd64d228f4dae49d4ae7483fe45d564bc` | 2-D RK4 formula and final clamp. |
| `SmoothLife/shaders/intrk3D.frag` | 1-31 | `da0553727cc8be65e669fb5873d98e942971a38cc3965145ce3ba1016a8bfc5e` | 3-D RK4 formula and final clamp. |
| `SmoothLifeSDL/main.cpp` | 1-230, 1230-1389, 2370-2631 | `7e0536e7a89cb12aecbc1f181ec736269ba0a83c58170b761812932b727f3787` | SDL buffers/parser/kernel and one-pass frame loop. |
| `SmoothLifeSDL/shaders/snm2D.frag` | 1-130 | `a148bdccbce861babd85b3af1f96854be0b5dfde9fda62957b7b30d66f394342` | One-pass Euler behavior and mode remnants. |
| `SmoothLifeSDL/readme_sdl.txt` | 1-3 | `fb7d3b6447893d8e3b43acd7a0de696b3bf4c974c62c84147fb155501ae0feb0` | Portability/dependency limitations; no separate model semantics. |
## Packed FFT
### Frozen interpretation
- **SOURCE FACT:** Adjacent real x samples are packed into complex `.rg`, spectra have `NX/2+1` x entries, and inverse conversion selects real/imaginary components by x parity (`SmoothLife/main.cpp:1610-1752`; copy shaders below).
- **SOURCE FACT:** x/y/z plans encode bit-reversed inputs and twiddles; each butterfly is scaled by `1/sqrt(2)`, with special real/complex tangle/untangle scaling (`SmoothLife/main.cpp:1442-1909`; FFT shaders below).
- **SOURCE FACT:** Spectral multiplication uses complex multiplication and scale `sqrt(NX*NY*NZ)/kernel_sum` (`SmoothLife/main.cpp:1912-1967,2603-2610`; kernel shaders below).
- **CORRECTED DEFAULT:** `FftAlgorithm::Standard` is the default: conventional unscaled forward transform, inverse scaled by sample count, then sampled-kernel normalization.
- **RETAINED HISTORICAL OPTION:** `FftAlgorithm::LegacyPackedUnitary` retains packing, half-width x spectra, plan stages, unitary scaling, conversion, and the `sqrt(sample_count)/kernel_sum` correction for supported power-of-two GPU 1-D/2-D/3-D paths.
- **REJECTED DEFECT:** The option does not retain legacy framebuffer/resource hazards, compatibility-profile syntax, relative shader discovery, incomplete kernels, or silent CPU/unsupported-GPU fallback. An unsupported explicit request fails clearly.
### Consulted packed-FFT shaders
| Source-relative path | Relevant lines | SHA-256 |
| --- | ---: | --- |
| `SmoothLife/shaders/fft1D.frag` | 1-41 | `b0810a5b89e9f8146a754c072360686573950c5a0010700d259d4726e3d8b238` |
| `SmoothLife/shaders/fft2D.frag` | 1-53 | `da1d4af18e4b6340a936a036ed010e7e03b3d548d4925f386c26f3fa310af99f` |
| `SmoothLife/shaders/fft3D.frag` | 1-61 | `7f0d58d93cacdfff249a277503b017dee371a1b1547a932d6b5305d2b973fae1` |
| `SmoothLife/shaders/copybufferrc1D.frag` | 1-13 | `f4869822b5e7b42abd3b05745e9802a27b00ba27f77c6c01271bfc5eaab28bcc` |
| `SmoothLife/shaders/copybufferrc2D.frag` | 1-13 | `5c325aaf44b5dfb6dd2f2cc1f831596cae1d61f1259bbafbdc5ddb55050e6914` |
| `SmoothLife/shaders/copybufferrc3D.frag` | 1-13 | `587f0fc491c7e887ab056136b164299cf6c7f508bd9f66f68584e8e8d3aeec2b` |
| `SmoothLife/shaders/copybuffercr1D.frag` | 1-22 | `eb436194b7e8db3db1ba825d0172db9f70069add9058cf85036ea88562d37249` |
| `SmoothLife/shaders/copybuffercr2D.frag` | 1-22 | `fb26244b12c6bad90fa84c9e784e72d409d024d1e66360fa7d0209c99059ab00` |
| `SmoothLife/shaders/copybuffercr3D.frag` | 1-22 | `54e93affdca4cb160ca8163615de5a1a8c272d3b3faf015258f9631daf404f90` |
| `SmoothLife/shaders/kernelmul1D.frag` | 1-19 | `426e730aaf723fb1ddc0b843f8ba3553add8e701e8bb3372e1b5cbb430f6caaf` |
| `SmoothLife/shaders/kernelmul2D.frag` | 1-19 | `3a7f923cbd689738e9c1c9337b75b675f39703d973b07b35010e56c68766acf4` |
| `SmoothLife/shaders/kernelmul3D.frag` | 1-19 | `fbe642d3bf78092d2d86ffcd02d6471fc6745e9c0dc97abbdf50f620ed4481b2` |
## Multiscale
### Frozen interpretation
- **SOURCE FACT:** Three parameter records and three disk/ring spectrum pairs are stored separately (`SmoothLifeMultiscale/main.cpp:64-177`). Startup consumes catalogue rows in consecutive triples and defaults to composition method 0 and kernel method 1 (`2760-2894`).
- **SOURCE FACT:** Independent inputs are `(ring_i(A),disk_i(A))`; chained inputs are `(ring_0,ring_1)`, `(ring_1,ring_2)`, `(ring_2,disk_2)` (`2926-3023`).
- **SOURCE FACT:** Sequential recomputes after each clamped update. Ordered clamped sum and arithmetic-mean increment evaluate one shared snapshot; their exact legacy accumulation is in `integrate2D.frag:17-51`.
- **CORRECTED DEFAULT:** The rewrite supports all six 2×3 combinations in 2-D with explicit current/snapshot references and corrected growth/relaxation increments. Sequential always recomputes; shared-snapshot methods never consume updated or stale fields.
- **CORRECTED DEFAULT:** Standard FFT, `512²`, independent inputs, sequential composition, deterministic seed/initializer, and palette 7 are migration-era defaults only if a future local preset is deliberately authored; the retired catalogue itself supplies none of those product dependencies.
- **REJECTED DEFECT:** Multiscale discrete dynamics is rejected until a target-aggregation enum exists. Also rejected: additive mode-0 targets, stale chained fields, undefined mode-2 source/reference, texture feedback, and empty 1-D/3-D integration branches (`SmoothLifeMultiscale/main.cpp:2038-2134`).
### Consulted multiscale files
| Source-relative path | Relevant lines | SHA-256 | Evidence |
| --- | ---: | --- | --- |
| `SmoothLifeMultiscale/main.cpp` | 1-240, 2030-2149, 2740-3112 | `8481ed79b68efb9b824c95d5707477e3d60d213b0ee2ff6cb99a7d2f2484e07e` | Controls/parser/storage, 2-D-only integration dispatch, defaults, six pipelines. |
| `SmoothLifeMultiscale/shaders/snm2D.frag` | 1-129 | `ecf572a8f4262e3cebdca2e086059c63d2cf553a3c195a679792757a3c9a507e` | Per-scale target/increment and missing clamp. |
| `SmoothLifeMultiscale/shaders/integrate2D.frag` | 1-51 | `3276fe14b814e48883dc3eec735e7bd68e06c12ef96acfb1d8ed32fdc924e0ee` | Sequential one-response update, ordered clamped sum, mean increment. |
## Sphere
### Frozen interpretation
- **SOURCE FACT:** Six active `K×K` faces use `K=128`, internal `R=K/2`, and a `3*K*6` by `3*K` atlas (`SmoothLifeSphere/main.cpp:62-76,351-397`).
- **SOURCE FACT:** Face directions use normalized face bases plus tangent coordinates; alpha stores spherical cell area (`512-564`). The original side table and gutter copies are at `765-858`.
- **SOURCE FACT:** Distance is `R*acos(dot(a,b))`; `ri=ra/3`, widths are one, planar radii are converted geodesically, cap areas provide analytic normalization, and updates are replacement or fixed `A+0.1(2S-1)` (`SmoothLifeSphere/main.cpp:1368-1392`; `program.frag:110-162`).
- **CORRECTED DEFAULT:** `SphereModel::Corrected` completes edge/corner mapping, clamps dot products, uses explicit initialized ping-pong resources, and normalizes each center by actual `cell_area*kernel_weight` sums so a constant field remains constant.
- **RETAINED HISTORICAL OPTION:** `SphereModel::Legacy` retains the six-face atlas, side gutters with masked corner sectors, tangent cube-sphere geometry, original radius conversion/search stencil, analytic cap normalization, and the original replacement/fixed-0.1 dynamics.
- **REJECTED DEFECT:** Both models reject unclamped `acos`, undefined atlas contents, and texture feedback. The legacy option is geometrical/numerical, not unsafe-resource compatibility.
### Consulted sphere files
| Source-relative path | Relevant lines | SHA-256 | Evidence |
| --- | ---: | --- | --- |
| `SmoothLifeSphere/main.cpp` | 1-180, 351-960, 1220-1506 | `c770fb36c562135d4bc65aeae36d71ae725286b05aa0ee37f92b6e6f0ddf8b1e` | Config/defaults, atlas allocation, directions/areas, face mapping, rendering, radius conversion, initialization, frame loop. |
| `SmoothLifeSphere/program.frag` | 1-162 | `037da34db6a050422c372bb458c78553df5ca00e22a26e1ced455f0007c6ce21` | Direct geodesic stencil, analytic normalization, common rule, fixed update. |
| `SmoothLifeSphere/SmoothLifeLog.txt` | 1-62 | `535680b9f1669e98ff8ac49882951bc0fa13112607d086d965bc40841f626cdd` | Recorded K/R, planar/geodesic radii, analytic areas, historical environment; evidence only. |
## Delayed time
### Frozen interpretation
- **SOURCE FACT:** The field is periodic in x/y and history is a repeat-wrapped 3-D texture of exactly 16 layers (`SmoothLifeDT/main.cpp:330-394`).
- **SOURCE FACT:** `ri=ra/3`, both widths are one, and the discrete stencil is numerically normalized (`SmoothLifeDT/main.cpp:709-740`).
- **SOURCE FACT:** Spatial radius selects a history coordinate offset by `-distance/16`; smooth mode reads one layer behind and adds fixed `0.1(2S-1)` (`SmoothLifeDT/program.frag:105-137`). The main loop writes output into `layer`, then increments modulo 16 (`SmoothLifeDT/main.cpp:932-1027`).
- **CORRECTED DEFAULT:** `head` means next layer to overwrite, `latest=wrap(head-1,16)`, and each radial sample uses `wrap(latest-floor(distance+0.5),16)`. Delay zero therefore reads the latest committed state. All layers are initialized from one deterministic seeded field.
- **CORRECTED DEFAULT:** A future local preset may deliberately choose `512²`, seed 1, 1,000 deterministic 1019 boxes, and palette 7; these are modern choices, not facts obtained from the retired catalogue.
- **REJECTED DEFECT:** The historical head anomaly—distance zero reading the layer currently being produced/uninitialized—is not selectable. Window-sized shape, gradual history filling, undefined layers, and platform-random boxes are also rejected.
### Consulted delayed-time files
| Source-relative path | Relevant lines | SHA-256 | Evidence |
| --- | ---: | --- | --- |
| `SmoothLifeDT/main.cpp` | 1-160, 330-419, 690-1069 | `0595a283ce29a42ff7e1afc5dc835488feb22e52b85d9ee2e73fdcd75ec767a4` | Config, periodic 16-layer allocation, sampled normalization, window-sized setup, head loop, random boxes. |
| `SmoothLifeDT/program.frag` | 1-137 | `ec5b83df80d39996e99603c5195a43c3ccd0facc22106ab5359e81c8ca318f8e` | Radius-dependent temporal lookup, common rule, fixed smooth update. |
| `SmoothLifeDT/SmoothLifeLog.txt` | 1-64 | `9e14a8712c71900d10787c8b057b18dfe260a4c694ba843da08bc95b50d9654e` | Recorded analytic/sampled sums, shape, and historical environment; evidence only. |
## CPU references
### Role and interpretation
- **SOURCE FACT:** `SmoothLifeFB.bas` is a double-precision, threaded packed-unitary 2-D reference. It contains sampled kernels, packed FFT stages, `sqrt(N)/kernel_sum`, growth derivative, and deterministic mathematical AB startup formulas (`1-300,500-809,870-908`).
- **SOURCE FACT:** `SmoothLifeFB_old.bas` is the earlier discrete packed-FFT reference; it directly commits the target and is useful as an independent data-flow check, not as a supported backend definition (`1-260,570-589`).
- **SOURCE FACT:** Matlab constructs periodic sampled kernels and standard FFT convolution and explicitly compares Euler, improved Euler, AB3 startup, and RK4 with intermediate clamps (`SmoothLifeMatlab/smoothlife.m:1-199`). `result.txt` is historical numerical evidence, not a golden generated by this rewrite.
- **SOURCE FACT:** `GliderConstructor.bas` is an inverse-design/lookup-table experiment with collision reporting, not a general simulator (`1-254`).
- **CORRECTED DEFAULT:** CPU code serves as an oracle for formulas and fixtures. Its platform RNG, drawing, fixed sizes, commented alternatives, and UI behavior are not implementation requirements.
- **REJECTED DEFECT:** AB4, improved Euler, the glider constructor, and old direct-discrete implementation do not expand the mandatory option set. Exactly three historical options remain: RK4 step-origin reference, packed-unitary FFT, and legacy sphere model.
### Consulted CPU/reference files
| Source-relative path | Relevant lines | SHA-256 |
| --- | ---: | --- |
| `SmoothLifeFreeBasic/SmoothLifeFB.bas` | 1-300, 500-909 | `e652379593823fee31cd1a9cf7acc779803aaf04d8efe3f1c0d2f3bf67acd1cb` |
| `SmoothLifeFreeBasic/SmoothLifeFB_old.bas` | 1-260, 570-589 | `0cf2f0eebeaf250209bccaf418dccbe99641ede080bb3930fe54d733b281dbd5` |
| `SmoothLifeFreeBasic/GliderConstructor.bas` | 1-254 | `3afe897aeb2b2ae24e0cc3c8f7371b4ab89ef033ad4524b3f6ef3320f8f5cd26` |
| `SmoothLifeMatlab/smoothlife.m` | 1-199 | `5de29b0fa2d46a9e53d8744f1eeb410bb6c8f7f490ec69be724f4c19a00b4643` |
| `SmoothLifeMatlab/result.txt` | 1-69 | `405c8b43beb77bf883bb00e2471abb88dd50497292162fb709539cd62de2fcf8` |
## Retired catalogues and logs
### Non-import policy
The legacy catalogues are **PROVENANCE ONLY**.
- There is **no JSONL capture** in this task.
- There is **no catalogue migration** in this task.
- There is **no product preset dependency** on any legacy config, catalogue, log, path, row order, or description.
- Application code, tests, builds, and future import outputs must not open the legacy root.
- A future local preset may quote a row only through a separate, explicit, reviewed decision that records its provenance and fills every modern field. It must not revive a live importer or hidden dependency.
Static reconciliation of the files actually present:
| Catalogue | Static contents and parser fact | Status |
| --- | --- | --- |
| Main | 188 lines begin with `1`, `2`, or `3`; `read_config` accepts exactly that first-byte condition. The historical log independently records 188. | Provenance only; no capture or migration. |
| SDL | 187 lines begin with `1`, `2`, or `3`. It is largely duplicate/drifted main data and includes a mode-4 row at line 174. | Provenance only; no SDL product backend. |
| Multiscale | 12 accepted rows, interpreted by startup as four consecutive triplets; the first triplet is duplicated in the `old` section. | Provenance only; grouping evidence only. |
| Sphere | Two numeric candidate rows are physically present at lines 1-2, but `read_config` performs one fixed sequence of `fscanf` calls and therefore loads only the first tuple at startup. | Runtime-accepted count is one; both physical rows remain provenance only. |
| Delayed time | 38 numeric candidate rows are physically present at lines 1-42, but `read_config` likewise loads only the first tuple. This does **not** reconcile with the obsolete Macrostep-00 draft expectation of 29 rows. | Record the discrepancy; do not normalize, capture, or migrate it. Runtime-accepted count is one. |
### Consulted catalogue/config/log files
| Source-relative path | Relevant lines | SHA-256 | Evidence |
| --- | ---: | --- | --- |
| `SmoothLife/SmoothLifeConfig.txt` | 1-219 | `7085be7825a98df0fc3c68d6e9b4da3558955d77dc9f11861d6b1575a0b618a7` | Main raw catalogue and comments. |
| `SmoothLife/SmoothLifeLog.txt` | 1-321 | `295c29ed08d3f9028128fbee49be4de614bf5c4b6778585335b72fe7f1601877` | Historical row count, environment, shader/resource trace, kernel sums; evidence only. |
| `SmoothLifeSDL/SmoothLifeConfig.txt` | 1-219 | `bc86b688f61d325d8b0eed56367dcd0e3eaf16d78bffa90611678f8d614d1c75` | SDL raw catalogue and drift. |
| `SmoothLifeMultiscale/SmoothLifeConfig.txt` | 1-37 | `fd8749d8557e0b927932796a4dcebdb881898fac77503f263b363320f8185985` | Twelve rows/four triplets. |
| `SmoothLifeMultiscale/SmoothLifeLog.txt` | 1-314 | `9f8deb473f9a3e110291537705f2dfdce1a202ffa3cfccf6267c335441ac471c` | Historical 12-row count and environment/resource trace; evidence only. |
| `SmoothLifeSphere/SmoothLifeConfig.txt` | 1-11 | `6aa28438e4a53cab66e0229192235c4699bc518ec8a41e5f6bf6650efa36afa6` | Two physical candidate rows; fixed parser consumes first only. |
| `SmoothLifeDT/SmoothLifeConfig.txt` | 1-50 | `717bf025c67c3bfc8fd266ba8c7b5c871813e3c0030455daa8f5ea87a6b93cb4` | Thirty-eight physical numeric rows; fixed parser consumes first only. |
## Future legacy-consultation protocol
Normal implementation must proceed from the local contract+spec set without reopening the legacy root. Future consultation is allowed only when all of the following hold:
1. A concrete discrepancy is stated: local authority is ambiguous, a fixture disagrees, or two local claims conflict.
2. The exact legacy source-relative path and 1-based line range are recorded.
3. The complete consulted file is SHA-256 hashed and added or updated here.
4. The finding is classified as **SOURCE FACT**, **CORRECTED DEFAULT**, **RETAINED HISTORICAL OPTION**, or **REJECTED DEFECT**.
5. Any implementation-relevant conclusion is copied into the local contract/spec and covered by a local fixture or test; a link to the legacy path is never sufficient.
6. No legacy executable is run. If static evidence cannot resolve the discrepancy, stop and request an explicit new decision rather than infer behavior from an unsafe run.
After that copy-back, the local contract+spec remains the implementation authority and the legacy tree returns to provenance-only status.

627
docs/model-contract.md Normal file
View File

@@ -0,0 +1,627 @@
# SmoothLife model contract
This document is the implementation authority for the mathematical model. It is intentionally independent of the retired application and is sufficient to implement CPU and GPU backends. `docs/historical-options.md` narrows the three historical choices; no other compatibility behavior exists.
## 1. Normative conventions
- Unless a section explicitly says `f32`, the semantic oracle and golden values use IEEE-754 binary64 (`f64`), round-to-nearest, ties-to-even. A production backend may use `f32`, but it must compare to the semantic fixtures under the tolerances below; reassociation or contraction is permitted only when the resulting values remain within the applicable tolerance.
- `TAU = 2π`. The old shader identifier named `pi` represented `TAU`, not π.
- `clamp01(x) = min(1, max(0, x))`. Configuration and input state must be finite; NaN and infinity are validation errors rather than values to be ordered by `min`/`max`.
- `mix(x,y,q) = (1-q)x + qy` in that written operation order.
- `wrap(i,N) = i mod N` with a result in `[0,N)`, including for negative `i`.
- A sum is accumulated in the canonical enumeration order stated below. CPU reference sums use `f64` without parallel reassociation.
- Equality signs and strict inequalities in this document are normative.
Contract fixtures encode authoritative f64 inputs/outputs as JSON numbers using enough decimal digits for binary64 round-trip. Branch/equality classification, indices, clamps, and values produced only by basic operations must be bit-exact in the scalar f64 CPU oracle. Values involving `sin`, `cos`, `atan`, `acos`, `exp`, or long reductions use `|actual-reference| ≤ atol+rtol*|reference|`: `atol=rtol=2^-48` for scalar curve/geometry fixtures and `2^-42` for normalized fields, integration fields, and sphere area/constant-field checks.
For a production f32 CPU/GPU backend compared directly with the f64 semantic fixtures, use `atol=rtol=2^-20` for scalar curves/rules and kernel weights, `2^-16` for one-step normalized planar or multiscale fields, and `2^-15` for one-step sphere or delayed-time fields. These are acceptance limits, not permission to alter branch selection, indexing, support membership, clamp points, or history-layer choice. NaN or infinity always fails. The retained packed-FFT stage-specific f32 tolerances are in section 6.2.
The common state invariant is:
> State is a scalar `A ∈ [0,1]` after every committed update.
Intermediate rule targets and derivatives need not be in `[0,1]`. They are clamped only at the explicit points below.
## 2. Domains, storage, and indexing
Base domains are a periodic one-dimensional circle, two-dimensional torus, or three-dimensional torus. Every active extent is positive. Storage is x-fast row-major:
```text
index1(x) = x
index2(x,y) = x + Nx*y
index3(x,y,z) = x + Nx*(y + Ny*z)
```
Coordinates are wrapped before indexing. Canonical field enumeration is `z`, then `y`, then `x`, with `x` innermost; absent dimensions are omitted. The sample count is `V=Nx`, `Nx*Ny`, or `Nx*Ny*Nz`.
For an even extent `N`, the signed periodic offset represented by an index `i∈[0,N)` is exactly
```text
offset(i,N) = i when i < N/2
i - N otherwise
```
Thus the Nyquist sample `i=N/2` has offset `-N/2`. For odd `N`, use `i` when `i≤floor(N/2)` and `i-N` otherwise. Euclidean lattice distance is the square root of the sum of squared signed offsets.
A sphere is one closed spherical surface represented by six active face arrays, not six independent boundaries. Delayed time has a two-dimensional periodic spatial torus and a circular temporal history.
## 3. Sampled kernels and convolution
### 3.1 Kernel functions
For the base model, validate `ra>0`, `rr>0`, and `rb>0`, then define
```text
ri = ra / rr
w = ra / rb
```
For `w>0`, the sampled ramp is
```text
L(r;a,w) = 0 when r < a-w/2
1 when r > a+w/2
(r-a)/w + 1/2 otherwise
```
At `r=a-w/2` the third branch evaluates to exactly zero; at `r=a+w/2` it evaluates to exactly one. The kernels are
```text
KD(r) = 1 - L(r;ri,w)
KR(r) = L(r;ri,w) * (1 - L(r;ra,w))
```
Weights are sampled at lattice points. A support entry exists iff its evaluated weight is strictly greater than zero. Generate the complete nonzero support by examining every unique periodic offset in the domain; never stop at the historical connected-component cutoff. Entries are stored in canonical field-index order.
For a radial kernel `K`, circular convolution is
```text
(A * K)(x) = Σ_o A(wrap(x-o)) K(o)
```
where wrapping is component-wise. Neighborhood fields are
```text
M = (A * KD) / Σ_o KD(o)
N = (A * KR) / Σ_o KR(o)
```
The two sums must be finite and strictly positive or validation fails. Division occurs after the complete numerator sum. A known two-dimensional check, using the canonical signed-offset grid and `ra=10`, `rr=3`, `rb=10`, is
```text
sum(KR) ≈ 279.216312
sum(KD) ≈ 35.524035
```
The check is diagnostic rather than a decimal truncation target.
### 3.2 Kernel suitability diagnostics
A configuration remains deterministic when support wraps, but must emit a non-fatal `support_touches_nyquist` warning if any nonzero support sample has `|o_j|=N_j/2` on an even axis. Equivalently for an isotropic base ring, the warning begins when `ra+w/2` is strictly greater than half an active extent; equality has zero outer-boundary weight and does not warn by itself. Emit `support_covers_domain` if every unique periodic offset has a nonzero disk or ring weight. Zero or non-finite normalization is an error, not a warning. These diagnostics never truncate support or alter weights.
## 4. Rule curves
All smooth widths `e`, including `sn` and `sm`, must be finite and strictly positive. Let
```text
u = (x-a+e/2)/e
logistic(x;a,e) = 1 / (1 + exp(-4(x-a)/e))
```
The rising curve `P_t(x,a,e)` is selected by `t∈0..7`:
```text
0 hard: x >= a ? 1 : 0
1 linear: 0 if x < a-e/2
1 if x > a+e/2
u otherwise
2 Hermite: 0 if x < a-e/2
1 if x > a+e/2
u²(3-2u) otherwise
3 sine: 0 if x < a-e/2
1 if x > a+e/2
0.5*sin((TAU/2)*(x-a)/e)+0.5 otherwise
4 logistic: logistic(x;a,e)
5 atan: atan((x-a)*(TAU/2)/e)/(TAU/2)+0.5
6 atan/cos: (1.1*atan((x-a)/e)/(TAU/4)*cos(1.4*(x-a))+1)/2
7 overshoot: (logistic(x;a,e)-0.5)
*(1+exp(-(x-a)²/e²))+0.5
```
At `x=a±e/2`, compact curves 13 use their formula branch (not the outer constant branch); that branch selection is fixed for fixtures. Curves 47 are not compactly clamped.
For window types `t∈0..7`,
```text
W_t(n;a,b,e) = P_t(n,a,e) * (1-P_t(n,b,e))
```
A hard window is exactly `[a,b)`: it is one at `n=a` and zero at `n=b`. Types 8 and 9 are complete windows and do not invoke `P_8` or `P_9`:
```text
base = logistic(n;a,e) * (1-logistic(n;b,e))
mid = (a+b)/2
g = exp(-(20*(n-mid))²)
W_8 = base*(1-0.2*g)
W_9 = base*(1+0.2*g)
```
The mixer curve has its own selector `m∈0..7` only:
```text
Q = P_m(M, 0.5, sm)
```
Curves 6 and 7, mixer values, interpolated thresholds, windows, and resulting targets can overshoot. Do not clamp them during rule evaluation.
With `B=W_t(N;b1,b2,sn)` and `D=W_t(N;d1,d2,sn)`, the four rule constructions are exactly
```text
1: S = mix(B,D,M)
2: S = mix(B,D,Q)
3: S = W_t(N, mix(b1,d1,M), mix(b2,d2,M), sn)
4: S = W_t(N, mix(b1,d1,Q), mix(b2,d2,Q), sn)
```
`M` and `N` are sampled normalized neighborhoods. No implicit clamp occurs between any expression above.
## 5. Dynamics and integration
For state `Y`, let `S(Y)` mean recomputing its neighborhoods and target.
```text
Discrete: next = clamp01(S(A))
Growth: f(Y) = 2*S(Y)-1
Relaxation: f(Y) = S(Y)-Y
```
Discrete dynamics ignore `dt` and the selected integrator. Experimental numeric modes 3 and 4 are unsupported.
### 5.1 Euler and AdamsBashforth
Euler commits
```text
A_next = clamp01(A + dt*k_n), k_n=f(A)
```
AB3 has deterministic startup and clamps only the committed result:
```text
no prior derivative: Δ = k_n
one prior derivative: Δ = (3*k_n-k_{n-1})/2
at least two derivatives: Δ = (23*k_n-16*k_{n-1}+5*k_{n-2})/12
A_next = clamp01(A + dt*Δ)
```
After a successful commit, shift in the derivative evaluated at the pre-step state. Undefined or invalid history is never partially reused.
### 5.2 RK4 and relaxation reference
Let `A0` be the state at step start. Every intermediate state and the final state is clamped element-wise:
```text
k1 = f1(A0)
Y2 = clamp01(A0 + dt*k1/2)
k2 = f2(Y2)
Y3 = clamp01(A0 + dt*k2/2)
k3 = f3(Y3)
Y4 = clamp01(A0 + dt*k3)
k4 = f4(Y4)
A_next = clamp01(A0 + dt*(k1+2*k2+2*k3+k4)/6)
```
For Growth, every `fj(Y)=2*S(Y)-1`. For Relaxation the local option is:
```text
StageState (default): f1(A0)=S(A0)-A0; fj(Y)=S(Y)-Y
StepOrigin: f1(A0)=S(A0)-A0; fj(Y)=S(Y)-A0, j=2..4
```
`Rk4RelaxationReference` exists structurally only for Relaxation+RK4. It is absent—not stored and ignored—for Discrete, Growth, Euler, and AB3.
### 5.3 AB history invalidation
Preserve AB derivative history across presentation-only changes, inspection changes, pause/resume, and scheduler-rate changes. Invalidate it completely on:
- reset;
- ordinary state load;
- initializer or seed change/use;
- rule change;
- dynamics change;
- timestep change;
- integrator change;
- kernel geometry change;
- any historical-option change;
- topology or shape change;
- variant change;
- backend change.
Switching away from AB3 and later back never revives derivatives. An exact continuation checkpoint may restore history only after its full validated model and backend-independent arithmetic descriptor matches: schema/version, variant, topology, shape, scalar representation, rule, dynamics, `dt`, integrator, kernels, applicable historical options, and arithmetic contract. A backend identifier may differ only where that backend is certified to implement the same arithmetic descriptor. Any missing, malformed, non-finite, wrong-shaped, or mismatched derivative discards all AB history. This list is authoritative.
## 6. FFT algorithms
Planar and multiscale convolution have one local algorithm selection.
### 6.1 Standard
For sample count `V`, Standard uses the conventional DFT
```text
F[k] = Σ_x A[x] exp(-i*TAU*k·x/N)
A[x] = (1/V) Σ_k F[k] exp(+i*TAU*k·x/N)
```
applied separably. Multiply state and sampled-kernel spectra, inverse transform, then divide by the corresponding sampled `f64` kernel sum. Forward is unscaled and inverse is scaled by `1/V`.
### 6.2 LegacyPackedUnitary arithmetic
`LegacyPackedUnitary` is a complete GPU algorithm, not a compatibility mode. Every active extent must be a power of two, `Nx≥2`, and x must be even. One-dimensional, two-dimensional, and three-dimensional arrays use adjacent-real x packing and a half-width spectrum.
For `m=Nx/2`, pack
```text
p[j] = complex(A[2j], A[2j+1]), j=0..m-1
```
and compute a unitary radix-2 FFT `P=U_m(p)`. For `k=0..m`, with indices modulo `m`,
```text
Pk = P[k mod m]
Qk = conjugate(P[(m-k) mod m])
E = (Pk+Qk)/2
O = (Pk-Qk)/(2i)
X[k] = (E + exp(-i*TAU*k/Nx)*O)/sqrt(2)
```
This is the unitary real transform. Transform the full y axis, then the full z axis, for every stored x frequency; absent axes are skipped. Each is the same unitary complex transform. The stored spectrum shape is `(Nx/2+1,Ny,Nz)` with omitted dimensions understood.
Inverse y and z stages run in reverse axis order. Recover packed x data for `k=0..m-1` by
```text
E = (X[k] + conjugate(X[m-k]))/sqrt(2)
O = conjugate(exp(-i*TAU*k/Nx))
*(X[k] - conjugate(X[m-k]))/sqrt(2)
P[k] = E + i*O
```
then apply the unitary inverse `U_m⁻¹` and unpack real and imaginary components to adjacent real samples.
A unitary inverse of a spectral product produces circular convolution divided by `sqrt(V)`. Therefore each spectral product is corrected, before inverse stages, by exactly
```text
correction = sqrt(V) / kernel_sum
H[k] = correction * (F_A[k] * F_K[k])
```
where `kernel_sum` is the complete sampled-kernel sum. This yields the normalized neighborhood directly.
The historical plan is an output-indexed radix-2 plan. For an ordinary complex axis of length `n=2^B`, execute stages `e=1..B`. At output index `q`, let `=2^e` and `j=q mod `. Select inputs
```text
j < /2: ia=q, ib=q+/2
otherwise: ia=q-/2, ib=q
```
At `e=1` only, replace both selected indices by their `B`-bit reversals. Later stages use them directly. The plan twiddle and output are
```text
w = exp(sign*i*TAU*j/) # sign=-1 forward, +1 inverse
out[q] = (input[ia] + w*input[ib]) / sqrt(2)
```
For `j≥/2`, the twiddle already contains the minus sign of the upper butterfly; do not rewrite that output as a separately ordered subtraction. Packed x first runs this plan at length `m=Nx/2` through stages `1..log2(m)`.
The x real/complex conversion is a distinct tangle stage and fixes the operation order behind the mathematical formulas above. For each stored `k=0..m`, set `a=P[k mod m]`, `b=conjugate(P[(m-k) mod m])`, and
```text
forward w = exp(-i*TAU*(k/Nx+1/4))
X[k] = (a+b + (a-b)*w) * (0.5/sqrt(2))
```
For inverse conversion only `k=0..m-1` is produced. Set `a=X[k]`, `b=conjugate(X[m-k])`, and
```text
inverse w = exp(+i*TAU*(k/Nx+1/4))
P[k] = (a+b + (a-b)*w) * (0.5*sqrt(2))
```
Then run ordinary packed-x stages with inverse sign. Forward axis order is packed x stages, forward tangle, y, z. Inverse order is z, y, inverse tangle, packed x stages. Stages ping-pong; no pass reads and writes the same resource.
All arithmetic in this subsection after input conversion is operation-ordered IEEE binary32. Let `fl` mean one round-to-nearest-ties-even `f32` operation. Subnormals are preserved and fused multiply-add is forbidden. An ordinary plan output is exactly
```text
p0=fl(wr*br); p1=fl(wi*bi)
r = fl(fl(ar+p0)-p1)
p2=fl(wr*bi); p3=fl(wi*br)
i = fl(fl(ai+p2)+p3)
out.r=fl(r*INV_SQRT_2_F32)
out.i=fl(i*INV_SQRT_2_F32)
```
A tangle first forms `dr=fl(ar-br)`, `di=fl(ai-bi)`, evaluates `(dr+i*di)*w` with the same four-product order, forms `sr=fl(ar+br)` and `si=fl(ai+bi)`, adds product to sum component-wise, then multiplies by its rounded tangle scale. Conjugating `b` is an exact sign-bit change before those operations.
`INV_SQRT_2_F32`, both tangle scales, twiddle components, and `correction` are each rounded once to `f32` before use; twiddles are generated from the corresponding binary64 `sin`/`cos`. For spectral multiplication, first compute `br=fl(F_K.r*correction)` and `bi=fl(F_K.i*correction)`, then multiply `F_A` by that scaled complex kernel using the same four-product sequence. Packing and unpacking are exact component copies. This operation order defines the CPU stage oracle.
Integer plans and bit-reversal indices must match exactly. Contract fixtures encode packed values, every bit-reversal and butterfly stage, real/complex conversion, spectra, correction, and normalized convolution as hexadecimal `f32` bits. The scalar CPU oracle must be bit-exact to them. GPU fixture comparison uses
```text
|actual-reference| <= atol + rtol*|reference|
```
with finite values required:
| Check | `atol` | `rtol` |
| --- | ---: | ---: |
| packing and one-butterfly microfixtures | `0` (bit-exact) | `0` |
| each complete FFT stage and real/complex conversion | `2^-20` | `2^-20` |
| final half spectrum | `2^-18` | `2^-18` |
| normalized convolution | `2^-16` | `2^-16` |
The operation-ordered f32 exception is local to this algorithm. Rule evaluation, integration, and their semantic fixtures remain defined by the f64 oracle, while a production GPU may keep convolution results in f32 and validate the subsequent common pipeline under the f32 backend tolerances above; no readback or widening is required during ordinary execution. An explicit request on CPU or unsupported GPU hardware fails clearly and never substitutes Standard. Complete kernels and safe resources are used by both algorithms.
## 7. Multiscale variant
There are exactly three scales, indexed in order `0,1,2`. Each has its own sampled disk/ring kernels, rule, and timestep `dt_i`. It supports Growth and corrected Relaxation only; Discrete is rejected because no target-aggregation policy is defined.
Two neighborhood interpretations are crossed with three composition policies, giving exactly six combinations:
| Inputs | Sequential | Ordered clamped sum | Mean increment |
| --- | --- | --- | --- |
| Independent | yes | yes | yes |
| Chained | yes | yes | yes |
At any evaluated state `Y`, Independent supplies
```text
scale i: (N_i,M_i) = (ring_i(Y), disk_i(Y))
```
Chained supplies
```text
scale 0: (N_0,M_0) = (ring_0(Y), ring_1(Y))
scale 1: (N_1,M_1) = (ring_1(Y), ring_2(Y))
scale 2: (N_2,M_2) = (ring_2(Y), disk_2(Y))
```
Tuple order is always `(N,M)`. A needed field is recomputed from the specified state even if another scale has the same nominal field.
For a scale target `S_i(Y)`, define
```text
Growth response: g_i(Y;Aref) = 2*S_i(Y)-1
Relaxation response: g_i(Y;Aref) = S_i(Y)-Aref
increment: I_i = dt_i*g_i
```
Composition is:
1. **Sequential stage relaxation.** Set `Y_0=A0`. For `i=0,1,2`, recompute every required field from `Y_i`; use `Aref=Y_i` for Relaxation; then `Y_{i+1}=clamp01(Y_i+dt_i*g_i(Y_i;Y_i))`. Commit `Y_3`.
2. **Ordered clamped sum.** Compute all three targets from one unchanged snapshot `A0`; Relaxation uses `Aref=A0`. Set `Y_0=A0`, then in scale order `Y_{i+1}=clamp01(Y_i+dt_i*g_i(A0;A0))`. Commit `Y_3`. Clamping the accumulator does not change any already computed response.
3. **Mean increment.** Compute all targets from snapshot `A0`, with Relaxation reference `A0`, then commit `clamp01(A0+(I_0+I_1+I_2)/3)` with one final clamp.
This is per-scale Euler composition; the base Euler/AB3/RK4 selector does not apply. Shared-snapshot methods never read an intermediate accumulator. Additive discrete responses, stale chained fields, feedback reads, and undefined relaxation sources are rejected.
## 8. Sphere variant
### 8.1 Geometry and area
`K` must be positive and even; the default is `K=128`. There are six active `K×K` face arrays, x-fast within each face, in this fixed order and frame:
| face | normal `n` | `u_axis` | `v_axis` |
| --- | --- | --- | --- |
| 0 `+X` | `(1,0,0)` | `(0,1,0)` | `(0,0,1)` |
| 1 `+Y` | `(0,1,0)` | `(-1,0,0)` | `(0,0,1)` |
| 2 `-X` | `(-1,0,0)` | `(0,-1,0)` | `(0,0,1)` |
| 3 `-Y` | `(0,-1,0)` | `(1,0,0)` | `(0,0,1)` |
| 4 `+Z` | `(0,0,1)` | `(1,0,0)` | `(0,1,0)` |
| 5 `-Z` | `(0,0,-1)` | `(1,0,0)` | `(0,-1,0)` |
`u_axis×v_axis=normal`. Let `R=K/2`. A face sample `(x,y)` is at
```text
u = 2*(x+0.5)/K - 1
v = 2*(y+0.5)/K - 1
d = normalize(n + tan(u*π/4)*u_axis + tan(v*π/4)*v_axis)
```
Cell area is the spherical area of its four mapped corner rays. For unit rays `a,b,c`, define
```text
tri(a,b,c) = 2*atan2(abs(a·(b×c)), 1+a·b+b·c+c·a)
```
With corners in `(u,v)` order `d00,d10,d11,d01`,
```text
cell_area = R²*(tri(d00,d10,d11)+tri(d00,d11,d01))
```
Canonical global enumeration is face `0..5`, then y, then x. Area totals must sum to `4πR²` within f64 fixture tolerance.
Geodesic distance is
```text
distance(a,b) = R*acos(clamp(a·b,-1,1))
```
The inner radius is first defined in planar units and both radii are converted once:
```text
ri_planar = ra_planar/3
ri = R*acos(1-ri_planar²/(2R²))
ra = R*acos(1-ra_planar²/(2R²))
```
Thus `ri=ra/3` is the planar-radius relation; nonlinear geodesic conversion is applied separately to each radius. Validate `0<ra_planar≤2R`; do not silently clamp either conversion argument. Both kernel transition widths are exactly `1`:
```text
KD(r)=1-L(r;ri,1)
KR(r)=L(r;ri,1)*(1-L(r;ra,1))
```
### 8.2 Corrected sphere
`SphereModel::Corrected` is storage-independent. For every center sample, enumerate all samples in all six active arrays, evaluate geodesic distance and every nonzero kernel weight, and accumulate in canonical global order:
```text
M(c) = Σ_j area(j)*KD(distance(c,j))*A(j)
/ Σ_j area(j)*KD(distance(c,j))
N(c) = Σ_j area(j)*KR(distance(c,j))*A(j)
/ Σ_j area(j)*KR(distance(c,j))
```
The denominators are the actual per-center sampled sums, not analytic cap areas or a shared center-independent approximation. This guarantees a constant field maps to the same constant, including at face edges and cube corners. Complete geodesic support includes every positive weight on any face; atlas adjacency and storage gutters must not affect the result. Nonpositive/non-finite denominators are errors. Emit a warning if the outer nonzero support crosses a hemisphere (`ra+0.5>πR/2`), but still globally enumerate it.
### 8.3 Legacy sphere
The precise retained Legacy geometry, deterministic side-gutter masking, normalization, and constraints are specified in `docs/historical-options.md`. It uses the same face frames and active-cell areas, but a face-local stencil rather than corrected global enumeration. Both sphere models clamp dot products, initialize all storage, and use ping-pong commits.
Sphere update modes are
```text
Direct: A_next = clamp01(S)
Smooth: A_next = clamp01(A + 0.1*(2*S-1))
```
`0.1` is fixed and is not a configurable timestep.
## 9. Delayed-time variant
The delayed-time field is a periodic `Nx×Ny` torus with depth `D=16`. Its sampled spatial kernels use `ri=ra/3` and transition width `1` for both disk and ring. Let `h=ceil(ra+0.5)` and enumerate the direct stencil in `dy=-h..h`, then `dx=-h..h` order. Evaluate both weights at every offset in that square; zero-weight offsets may be skipped by an optimized implementation but do not affect either sum. Spatial coordinates wrap only when sampling history, so distinct stencil offsets remain distinct even if a small torus maps them to the same cell. Emit a non-fatal geometry warning when `h` exceeds half either spatial extent.
History is `H[0..15]`. `head` is the next layer to overwrite and
```text
latest = wrap(head-1,16)
delay(o) = floor(distance(o)+0.5)
layer(o) = wrap(latest-delay(o),16)
```
The `+0.5` equality rounds upward: a distance exactly `q+0.5` selects delay `q+1`. Delay zero reads the latest committed state. The delayed neighborhoods are
```text
M(x) = Σ_o KD(distance(o))*H[layer(o)][wrap(x-o)] / Σ_o KD(distance(o))
N(x) = Σ_o KR(distance(o))*H[layer(o)][wrap(x-o)] / Σ_o KR(distance(o))
```
Both sums use the direct `dy`, then `dx` stencil order above. Temporal layer choice does not change normalization.
A step reads the entire old history and computes from `A=H[latest]`:
```text
Direct: next = clamp01(S)
Smooth: next = clamp01(H[latest] + 0.1*(2*S-1))
```
It then writes `next` to `H[head]` and only afterward sets `head=wrap(head+1,16)`. No pass reads the layer it is currently writing. Initialization replicates one field into every layer and sets `head=0`, so the initial latest layer is 15. The old non-causal head anomaly is not configurable.
## 10. Deterministic PRNG
All seeded initializers use ChaCha12 with the original 16-word layout:
```text
state[0..4] = LE words of ASCII "expand 32-byte k"
state[4..12] = eight little-endian key words
state[12..14] = little-endian 64-bit block counter
state[14..16] = little-endian 64-bit stream value
```
The 32-byte key is the seed encoded as one little-endian `u64` followed by 24 zero bytes. Counter and stream both start at zero. A block applies six ChaCha double rounds (12 rounds total), adds the original state word-by-word with wrapping `u32` addition, emits words `0..15` sequentially in little-endian order, and increments only the 64-bit counter modulo `2^64`.
A quarter round on `(a,b,c,d)` is exactly
```text
a += b; d ^= a; d = rotl(d,16)
c += d; b ^= c; b = rotl(b,12)
a += b; d ^= a; d = rotl(d, 8)
c += d; b ^= c; b = rotl(b, 7)
```
Each double round applies columns `(0,4,8,12)`, `(1,5,9,13)`, `(2,6,10,14)`, `(3,7,11,15)`, then diagonals `(0,5,10,15)`, `(1,6,11,12)`, `(2,7,8,13)`, `(3,4,9,14)`.
`next_u32` consumes sequential output words. `next_u64` consumes two words and places the first in the low half:
```text
lo = next_u32(); hi = next_u32()
value = u64(lo) | (u64(hi)<<32)
```
A unit binary64 draw is
```text
unit = f64(next_u64() >> 11) * 2^-53 # [0,1)
```
For an unbiased integer in `[0,b)`, require `0<b≤2^64`, set `limit=2^64-(2^64 mod b)`, repeatedly draw `x=next_u64()` while `x≥limit`, then return `x mod b`. Rejected words are consumed. For `b=2^64`, every word is accepted directly. An integer range offsets this result; a real `[lo,hi)` draw is `lo+(hi-lo)*unit` in written order. There are no hidden warm-up draws, per-thread streams, or implementation-library distribution calls.
## 11. Initializers
All initializers are newly authored, topology-native algorithms. They do not reproduce, capture, import, or migrate legacy preset state. Painting loops use canonical storage enumeration and consume no random values.
### 11.1 Periodic planar splats
Clear the field to zero. For dimension `d`, calculate once
```text
count = floor(product(axis_extent)
/ product(min(2*ra,axis_extent))) + 1
```
using f64 products in axis order x, y, z. For each splat, draw continuous center coordinates in axis order (`x`, then `y`, then `z` as applicable), each uniform in `[0,extent)`, then draw radius uniform in `[0.5*ra,ra)`. Thus each splat consumes `d+1` unit draws.
For every lattice sample, compute component-wise shortest periodic distance from its integer coordinate to the continuous center, then Euclidean distance. Set the sample to one iff `distance < radius`; equality is not painted. Splats overwrite with one and overlaps consume no draws. This applies to the circle, 2-D torus, and 3-D torus.
### 11.2 Geodesic sphere overlays
Clear all six active face arrays to zero. Perform exactly 1,000 overlays. For each overlay, in this order:
1. `face = bounded_u64(6)`;
2. `cx = bounded_u64(K)` and `cy = bounded_u64(K)`;
3. `radius = 2 + bounded_u64(6)`, giving integers 2 through 7 inclusive;
4. `value = bounded_u64(2)` converted to `0.0` or `1.0`.
Use the selected active sample `(face,cx,cy)` as the center. Enumerate every sample on every face and assign `value` iff its clamped-dot geodesic distance from the center is strictly `< radius`. Equality is not painted. Later overlays win. Global geodesic painting deliberately crosses every face seam and cube corner and is used for both sphere models; no atlas gutter is initialized by randomness.
### 11.3 Periodic delayed-time boxes
Require positive extents. Clear one `Nx×Ny` field to zero and perform exactly 1,000 boxes. For each box draw, in order,
```text
x0 = bounded_u64(Nx)
y0 = bounded_u64(Ny)
w = 10 + bounded_u64(10) # 10..19 inclusive
h = 10 + bounded_u64(10) # 10..19 inclusive
```
Set to one all wrapped coordinates `(x0+dx,y0+dy)` with `dx∈[0,w)` and `dy∈[0,h)`. Boxes are half-open on their high edges and later boxes also write one. Replicate the completed field bit-for-bit into all 16 history layers and set `head=0`. There is no gradual fill and no undefined layer.
## 12. Newly authored preset policy
Bundled presets are authored directly in the versioned product schema. There is no legacy row capture, importer, conversion, deduplication, migration, count target, grouping target, or value-correspondence requirement. Each preset has a stable ID, name, description, tags, authoring provenance, explicit variant/rules/dynamics/shape/backend, explicit seed and initializer, and presentation recommendations. It stores only historical-option fields applicable to that run.
Unless a newly authored preset explicitly overrides them, authoring baselines are:
- Base: shape `1024`, `512×512`, or `64×64×64` by dimension; Euler; Standard FFT; seed 1; periodic planar splats; palette 2; 3-D volume style 2. Euler stores no RK4-reference field. A newly applicable Relaxation+RK4 field defaults to StageState.
- Multiscale: `512×512`; Sequential; Independent; Growth or Relaxation only; Standard FFT; seed 1; periodic planar splats; palette 7.
- Sphere: `K=128`, `R=64`; Corrected; seed 1; geodesic overlays; palette 1. A Legacy preset must be separately named and explicitly select only that local option.
- Delayed time: `512×512×16` history; causal delay; seed 1; one periodic box field replicated to every layer; palette 7.
These are modern product-authoring choices, not inferred migration defaults. Overrides are explicit and remain subject to this contract.
## 13. Clamp-point summary
No rule curve, mixer, threshold interpolation, kernel convolution, or target is implicitly clamped. Clamp exactly at:
- Discrete direct commit;
- Euler and every AB committed state;
- every RK4 intermediate state and final state;
- each Sequential multiscale stage;
- each Ordered-clamped-sum accumulator stage;
- the one Mean-increment multiscale commit;
- sphere Direct/Smooth commits;
- delayed-time Direct/Smooth commits;
- dot products immediately before every `acos`, to `[-1,1]`.
Initializers produce only zero or one. This list is exhaustive.

View File

@@ -13,7 +13,6 @@ None.
- `docs/model-contract.md`: exact mathematics and update semantics. - `docs/model-contract.md`: exact mathematics and update semantics.
- `docs/historical-options.md`: the three retained options, their exact semantics, constraints, and rejected historical behavior. - `docs/historical-options.md`: the three retained options, their exact semantics, constraints, and rejected historical behavior.
- `docs/legacy-source-map.md`: provenance with source file, line range, and source-file hash. - `docs/legacy-source-map.md`: provenance with source file, line range, and source-file hash.
- `tests/fixtures/legacy/preset_rows.jsonl`: schema-neutral bootstrap capture of every accepted legacy row.
- `tests/fixtures/contract/`: small deterministic scalar/field fixtures. - `tests/fixtures/contract/`: small deterministic scalar/field fixtures.
## Phase 0.1 — Freeze the shared mathematical contract ## Phase 0.1 — Freeze the shared mathematical contract
@@ -178,31 +177,31 @@ Freeze:
The historical head anomaly is discarded and has no configuration option. The historical head anomaly is discarded and has no configuration option.
## Phase 0.4 — Preset and fixture capture ## Phase 0.4 — Preset baseline and fixture capture
### Substep 0.4.1 — Schema-neutral bootstrap capture ### Substep 0.4.1 — Freeze product preset authoring policy
Before the Rust schema exists, use a tiny reviewed extraction script to copy accepted source rows verbatim into JSONL with source path, line number, parsed columns, description, and source hash. This is evidence capture, not the final application importer. Expected counts are: All bundled product presets are newly authored directly in the versioned product schema established by Macrostep 01. Do not extract, convert, deduplicate, or migrate legacy preset rows, and do not require a product-preset count, grouping, or value correspondence with any legacy catalogue.
- main catalogue: 188 valid rows; Each bundled preset must be a complete deterministic run definition with:
- SDL catalogue: 187 rows, largely duplicate and containing drift;
- multiscale catalogue: 12 rows = 4 triplets;
- sphere catalogue: 1 row;
- DT catalogue: 29 rows.
Do not deduplicate the evidence and do not run legacy executables. Macrostep 01 converts this frozen capture into the versioned schema and performs deliberate deduplication. - a stable ID, name, description, tags, and authoring provenance;
- explicit variant, rules, dynamics, shape, backend, and presentation recommendations;
- an explicit seed and deterministic initializer;
- only the localized historical-option fields applicable to that run, with modern defaults otherwise.
### Substep 0.4.2 — Freeze migration defaults absent from rows Preset reviews validate schema completeness, unique identity, deterministic reproduction, and intentional coverage of supported variants and dynamics. Legacy sources remain documented provenance for the mathematical contract and retained option semantics, not input data for product presets.
Commit a mapping table so every imported row becomes a complete run: ### Substep 0.4.2 — Freeze deterministic preset baselines
- Base: shape `1024`/`512²`/`64³` by dimension, Euler, standard FFT, seed `1`, cleared C++-inspired periodic splats, palette 2, and 3-D volume style 2. Because Euler is selected, no RK4-reference field is stored; creating relaxation+RK4 later defaults that newly applicable field to `StageState`. Use these modern baselines when authoring bundled presets; an individual preset may intentionally override them, but every override is explicit in that preset rather than inferred by migration:
- SDL rows: same model mapping, source-tagged and deduplicated against main rows; no separate backend.
- Base: recommended shape `1024`/`512²`/`64³` by dimension, Euler, standard FFT, seed `1`, cleared C++-inspired periodic splats, palette 2, and 3-D volume style 2. Because Euler is selected, no RK4-reference field is stored; a newly authored relaxation+RK4 preset defaults its applicable field to `StageState`.
- Multiscale: `512²`, sequential composition, independent kernels, growth/relaxation only, standard FFT, seed `1`, cleared base-style splats, and palette 7. - Multiscale: `512²`, sequential composition, independent kernels, growth/relaxation only, standard FFT, seed `1`, cleared base-style splats, and palette 7.
- Sphere: `K=128`, `R=K/2`, seed `1`, cleared then seeded overlays, and palette 1. Generate a corrected-default preset and a separately named `SphereModel::Legacy` preset from the historical row. - Sphere: `K=128`, `R=K/2`, `SphereModel::Corrected`, seed `1`, cleared then seeded overlays, and palette 1. Any newly authored preset selecting `SphereModel::Legacy` is separately named and explicitly selects only that local option.
- DT: no historical fixed shape existed because it followed the window; choose and record `512²`, depth 16, causal delay, seed `1`, a seeded box field replicated into all layers, and palette 7. Record this initializer/shape as an intentional modern replacement. - DT: `512²`, depth 16, causal delay, seed `1`, a seeded box field replicated into all layers, and palette 7. The fixed model shape and all-layer initializer are intentional modern choices because the historical implementation followed the window.
The table must distinguish source facts from chosen deterministic replacements. These baselines are product-authoring policy, not legacy-row defaults.
### Substep 0.4.3 — Freeze initializer algorithms ### Substep 0.4.3 — Freeze initializer algorithms
@@ -234,11 +233,11 @@ Use explicit arrays for essential goldens, not only PRNG seeds.
2. Record source paths and relevant line ranges. 2. Record source paths and relevant line ranges.
3. Mark the specification plus the new contract as the implementation authority. 3. Mark the specification plus the new contract as the implementation authority.
4. Future legacy consultation requires a concrete discrepancy, and its finding must be copied back into the contract with provenance. 4. Future legacy consultation requires a concrete discrepancy, and its finding must be copied back into the contract with provenance.
5. The application, build, tests, and importer outputs must not require the legacy path. 5. The application, build, tests, and bundled preset assets must not require the legacy path.
## Exit gate ## Exit gate
- Every formula, constant, mode, topology, retained historical option, and discarded defect has a local documented decision. - Every formula, constant, mode, topology, retained historical option, and discarded defect has a local documented decision.
- Preset counts/grouping reconcile with the source catalogues. - The newly authored bundled-preset policy and deterministic baselines are frozen independently of legacy catalogue rows.
- Fixtures are readable without legacy tools. - Fixtures are readable without legacy tools.
- A new implementer can build any mandatory backend using only this repository. - A new implementer can build any mandatory backend using only this repository.

View File

@@ -72,7 +72,7 @@ Validation returns field-specific, actionable errors and rejects:
- invalid/zero shape extents; - invalid/zero shape extents;
- unsupported dynamics/integrator combinations (the tagged schema makes an irrelevant RK4 reference unrepresentable); - unsupported dynamics/integrator combinations (the tagged schema makes an irrelevant RK4 reference unrepresentable);
- statically invalid `LegacyPackedUnitary` combinations such as CPU backend, non-power-of-two shape, or unsupported dimension; - statically invalid `LegacyPackedUnitary` combinations such as CPU backend, non-power-of-two shape, or unsupported dimension;
- empty scales or a non-three-scale imported multiscale preset; - empty scales or any multiscale configuration whose scale count is not three;
- impossible history depth or sphere face size. - impossible history depth or sphere face size.
Split validation into two stages. Macrostep 01 performs schema/static validation without a graphics context. Macrostep 05 adds runtime capability validation for texture limits, float FBOs, resource counts, memory, and actual legacy-FFT support; loading may be statically valid yet fail a requested GPU construction with an actionable runtime error. Split validation into two stages. Macrostep 01 performs schema/static validation without a graphics context. Macrostep 05 adds runtime capability validation for texture limits, float FBOs, resource counts, memory, and actual legacy-FFT support; loading may be statically valid yet fail a requested GPU construction with an actionable runtime error.
@@ -86,7 +86,7 @@ Every preset has:
- stable slug/UUID; - stable slug/UUID;
- display name and description; - display name and description;
- variant and schema version; - variant and schema version;
- source provenance and optional original row; - authoring provenance and optional links to relevant model-contract or legacy-source-map entries;
- tags; - tags;
- deterministic default seed; - deterministic default seed;
- relevant localized historical-option values; - relevant localized historical-option values;
@@ -104,15 +104,11 @@ Define a normalized run descriptor suitable for logs, state exports, and bug rep
- Save settings atomically through temp-file + rename. - Save settings atomically through temp-file + rename.
- Preserve unknown newer schema versions by refusing destructive writes. - Preserve unknown newer schema versions by refusing destructive writes.
### Substep 1.3.2 — Schema-backed legacy conversion ### Substep 1.3.2 — Newly authored bundled presets
Consume Macrostep 00's frozen JSONL evidence and migration-default table to generate final versioned presets: Author bundled product presets directly in the versioned schema using Macrostep 00's deterministic preset baselines. There is no legacy-row converter, migration-default layer, catalogue deduplication, or required correspondence to legacy source counts or groupings.
- 15-column base rows; Require every bundled preset to specify all run-defining fields, a stable unique identity, authoring provenance, deterministic seed/initializer, applicable localized historical options, and recommended presentation. Validate and round-trip each preset independently, reject duplicate IDs, verify deterministic reproduction from its run descriptor, and record a manifest hash for the authored set.
- groups of three multiscale rows;
- 11-column sphere/DT rows.
The converter must report accepted/rejected entries, apply every absent-field default explicitly, preserve source rows/descriptions/numeric precision, and deduplicate only by a documented normalized key. A direct legacy-text parser may exist as a separately tested maintenance command, but generation and normal runtime loading use the committed evidence, not the external tree. Verify expected source counts, multiscale grouping, generated counts, and a manifest hash.
### Substep 1.3.3 — Command-line contract ### Substep 1.3.3 — Command-line contract
@@ -161,8 +157,8 @@ Essential shaders should be embedded with `include_str!` or packaged under a com
## Deliverables ## Deliverables
- Headless library skeleton and feature-gated app skeleton. - Headless library skeleton and feature-gated app skeleton.
- Versioned schema, validator, preset library, importer, and CLI. - Versioned schema, validator, newly authored preset library, and CLI.
- Deterministic run descriptor, finalized preset catalogue, and structured errors/logging. - Deterministic run descriptor, finalized newly authored preset catalogue, and structured errors/logging.
- CI quality baseline. - CI quality baseline.
## Exit gate ## Exit gate

View File

@@ -128,7 +128,7 @@ If targets fail, preserve correctness, lower recommended defaults, and file meas
- Complete standard GPU base pipeline with CPU fallback and selectable legacy packed-unitary GPU pipeline. - Complete standard GPU base pipeline with CPU fallback and selectable legacy packed-unitary GPU pipeline.
- 1-D profile/history, 2-D production field, 3-D slices and volume renderer. - 1-D profile/history, 2-D production field, 3-D slices and volume renderer.
- CPU/GPU state transfer and all base inspection channels. - CPU/GPU state transfer and all base inspection channels.
- Imported and validated base preset catalogue. - Newly authored and validated deterministic base preset catalogue.
## Exit gate ## Exit gate

View File

@@ -12,7 +12,7 @@ Macrosteps 0006 complete.
- Mandatory scope is three scales on a periodic 2-D domain. - Mandatory scope is three scales on a periodic 2-D domain.
- The historical 1-D/3-D infrastructure was nonfunctional because integration existed only in 2-D; do not claim support there. - The historical 1-D/3-D infrastructure was nonfunctional because integration existed only in 2-D; do not claim support there.
- Generalizing to `n` scales is acceptable internally, but imported fixtures remain exactly three. - Generalizing to `n` scales is acceptable internally, but mandatory product presets and contract fixtures remain exactly three scales.
## Phase 7.1 — Typed semantic model ## Phase 7.1 — Typed semantic model
@@ -31,7 +31,7 @@ Composition
Each `ScaleConfig` owns radius/ratios, `dt`, dynamics, and rule. Dimension/shape belongs to the common domain, not each scale. Each `ScaleConfig` owns radius/ratios, `dt`, dynamics, and rule. Dimension/shape belongs to the common domain, not each scale.
Validate positive geometry, valid rules, exactly three scales for imported presets, growth/relaxation dynamics only, and descending/nested scales for chained bands. Nonnested bands are defined but produce a warning explaining their meaning. Validate positive geometry, valid rules, exactly three scales, growth/relaxation dynamics only, and descending/nested scales for chained bands. Nonnested bands are defined but produce a warning explaining their meaning.
## Phase 7.2 — Neighborhood evaluation ## Phase 7.2 — Neighborhood evaluation
@@ -145,13 +145,13 @@ Provide:
- combined increment and clamp-stage views; - combined increment and clamp-stage views;
- scale-color overlay and radial-band diagram; - scale-color overlay and radial-band diagram;
- clear validation for unsupported discrete dynamics and warnings for nonnested chained radii; - clear validation for unsupported discrete dynamics and warnings for nonnested chained radii;
- imported triplet preset browser preserving group identity. - newly authored three-scale preset browser with stable preset identity.
## Deliverables ## Deliverables
- CPU and GPU multiscale 2-D backend. - CPU and GPU multiscale 2-D backend.
- All six kernel/composition combinations for growth/relaxation, using either selectable GPU FFT algorithm. - All six kernel/composition combinations for growth/relaxation, using either selectable GPU FFT algorithm.
- Per-scale inspectors and imported four legacy triplet groups. - Per-scale inspectors and newly authored deterministic multiscale presets.
## Exit gate ## Exit gate

View File

@@ -136,7 +136,7 @@ At 512² and `ra≈12`, target p95 GPU step below 33 ms on the designated machin
- CPU delayed-history oracle and GPU backend. - CPU delayed-history oracle and GPU backend.
- One explicit causal indexing policy. - One explicit causal indexing policy.
- Imported DT catalogue with named presets. - Newly authored deterministic DT presets with stable names and identities.
- History/radial-delay workbench views, restart-state export, and exact continuation checkpoints. - History/radial-delay workbench views, restart-state export, and exact continuation checkpoints.
## Exit gate ## Exit gate

View File

@@ -28,7 +28,7 @@ The original source position is ~/Nextcloud/VecchiProgetti/SmoothLifeAll/
4. **Inspectable by design.** `A`, `M`, `N`, target `S`, derivative/increment, kernels, scale outputs, history layers, and topology diagnostics are first-class channels. 4. **Inspectable by design.** `A`, `M`, `N`, target `S`, derivative/increment, kernels, scale outputs, history layers, and topology diagnostics are first-class channels.
5. **Historical simulation behavior is exceptional and local.** There is no general legacy engine or compatibility profile. Only the explicitly retained RK4 relaxation reference, packed-unitary FFT, and sphere model are selectable; all other simulation behavior follows the new deterministic design. 5. **Historical simulation behavior is exceptional and local.** There is no general legacy engine or compatibility profile. Only the explicitly retained RK4 relaxation reference, packed-unitary FFT, and sphere model are selectable; all other simulation behavior follows the new deterministic design.
6. **Deterministic replay and continuation.** A run is identified by schema version, preset ID, seed, shape, backend, only the localized historical options relevant to that run, and step count. Restart-state files reset numerical history; exact checkpoints additionally store AB history, DT history/head, generation, and deterministic RNG state. 6. **Deterministic replay and continuation.** A run is identified by schema version, preset ID, seed, shape, backend, only the localized historical options relevant to that run, and step count. Restart-state files reset numerical history; exact checkpoints additionally store AB history, DT history/head, generation, and deterministic RNG state.
7. **No runtime dependency on the old project.** Legacy catalogues are imported once and committed in the new schema. 7. **No runtime dependency on the old project.** Bundled product presets are newly authored in the new schema; legacy sources remain provenance for the model contract, not preset input.
8. **No steady-state allocation or readback.** Buffers and plans are reused; GPU readback is only for tests and explicit exports. 8. **No steady-state allocation or readback.** Buffers and plans are reused; GPU readback is only for tests and explicit exports.
## Intended repository shape ## Intended repository shape
@@ -39,7 +39,7 @@ smoothlife/
├── src/ ├── src/
│ ├── lib.rs # raylib-free public core │ ├── lib.rs # raylib-free public core
│ ├── main.rs # application entry point │ ├── main.rs # application entry point
│ ├── config/ # schema, validation, preset library/import │ ├── config/ # schema, validation, preset library
│ ├── field/ # shapes, storage, indexing, inspection data │ ├── field/ # shapes, storage, indexing, inspection data
│ ├── math/ # curves, rules, kernels │ ├── math/ # curves, rules, kernels
│ ├── integration/ # discrete, Euler, AB3, RK4 │ ├── integration/ # discrete, Euler, AB3, RK4

5
pyproject.toml Normal file
View File

@@ -0,0 +1,5 @@
[project]
name = "smoothlife"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []

310
tests/fixtures/contract/curves.json vendored Normal file
View File

@@ -0,0 +1,310 @@
{
"format_version": 1,
"description": "Boundary samples for every scalar curve and explicit overshoot samples.",
"rising": {
"center": 0.5,
"width": 0.2,
"x": [
0.3,
0.39999999999900004,
0.4,
0.5,
0.6,
0.600000000001,
0.7
],
"curves": [
{
"type": 0,
"values": [
0.0,
0.0,
0.0,
1.0,
1.0,
1.0,
1.0
]
},
{
"type": 1,
"values": [
0.0,
0.0,
1.3877787807814457e-16,
0.5,
0.9999999999999999,
1.0,
1.0
]
},
{
"type": 2,
"values": [
0.0,
0.0,
5.777789833161706e-32,
0.5,
1.0,
1.0,
1.0
]
},
{
"type": 3,
"values": [
0.0,
0.0,
0.0,
0.5,
1.0,
1.0,
1.0
]
},
{
"type": 4,
"values": [
0.017986209962091555,
0.1192029220200178,
0.1192029220221176,
0.5,
0.8807970779778823,
0.8807970779799822,
0.9820137900379085
]
},
{
"type": 5,
"values": [
0.09809326195229362,
0.18045353661261226,
0.18045353661405417,
0.5,
0.8195464633859458,
0.8195464633873877,
0.9019067380477064
]
},
{
"type": 6,
"values": [
0.23570975446453796,
0.3392463751246173,
0.3392463751259724,
0.5,
0.6607536248740276,
0.6607536248753827,
0.7642902455354621
]
},
{
"type": 7,
"values": [
-0.15933675375398315,
-0.1773621405006125,
-0.17736214049836008,
0.5,
1.17736214049836,
1.1773621405006125,
1.1593367537539834
]
}
]
},
"windows": {
"a": 0.3,
"b": 0.7,
"width": 0.1,
"x": [
0.249999999999,
0.25,
0.3,
0.5,
0.7,
0.75,
0.750000000001
],
"curves": [
{
"type": 0,
"values": [
0.0,
0.0,
1.0,
1.0,
0.0,
0.0,
0.0
]
},
{
"type": 1,
"values": [
0.0,
1.3877787807814457e-16,
0.5,
1.0,
0.5,
-4.440892098500626e-16,
0.0
]
},
{
"type": 2,
"values": [
0.0,
5.777789833161706e-32,
0.5,
1.0,
0.5,
0.0,
0.0
]
},
{
"type": 3,
"values": [
0.0,
0.0,
0.5,
1.0,
0.5,
0.0,
0.0
]
},
{
"type": 4,
"values": [
0.11920292020245979,
0.11920292020665954,
0.49999994373241896,
0.9993294121987771,
0.49999994373241896,
0.11920292020665929,
0.11920292020245965
]
},
{
"type": 5,
"values": [
0.17639723377201547,
0.17639723377482563,
0.48736148525244144,
0.9020455236180753,
0.48736148525244144,
0.17639723377482552,
0.1763972337720154
]
},
{
"type": 6,
"values": [
0.29835047783952967,
0.2983504778420687,
0.4466576363081552,
0.7613624350113533,
0.4466576363081552,
0.29835047784206853,
0.29835047783952956
]
},
{
"type": 7,
"values": [
-0.1773621379439966,
-0.17736213793949165,
0.4999999718662063,
1.0177103819486473,
0.4999999718662063,
-0.17736213793949188,
-0.17736213794399672
]
},
{
"type": 8,
"values": [
0.11920292020212869,
0.11920292020632844,
0.4999999324789028,
0.7994635297590218,
0.4999999324789028,
0.1192029202063282,
0.11920292020212855
]
},
{
"type": 9,
"values": [
0.11920292020279089,
0.11920292020699064,
0.49999995498593514,
1.1991952946385325,
0.49999995498593514,
0.11920292020699039,
0.11920292020279075
]
}
]
},
"overshoot": [
{
"type": 6,
"samples": [
{
"x": 0.096,
"value": 0.1715399896768695
},
{
"x": 0.364,
"value": 0.29468269649147805
},
{
"x": 0.5,
"value": 0.5
},
{
"x": 0.636,
"value": 0.705317303508522
},
{
"x": 0.904,
"value": 0.8284600103231305
}
]
},
{
"type": 7,
"samples": [
{
"x": 0.096,
"value": -0.008135544802172934
},
{
"x": 0.364,
"value": -0.21415973191618876
},
{
"x": 0.5,
"value": 0.5
},
{
"x": 0.636,
"value": 1.2141597319161888
},
{
"x": 0.904,
"value": 1.008135544802173
}
]
}
],
"hard_window_exact_interval": {
"a": 0.3,
"b": 0.7,
"at_a": 1.0,
"below_b": 1.0,
"at_b": 0.0
}
}

1121
tests/fixtures/contract/delayed_time.json vendored Normal file

File diff suppressed because it is too large Load Diff

657
tests/fixtures/contract/indexing.json vendored Normal file
View File

@@ -0,0 +1,657 @@
{
"format_version": 1,
"description": "Canonical x-fast indexing, inverse indexing, wrapping, and signed even offsets.",
"shapes": [
{
"shape": [
8
],
"x_fast_formula": "x + Nx*(y + Ny*z) (truncated to rank)",
"entries": [
{
"coords": [
0
],
"index": 0
},
{
"coords": [
1
],
"index": 1
},
{
"coords": [
2
],
"index": 2
},
{
"coords": [
3
],
"index": 3
},
{
"coords": [
4
],
"index": 4
},
{
"coords": [
5
],
"index": 5
},
{
"coords": [
6
],
"index": 6
},
{
"coords": [
7
],
"index": 7
}
],
"inverse": [
[
0
],
[
1
],
[
2
],
[
3
],
[
4
],
[
5
],
[
6
],
[
7
]
]
},
{
"shape": [
5,
3
],
"x_fast_formula": "x + Nx*(y + Ny*z) (truncated to rank)",
"entries": [
{
"coords": [
0,
0
],
"index": 0
},
{
"coords": [
1,
0
],
"index": 1
},
{
"coords": [
2,
0
],
"index": 2
},
{
"coords": [
3,
0
],
"index": 3
},
{
"coords": [
4,
0
],
"index": 4
},
{
"coords": [
0,
1
],
"index": 5
},
{
"coords": [
1,
1
],
"index": 6
},
{
"coords": [
2,
1
],
"index": 7
},
{
"coords": [
3,
1
],
"index": 8
},
{
"coords": [
4,
1
],
"index": 9
},
{
"coords": [
0,
2
],
"index": 10
},
{
"coords": [
1,
2
],
"index": 11
},
{
"coords": [
2,
2
],
"index": 12
},
{
"coords": [
3,
2
],
"index": 13
},
{
"coords": [
4,
2
],
"index": 14
}
],
"inverse": [
[
0,
0
],
[
1,
0
],
[
2,
0
],
[
3,
0
],
[
4,
0
],
[
0,
1
],
[
1,
1
],
[
2,
1
],
[
3,
1
],
[
4,
1
],
[
0,
2
],
[
1,
2
],
[
2,
2
],
[
3,
2
],
[
4,
2
]
]
},
{
"shape": [
4,
3,
2
],
"x_fast_formula": "x + Nx*(y + Ny*z) (truncated to rank)",
"entries": [
{
"coords": [
0,
0,
0
],
"index": 0
},
{
"coords": [
1,
0,
0
],
"index": 1
},
{
"coords": [
2,
0,
0
],
"index": 2
},
{
"coords": [
3,
0,
0
],
"index": 3
},
{
"coords": [
0,
1,
0
],
"index": 4
},
{
"coords": [
1,
1,
0
],
"index": 5
},
{
"coords": [
2,
1,
0
],
"index": 6
},
{
"coords": [
3,
1,
0
],
"index": 7
},
{
"coords": [
0,
2,
0
],
"index": 8
},
{
"coords": [
1,
2,
0
],
"index": 9
},
{
"coords": [
2,
2,
0
],
"index": 10
},
{
"coords": [
3,
2,
0
],
"index": 11
},
{
"coords": [
0,
0,
1
],
"index": 12
},
{
"coords": [
1,
0,
1
],
"index": 13
},
{
"coords": [
2,
0,
1
],
"index": 14
},
{
"coords": [
3,
0,
1
],
"index": 15
},
{
"coords": [
0,
1,
1
],
"index": 16
},
{
"coords": [
1,
1,
1
],
"index": 17
},
{
"coords": [
2,
1,
1
],
"index": 18
},
{
"coords": [
3,
1,
1
],
"index": 19
},
{
"coords": [
0,
2,
1
],
"index": 20
},
{
"coords": [
1,
2,
1
],
"index": 21
},
{
"coords": [
2,
2,
1
],
"index": 22
},
{
"coords": [
3,
2,
1
],
"index": 23
}
],
"inverse": [
[
0,
0,
0
],
[
1,
0,
0
],
[
2,
0,
0
],
[
3,
0,
0
],
[
0,
1,
0
],
[
1,
1,
0
],
[
2,
1,
0
],
[
3,
1,
0
],
[
0,
2,
0
],
[
1,
2,
0
],
[
2,
2,
0
],
[
3,
2,
0
],
[
0,
0,
1
],
[
1,
0,
1
],
[
2,
0,
1
],
[
3,
0,
1
],
[
0,
1,
1
],
[
1,
1,
1
],
[
2,
1,
1
],
[
3,
1,
1
],
[
0,
2,
1
],
[
1,
2,
1
],
[
2,
2,
1
],
[
3,
2,
1
]
]
}
],
"signed_offsets": [
{
"extent": 2,
"values": [
0,
-1
]
},
{
"extent": 4,
"values": [
0,
1,
-2,
-1
]
},
{
"extent": 6,
"values": [
0,
1,
2,
-3,
-2,
-1
]
},
{
"extent": 8,
"values": [
0,
1,
2,
3,
-4,
-3,
-2,
-1
]
}
],
"wrap_examples": [
{
"value": -9,
"extent": 8,
"wrapped": 7
},
{
"value": -1,
"extent": 8,
"wrapped": 7
},
{
"value": 8,
"extent": 8,
"wrapped": 0
},
{
"value": 17,
"extent": 8,
"wrapped": 1
},
{
"value": -7,
"extent": 6,
"wrapped": 5
}
]
}

5409
tests/fixtures/contract/initializers.json vendored Normal file

File diff suppressed because it is too large Load Diff

345
tests/fixtures/contract/integration.json vendored Normal file
View File

@@ -0,0 +1,345 @@
{
"format_version": 1,
"description": "Clamp-at-commit Euler/AB and clamp-every-stage RK4, including both relaxation references.",
"initial": [
0.02,
0.37,
0.81,
0.98
],
"time": 0.25,
"dt": 0.4,
"synthetic_derivative": "k_i(A,t)=0.34-0.45*A_i+(i+1)*0.08*t",
"euler": {
"k1": [
0.35100000000000003,
0.21350000000000002,
0.035499999999999976,
-0.020999999999999977
],
"next": [
0.16040000000000001,
0.4554,
0.8242,
0.9716
]
},
"ab3_startup": {
"methods": [
"Euler",
"AB2",
"AB3"
],
"states": [
[
0.02,
0.37,
0.81,
0.98
],
[
0.16040000000000001,
0.4554,
0.8242,
0.9716
],
[
0.282092,
0.556142,
0.892166,
1.0
],
[
0.39776626000000004,
0.6618190100000001,
0.97742673,
1.0
]
],
"derivatives": [
[
0.35100000000000003,
0.21350000000000002,
0.035499999999999976,
-0.020999999999999977
],
[
0.31982,
0.23907,
0.12510999999999997,
0.11078000000000005
],
[
0.2970586,
0.2577361,
0.1905253,
0.22600000000000003
]
],
"ab2_combination": [
0.30423,
0.251855,
0.16991499999999998,
0.17667000000000005
],
"ab3_combination": [
0.28918565,
0.26419252500000007,
0.21315182500000004,
0.27670999999999996
]
},
"rk4": {
"stage_states": [
[
0.02,
0.37,
0.81,
0.98
],
[
0.09020000000000002,
0.4127,
0.8171,
0.9758
],
[
0.087082,
0.415257,
0.826061,
0.988978
],
[
0.15472523999999999,
0.46005374,
0.84050902,
0.99558396
]
],
"derivatives": [
[
0.35100000000000003,
0.21350000000000002,
0.035499999999999976,
-0.020999999999999977
],
[
0.33541,
0.22628500000000001,
0.08030499999999997,
0.04489000000000004
],
[
0.3368131,
0.22513435000000004,
0.07627255000000001,
0.03895990000000005
],
[
0.322373642,
0.23697581700000003,
0.11777094099999999,
0.09998721799999999
]
],
"combined": [
0.33630330699999994,
0.22555241950000002,
0.07773767349999999,
0.04111450300000003
],
"next": [
0.15452132279999997,
0.4602209678,
0.8410950694,
0.9964458012
]
},
"relaxation_target": "S_i(A)=0.1+0.65*A_(i-1)+0.2*A_(i+1), periodic",
"rk4_relaxation_stage_state": {
"reference": "stage_state",
"stage_states": [
[
0.02,
0.37,
0.81,
0.98
],
[
0.1782,
0.351,
0.7553000000000001,
0.9101
],
[
0.136713,
0.373178,
0.760974,
0.923297
],
[
0.27522626000000006,
0.3571521,
0.7165004400000001,
0.85947148
]
],
"targets": [
[
0.8109999999999999,
0.275,
0.5365,
0.6305000000000001
],
[
0.761765,
0.36689000000000005,
0.51017,
0.6265850000000001
],
[
0.77477865,
0.34105825,
0.5272251000000001,
0.6219757
],
[
0.7300868819999999,
0.42219715700000005,
0.504043161,
0.620770538
]
],
"derivatives": [
[
0.7909999999999999,
-0.09499999999999997,
-0.2735000000000001,
-0.3494999999999999
],
[
0.583565,
0.01589000000000007,
-0.24513000000000007,
-0.28351499999999996
],
[
0.63806565,
-0.03211975,
-0.23374889999999993,
-0.3013213
],
[
0.4548606219999999,
0.06504505700000007,
-0.2124572790000001,
-0.23870094199999992
]
],
"combined": [
0.6148536536666667,
-0.010402407166666627,
-0.24061917983333336,
-0.2929789236666666
],
"next": [
0.2659414614666667,
0.3658390371333333,
0.7137523280666667,
0.8628084305333333
]
},
"rk4_relaxation_step_origin": {
"reference": "step_origin",
"stage_states": [
[
0.02,
0.37,
0.81,
0.98
],
[
0.1782,
0.351,
0.7553000000000001,
0.9101
],
[
0.168353,
0.369378,
0.7500340000000001,
0.909317
],
[
0.3179726600000001,
0.3657745,
0.69478364,
0.83647708
]
],
"targets": [
[
0.8109999999999999,
0.275,
0.5365,
0.6305000000000001
],
[
0.761765,
0.36689000000000005,
0.51017,
0.6265850000000001
],
[
0.7649316500000001,
0.35943625,
0.5219591,
0.6211927
],
[
0.716865002,
0.4456389570000001,
0.505048841,
0.615203898
]
],
"derivatives": [
[
0.7909999999999999,
-0.09499999999999997,
-0.2735000000000001,
-0.3494999999999999
],
[
0.741765,
-0.003109999999999946,
-0.29983000000000004,
-0.3534149999999999
],
[
0.7449316500000001,
-0.010563749999999983,
-0.28804090000000004,
-0.35880729999999994
],
[
0.696865002,
0.07563895700000012,
-0.304951159,
-0.364796102
]
],
"combined": [
0.7435430503333333,
-0.007784757166666619,
-0.2923654931666667,
-0.3564567836666666
],
"next": [
0.31741722013333334,
0.36688609713333337,
0.6930538027333334,
0.8374172865333334
]
}
}

File diff suppressed because it is too large Load Diff

72
tests/fixtures/contract/manifest.json vendored Normal file
View File

@@ -0,0 +1,72 @@
{
"format_version": 1,
"contract": "Macrostep 00 deterministic oracle fixtures",
"generator": "tools/generate_contract_fixtures.py",
"oracle": "tools/contract_oracle.py",
"standard_library_only": true,
"semantic_float": "IEEE-754 binary64 represented as JSON numbers",
"packed_fft_float": "IEEE-754 binary32 after every operation, with decimal values and hexadecimal bit patterns",
"determinism": "No timestamp, locale, platform RNG, external source tree, or runtime dependency is used.",
"files": [
{
"path": "indexing.json",
"description": "Indexing and signed offsets",
"bytes": 9147,
"sha256": "c6b8948d6048bb3a8e7199a0736ea9f0f0bef2b0d0852593544a51e2aea51cdb"
},
{
"path": "curves.json",
"description": "Scalar curves and boundaries",
"bytes": 5751,
"sha256": "a70fef48668b1205fe420d3ea9de55860355ebcb34ef24ace70a838f707498cc"
},
{
"path": "rule_surface.json",
"description": "Complete rule enum surface",
"bytes": 109184,
"sha256": "169fb64eb333c49b41e425ecbeea4a47ae9e0be8d5c3271363306927838091c0"
},
{
"path": "planar_fields.json",
"description": "Sampled kernels and direct convolution",
"bytes": 20013,
"sha256": "b3c6b226e64c079fd3a507abac993745572b670d9babf3673afd7bc7b3955881"
},
{
"path": "legacy_packed_fft.json",
"description": "Packed-unitary f32 FFT oracle",
"bytes": 113770,
"sha256": "28e23e349faa9f330800be1a73dd0ef5e83a637b6dbab256c6a6adb51d33452b"
},
{
"path": "integration.json",
"description": "Euler, AB startup, RK4 references",
"bytes": 6229,
"sha256": "0bd9f394e3828254c25cf5958b58ad68d6fd0f6e5cda460e8ef8f89d4cf3cee0"
},
{
"path": "multiscale.json",
"description": "All multiscale policies",
"bytes": 69725,
"sha256": "31a79957d2f0b3aebd5f33610a5c95bbd75dc1efe514d67eab929348dec94d22"
},
{
"path": "sphere.json",
"description": "Corrected and legacy sphere",
"bytes": 126178,
"sha256": "b95d649556bc86636ed75ac28c6688afd3a998bbc403897c840115d81be87faa"
},
{
"path": "delayed_time.json",
"description": "Causal delayed-time shells",
"bytes": 21449,
"sha256": "02088cbb78a8e58326235d80d75e0fb89d056501696c4c647b8b977ecb582a81"
},
{
"path": "initializers.json",
"description": "ChaCha12 and exact seeded arrays",
"bytes": 71752,
"sha256": "7a549a1270a89c7cb5bc3fdaae5c6cb5b3bf51ffc0583f1ec1d9994658ace576"
}
]
}

2696
tests/fixtures/contract/multiscale.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,971 @@
{
"format_version": 1,
"description": "Asymmetric f64 fields with complete sampled kernels and direct circular convolutions.",
"convolution_sign": "out[x] = sum_offset field[x-offset] * kernel[offset] with every axis periodic",
"cases": [
{
"rank": 1,
"shape": [
8
],
"field": [
0.0,
0.17,
0.91,
0.26,
0.73,
0.42,
0.08,
0.64
],
"kernel": {
"shape": [
8
],
"ra": 1.45,
"rr": 3.0,
"rb": 5.0,
"ri": 0.48333333333333334,
"width": 0.29,
"raw_disk": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"raw_ring": [
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0
],
"disk_sum": 1.0,
"ring_sum": 2.0,
"normalized_disk": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"normalized_ring": [
0.0,
0.5,
0.0,
0.0,
0.0,
0.0,
0.0,
0.5
],
"support": [
{
"coords": [
0
],
"offset": [
0
],
"radius": 0.0,
"disk": 1.0,
"ring": 0.0
},
{
"coords": [
1
],
"offset": [
1
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
7
],
"offset": [
-1
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
}
],
"warnings": []
},
"direct_disk_convolution_raw": [
0.0,
0.17,
0.91,
0.26,
0.73,
0.42,
0.08,
0.64
],
"direct_ring_convolution_raw": [
0.81,
0.91,
0.43000000000000005,
1.6400000000000001,
0.6799999999999999,
0.8099999999999999,
1.06,
0.08
],
"M_disk_normalized": [
0.0,
0.17,
0.91,
0.26,
0.73,
0.42,
0.08,
0.64
],
"N_ring_normalized": [
0.405,
0.455,
0.21500000000000002,
0.8200000000000001,
0.33999999999999997,
0.40499999999999997,
0.53,
0.04
]
},
{
"rank": 2,
"shape": [
5,
4
],
"field": [
0.10714285714285714,
0.5,
0.8928571428571429,
0.25,
0.6428571428571429,
0.0,
0.39285714285714285,
0.7857142857142857,
0.14285714285714285,
0.5357142857142857,
0.9285714285714286,
0.2857142857142857,
0.6785714285714286,
0.03571428571428571,
0.42857142857142855,
0.8214285714285714,
0.17857142857142858,
0.5714285714285714,
0.9642857142857143,
0.32142857142857145
],
"kernel": {
"shape": [
5,
4
],
"ra": 1.45,
"rr": 3.0,
"rb": 5.0,
"ri": 0.48333333333333334,
"width": 0.29,
"raw_disk": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"raw_ring": [
0.0,
1.0,
0.0,
0.0,
1.0,
1.0,
0.6234015090582925,
0.0,
0.0,
0.6234015090582925,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.6234015090582925,
0.0,
0.0,
0.6234015090582925
],
"disk_sum": 1.0,
"ring_sum": 6.49360603623317,
"normalized_disk": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"normalized_ring": [
0.0,
0.15399763928088298,
0.0,
0.0,
0.15399763928088298,
0.15399763928088298,
0.09600236071911702,
0.0,
0.0,
0.09600236071911702,
0.0,
0.0,
0.0,
0.0,
0.0,
0.15399763928088298,
0.09600236071911702,
0.0,
0.0,
0.09600236071911702
],
"support": [
{
"coords": [
0,
0
],
"offset": [
0,
0
],
"radius": 0.0,
"disk": 1.0,
"ring": 0.0
},
{
"coords": [
1,
0
],
"offset": [
1,
0
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
4,
0
],
"offset": [
-1,
0
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
0,
1
],
"offset": [
0,
1
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
1,
1
],
"offset": [
1,
1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
4,
1
],
"offset": [
-1,
1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
0,
3
],
"offset": [
0,
-1
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
1,
3
],
"offset": [
1,
-1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
4,
3
],
"offset": [
-1,
-1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
}
],
"warnings": []
},
"direct_disk_convolution_raw": [
0.10714285714285714,
0.5,
0.8928571428571429,
0.25,
0.6428571428571429,
0.0,
0.39285714285714285,
0.7857142857142857,
0.14285714285714285,
0.5357142857142857,
0.9285714285714286,
0.2857142857142857,
0.6785714285714286,
0.03571428571428571,
0.42857142857142855,
0.8214285714285714,
0.17857142857142858,
0.5714285714285714,
0.9642857142857143,
0.32142857142857145
],
"direct_ring_convolution_raw": [
2.854859298654704,
2.92955328759128,
3.153566818776419,
4.023246198629076,
2.41656005318385,
3.1220313739654,
3.1967253629019767,
2.775073045419599,
3.2547039882254873,
2.0380662798270297,
2.4262878700832746,
3.536696144734137,
2.7249953902049913,
3.594674770057648,
3.023702910326707,
2.693459945393972,
3.8038682200448344,
3.382215902562456,
2.8261325596540585,
3.6809234226841716
],
"M_disk_normalized": [
0.10714285714285714,
0.5,
0.8928571428571429,
0.25,
0.6428571428571429,
0.0,
0.39285714285714285,
0.7857142857142857,
0.14285714285714285,
0.5357142857142857,
0.9285714285714286,
0.2857142857142857,
0.6785714285714286,
0.03571428571428571,
0.42857142857142855,
0.8214285714285714,
0.17857142857142858,
0.5714285714285714,
0.9642857142857143,
0.32142857142857145
],
"N_ring_normalized": [
0.43964159247190165,
0.45114429043660675,
0.4856418454060926,
0.6195704168346641,
0.3721445433707979,
0.48078546135152317,
0.4922881593162283,
0.42735469782662877,
0.5012167307447998,
0.31385739579133404,
0.37364260420866596,
0.5446428571428571,
0.4196428571428572,
0.5535714285714286,
0.4656431100770483,
0.41478647308828764,
0.5857867260224788,
0.5208532645328791,
0.4352177424815642,
0.5668535174670701
]
},
{
"rank": 3,
"shape": [
4,
3,
2
],
"field": [
0.1388888888888889,
0.5,
0.8611111111111112,
0.19444444444444445,
0.5555555555555556,
0.9166666666666666,
0.25,
0.6111111111111112,
0.9722222222222222,
0.3055555555555556,
0.6666666666666666,
0.0,
0.3611111111111111,
0.7222222222222222,
0.05555555555555555,
0.4166666666666667,
0.7777777777777778,
0.1111111111111111,
0.4722222222222222,
0.8333333333333334,
0.16666666666666666,
0.5277777777777778,
0.8888888888888888,
0.2222222222222222
],
"kernel": {
"shape": [
4,
3,
2
],
"ra": 1.45,
"rr": 3.0,
"rb": 5.0,
"ri": 0.48333333333333334,
"width": 0.29,
"raw_disk": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"raw_ring": [
0.0,
1.0,
0.0,
1.0,
1.0,
0.6234015090582925,
0.0,
0.6234015090582925,
1.0,
0.6234015090582925,
0.0,
0.6234015090582925,
1.0,
0.6234015090582925,
0.0,
0.6234015090582925,
0.6234015090582925,
0.0,
0.0,
0.0,
0.6234015090582925,
0.0,
0.0,
0.0
],
"disk_sum": 1.0,
"ring_sum": 9.98721207246634,
"normalized_disk": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"normalized_ring": [
0.0,
0.10012804301581735,
0.0,
0.10012804301581735,
0.10012804301581735,
0.06241997311511416,
0.0,
0.06241997311511416,
0.10012804301581735,
0.06241997311511416,
0.0,
0.06241997311511416,
0.10012804301581735,
0.06241997311511416,
0.0,
0.06241997311511416,
0.06241997311511416,
0.0,
0.0,
0.0,
0.06241997311511416,
0.0,
0.0,
0.0
],
"support": [
{
"coords": [
0,
0,
0
],
"offset": [
0,
0,
0
],
"radius": 0.0,
"disk": 1.0,
"ring": 0.0
},
{
"coords": [
1,
0,
0
],
"offset": [
1,
0,
0
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
3,
0,
0
],
"offset": [
-1,
0,
0
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
0,
1,
0
],
"offset": [
0,
1,
0
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
1,
1,
0
],
"offset": [
1,
1,
0
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
3,
1,
0
],
"offset": [
-1,
1,
0
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
0,
2,
0
],
"offset": [
0,
-1,
0
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
1,
2,
0
],
"offset": [
1,
-1,
0
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
3,
2,
0
],
"offset": [
-1,
-1,
0
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
0,
0,
1
],
"offset": [
0,
0,
-1
],
"radius": 1.0,
"disk": 0.0,
"ring": 1.0
},
{
"coords": [
1,
0,
1
],
"offset": [
1,
0,
-1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
3,
0,
1
],
"offset": [
-1,
0,
-1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
0,
1,
1
],
"offset": [
0,
1,
-1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
},
{
"coords": [
0,
2,
1
],
"offset": [
0,
-1,
-1
],
"radius": 1.4142135623730951,
"disk": 0.0,
"ring": 0.6234015090582925
}
],
"warnings": [
"nonzero support touches periodic Nyquist offset on axis 2"
]
},
"direct_disk_convolution_raw": [
0.1388888888888889,
0.5,
0.8611111111111112,
0.19444444444444445,
0.5555555555555556,
0.9166666666666666,
0.25,
0.6111111111111112,
0.9722222222222222,
0.3055555555555556,
0.6666666666666666,
0.0,
0.3611111111111111,
0.7222222222222222,
0.05555555555555555,
0.4166666666666667,
0.7777777777777778,
0.1111111111111111,
0.4722222222222222,
0.8333333333333334,
0.16666666666666666,
0.5277777777777778,
0.8888888888888888,
0.2222222222222222
],
"direct_ring_convolution_raw": [
5.024989243811645,
5.126349726148468,
4.368073205919266,
4.46943368825609,
4.957853730727446,
4.925813310438447,
5.3287154706128455,
4.655956832680603,
3.729539537239647,
5.886455575132025,
4.4874608372595794,
5.229539537239647,
4.6638781327005345,
4.378179054902825,
5.4217994327204675,
4.108322577144979,
4.850401277125047,
5.338821319596402,
5.221263017010446,
4.041187064060779,
4.783265764040847,
5.138284903886381,
4.126349726148468,
4.868428426128536
],
"M_disk_normalized": [
0.1388888888888889,
0.5,
0.8611111111111112,
0.19444444444444445,
0.5555555555555556,
0.9166666666666666,
0.25,
0.6111111111111112,
0.9722222222222222,
0.3055555555555556,
0.6666666666666666,
0.0,
0.3611111111111111,
0.7222222222222222,
0.05555555555555555,
0.4166666666666667,
0.7777777777777778,
0.1111111111111111,
0.4722222222222222,
0.8333333333333334,
0.16666666666666666,
0.5277777777777778,
0.8888888888888888,
0.2222222222222222
],
"N_ring_normalized": [
0.5031423391583919,
0.5132913658939172,
0.4373666218585235,
0.44751564859404896,
0.4964201916164082,
0.4932120470354665,
0.5335538518605744,
0.46619184602243213,
0.3734314952139229,
0.5893992770375173,
0.4493206717449229,
0.523623559737649,
0.466984990291569,
0.4383785007402606,
0.5428741668225691,
0.41135829972722604,
0.4856611877199521,
0.5345657307423113,
0.5227948479641182,
0.4046361521852423,
0.47893904017796834,
0.5144864118838605,
0.4131633228780999,
0.487466210870826
]
}
],
"known_2d_check": {
"shape": [
64,
64
],
"ra": 10.0,
"rr": 3.0,
"rb": 10.0,
"ring_sum": 279.21631227194473,
"disk_sum": 35.52403518160772
}
}

5583
tests/fixtures/contract/rule_surface.json vendored Normal file

File diff suppressed because it is too large Load Diff

4864
tests/fixtures/contract/sphere.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,371 @@
#!/usr/bin/env python3
"""Invariant and committed-golden tests for the Macrostep 00 Python oracle."""
from __future__ import annotations
import hashlib
import importlib.util
import json
import math
import struct
import subprocess
import sys
import unittest
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
TOOLS = ROOT / "tools"
FIXTURES = ROOT / "tests" / "fixtures" / "contract"
sys.path.insert(0, str(TOOLS))
_ORACLE_SPEC = importlib.util.spec_from_file_location(
"contract_oracle", TOOLS / "contract_oracle.py"
)
if _ORACLE_SPEC is None or _ORACLE_SPEC.loader is None:
raise RuntimeError("could not load tools/contract_oracle.py")
oracle = importlib.util.module_from_spec(_ORACLE_SPEC)
sys.modules[_ORACLE_SPEC.name] = oracle
_ORACLE_SPEC.loader.exec_module(oracle)
def load_fixture(name: str) -> dict[str, Any]:
try:
with (FIXTURES / name).open("r", encoding="utf-8") as handle:
value = json.load(handle)
except (OSError, json.JSONDecodeError) as error:
raise AssertionError(
f"could not load committed fixture {name}: {error}"
) from error
if not isinstance(value, dict):
raise AssertionError(f"fixture {name} is not a JSON object")
return value
class IndexAndRuleTests(unittest.TestCase):
def test_x_fast_index_round_trip_and_signed_offsets(self) -> None:
for shape in ((8,), (5, 3), (4, 3, 2)):
for expected, coords in enumerate(oracle.iter_coords(shape)):
self.assertEqual(oracle.flat_index(coords, shape), expected)
self.assertEqual(oracle.unflatten_index(expected, shape), coords)
self.assertEqual(
[oracle.signed_offset(i, 8) for i in range(8)], [0, 1, 2, 3, -4, -3, -2, -1]
)
def test_curve_boundaries_overshoot_and_hard_window(self) -> None:
center, width = 0.5, 0.2
for curve_type in (1, 2, 3):
self.assertAlmostEqual(
oracle.rising_curve(curve_type, center - width / 2, center, width),
0.0,
places=14,
)
self.assertAlmostEqual(
oracle.rising_curve(curve_type, center + width / 2, center, width),
1.0,
places=14,
)
self.assertEqual(oracle.window_curve(0, 0.3, 0.3, 0.7, 0.1), 1.0)
self.assertEqual(
oracle.window_curve(0, math.nextafter(0.7, -math.inf), 0.3, 0.7, 0.1), 1.0
)
self.assertEqual(oracle.window_curve(0, 0.7, 0.3, 0.7, 0.1), 0.0)
self.assertLess(oracle.rising_curve(7, 0.364, center, width), 0.0)
self.assertGreater(oracle.rising_curve(7, 0.636, center, width), 1.0)
def test_rule_fixture_covers_every_valid_enum_triple(self) -> None:
fixture = load_fixture("rule_surface.json")
axes = fixture["axes"]
self.assertEqual(axes["sigmode"], [1, 2, 3, 4])
self.assertEqual(axes["sigtype"], list(range(10)))
self.assertEqual(axes["mixtype"], list(range(8)))
seen = 0
for modes in fixture["values"]:
self.assertEqual(len(modes), 10)
for types in modes:
self.assertEqual(len(types), 8)
for surface in types:
self.assertEqual((len(surface), len(surface[0])), (3, 3))
self.assertTrue(
all(math.isfinite(value) for row in surface for value in row)
)
seen += 1
self.assertEqual(seen, fixture["enum_combination_count"])
self.assertEqual(seen, 320)
class KernelAndFftTests(unittest.TestCase):
def test_complete_sampled_kernels_and_direct_convolution(self) -> None:
fixture = load_fixture("planar_fields.json")
for case in fixture["cases"]:
kernels = case["kernel"]
self.assertAlmostEqual(sum(kernels["normalized_disk"]), 1.0, places=14)
self.assertAlmostEqual(sum(kernels["normalized_ring"]), 1.0, places=14)
support_indices = {
oracle.flat_index(entry["coords"], case["shape"])
for entry in kernels["support"]
}
expected_support = {
index
for index, pair in enumerate(
zip(kernels["raw_disk"], kernels["raw_ring"], strict=True)
)
if pair != (0.0, 0.0)
}
self.assertEqual(support_indices, expected_support)
constant = [0.375] * oracle.product(case["shape"])
disk = oracle.circular_convolution(
constant, kernels["normalized_disk"], case["shape"]
)
ring = oracle.circular_convolution(
constant, kernels["normalized_ring"], case["shape"]
)
self.assertTrue(all(abs(value - 0.375) < 1e-14 for value in disk + ring))
known = fixture["known_2d_check"]
self.assertAlmostEqual(known["ring_sum"], 279.216312, places=6)
self.assertAlmostEqual(known["disk_sum"], 35.524035, places=6)
def test_packed_fft_bits_plans_and_convolution(self) -> None:
fixture = load_fixture("legacy_packed_fft.json")
self.assertEqual(
[case["shape"] for case in fixture["cases"]], [[8], [4, 4], [4, 2, 2]]
)
for case in fixture["cases"]:
for decimal, bits in zip(
case["input_f32"]["decimal"], case["input_f32"]["bits"], strict=True
):
decoded = struct.unpack("<f", struct.pack("<I", int(bits, 16)))[0]
self.assertEqual(decoded, decimal)
self.assertLess(case["max_abs_error_to_direct"], 2e-6)
self.assertEqual(
len(case["convolution_f32"]["decimal"]), math.prod(case["shape"])
)
first_plan = case["plans"]["x_forward_butterflies"][0]["entries"]
self.assertEqual(first_plan[0]["source"][0], 0)
self.assertEqual(first_plan[0]["twiddle"]["bits"][0], "0x3f800000")
# The 1-D Nx=8 stage-1 plan starts from 2-bit bit-reversed packed indices.
first = fixture["cases"][0]["plans"]["x_forward_butterflies"][0]["entries"]
self.assertEqual(
[entry["source"] for entry in first], [[0, 2], [0, 2], [1, 3], [1, 3]]
)
class RandomAndInitializerTests(unittest.TestCase):
def test_chacha12_known_block_and_word_composition(self) -> None:
rng = oracle.ChaCha12(0)
self.assertEqual(
[rng.u32() for _ in range(4)],
[0x6A9AF49B, 0x53F95507, 0x12CE1F81, 0xD583265F],
)
words = oracle.ChaCha12(1)
low, high = words.u32(), words.u32()
combined = oracle.ChaCha12(1).u64()
self.assertEqual(combined, low | (high << 32))
value = oracle.ChaCha12(1).float53()
self.assertEqual(value, (combined >> 11) * 2.0**-53)
def test_integer_rejection_skips_incomplete_high_residue(self) -> None:
class Scripted(oracle.ChaCha12):
def __init__(self) -> None:
self.values = iter([(1 << 64) - 1, 5])
def u64(self) -> int:
return next(self.values)
# For span 2^63+1, the first scripted value is above the acceptance limit.
self.assertEqual(Scripted().integer(0, (1 << 63) + 1), 5)
def test_initializer_arrays_are_topology_native_and_explicit(self) -> None:
fixture = load_fixture("initializers.json")
planar = fixture["planar_periodic_splats"]
self.assertEqual(
[entry["shape"] for entry in planar], [[12], [8, 6], [6, 5, 4]]
)
for entry in planar:
self.assertEqual(len(entry["field"]), math.prod(entry["shape"]))
self.assertTrue(set(entry["field"]) <= {0.0, 1.0})
self.assertEqual(len(entry["splats"]), entry["count"])
sphere = fixture["sphere_geodesic_overlays"]
self.assertEqual(sphere["draw_count"], 1000)
self.assertEqual(len(sphere["field"]), 6 * sphere["k"] * sphere["k"])
self.assertTrue(all(2 <= draw["radius"] <= 7 for draw in sphere["first_draws"]))
delayed = fixture["delayed_time_periodic_boxes"]
self.assertEqual(delayed["depth"], 16)
self.assertEqual(len(delayed["history"]), 16)
self.assertTrue(all(layer == delayed["frame"] for layer in delayed["history"]))
self.assertTrue(
all(
10 <= box["width"] <= 19 and 10 <= box["height"] <= 19
for box in delayed["first_boxes"]
)
)
class DynamicsAndTopologyTests(unittest.TestCase):
def test_integrator_startup_and_relaxation_references(self) -> None:
fixture = load_fixture("integration.json")
startup = fixture["ab3_startup"]
self.assertEqual(startup["methods"], ["Euler", "AB2", "AB3"])
dt = fixture["dt"]
for before, derivative, after in zip(
startup["states"][0],
startup["derivatives"][0],
startup["states"][1],
strict=True,
):
self.assertAlmostEqual(
after, oracle.clamp(before + dt * derivative), places=15
)
current, previous = startup["derivatives"][1], startup["derivatives"][0]
expected_ab2 = [
(3 * now - old) / 2 for now, old in zip(current, previous, strict=True)
]
self.assertEqual(startup["ab2_combination"], expected_ab2)
for stage in fixture["rk4"]["stage_states"]:
self.assertTrue(all(0.0 <= value <= 1.0 for value in stage))
stage_state = fixture["rk4_relaxation_stage_state"]
step_origin = fixture["rk4_relaxation_step_origin"]
self.assertEqual(stage_state["derivatives"][0], step_origin["derivatives"][0])
self.assertNotEqual(
stage_state["derivatives"][1], step_origin["derivatives"][1]
)
self.assertNotEqual(stage_state["next"], step_origin["next"])
def test_all_multiscale_combinations_and_composition_equations(self) -> None:
fixture = load_fixture("multiscale.json")
cases = fixture["cases"]
triples = {
(case["dynamics"], case["interpretation"], case["composition"])
for case in cases
}
self.assertEqual(len(cases), 12)
self.assertEqual(len(triples), 12)
for case in cases:
if case["composition"] == "ordered_clamped_sum":
state = fixture["field"][:]
for increment in case["increments"]:
state = [
oracle.clamp(value + change)
for value, change in zip(state, increment, strict=True)
]
self.assertEqual(state, case["next"])
elif case["composition"] == "mean_increment":
expected = [
oracle.clamp(value + sum(changes) / 3.0)
for value, changes in zip(
fixture["field"], zip(*case["increments"]), strict=True
)
]
self.assertEqual(expected, case["next"])
else:
self.assertEqual(case["next"], case["clamp_stages"][-1])
def test_corrected_and_legacy_sphere_invariants(self) -> None:
fixture = load_fixture("sphere.json")
geometry = fixture["geometry"]
self.assertTrue(
all(area > 0.0 and math.isfinite(area) for area in geometry["areas"])
)
self.assertAlmostEqual(
geometry["total_area"], geometry["analytic_area"], places=12
)
corrected = fixture["models"]["corrected"]
legacy = fixture["models"]["legacy"]
for channel in ("m", "n"):
self.assertTrue(
all(
abs(value - 0.375) < 5e-15
for value in corrected["constant_field"][channel]
)
)
self.assertTrue(
any(abs(value - 0.375) > 1e-3 for value in legacy["constant_field"]["m"])
)
self.assertEqual(corrected["disk_denominators"], corrected["visited_disk_sums"])
self.assertNotEqual(legacy["disk_denominators"], legacy["visited_disk_sums"])
for mapping in fixture["legacy_side_and_mask_map"]:
if mapping["region"].startswith("masked"):
self.assertIsNone(mapping["mapped"])
else:
self.assertIsNotNone(mapping["mapped"])
for model in (corrected, legacy):
expected_length = 6 * fixture["k"] * fixture["k"]
self.assertEqual(len(model["m"]), expected_length)
self.assertEqual(len(model["constant_field"]["s"]), expected_length)
self.assertEqual(
len(model["constant_field"]["next_discrete"]), expected_length
)
self.assertEqual(
len(model["constant_field"]["next_smooth"]), expected_length
)
self.assertTrue(
all(
0.0 <= value <= 1.0
for value in model["next_discrete"] + model["next_smooth"]
)
)
def test_delayed_time_causal_shells_and_updates(self) -> None:
fixture = load_fixture("delayed_time.json")
evaluation = fixture["evaluation"]
self.assertEqual(evaluation["latest"], (fixture["head"] - 1) % fixture["depth"])
for shell in fixture["shell_offsets_by_rounded_age"]:
self.assertEqual(
shell["selected_layer"],
(evaluation["latest"] - shell["age"]) % fixture["depth"],
)
stencil = evaluation["stencil"]
expected_m = (
sum(
(((evaluation["latest"] - entry["delay"]) % fixture["depth"]) / 15.0)
* entry["disk"]
for entry in stencil["entries"]
)
/ stencil["disk_sum"]
)
expected_n = (
sum(
(((evaluation["latest"] - entry["delay"]) % fixture["depth"]) / 15.0)
* entry["ring"]
for entry in stencil["entries"]
)
/ stencil["ring_sum"]
)
self.assertTrue(
all(abs(value - expected_m) < 1e-14 for value in evaluation["m"])
)
self.assertTrue(
all(abs(value - expected_n) < 1e-14 for value in evaluation["n"])
)
latest_value = evaluation["latest"] / 15.0
for target, smooth in zip(
evaluation["s"], evaluation["next_smooth"], strict=True
):
self.assertAlmostEqual(
smooth, oracle.clamp(latest_value + 0.1 * (2 * target - 1)), places=15
)
class CommittedFixtureTests(unittest.TestCase):
def test_manifest_hashes(self) -> None:
manifest = load_fixture("manifest.json")
self.assertTrue(manifest["standard_library_only"])
for entry in manifest["files"]:
content = (FIXTURES / entry["path"]).read_bytes()
self.assertEqual(len(content), entry["bytes"])
self.assertEqual(hashlib.sha256(content).hexdigest(), entry["sha256"])
def test_generator_check_mode(self) -> None:
completed = subprocess.run(
[sys.executable, str(TOOLS / "generate_contract_fixtures.py"), "--check"],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
self.assertIn("verified 11 contract fixture files", completed.stdout)
if __name__ == "__main__":
unittest.main()

1453
tools/contract_oracle.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,709 @@
#!/usr/bin/env python3
"""Generate or verify the committed Macrostep 00 contract fixtures."""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import sys
from dataclasses import asdict
from pathlib import Path
from typing import Any, Mapping, Sequence
TOOLS_DIR = Path(__file__).resolve().parent
ROOT = TOOLS_DIR.parent
FIXTURE_DIR = ROOT / "tests" / "fixtures" / "contract"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from contract_oracle import ( # noqa: E402
ChaCha12,
Rule,
Scale,
ab3_startup,
circular_convolution,
delayed_boxes,
delayed_step,
euler_step,
f32,
f32_bits,
flat_index,
iter_coords,
legacy_butterfly_plan,
legacy_convolution,
legacy_sphere_map,
legacy_x_conversion_plan,
multiscale_step,
neighborhoods,
planar_splats,
product,
rising_curve,
rk4_relaxation,
rk4_step,
rule_target,
sampled_kernels,
signed_offset,
sphere_geometry,
sphere_overlays,
sphere_step,
unflatten_index,
window_curve,
)
FORMAT_VERSION = 1
def _render(value: Any) -> bytes:
return (
json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + "\n"
).encode("utf-8")
def _f32_scalar(value: float) -> dict[str, Any]:
rounded = f32(value)
return {"decimal": rounded, "bits": f"0x{f32_bits(rounded):08x}"}
def _f32_series(values: Sequence[float]) -> dict[str, Any]:
rounded = [f32(value) for value in values]
return {
"decimal": rounded,
"bits": [f"0x{f32_bits(value):08x}" for value in rounded],
}
def _complex_f32_series(values: Sequence[tuple[float, float]]) -> dict[str, Any]:
rounded = [(f32(real), f32(imag)) for real, imag in values]
return {
"decimal": [[real, imag] for real, imag in rounded],
"bits": [
[f"0x{f32_bits(real):08x}", f"0x{f32_bits(imag):08x}"]
for real, imag in rounded
],
}
def _encode_plan(entries: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]:
return [
{
"output": entry["output"],
"source": entry["source"],
"twiddle": _f32_series(entry["twiddle"]),
}
for entry in entries
]
def _encode_stages(stages: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
encoded: list[dict[str, Any]] = []
for stage in stages:
values = stage["values"]
if values and isinstance(values[0], tuple):
encoded_values = _complex_f32_series(values)
else:
encoded_values = _f32_series(values)
encoded.append({"name": stage["name"], "values": encoded_values})
return encoded
def indexing_fixture() -> dict[str, Any]:
shape_records = []
for shape in ((8,), (5, 3), (4, 3, 2)):
shape_records.append(
{
"shape": list(shape),
"x_fast_formula": "x + Nx*(y + Ny*z) (truncated to rank)",
"entries": [
{"coords": list(coords), "index": flat_index(coords, shape)}
for coords in iter_coords(shape)
],
"inverse": [
list(unflatten_index(index, shape))
for index in range(product(shape))
],
}
)
return {
"format_version": FORMAT_VERSION,
"description": "Canonical x-fast indexing, inverse indexing, wrapping, and signed even offsets.",
"shapes": shape_records,
"signed_offsets": [
{
"extent": extent,
"values": [signed_offset(index, extent) for index in range(extent)],
}
for extent in (2, 4, 6, 8)
],
"wrap_examples": [
{"value": value, "extent": extent, "wrapped": value % extent}
for value, extent in ((-9, 8), (-1, 8), (8, 8), (17, 8), (-7, 6))
],
}
def curves_fixture() -> dict[str, Any]:
center = 0.5
width = 0.2
epsilon = 1e-12
rising_x = [
center - width,
center - width / 2.0 - epsilon,
center - width / 2.0,
center,
center + width / 2.0,
center + width / 2.0 + epsilon,
center + width,
]
window_x = [0.249999999999, 0.25, 0.3, 0.5, 0.7, 0.75, 0.750000000001]
overshoot_points = [0.096, 0.364, 0.5, 0.636, 0.904]
return {
"format_version": FORMAT_VERSION,
"description": "Boundary samples for every scalar curve and explicit overshoot samples.",
"rising": {
"center": center,
"width": width,
"x": rising_x,
"curves": [
{
"type": curve_type,
"values": [
rising_curve(curve_type, x, center, width) for x in rising_x
],
}
for curve_type in range(8)
],
},
"windows": {
"a": 0.3,
"b": 0.7,
"width": 0.1,
"x": window_x,
"curves": [
{
"type": curve_type,
"values": [
window_curve(curve_type, x, 0.3, 0.7, 0.1) for x in window_x
],
}
for curve_type in range(10)
],
},
"overshoot": [
{
"type": curve_type,
"samples": [
{"x": x, "value": rising_curve(curve_type, x, center, width)}
for x in overshoot_points
],
}
for curve_type in (6, 7)
],
"hard_window_exact_interval": {
"a": 0.3,
"b": 0.7,
"at_a": window_curve(0, 0.3, 0.3, 0.7, 0.1),
"below_b": window_curve(0, math.nextafter(0.7, -math.inf), 0.3, 0.7, 0.1),
"at_b": window_curve(0, 0.7, 0.3, 0.7, 0.1),
},
}
def rule_surface_fixture() -> dict[str, Any]:
m_axis = [0.2, 0.5, 0.8]
n_axis = [0.2, 0.5, 0.8]
base = Rule()
values = []
for sigmode in range(1, 5):
by_sigtype = []
for sigtype in range(10):
by_mixtype = []
for mixtype in range(8):
rule = Rule(
b1=base.b1,
b2=base.b2,
d1=base.d1,
d2=base.d2,
sn=base.sn,
sm=base.sm,
sigmode=sigmode,
sigtype=sigtype,
mixtype=mixtype,
)
by_mixtype.append(
[[rule_target(n, m, rule) for n in n_axis] for m in m_axis]
)
by_sigtype.append(by_mixtype)
values.append(by_sigtype)
return {
"format_version": FORMAT_VERSION,
"description": "Rule surface [sigmode][sigtype][mixtype][M][N], including all 320 enum triples.",
"rule_parameters": {
key: value
for key, value in asdict(base).items()
if key not in ("sigmode", "sigtype", "mixtype")
},
"axes": {
"sigmode": list(range(1, 5)),
"sigtype": list(range(10)),
"mixtype": list(range(8)),
"M": m_axis,
"N": n_axis,
},
"enum_combination_count": 4 * 10 * 8,
"values": values,
}
def planar_fields_fixture() -> dict[str, Any]:
cases = [
((8,), [0.0, 0.17, 0.91, 0.26, 0.73, 0.42, 0.08, 0.64]),
((5, 4), [((index * 11 + 3) % 29) / 28.0 for index in range(20)]),
((4, 3, 2), [((index * 13 + 5) % 37) / 36.0 for index in range(24)]),
]
encoded_cases = []
for shape, field in cases:
m, n, kernels = neighborhoods(field, shape, 1.45, 3.0, 5.0)
encoded_cases.append(
{
"rank": len(shape),
"shape": list(shape),
"field": field,
"kernel": kernels,
"direct_disk_convolution_raw": circular_convolution(
field, kernels["raw_disk"], shape
),
"direct_ring_convolution_raw": circular_convolution(
field, kernels["raw_ring"], shape
),
"M_disk_normalized": m,
"N_ring_normalized": n,
}
)
known = sampled_kernels((64, 64), 10.0, 3.0, 10.0)
return {
"format_version": FORMAT_VERSION,
"description": "Asymmetric f64 fields with complete sampled kernels and direct circular convolutions.",
"convolution_sign": "out[x] = sum_offset field[x-offset] * kernel[offset] with every axis periodic",
"cases": encoded_cases,
"known_2d_check": {
"shape": [64, 64],
"ra": 10.0,
"rr": 3.0,
"rb": 10.0,
"ring_sum": known["ring_sum"],
"disk_sum": known["disk_sum"],
},
}
def _fft_case(
shape: tuple[int, ...], field: list[float], kernel: list[float]
) -> dict[str, Any]:
result = legacy_convolution(field, kernel, shape, capture_stages=True)
nx = shape[0]
plans: dict[str, Any] = {
"x_forward_butterflies": [
{
"stage": stage,
"entries": _encode_plan(legacy_butterfly_plan(nx // 2, stage, -1)),
}
for stage in range(1, (nx // 2).bit_length())
],
"x_forward_real_conversion": _encode_plan(legacy_x_conversion_plan(nx, -1)),
"x_inverse_real_conversion": _encode_plan(legacy_x_conversion_plan(nx, 1)),
"x_inverse_butterflies": [
{
"stage": stage,
"entries": _encode_plan(legacy_butterfly_plan(nx // 2, stage, 1)),
}
for stage in range(1, (nx // 2).bit_length())
],
}
for axis, name in enumerate("yz", start=1):
if axis < len(shape):
plans[f"{name}_forward_butterflies"] = [
{
"stage": stage,
"entries": _encode_plan(
legacy_butterfly_plan(shape[axis], stage, -1)
),
}
for stage in range(1, shape[axis].bit_length())
]
plans[f"{name}_inverse_butterflies"] = [
{
"stage": stage,
"entries": _encode_plan(
legacy_butterfly_plan(shape[axis], stage, 1)
),
}
for stage in range(1, shape[axis].bit_length())
]
return {
"shape": list(shape),
"packed_shape": [nx // 2 + 1, *shape[1:]],
"input_f32": _f32_series(field),
"kernel_f32": _f32_series(kernel),
"kernel_sum_f64": sum(kernel),
"scales_f32": {
"unitary_butterfly": _f32_scalar(1.0 / math.sqrt(2.0)),
"forward_real_conversion": _f32_scalar(0.5 / math.sqrt(2.0)),
"inverse_real_conversion": _f32_scalar(0.5 * math.sqrt(2.0)),
"spectral_correction": _f32_scalar(result["correction"]),
},
"plans": plans,
"forward_field_stages": _encode_stages(result["field_stages"]),
"field_spectrum": _complex_f32_series(result["field_spectrum"]),
"kernel_spectrum": _complex_f32_series(result["kernel_spectrum"]),
"spectral_product": _complex_f32_series(result["spectral_product"]),
"inverse_product_stages": _encode_stages(result["inverse_stages"]),
"convolution_f32": _f32_series(result["output"]),
"direct_normalized_f64": result["direct_normalized"],
"max_abs_error_to_direct": max(
abs(actual - expected)
for actual, expected in zip(
result["output"], result["direct_normalized"], strict=True
)
),
}
def packed_fft_fixture() -> dict[str, Any]:
definitions = []
for shape in ((8,), (4, 4), (4, 2, 2)):
count = product(shape)
field = [((index * 7 + 1) % 19) / 18.0 for index in range(count)]
kernel = [0.2 + ((index * 5 + 2) % 11) / 13.0 for index in range(count)]
definitions.append(_fft_case(shape, field, kernel))
return {
"format_version": FORMAT_VERSION,
"description": "Legacy adjacent-real packed unitary 1D/2D/3D FFT with operation-ordered IEEE-f32 stages.",
"operation_order": {
"butterfly_real": "f32(f32(f32(a.r + f32(cos*b.r)) - f32(sin*b.i)) * f32(1/sqrt(2)))",
"butterfly_imag": "f32(f32(f32(a.i + f32(cos*b.i)) + f32(sin*b.r)) * f32(1/sqrt(2)))",
"complex_multiply": "real=f32(f32(ar*br)-f32(ai*bi)); imag=f32(f32(ar*bi)+f32(ai*br))",
"kernel_scaling": "scale each kernel spectrum component first by f32(sqrt(sample_count)/kernel_sum)",
},
"layout": "adjacent x samples become real/imag; spectrum x extent is Nx/2+1; x remains fastest",
"cases": definitions,
}
def integration_fixture() -> dict[str, Any]:
initial = [0.02, 0.37, 0.81, 0.98]
time = 0.25
dt = 0.4
def derivative(state: Sequence[float], current_time: float) -> list[float]:
return [
0.34 - 0.45 * value + (index + 1) * 0.08 * current_time
for index, value in enumerate(state)
]
def target(state: Sequence[float]) -> list[float]:
return [
0.1
+ 0.65 * state[(index - 1) % len(state)]
+ 0.2 * state[(index + 1) % len(state)]
for index in range(len(state))
]
return {
"format_version": FORMAT_VERSION,
"description": "Clamp-at-commit Euler/AB and clamp-every-stage RK4, including both relaxation references.",
"initial": initial,
"time": time,
"dt": dt,
"synthetic_derivative": "k_i(A,t)=0.34-0.45*A_i+(i+1)*0.08*t",
"euler": euler_step(initial, time, dt, derivative),
"ab3_startup": ab3_startup(initial, time, dt, derivative),
"rk4": rk4_step(initial, time, dt, derivative),
"relaxation_target": "S_i(A)=0.1+0.65*A_(i-1)+0.2*A_(i+1), periodic",
"rk4_relaxation_stage_state": rk4_relaxation(
initial, dt, target, "stage_state"
),
"rk4_relaxation_step_origin": rk4_relaxation(
initial, dt, target, "step_origin"
),
}
def multiscale_fixture() -> dict[str, Any]:
shape = (5, 4)
field = [((index * 17 + 4) % 31) / 30.0 for index in range(product(shape))]
scales = [
Scale(1.65, 3.0, 6.0, 0.18, Rule(sigmode=2, sigtype=4, mixtype=3)),
Scale(1.25, 3.0, 6.0, 0.11, Rule(sigmode=3, sigtype=2, mixtype=5)),
Scale(0.95, 3.0, 6.0, 0.07, Rule(sigmode=4, sigtype=7, mixtype=1)),
]
cases = []
for dynamics in ("growth", "relaxation"):
for interpretation in ("independent", "chained"):
for composition in ("sequential", "ordered_clamped_sum", "mean_increment"):
cases.append(
multiscale_step(
field, shape, scales, interpretation, composition, dynamics
)
)
return {
"format_version": FORMAT_VERSION,
"description": "All 2 interpretations x 3 compositions for growth and corrected relaxation.",
"shape": list(shape),
"field": field,
"scales": [
{
"ra": scale.ra,
"rr": scale.rr,
"rb": scale.rb,
"dt": scale.dt,
"rule": asdict(scale.rule),
}
for scale in scales
],
"chained_inputs": [
["ring_0", "ring_1"],
["ring_1", "ring_2"],
["ring_2", "disk_2"],
],
"case_count": len(cases),
"cases": cases,
}
def sphere_fixture() -> dict[str, Any]:
k = 4
ra = 1.4
rule = Rule(sigmode=2, sigtype=4, mixtype=4)
geometry = sphere_geometry(k)
field = [((index * 19 + 7) % 43) / 42.0 for index in range(6 * k * k)]
constant_value = 0.375
probes = [
{"label": "face0_center", "face": 0, "x": 1, "y": 1},
{"label": "face1_center", "face": 1, "x": 2, "y": 2},
{"label": "face0_left_edge", "face": 0, "x": 0, "y": 1},
{"label": "face2_top_edge", "face": 2, "x": 2, "y": 3},
{"label": "face0_bottom_left_corner", "face": 0, "x": 0, "y": 0},
{"label": "face4_top_right_corner", "face": 4, "x": 3, "y": 3},
]
models = {}
for model in ("corrected", "legacy"):
result = sphere_step(field, k, ra, rule, model)
constant = sphere_step([constant_value] * (6 * k * k), k, ra, rule, model)
probe_values = []
for probe in probes:
index = probe["face"] * k * k + probe["y"] * k + probe["x"]
probe_values.append(
{
**probe,
"index": index,
"M": result["m"][index],
"N": result["n"][index],
"S": result["s"][index],
"next_discrete": result["next_discrete"][index],
"next_smooth": result["next_smooth"][index],
}
)
models[model] = {**result, "constant_field": constant, "probes": probe_values}
legacy_map = []
for face in range(6):
for label, x, y in (
("left", -1, 1),
("right", k, 1),
("bottom", 1, -1),
("top", 1, k),
("masked_bottom_left_diagonal", -1, -1),
("masked_top_right_diagonal", k, k),
):
mapped = legacy_sphere_map(face, x, y, k)
legacy_map.append(
{
"face": face,
"region": label,
"query": [x, y],
"mapped": list(mapped) if mapped is not None else None,
}
)
return {
"format_version": FORMAT_VERSION,
"description": "Compact even-K corrected global enumeration and deterministic legacy side-gutter sphere goldens.",
"k": k,
"radius": k / 2.0,
"ra_planar": ra,
"rule": asdict(rule),
"field": field,
"geometry": geometry,
"probe_definitions": probes,
"legacy_side_and_mask_map": legacy_map,
"models": models,
}
def delayed_time_fixture() -> dict[str, Any]:
shape = (5, 4)
depth = 16
head = 5
history = [[layer / 15.0] * product(shape) for layer in range(depth)]
rule = Rule(sigmode=2, sigtype=4, mixtype=4)
result = delayed_step(history, shape, head, 2.2, rule)
shells: dict[int, list[list[int]]] = {}
for entry in result["stencil"]["entries"]:
shells.setdefault(entry["delay"], []).append([entry["dx"], entry["dy"]])
return {
"format_version": FORMAT_VERSION,
"description": "Layer-coded causal radial shells; head is next overwrite; both direct and fixed smooth updates.",
"shape": list(shape),
"depth": depth,
"history": history,
"head": head,
"latest": (head - 1) % depth,
"rule": asdict(rule),
"shell_offsets_by_rounded_age": [
{
"age": age,
"offsets": offsets,
"selected_layer": ((head - 1) - age) % depth,
}
for age, offsets in sorted(shells.items())
],
"evaluation": result,
}
def initializers_fixture() -> dict[str, Any]:
zero_rng = ChaCha12(0)
seed_one_rng = ChaCha12(1)
seed_one_u64_rng = ChaCha12(1)
seed_one_float_rng = ChaCha12(1)
return {
"format_version": FORMAT_VERSION,
"description": "Exact topology-native seeded f64 initializer arrays; arrays, not hashes, are authoritative.",
"prng": {
"algorithm": "ChaCha12, 256-bit key = LE seed u64 + 24 zero bytes, original 64-bit counter and 64-bit stream both zero",
"u64_order": "low u32 then high u32",
"float": "top 53 bits of u64 multiplied by 2^-53",
"integer": "reject u64 values at or above 2^64-(2^64 mod span), then modulo span",
"seed_0_first_16_u32_hex": [f"0x{zero_rng.u32():08x}" for _ in range(16)],
"seed_1_first_16_u32_hex": [
f"0x{seed_one_rng.u32():08x}" for _ in range(16)
],
"seed_1_first_8_u64_hex": [
f"0x{seed_one_u64_rng.u64():016x}" for _ in range(8)
],
"seed_1_first_8_float53": [seed_one_float_rng.float53() for _ in range(8)],
},
"planar_periodic_splats": [
planar_splats((12,), 3.0, 1),
planar_splats((8, 6), 2.0, 1),
planar_splats((6, 5, 4), 1.6, 1),
],
"sphere_geodesic_overlays": sphere_overlays(6, 1, 1000),
"delayed_time_periodic_boxes": delayed_boxes((17, 16), 1, 16, 1000),
}
DESCRIPTIONS = {
"indexing.json": "Indexing and signed offsets",
"curves.json": "Scalar curves and boundaries",
"rule_surface.json": "Complete rule enum surface",
"planar_fields.json": "Sampled kernels and direct convolution",
"legacy_packed_fft.json": "Packed-unitary f32 FFT oracle",
"integration.json": "Euler, AB startup, RK4 references",
"multiscale.json": "All multiscale policies",
"sphere.json": "Corrected and legacy sphere",
"delayed_time.json": "Causal delayed-time shells",
"initializers.json": "ChaCha12 and exact seeded arrays",
}
def build_payloads() -> dict[str, Any]:
return {
"indexing.json": indexing_fixture(),
"curves.json": curves_fixture(),
"rule_surface.json": rule_surface_fixture(),
"planar_fields.json": planar_fields_fixture(),
"legacy_packed_fft.json": packed_fft_fixture(),
"integration.json": integration_fixture(),
"multiscale.json": multiscale_fixture(),
"sphere.json": sphere_fixture(),
"delayed_time.json": delayed_time_fixture(),
"initializers.json": initializers_fixture(),
}
def expected_files() -> dict[str, bytes]:
payloads = build_payloads()
rendered = {name: _render(payload) for name, payload in payloads.items()}
manifest = {
"format_version": FORMAT_VERSION,
"contract": "Macrostep 00 deterministic oracle fixtures",
"generator": "tools/generate_contract_fixtures.py",
"oracle": "tools/contract_oracle.py",
"standard_library_only": True,
"semantic_float": "IEEE-754 binary64 represented as JSON numbers",
"packed_fft_float": "IEEE-754 binary32 after every operation, with decimal values and hexadecimal bit patterns",
"determinism": "No timestamp, locale, platform RNG, external source tree, or runtime dependency is used.",
"files": [
{
"path": name,
"description": DESCRIPTIONS[name],
"bytes": len(rendered[name]),
"sha256": hashlib.sha256(rendered[name]).hexdigest(),
}
for name in payloads
],
}
rendered["manifest.json"] = _render(manifest)
return rendered
def write_files(files: dict[str, bytes]) -> None:
FIXTURE_DIR.mkdir(parents=True, exist_ok=True)
for name, content in files.items():
(FIXTURE_DIR / name).write_bytes(content)
def check_files(files: dict[str, bytes]) -> int:
failures: list[str] = []
for name, expected in files.items():
path = FIXTURE_DIR / name
if not path.exists():
failures.append(f"missing: {path.relative_to(ROOT)}")
elif path.read_bytes() != expected:
failures.append(f"stale: {path.relative_to(ROOT)}")
if FIXTURE_DIR.exists():
extras = sorted(
path.name for path in FIXTURE_DIR.glob("*.json") if path.name not in files
)
failures.extend(
f"unexpected: {(FIXTURE_DIR / name).relative_to(ROOT)}" for name in extras
)
if failures:
sys.stderr.write("contract fixtures are not current:\n")
for failure in failures:
sys.stderr.write(f" {failure}\n")
return 1
sys.stdout.write(f"verified {len(files)} contract fixture files\n")
return 0
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--check", action="store_true", help="verify committed bytes without writing"
)
args = parser.parse_args(argv)
files = expected_files()
if args.check:
return check_files(files)
write_files(files)
sys.stdout.write(
f"wrote {len(files)} contract fixture files under "
f"{FIXTURE_DIR.relative_to(ROOT)}\n"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())