Commit graph CultLib/packages/cultmath
Author SHA1 Message Date
Meta Vens
0acc3728ed CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
MPL-2.0 is file-level and the Phacelle port sat inside MIT files. phacelle and
CultPhasor now live in math.Phacelle.cs and CultPhasor.cs, and cultmath_phacelle
in CultMath.Phacelle.hlsl (included by CultMath.hlsl, both tracked copies), each
with the MPL header and Johansen / Luke Mitchell attribution. math stays one
partial class, so math.phacelle is called exactly as before. The GameCult.Math
package declares MIT AND MPL-2.0 and packs THIRD-PARTY-NOTICES.md and the new
include; the Unity LICENSE.md points at CultLib; build-unity-package.ps1
requires the new include.

The random sweeps never met a cell whose box bound sits just under the 2.25 cut
while its weight is still nonzero, so cuts of 2.245, 2.24 and 2.2 survived.
Six hard-coded points (found by an offline search) sit next to such cells; the
C# pruned-vs-unpruned test and the HLSL mirror test both use them, with a
witness check that re-derives the property from the hash.

Docs: the prune keeps about 45 of 64 cells (14 is the nonzero-weight count),
the support radius is 1.49998 (d^2 = 2.24995), normalization's range and
saturation, W = 0 only for adversarial hashes, and the support-edge gradient
seam size.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:34:27 +02:00
Meta Vens
2a3cab1bd7 CultMath tests: a dense sweep pins cultmath_phacelle against C# bit for bit
The generic mirror comparison passes about a hundred inputs through the shader and
missed a mirror whose prune cut was 2.0 instead of 2.25, because only a rare cell has
a nonzero weight under that cut. Twenty thousand random arguments compare the shader
text and math.phacelle field by field and kill that mutant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:58:43 +02:00
Meta Vens
9f18a432cb CultMath: document phacelle and record Johansen's MPL-2.0 provenance
design.md names CultPhasor as the second struct-return consumer and describes
phacelle (stripe wave vector, weight, normalization, exact gradient and its two
discontinuity classes, exact pruning). THIRD-PARTY-NOTICES.md, in both the package
root and the Unity package, cites Rune Skovbo Johansen's Phacelle Noise (MPL-2.0)
and records that the 3D-cell generalisation, the caller-supplied side vector, the
exact gradient and the exact pruning are new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:51:04 +02:00
Meta Vens
7904e15ed3 CultMath: phacelle, Johansen's Phacelle noise on 3D cells with an exact gradient
Cut 2a-ii-b. phacelle(float3 p, float3 side, float offset, float normalization)
returns CultPhasor { float4 cos; float4 sin }, each float4(gradient.xyz, value.w).

- 4x4x4 cells, pcg3d-hashed jitter in [-0.5, 0.5), weight max(0, exp(-2 d^2) - 0.01111),
  normalization as upstream (lpmitchell/AdvancedTerrainErosion PhacelleNoise). The
  caller supplies the stripe wave vector; offset is in cycles.
- The gradient is exact: weight derivatives and the normalization are included, not
  upstream's grad(cos) ~ -sin * side.
- Cells whose per-axis lower bound is at least 2.25 have weight exactly 0 and are
  skipped. An internal overload switches the prune off so the test can pin bit
  equality without a second copy of the loop.
- cultmath_phacelle and CultPhasor mirror it in both tracked CultMath.hlsl copies
  (the same blob). The mirror test compares the struct field by field; it now covers
  34 functions.
- PhacelleTests: value against an independent double-precision reference, quarter-cycle
  phase rotation, gradient against central differences on both sides of the
  normalization threshold (excluding the threshold and each cell's weight-support
  edge, where the exact gradient is discontinuous), stripes along the flow on the unit
  sphere (0.215 against the 0.25 bound), step-difference continuity across cell planes,
  and pruned == unpruned bit for bit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:51:02 +02:00
Meta Vens
65311c9ad7 Merge the snoise seam fix (Asura Q-N1 a): upstream kernel r2=0.5, scale 105, permute +10
Soul: no defect; independent port of stegu 22434e04d7 matches (3D and 2D), goldens
confirmed independently, value jump 2.2e-6 over 20k crossings, gradient FD 9.7e-6,
every hand mutant killed. 2D follows upstream by ruling. Release follow-ups: rebuild
the Unity CultMath.dll, CHANGELOG entry, pin the two HLSL copies equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:55:08 +02:00
Meta Vens
09507901d0 cultmath: BoundedLeastSquares doc states the cost gap is relative to |b|^2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:14:21 +02:00
Meta Vens
c8227c6347 cultmath: BoundedLeastSquares tests drive stalled passes to the stall rule
Batches of small boxed problems whose passes end with the KKT test failing and
the cost unmoved (dependent free columns, releases the factor drops, weak-direction
noise-floor problems). They must report Converged, and land within the documented
gap of an exact active-set enumeration. Pins the stall comparison and the cost
scale in Objective, which no test reached before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:14:21 +02:00
x
0a5f4bf571 CultMath: regenerate snoise golden values for the upstream kernel
Seven values change: four snoise(float3) in HlslSemanticsTests and three snoise(float2)
in MathTests (the 2D (0,0) golden is 0 either way). Regenerated from the code after the
continuity probe passed. The tests no longer claim to match Unity.Mathematics, whose
snoise keeps the old constants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:09:43 +02:00
x
641041822d CultMath: noise gradient tests carry only numbers the harness produces
Drops the unreproducible floors and detection-scale claims, and the seam-fluke
consistency filter (the spikes it hid were the snoise seam, now fixed and pinned by the
continuity probe). Tightens the tolerances to at least 5x the harness maximum (ridged
0.5 -> 0.05), corrects the point count in the bit-equality test, and caps the ridged
compounding loop so a mutant fails loudly instead of hanging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:04:08 +02:00
Meta Vens
8703b44636 CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
Meta Vens
4ca1cf388a CultMath: continuity probe for snoise across simplex-cell boundaries (red on the 0.6 kernel)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:10:18 +02:00
Meta Vens
6328f15345 cultmath: BoundedLeastSquares test drives the tolerance below the noise floor
With b along the smallest left singular vector, |A^T b| is tiny, so the KKT
tolerance (1e-6 of it) sits below the gradient's rounding noise at cond(A) >=
1e6 and can never be met. The solver must stop on the progress rule, in at most
six passes, never at the cap. This is the path the progress rule exists for;
the earlier fixtures reached it only by accident.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:52:39 +02:00
Meta Vens
854f01e4c7 cultmath: BoundedLeastSquares tests pin the tolerance scale and the zero gradient
Two tolerance behaviours had no observer: the scale taken at the box point
nearest the origin (a 6e-5 release on a 1000-scale problem, in either column
position), and the strict comparisons when the scale and the gradient are both
exactly zero (b = 0 at the origin takes no iteration).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:37:27 +02:00
Meta Vens
0a80624930 cultmath: BoundedLeastSquares tests observe the warm-start clamp and the workspace
The full-step clamp repairs an out-of-box start on a free column, so the
warm-start clamp was invisible to the old test (Stryker: five survivors). The
new table runs one column with b beyond the box and a cap of 0, where the
solver returns the clamped start untouched, across finite, infinite and NaN
starts and finite and infinite bounds.

Workspace junk (including NaN doubles) is now checked on random boxed problems
that release bounds and use the tolerance, with bit-identical x and equal
iteration counts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:44:04 +02:00
Meta Vens
d808941b87 cultmath: BoundedLeastSquares sheds redundant checks and scratch arithmetic
Measured with Stryker once Solve was actually mutated (21 survivors), most of
them were code that decided nothing:

- The finite-input loops over A and b duplicated the non-finite test on the
  accumulated A^T b, which is exactly equivalent (floats cannot overflow a
  double product, and NaN/infinity never cancel to a finite value). One check
  on each A^T b entry remains.
- The tolerance scale is the gradient at the box point nearest the origin,
  computed inline; the max with |A^T b| duplicated it whenever the origin is in
  the box, and the scratch vector for it is gone.
- Bound violation is state * gradient, which is zero for a free column; the
  three-way ternary duplicated the free-stationarity check.
- The warm-start clamp is Math.Clamp plus a NaN test.
- Workspace slices come off a cursor (Take) instead of hand-summed offsets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:44:04 +02:00
Meta Vens
4072fce0e7 cultmath: BoundedLeastSquares drops the refinement step it cannot observe
Stryker left the refinement step (removed, negated, or subtracted) unobserved,
and it is unobservable for a reason: refinement with a same-precision residual
does not improve forward error, and Cholesky is backward stable, so the
residual the KKT test reads is already at rounding level. Measured on n=24
problems at cond(A) 1e3..1e6, the solver with and without the step gives the
same iteration counts and the same x error against a Gram-Schmidt QR
reference (both limited by cond(A^T A) * eps).

The refinement pass, its dp buffer and the scratch vector are deleted;
WorkspaceLength returns to 4n^2 + 15n. The doc states where the normal
equations stop being optimal (cost gap below 1e-6 |b|^2 through cond(A) 3e5,
about 2e-5 at 1e6, independent of the scale of A) and that a ridge row beyond
that is the caller's policy.

status is now initialised: the unassigned local made Stryker discard every
mutant inside Solve as a compile error, so 255 mutants were never measured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:29:06 +02:00
Meta Vens
6982adfaaf cultmath: BoundedLeastSquares tests pin iteration counts and the b = 0 start
- n=24 fully free problems at cond 1e2/1e3/1e4, and A x 1e6 with unscaled
  bounds, converge in at most two iterations (a regression to cap-spinning
  fails on the count, not on a clock).
- b = 0 from a far start reaches the optimum instead of the start.
- Warm-start clamp table, free duplicate column gets no step, parallel columns
  at absolute scales 1e3 and 1e-3, near-singular free sets confirm once and
  stop, near-collinear optimum at A x 1e6, and zero allocation on the invalid,
  converged and cap paths (first and second call).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 05:36:50 +02:00
Meta Vens
43035bc827 cultmath: BoundedLeastSquares iterates in double and stops on progress
The working iterate now lives in double inside the workspace (WorkspaceLength
4n^2 + 17n); A, b and the API x stay float and x is converted in at the start
and out at the end. The gradient, the free-stationarity check and the KKT test
all run in the double domain, which removes the float noise floor that made the
free-stationarity re-solve spin to the cap on optimal points (cond >= ~20).

A fixed tolerance is no longer the only stopping rule: after a stationarity
miss or a release, a pass that lowers the double cost by no more than rounding
reports Converged.

The tolerance scale is the larger of the gradient at the origin and at the box
point nearest the origin, so it never depends on the warm start; b = 0 with a
far start is no longer judged converged by its own steepness.

Two clauses were equivalent or unobservable and are deleted: the d <= 0 pivot
test (implied by d <= tol * diag) and the dependent[] exclusion from the
free-stationarity check (the progress rule now terminates it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 05:36:50 +02:00
Meta Vens
50f26de4a7 cultmath: BoundedLeastSquares factors the normal equations in double
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:49:29 +02:00
Meta Vens
44affbb9b6 cultmath: scale-free KKT, InvalidInput contract, free-set refinement
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:46:36 +02:00
Meta Vens
8b1a1ffde0 cultmath: near-collinear test uses generators that reproduce the gap on the old solver
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:44:57 +02:00
Meta Vens
6f8cb642ff cultmath: InvalidInput status and Soul tests (red on the old solver)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:43:32 +02:00
Meta Vens
986711c9d5 cultmath: tests for Stryker survivors (dirty workspace, exact caps, spans, near-duplicates)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:19:28 +02:00
Meta Vens
32fb7c1503 cultmath: FISTA reference keeps the random KKT test fast
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:52:51 +02:00
Meta Vens
c65777b400 cultmath: stop the projected-gradient reference once it stops moving
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:33:51 +02:00
Meta Vens
5382b81036 cultmath: widen fixed-column fixture so no other column blocks
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:14:39 +02:00
Meta Vens
eec1e40c25 cultmath: pin that a fixed column is never released
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:38:56 +02:00
Meta Vens
60c3d6fb0d cultmath: BoundedLeastSquares primal active-set solver
Dense small bounded LS, zero allocation, caller workspace, C#-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:33:08 +02:00
Meta Vens
c9dabbeef6 cultmath: fix stale test-name references in snoise_grad/fbm_grad/ridged_grad comments
The doc comments named test classes/methods (SnoiseGradTests, FbmGradTests,
RidgedGradTests) that were never committed under those names; the actual
coverage lives in NoiseGradTests. Point the comments at the real methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 03:05:08 +02:00
Meta Vens
bbf00394de tests: behavioural coverage for snoise_grad, fbm_grad, ridged_grad
snoise_grad.w is bit-equal to snoise (stronger than the spec's 1e-6
floor, measured over 20,000 points). fbm_grad/ridged_grad with 1/3
octaves are pinned bit-exact against a closed form built with the same
incremental gain/lacunarity multiply chain the production loop uses,
catching both a swapped-argument mutant and an applied-once mutant.
The shared invariant-8 property harness central-differences all three
functions' own .w, filtering simplex-lattice seam flukes by comparing
the estimate at two step sizes (measured tolerances and detection
floor in the harness comment). A dedicated fixture bisects to a snoise
zero crossing and pins that ridged_grad's two one-sided gradients there
have opposite sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:54:17 +02:00
Meta Vens
256f8e20be tests: cover snoise_grad, fbm_grad, ridged_grad mirrors in HLSL compat test
Adds explicit function-presence assertions and bumps the reflective
bit-parity oracle's expected mirror count from 30 to 33 now that the
three new cultmath_ mirrors are covered automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:54:17 +02:00
Meta Vens
03f0ed7a0a docs: describe snoise_grad, fbm_grad, ridged_grad in cultmath design.md
Extends the invariant-8 primitives list with the new gradient-noise
family: the constant-retargeting for snoise_grad, the per-octave
frequency/amplitude chain rule, ridged_grad's deliberate zero crease,
and the octave clamp's reason (the HLSL mirror test's bit-parity
oracle).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:37:05 +02:00
Meta Vens
1249167989 cultmath: mirror snoise_grad, fbm_grad, ridged_grad to HLSL (cut 2a-ii-a)
cultmath_snoise_grad/fbm_grad/ridged_grad mirror the new C# math
functions in both tracked CultMath.hlsl copies, which stay one blob.
Extends the existing Ashima notice entry in both THIRD-PARTY-NOTICES.md
copies to cover the ported differentiation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:36:59 +02:00
Meta Vens
e87dcc9ec5 cultmath: add snoise_grad, fbm_grad, ridged_grad to C# math (cut 2a-ii-a)
Analytic-gradient simplex noise, ported from webgl-noise's
noise3Dgrad.glsl differentiation but retargeted onto this repo's own
snoise(float3) constants (0.6 falloff radius, +1 permute offset, 42
scale) so snoise_grad.w stays exactly snoise's own field. fbm_grad and
ridged_grad are octave sums with per-octave frequency/amplitude
compounding (invariant 8). octaves is clamped to [0, 16] so the HLSL
mirror test's full-int32-range oracle cannot turn a garbage octave
count into a multi-billion-iteration loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:36:52 +02:00
Meta Vens
4922438334 cultmath: correct F6 proof arithmetic and chi-square provenance in cellular docs
Soul pass 3 confirmed two documentation defects: the F6 search-radius proof
misstated 3 - 2m + 3m^2 at m=1/2 as 2.5 (actual 2.75; true min on [0,1/2] is
8/3 at m=1/3), and both math.cs and design.md cited a fabricated 203.8
chi-square statistic for float-bit hashing that Soul could not reproduce
(6+ seeds scored 7.7-30.9). Corrected the arithmetic and replaced the
unreproducible measurement with what actually pins integer hashing: the
oracle and mirror tests, and the chi-square test's real scope (the hash as
cellular calls it, not the call site itself). No test or code changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:23:55 +02:00
Meta Vens
05ca826091 tests: pin cellular's HLSL mirror against radius-2, 2^24, and tie inputs
EveryMirrorFunctionMatchesCSharpMath's generic random/special-value cases
never happen to hit a point where a radius-2 cell wins (about 3.5e-5 of
points) or an integer point at or past 2^23, so an HLSL mutant of cellular's
search radius, its lower-bound prune, or the removed F2=0 guard could change
cultmath_cellular's real output and still pass this test.

Adds three targeted point cases, reusing CellularAndSminGradTests' own
fixtures so the mirror test does not carry a second copy of the construction:

- the six points from AxisAlignedRadiusTwoOffsets/FindPointRealizingOffset,
  each of which realizes a different axis-aligned radius-2 offset as F1 or
  F2 in the independent 7x7x7 oracle;
- four integer points at 2^24 (both signs), where distinct cells' jittered
  features can round onto the same float32 value, reaching the F2 = 0 guard;
- one exact F1 = F2 tie point, the midpoint of two adjacent cells' feature
  points (bit-exact-equidistant from both by construction, no search
  needed).

Verified against four HLSL mutants applied to shaders/CultMath.hlsl on
scratch copies (not committed) and reverted after each check:
- `dz < 2` instead of `dz <= 2`: killed by the axis-slice point at
  (-56.671272, -67.01973, -79.001).
- `dx` starting at -1 instead of -2: killed by the axis-slice point at
  (62, -77.91331, -79.42451).
- pruning against f1 instead of f2: killed by an axis-slice point and both
  of the 2^24 integer points (3 mismatches).
- the F2 = 0 guard removed (`grad2 = (p - c2) / f2`, unconditional): killed
  by the (2^24, 2^24, 2^24) integer point (NaN vs the guarded zero).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:14:10 +02:00
Meta Vens
91596884b0 tests: fix hashing fixtures, false comments, and gradient tolerance
CellularAndSminGradTests.cs changes for cut 2a-i's pass-2 findings:

- FeaturePoint and the two other independent pcg3d/pcg4d call sites now hash
  int3(cell)/int4(int3(cellId), 0) to match production's integer-hashing fix
  (previous commit); the oracle would otherwise diverge from cellular() and
  every comparison test would fail.

- New JitterAndIdPassAChiSquareUniformityTest: jitter.x/y/z and id each pass
  a chi-square uniformity test (16 bins, 15 df, critical value 37.7 at
  alpha=0.001) over 200,000 cells. This is the behavioural pin for the
  hashing fix; reverting to pcg3d(cell)'s float-bit hash fails it.

- Two false comments corrected to the measured facts: the
  NearestFeatureWins test's claim that |component|=2 offsets are "provably
  never the true nearest" is false (their lower bound is 1, not sqrt(3); Soul
  measured 3 F1 wins in 2,000,000 samples), and SearchReachesEveryAxis...'s
  claim of "lower bound is exactly 2" and "zero hits in 3,000,000" is also
  false (lower bound is 1; Soul measured about 1 hit in 28,000).

- PrunedSearchIsBitIdentical... renamed to
  ...BelowTwoToTheTwentyFifth and its comment states that domain explicitly
  (design.md, previous commit).

- GradientTolerance split into SminGradientTolerance (2.5e-4f) and
  CellularGradientTolerance (1.3e-3f): the two functions have different
  curvature (smin_grad is exactly quadratic in a band; cellular's F1/F2 are
  real distance fields), so a shared 3e-4 tolerance carried almost no
  headroom over either's actual measured noise (re-measured here at 5.3e-4
  for smin's old eps, 2.0e-4/2.6e-4 for cellular), not the "~6e-5" the old
  comment claimed. smin_grad's eps moves from 0.001f to 0.01f (no truncation
  cost there; dilutes float noise 8x) with a matching widened seam-exclusion
  band so p +/- eps cannot step across the seam the check at p already
  avoided. cellular's eps stays at 0.0005f (a small sweep found it already
  near the truncation/noise sweet spot for a real distance field). Both new
  tolerances keep >5x headroom over their own measured worst case and both
  still die under a 1.01x-scale mutant on the returned gradient — smin down
  to 1.0005x, cellular (M9/M16-style, either gradient) down to 1.002x.

- GradientMatchesCentralDifferencesAwayFromTheF1EqualsF2Seam also excludes a
  band around F2 = F3 now, not just F1 = F2: the second-nearest feature's
  identity swaps there too, a genuine kink the old test could silently hit
  (an unfiltered large sample found a 0.78 edge error this way, swamping the
  ~2e-4 the filtered test actually sees).

- AxisAlignedRadiusTwoOffsets and FindPointRealizingOffset are extracted from
  SearchReachesEveryAxisAlignedRadiusTwoSlice as internal fixtures so
  HlslSourceCompatibilityTests (next commit) can reuse the same construction
  instead of a second copy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:50 +02:00
Meta Vens
cb94b74ae3 docs: correct F6 proof, integer hashing, and the prune's precision domain
design.md follow-ups matching the math.cs/HLSL fix in the previous commit:

- The F6 "5x5x5 is exact" argument is rewritten with the corrected near-side
  neighbour bound (choose the axis closest to its own boundary, not a fixed
  axis), the m <= 1/2 derivation, and the counterexample that showed the old
  argument wrong. Same text as the math.cs comment, so the two do not drift.

- "Integer hashing" now states that cellular hashes the winning cell's
  integer coordinate, not its float32 bit pattern, and records the measured
  chi-square evidence (203.8 over 1,000,000 cells, 15 df, critical value
  37.7) for why the old bit-pattern hash failed a uniformity check.

- "Precision domain" gets a new paragraph: the lower-bound prune's bit
  identity to the unpruned 5x5x5 search holds for |p| < 2^25, not
  unconditionally. Above that, cell + offset itself starts to round in
  float32, so the pruned and unpruned loops can visit different actual
  neighbours at the same nominal offset (Soul, cut 2a-i: 180 of 1,100,000
  points differ at |p| ~= 3.4e7). This is a float32 domain limit on the
  equivalence proof, not a bug in the prune; the committed test's [-50, 50]^3
  sweep is comfortably inside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:25 +02:00
Meta Vens
9207a59280 cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:

- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
  integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
  instead of the integer coordinate itself. Two adjacent cells' bit patterns
  share more structure (same exponent, mantissa one increment apart) than
  the integers do going into pcg3d's own mixing, and that structure survived
  into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
  cells against 15 degrees of freedom (critical value 37.7). Both call sites
  now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
  HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
  as before). This changes cellular's noise output — nothing has shipped, so
  that is acceptable, but it is a real behavioural change, not a comment fix.

- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
  picking the near-side neighbour on a FIXED axis is not always within
  sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
  jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
  exclusion). The comment is rewritten to the correct argument: choose the
  axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
  1/2 on that axis), and the near-side neighbour on THAT axis has squared
  distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
  strictly below 3 since the jitter never reaches the boundary. Margin
  2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
  sqrt(3), only in the unattainable corner limit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
Meta Vens
e739ff1e52 tests: kill a single-slice loop-bound gap in cellular's 5x5x5 search
Stryker (mtp, --since 29e50ad3) found 261 survivors / 24 timeouts across the
whole repo; triaging the ones in this fix batch's own new code (cellular's
loop, its prune, cellular_unit) surfaced one real gap disguised as
collateral timeout noise from a batched infinite-loop mutant: dz < 2 (and
the dy/dx equivalents), silently dropping the +2 layer on one axis alone,
passed every existing random-sampled test. That is not the sweeps being
weak: the exactness proof's own margin (F1, F2 always < sqrt(3), radius-2
cells' lower bound is exactly 2) makes a genuine win from any single
|offset component| = 2 cell astronomically rare over uniformly random
points (measured zero hits in 3,000,000 samples).

SearchReachesEveryAxisAlignedRadiusTwoSlice constructs a real example for
each of the six axis-aligned radius-2 offsets directly: for a candidate
integer cell, build the query point that is the closest point of that
cell's own unit cube to the target neighbour's actual jittered feature.
That construction finds a hit for every offset within a couple thousand
candidate cells, deterministically. This kills dz < 2 / dy < 2 / dx < 2 (all
manually confirmed).

Also manually confirmed via Stryker's own reported mutants: the dz--/dy--/
dx-- (increment-to-decrement) mutants are genuine infinite loops, correctly
caught as timeouts. The f2 >= 0.0f mutant (disabling the F2 = 0 guard) is
killed by the existing NoNaNAtIntegerPointsNear2To24 test. The remaining
survivors/timeouts in the new code (the lower-bound prune's >>= vs >
comparison, its arithmetic terms, and cellular_unit's >> vs >>> shift on an
already-unsigned operand) are equivalent mutants: since the per-axis lower
bound is always 0 or 1 by construction, and pruning is a proven-safe
optimization that never changes cellular's output, no mutation confined to
"when does the prune trigger" can be observed by any value-based test.
Verified by hand: disabling pruning entirely reproduces the unpruned
reference bit for bit, as it must.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 21:28:36 +02:00
Meta Vens
3b1ea71eef docs: correct cellular/smin_grad design.md to match fix batch 1
- smin_grad is Quilez's quadratic-polynomial smin, not cubic.
- cellular's search radius is 5x5x5 (F6), not 3x3x3; adds the exactness
  proof and the pruning claim.
- id source is now pcg4d over the winning cell, not the pcg3d hash that
  produced its jitter (F4).
- F2 = 0 is documented as reachable (past |p| of about 2^23), not
  unreachable; removes the false "unreachable" claim and adds a "Precision
  domain" note stating the float32 feature-spacing/tie behaviour honestly
  (F3, minor finding).
- The struct-mirror comparison note now says it also matches field name and
  type in declaration order, not just position (F2).

No entry added to THIRD-PARTY-NOTICES for the Quilez/Worley formulas: per
Self's standing ruling, a published formula is not ported code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:43 +02:00
Meta Vens
f6fb589ac3 Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):

- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
  3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
  comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
  are always < sqrt(3), every excluded cell is always > 2). A cheap
  position-only lower-bound prune skips the pcg3d/jitter/distance work for
  cells that provably cannot beat the running F2, without changing any
  output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
  fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
  hash that produced that cell's jitter, so id and jitter are no longer
  deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
  the mapping is always inside [0, 1); the old formula rounded
  0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
  noise at the committed eps is about 6e-5), and cellular's own gradient
  test now differentiates cellular's RETURNED .w values instead of a
  duplicate FindF1F2 search, so a mutant that corrupts only the returned
  gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
  field name and declared type in declaration order, not just position, so
  a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
  (H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
  pin production's exact agreement with it, the pruned search's bit-for-bit
  equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
  correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
  cubic.

Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.

Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
Meta Vens
1add078eea Delete cellular's unreachable F2=0 guard instead of leaving it undefended
Stryker (scoped to this cut's diff) survived two mutants on the grad2 ternary
in math.cellular: forcing the true branch unconditionally, and loosening
f2 > 0.0f to f2 >= 0.0f. Both are real findings about the code, not the
tests: F2 = 0 needs two distinct integer cells' pcg3d-jittered feature points
to land on the exact float32 value in every component, which the jitter's
~24-bit float precision makes unreachable for any p this function is
actually called with. The spec only asks for the degenerate guard at F1 = 0;
F2's copy of it was added for symmetry and had no reachable case to defend.

Deleted the guard (grad2 is now a bare (p - c2) / f2) in math.cs and both
HLSL mirrors, and corrected design.md's invariant-8 section, which had
overstated this as symmetric with F1's guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:47:36 +02:00
Meta Vens
08b0dced53 Fix flaky central-difference assertions: use absolute tolerance, not rounding
dotnet test found two failures where a fixed-decimal-place Assert.Equal
snapped two numbers that already agreed to within about 2e-5 to different
rounded digits, because their true difference straddled a rounding boundary
(0.824987829 vs 0.825006127 -> 0.82 vs 0.83). Both were central-difference
checks (smin_grad's own invariant-8 property test, and cellular's gradient-
matches-central-differences test), not a correctness bug in the primitives.

AssertWithinTolerance replaces the precision-based assertion for both central-
difference comparisons with an explicit absolute-difference check, which
states the actual claim (agreement within the finite-difference step's own
truncation error) without the rounding-boundary artifact. Also dropped an
unused local function (F2At) the build flagged.

Found by the third ygg-verify.sh test run on 77c3580.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:28:36 +02:00
Meta Vens
77c35808f5 Fix a build break: bare Random resolved to CultMath's own xorshift32
Inside namespace CultMath.Tests, unqualified `Random` resolves to the
enclosing CultMath namespace's own Random struct (design.md: "Random is
CultMath's own xorshift32") before `using System;` is even considered, so
`new Random(seed)` in the new test file was constructing CultMath.Random,
which has no NextSingle(). HlslSourceCompatibilityTests.cs already works
around this the same way (`new System.Random(...)`); CellularAndSminGradTests
now qualifies every use the same way.

Found by the second ygg-verify.sh build/test run on e613ed6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:23:54 +02:00
Meta Vens
e613ed6277 Fix a build break: float3.zero shadowed by math's own float3(...) method
Inside the static math class, the unqualified name `float3` resolves to
math's own `float3(float,float,float)` constructor-function first, not the
CultMath.float3 type, so `float3.zero` fails to compile there (CS0119). No
prior code in math.cs used the Type.zero static-field spelling from inside
math itself for this reason. Switched cellular's zero vectors to
`new float3(0.0f, 0.0f, 0.0f)`, which is unambiguous under `new`.

Found by the first ygg-verify.sh build/test run on 3eed434.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:21:37 +02:00
Meta Vens
3eed434b31 Test smin_grad and cellular's invariants behaviourally
CellularAndSminGradTests pins the rules the cut map calls out, each against a
mutant it must kill:

- smin's value never exceeds the ordinary minimum, and matches min with that
  input's own gradient once |a.w - b.w| exceeds k on either side.
- smin_grad's gradient matches central differences of its own .w at random
  seeded points (the formula is exact, not an approximation, so no exclusion
  band is needed away from the |a.w-b.w|=k seam itself).
- A direct case falsifies blending the gradient with a constant 0.5 or with
  (1 - h) instead of h.
- cellular's F1 never exceeds F2, matched against an independent brute-force
  Worley search that shares only the public pcg3d hash, not cellular's control
  flow.
- The true nearest feature wins even when an independent search finds it in
  the (1, 1, 1) neighbour offset, the last one cellular's own loop order
  visits, ruling out a "first found" or early-exit mutant.
- The gradient at a feature point is finite zero, not NaN.
- Both nearest and edge gradients match central differences away from a band
  around the F1 = F2 set, where the nearest and second-nearest identity swap
  discontinuously.
- id is constant across small moves that stay in the same feature's region and
  differs from a distant cell's id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:18:43 +02:00
Meta Vens
c13e7f7871 Extend the HLSL mirror comparison to one struct return shape
HlslSourceCompatibilityTests previously flattened only bare vector returns to
scalar leaves. Values() now recurses through struct fields of any type, so a
struct return (invariant 8's one shape, e.g. CultCellular) is compared field
by field down to bit-for-bit scalars, exactly like a vector return already
was. Components() stays scoped to scalar-typed fields, since it only ever
builds function arguments and must not recurse.

TransformHlslToCSharp gains transformation 5: struct fields gain `public`,
since HLSL has no field access-modifier concept and a C# struct's fields
default to private, which would hide them from Values()' reflection. The old
wrap-in-a-class transformation is renumbered 6. design.md's transformation
list documents both, matching design.md:92's numbered list.

StructReturnComparisonRejectsAMismatchedField is the extension's own test: two
hand-built structs with the same shape (mirroring a real mirror-vs-C#
comparison) prove the field-by-field, bit-for-bit walk actually rejects a
struct whose leaf field differs, or whose nested vector's own component
differs, not just a length check.

EveryMirrorFunctionMatchesCSharpMath and HlslMirrorTests' required-symbol list
now cover cultmath_smin_grad and cultmath_cellular; the mirrored-function
count moves from 28 to 30.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:18:21 +02:00
Meta Vens
d6bfc166da Mirror smin_grad and cellular in CultMath.hlsl
Adds cultmath_smin_grad, the CultCellular struct, and cultmath_cellular to
both tracked copies of CultMath.hlsl (packages/cultmath/shaders and the Unity
package), placed after cultmath_pcg4d so cultmath_cellular can call
cultmath_pcg3d and cultmath_distance, both of which HLSL requires to be
declared earlier in the file. Both copies stay byte-identical.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:18:01 +02:00
Meta Vens
1dca43596e Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.

cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.

Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.

design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00