2026-09-26 02:54:17 +02:00
using System ;
using CultMath ;
using Xunit ;
namespace CultMath.Tests ;
public sealed class NoiseGradTests
{
// ---- Invariant-8 property harness (shared by snoise_grad, fbm_grad, ridged_grad) ----
//
2026-09-29 12:04:08 +02:00
// Each test compares a function's analytic gradient with central differences of its own .w at
// random points in [-3, 3]^3. eps trades curvature error (eps too large) against float32
// rounding noise (eps too small). snoise is continuous across simplex-cell boundaries (the
// continuity test below), so no point needs excluding for a seam; ridged_grad excludes a band
// around its own deliberate crease. Each tolerance is at least 5x the largest error the harness
// itself produces at its seed.
2026-09-26 02:54:17 +02:00
private const float PropertyEps = 0.0001f ;
2026-09-29 12:04:08 +02:00
private const float SnoiseGradTolerance = 0.03f ;
private const float FbmGradTolerance = 0.05f ;
private const float RidgedGradTolerance = 0.05f ;
2026-09-26 02:54:17 +02:00
[Fact]
public void SnoiseGradGradientMatchesCentralDifferencesOfItsOwnValue ( )
{
var random = new System . Random ( 0xA11CE ) ;
var tested = 0 ;
var attempts = 0 ;
while ( tested < 200 & & attempts < 20000 )
{
attempts + + ;
var p = RandomPoint ( random ) ;
var g = math . snoise_grad ( p ) ;
float F ( float3 q ) = > math . snoise_grad ( q ) . w ;
var fine = CentralDifference ( F , p , PropertyEps ) ;
tested + + ;
AssertGradientWithinTolerance ( fine , g , SnoiseGradTolerance ) ;
}
2026-09-29 12:04:08 +02:00
Assert . True ( tested > = 150 , $"only {tested} of {attempts} points were usable" ) ;
2026-09-26 02:54:17 +02:00
}
[Fact]
public void FbmGradGradientMatchesCentralDifferencesOfItsOwnValue ( )
{
const int octaves = 4 ;
const float lacunarity = 2.0f ;
const float gain = 0.5f ;
var random = new System . Random ( 0xA11CF ) ;
var tested = 0 ;
var attempts = 0 ;
while ( tested < 200 & & attempts < 40000 )
{
attempts + + ;
var p = RandomPoint ( random ) ;
var g = math . fbm_grad ( p , octaves , lacunarity , gain ) ;
float F ( float3 q ) = > math . fbm_grad ( q , octaves , lacunarity , gain ) . w ;
var fine = CentralDifference ( F , p , PropertyEps ) ;
tested + + ;
AssertGradientWithinTolerance ( fine , g , FbmGradTolerance ) ;
}
2026-09-29 12:04:08 +02:00
Assert . True ( tested > = 150 , $"only {tested} of {attempts} points were usable" ) ;
2026-09-26 02:54:17 +02:00
}
[Fact]
public void RidgedGradGradientMatchesCentralDifferencesExcludingAnyOctavesZeroBand ( )
{
// ridged_grad's own crease (any octave's n_i = 0) is a real, deliberate discontinuity
// (design.md, "the operator's dune term"), not a numerical artifact: exclude a band around it
// for every octave, the same way cellular excludes its F1 = F2 seam.
const float zeroBand = 0.1f ;
const int octaves = 3 ;
const float lacunarity = 2.0f ;
const float gain = 0.5f ;
var random = new System . Random ( 0xA11D0 ) ;
var tested = 0 ;
var attempts = 0 ;
while ( tested < 200 & & attempts < 80000 )
{
attempts + + ;
var p = RandomPoint ( random ) ;
if ( AnyOctaveNearZero ( p , octaves , lacunarity , zeroBand ) )
continue ;
var g = math . ridged_grad ( p , octaves , lacunarity , gain ) ;
float F ( float3 q ) = > math . ridged_grad ( q , octaves , lacunarity , gain ) . w ;
var fine = CentralDifference ( F , p , PropertyEps ) ;
tested + + ;
AssertGradientWithinTolerance ( fine , g , RidgedGradTolerance ) ;
}
2026-09-29 12:04:08 +02:00
Assert . True ( tested > = 150 , $"only {tested} of {attempts} points were outside the zero band" ) ;
2026-09-26 02:54:17 +02:00
}
private static bool AnyOctaveNearZero ( float3 p , int octaves , float lacunarity , float band )
{
var frequency = 1.0f ;
for ( var i = 0 ; i < octaves ; i + + )
{
if ( MathF . Abs ( math . snoise_grad ( p * frequency ) . w ) < band )
return true ;
frequency * = lacunarity ;
}
return false ;
}
private static float3 RandomPoint ( System . Random random ) = > new (
random . NextSingle ( ) * 6.0f - 3.0f , random . NextSingle ( ) * 6.0f - 3.0f , random . NextSingle ( ) * 6.0f - 3.0f ) ;
private static float3 CentralDifference ( Func < float3 , float > f , float3 p , float eps ) = > new (
( f ( p + new float3 ( eps , 0.0f , 0.0f ) ) - f ( p - new float3 ( eps , 0.0f , 0.0f ) ) ) / ( 2.0f * eps ) ,
( f ( p + new float3 ( 0.0f , eps , 0.0f ) ) - f ( p - new float3 ( 0.0f , eps , 0.0f ) ) ) / ( 2.0f * eps ) ,
( f ( p + new float3 ( 0.0f , 0.0f , eps ) ) - f ( p - new float3 ( 0.0f , 0.0f , eps ) ) ) / ( 2.0f * eps ) ) ;
private static void AssertGradientWithinTolerance ( float3 numeric , float4 analytic , float tolerance )
{
AssertWithinTolerance ( numeric . x , analytic . x , tolerance ) ;
AssertWithinTolerance ( numeric . y , analytic . y , tolerance ) ;
AssertWithinTolerance ( numeric . z , analytic . z , tolerance ) ;
}
private static void AssertWithinTolerance ( float expected , float actual , float tolerance ) = >
Assert . True ( MathF . Abs ( expected - actual ) < = tolerance , $"expected {expected}, actual {actual}, tolerance {tolerance}" ) ;
2026-09-29 11:10:18 +02:00
// ---- snoise is continuous across simplex-cell boundaries ----
// The simplex corner set (i, i1, i2) summed by snoise(float3), derived the same way snoise does.
private static ( float3 i , float3 i1 , float3 i2 ) SimplexCell ( float3 p )
{
var i = math . floor ( p + math . dot ( p , new float3 ( 1.0f / 3.0f , 1.0f / 3.0f , 1.0f / 3.0f ) ) ) ;
var x0 = p - i + math . dot ( i , new float3 ( 1.0f / 6.0f , 1.0f / 6.0f , 1.0f / 6.0f ) ) ;
snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.
NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:07:02 +02:00
var g = new float3 ( math . step ( x0 . y , x0 . x ) , math . step ( x0 . z , x0 . y ) , 1.0f - math . step ( x0 . z , x0 . x ) ) ;
2026-09-29 11:10:18 +02:00
var l = 1.0f - g ;
var lzxy = new float3 ( l . z , l . x , l . y ) ;
return ( i , math . min ( g , lzxy ) , math . max ( g , lzxy ) ) ;
}
[Fact]
public void SnoiseAndItsGradientAreContinuousAcrossSimplexCellBoundaries ( )
{
// March random lines in steps of 1e-3 until the corner set changes, bisect the crossing to
// float precision, and compare the two sides. With a kernel radius^2 above 0.5 a lattice
// vertex outside the four summed corners can lie inside the kernel, which leaves a value
// jump at the boundary (stegu/webgl-noise fixed it in 21d9fe23d7); at 0.5 the jump is float
// noise. A value jump below 1e-5 and a gradient jump below 1e-4 is float noise on both sides.
const int lines = 400 ;
const int maxSteps = 20000 ;
const float dt = 1.0e-3f ;
var random = new System . Random ( 0x5EA4 ) ;
var crossings = 0 ;
var maxValueJump = 0.0f ;
var maxGradientJump = 0.0f ;
for ( var line = 0 ; line < lines ; line + + )
{
var origin = RandomPoint ( random ) ;
var direction = math . normalize ( new float3 (
random . NextSingle ( ) - 0.5f , random . NextSingle ( ) - 0.5f , random . NextSingle ( ) - 0.5f ) ) ;
var start = SimplexCell ( origin ) ;
var t = 0.0f ;
var found = false ;
for ( var step = 0 ; step < maxSteps & & ! found ; step + + )
{
t + = dt ;
found = ! SimplexCell ( origin + direction * t ) . Equals ( start ) ;
}
Assert . True ( found , $"line {line} never left its simplex cell in {maxSteps} steps" ) ;
double lo = t - dt ;
double hi = t ;
for ( var bisect = 0 ; bisect < 60 ; bisect + + )
{
var mid = 0.5 * ( lo + hi ) ;
if ( SimplexCell ( origin + direction * ( float ) mid ) . Equals ( start ) ) lo = mid ; else hi = mid ;
}
var a = math . snoise_grad ( origin + direction * ( float ) lo ) ;
var b = math . snoise_grad ( origin + direction * ( float ) hi ) ;
crossings + + ;
maxValueJump = MathF . Max ( maxValueJump , MathF . Abs ( a . w - b . w ) ) ;
maxGradientJump = MathF . Max ( maxGradientJump , MathF . Max ( MathF . Abs ( a . x - b . x ) , MathF . Max ( MathF . Abs ( a . y - b . y ) , MathF . Abs ( a . z - b . z ) ) ) ) ;
}
Assert . Equal ( lines , crossings ) ;
Assert . True ( maxValueJump < 1.0e-5f , $"value jump {maxValueJump} across a simplex boundary" ) ;
Assert . True ( maxGradientJump < 1.0e-4f , $"gradient jump {maxGradientJump} across a simplex boundary" ) ;
}
snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.
NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:07:02 +02:00
// The simplex diagonals are where x0's three components compare equal. The tie rule must be a total
// order there (math.snoise): with a cyclic or all-strict compare the four corners stop being a
2026-10-09 23:07:48 +02:00
// simplex and the value jumps wherever x0 has three equal components (inputs whose differences y - x and
// z - x are whole numbers, the line (t, t, t) among them): about 0.77 near the origin, up to 1.56 at
// magnitudes of 64 and above. SnoiseTieScopeTests pins the set against the 0.3.0 values.
snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.
NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:07:02 +02:00
[Fact]
public void SnoiseIsContinuousOnTheSimplexDiagonal ( )
{
const int vertices = 160 ;
const int perVertex = 8 ;
var random = new System . Random ( 0x71E5 ) ;
var points = new System . Collections . Generic . List < float3 > ( ) ;
for ( var k = 0 ; k < 400 ; k + + )
{
var t = ( float ) ( random . NextDouble ( ) * 128.0 - 64.0 ) ;
points . Add ( new float3 ( t , t , t ) ) ;
}
points . Add ( new float3 ( 0.0f , 0.0f , 0.0f ) ) ;
for ( var k = 0 ; k < vertices ; k + + )
{
var lx = random . Next ( - 64 , 65 ) ;
var ly = random . Next ( - 64 , 65 ) ;
var lz = random . Next ( - 64 , 65 ) ;
var shift = ( lx + ly + lz ) / 6.0f ;
var v = new float3 ( lx - shift , ly - shift , lz - shift ) ;
for ( var j = 0 ; j < perVertex ; j + + )
{
var s = j = = 0 ? 0.0f : random . NextSingle ( ) ;
points . Add ( new float3 ( v . x + s , v . y + s , v . z + s ) ) ;
}
}
foreach ( var p in points )
{
var value = math . snoise ( p ) ;
for ( var axis = 0 ; axis < 3 ; axis + + )
{
for ( var direction = - 1 ; direction < = 1 ; direction + = 2 )
{
var q = p ;
for ( var ulp = 1 ; ulp < = 2 ; ulp + + )
{
var component = axis = = 0 ? q . x : axis = = 1 ? q . y : q . z ;
component = direction < 0 ? MathF . BitDecrement ( component ) : MathF . BitIncrement ( component ) ;
q = axis = = 0 ? new float3 ( component , q . y , q . z ) : axis = = 1 ? new float3 ( q . x , component , q . z ) : new float3 ( q . x , q . y , component ) ;
var jump = MathF . Abs ( math . snoise ( q ) - value ) ;
Assert . True ( jump < = 1.0e-4f , $"snoise jumps {jump} within {ulp} ulp of {p}" ) ;
}
}
}
var grad = math . snoise_grad ( p ) ;
Assert . Equal ( BitConverter . SingleToInt32Bits ( value ) , BitConverter . SingleToInt32Bits ( grad . w ) ) ;
var slope = math . length ( new float3 ( grad . x , grad . y , grad . z ) ) ;
Assert . True ( slope < = math . SNOISE_LIPSCHITZ , $"|snoise_grad| {slope} exceeds SNOISE_LIPSCHITZ at {p}" ) ;
}
}
2026-09-26 02:54:17 +02:00
// ---- snoise_grad.w vs snoise ----
[Fact]
public void SnoiseGradValueIsBitEqualToSnoise ( )
{
2026-09-29 12:04:08 +02:00
// Both compute 105 * dot(m4, px) via the exact same operation order (math.cs comment on
// snoise_grad), so this asserts bit equality over 5,000 random points in [-100, 100]^3.
2026-09-26 02:54:17 +02:00
var random = new System . Random ( 0x50F2 ) ;
for ( var i = 0 ; i < 5000 ; i + + )
{
var p = new float3 (
random . NextSingle ( ) * 200.0f - 100.0f , random . NextSingle ( ) * 200.0f - 100.0f , random . NextSingle ( ) * 200.0f - 100.0f ) ;
var expected = math . snoise ( p ) ;
var actual = math . snoise_grad ( p ) . w ;
Assert . Equal ( BitConverter . SingleToInt32Bits ( expected ) , BitConverter . SingleToInt32Bits ( actual ) ) ;
}
}
// ---- fbm_grad / ridged_grad: per-octave gain and lacunarity ----
[Fact]
public void FbmGradWithOneOctaveEqualsSnoiseGrad ( )
{
var random = new System . Random ( 0xF6B10 ) ;
for ( var i = 0 ; i < 64 ; i + + )
{
var p = RandomPoint ( random ) ;
var lacunarity = 1.0f + random . NextSingle ( ) * 3.0f ;
var gain = 0.1f + random . NextSingle ( ) * 0.8f ;
var expected = math . snoise_grad ( p ) ;
var actual = math . fbm_grad ( p , 1 , lacunarity , gain ) ;
Assert . Equal ( expected . x , actual . x ) ;
Assert . Equal ( expected . y , actual . y ) ;
Assert . Equal ( expected . z , actual . z ) ;
Assert . Equal ( expected . w , actual . w ) ;
}
}
[Fact]
public void FbmGradCompoundsGainAndLacunarityPerOctaveNotOnce ( )
{
// Falsifies two mutant shapes at once: applying gain/lacunarity once for the whole sum
// instead of compounding per octave, and swapping which argument drives amplitude versus
// frequency. lacunarity != gain and both != 1 so a swap changes the result, and 3 octaves
// gives amplitude/frequency three distinct compounded values (1, gain, gain^2 and
// 1, lacunarity, lacunarity^2) so "applied once" mutants cannot coincidentally match.
// The expected value is built by the same incremental multiply chain fbm_grad itself uses
// (frequency *= lacunarity, amplitude *= gain, one step at a time, not lacunarity*lacunarity
// or MathF.Pow), so this asserts bit-exact equality rather than needing a tolerance.
const float lacunarity = 2.5f ;
const float gain = 0.6f ;
var random = new System . Random ( 0xF6B11 ) ;
for ( var i = 0 ; i < 32 ; i + + )
{
var p = RandomPoint ( random ) ;
var frequency = 1.0f ;
var amplitude = 1.0f ;
var n0 = math . snoise_grad ( p * frequency ) ;
var expectedValue = amplitude * n0 . w ;
var expectedGradient = amplitude * frequency * new float3 ( n0 . x , n0 . y , n0 . z ) ;
frequency * = lacunarity ;
amplitude * = gain ;
var n1 = math . snoise_grad ( p * frequency ) ;
expectedValue + = amplitude * n1 . w ;
expectedGradient + = amplitude * frequency * new float3 ( n1 . x , n1 . y , n1 . z ) ;
frequency * = lacunarity ;
amplitude * = gain ;
var n2 = math . snoise_grad ( p * frequency ) ;
expectedValue + = amplitude * n2 . w ;
expectedGradient + = amplitude * frequency * new float3 ( n2 . x , n2 . y , n2 . z ) ;
var actual = math . fbm_grad ( p , 3 , lacunarity , gain ) ;
Assert . Equal ( expectedValue , actual . w ) ;
Assert . Equal ( expectedGradient . x , actual . x ) ;
Assert . Equal ( expectedGradient . y , actual . y ) ;
Assert . Equal ( expectedGradient . z , actual . z ) ;
}
}
[Fact]
public void RidgedGradCompoundsGainAndLacunarityPerOctaveNotOnce ( )
{
// Same construction as FbmGradCompoundsGainAndLacunarityPerOctaveNotOnce, folded: value is
// Sum a_i*(1-|n_i|), gradient is -Sum a_i*f_i*sign(n_i)*grad(n_i) (math.cs comment on
// ridged_grad). p is offset away from the origin and re-tried on a near-zero octave so no
// octave's n_i lands in the fold's own zero band, which would make the closed form's sign()
// ambiguous between runs.
const float lacunarity = 2.5f ;
const float gain = 0.6f ;
var random = new System . Random ( 0xF6B12 ) ;
var tested = 0 ;
2026-09-29 12:04:08 +02:00
var attempts = 0 ;
while ( tested < 32 & & attempts < 4000 )
2026-09-26 02:54:17 +02:00
{
2026-09-29 12:04:08 +02:00
attempts + + ;
2026-09-26 02:54:17 +02:00
var p = RandomPoint ( random ) ;
if ( AnyOctaveNearZero ( p , 3 , lacunarity , 0.1f ) )
continue ;
tested + + ;
var frequency = 1.0f ;
var amplitude = 1.0f ;
var n0 = math . snoise_grad ( p * frequency ) ;
var s0 = ( float ) MathF . Sign ( n0 . w ) ;
var expectedValue = amplitude * ( 1.0f - MathF . Abs ( n0 . w ) ) ;
var expectedGradient = - amplitude * frequency * s0 * new float3 ( n0 . x , n0 . y , n0 . z ) ;
frequency * = lacunarity ;
amplitude * = gain ;
var n1 = math . snoise_grad ( p * frequency ) ;
var s1 = ( float ) MathF . Sign ( n1 . w ) ;
expectedValue + = amplitude * ( 1.0f - MathF . Abs ( n1 . w ) ) ;
expectedGradient + = - amplitude * frequency * s1 * new float3 ( n1 . x , n1 . y , n1 . z ) ;
frequency * = lacunarity ;
amplitude * = gain ;
var n2 = math . snoise_grad ( p * frequency ) ;
var s2 = ( float ) MathF . Sign ( n2 . w ) ;
expectedValue + = amplitude * ( 1.0f - MathF . Abs ( n2 . w ) ) ;
expectedGradient + = - amplitude * frequency * s2 * new float3 ( n2 . x , n2 . y , n2 . z ) ;
var actual = math . ridged_grad ( p , 3 , lacunarity , gain ) ;
Assert . Equal ( expectedValue , actual . w ) ;
Assert . Equal ( expectedGradient . x , actual . x ) ;
Assert . Equal ( expectedGradient . y , actual . y ) ;
Assert . Equal ( expectedGradient . z , actual . z ) ;
}
2026-09-29 12:04:08 +02:00
Assert . True ( tested = = 32 , $"only {tested} of {attempts} points were outside the zero band" ) ;
2026-09-26 02:54:17 +02:00
}
// ---- ridged_grad's deliberate crease at n = 0 ----
[Fact]
public void RidgedGradOneSidedGradientsAcrossAZeroCrossingDifferInSign ( )
{
// 1 octave: ridged_grad(p) = 1 - |snoise(p)|, gradient = -sign(snoise(p)) * grad(snoise)(p).
// Bisect along a fixed direction from a random start point to find a tight straddle of a
// snoise zero crossing, then check the two ridged_grad gradients just either side of it point
// in near-opposite directions (their dot product is negative): the crease is real, not a
// numerical accident (design.md, "the operator's dune term").
var random = new System . Random ( 0xC2E55 ) ;
for ( var attempt = 0 ; attempt < 200 ; attempt + + )
{
var origin = RandomPoint ( random ) ;
var direction = new float3 ( random . NextSingle ( ) - 0.5f , random . NextSingle ( ) - 0.5f , random . NextSingle ( ) - 0.5f ) ;
if ( math . length ( direction ) < 1.0e-3f )
continue ;
float NoiseAt ( float t ) = > math . snoise ( origin + direction * t ) ;
var lo = - 3.0f ;
var hi = 3.0f ;
if ( Math . Sign ( NoiseAt ( lo ) ) = = Math . Sign ( NoiseAt ( hi ) ) )
continue ;
for ( var step = 0 ; step < 40 ; step + + )
{
var mid = ( lo + hi ) * 0.5f ;
if ( Math . Sign ( NoiseAt ( lo ) ) = = Math . Sign ( NoiseAt ( mid ) ) ) lo = mid ; else hi = mid ;
}
// lo and hi now straddle the crossing within about 3 * 2^-40 of parameter t; step off by
// a small margin either side so each sample is cleanly on one side, not exactly on it.
const float margin = 1.0e-4f ;
var minusSide = origin + direction * ( lo - margin ) ;
var plusSide = origin + direction * ( hi + margin ) ;
if ( MathF . Sign ( NoiseAt ( lo - margin ) ) = = MathF . Sign ( NoiseAt ( hi + margin ) ) )
continue ; // margin stepped back across the crossing; try another direction.
var gradMinus = math . ridged_grad ( minusSide , 1 , 2.0f , 0.5f ) ;
var gradPlus = math . ridged_grad ( plusSide , 1 , 2.0f , 0.5f ) ;
var dot = gradMinus . x * gradPlus . x + gradMinus . y * gradPlus . y + gradMinus . z * gradPlus . z ;
Assert . True ( dot < 0.0f , $"one-sided gradients did not differ in sign: minus=({gradMinus.x},{gradMinus.y},{gradMinus.z}) plus=({gradPlus.x},{gradPlus.y},{gradPlus.z}) dot={dot}" ) ;
return ;
}
Assert . Fail ( "did not find a snoise zero crossing to straddle" ) ;
}
}