From 6a3132f1399e91e57dbbf375926336c19cfe63a9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:53:37 +0200 Subject: [PATCH] cultmath: span stage value and gradient against an independent octave assembly SpanStageGradientMatchesDifferences now also compares field.w and field.xyz with an octave sum assembled in the test from Stage.Point and snoise_grad, so shift, offset, octaveStep, M, lacunarity and gain are pinned numerically, which a value/gradient self-consistency check cannot do. Co-Authored-By: Claude Sonnet 5.5 --- .../tests/CultMath.Tests/SpanStageTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index bebed1cd..7ec26cbc 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -217,6 +217,28 @@ public sealed class SpanStageTests var span = Span.Draw(random, q, 0.1f, 0.1f); SpanNoise At(float3 p) => ridged ? stage.Ridged(p, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength) : stage.Fbm(p, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); var centre = At(q); + // The value and gradient from an independent assembly of the octaves: pins shift, offset, octaveStep, + // M, lacunarity and gain themselves, which a value/gradient self-consistency check cannot see. + var matrix = stage.Matrix(); + var referenceValue = 0.0; + var referenceGradient = new double[3]; + for (var o = 0; o < stage.Octaves; o++) + { + var n = snoise_grad(stage.Point(q, o)); + var sign = ridged ? -Math.Sign(n.w) : 1.0; + referenceValue += stage.Amplitude(o) * (ridged ? 1.0 - Math.Abs(n.w) : n.w); + var g = new double[] { n.x, n.y, n.z }; + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + referenceGradient[i] += sign * stage.Amplitude(o) * stage.Scale(o) * matrix[i, j] * g[j]; + } + + var weight = 0.0; + for (var o = 0; o < stage.Octaves; o++) + weight += stage.Amplitude(o) * stage.Scale(o) * stage.Norm(); + Assert.True(Math.Abs(referenceValue - centre.field.w) <= 5.0e-4 * (1.0 + weight), $"stage {stage}: the value is {centre.field.w:R}, an independent octave sum gives {referenceValue:R}"); + for (var i = 0; i < 3; i++) + Assert.True(Math.Abs(referenceGradient[i] - centre.field[i]) <= 5.0e-3 * (1.0 + weight), $"stage {stage}: gradient {"xyz"[i]} is {centre.field[i]:R}, an independent octave sum gives {referenceGradient[i]:R}"); // A step of 0.02 in the noise space of the fastest octave. var fastest = stage.Norm() * Math.Pow(stage.Lacunarity, stage.Octaves - 1); var h = (float)(0.02 / fastest);