NoiseBoundTests: FogField.Slice returns the final af_snoise arguments; AffineBound has no term left to wire

Slice builds each phase's coverage argument and the detail argument (centre,
axis, radius in noise space); AffineBound and its gate read them whole.
AffineWarpStaysEnclosed checks the rays against those arguments and their
derivation from CentredWarp; AffineCompositionIsTighterThanItsRanges sees the
shared-eps composition and the intersection.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-04 07:20:56 +02:00
commit 922e4da458

View file

@ -842,6 +842,25 @@ public sealed class NoiseBoundTests
var slice = field.Slice(tile, z0, z1);
var (_, shift0, shift1) = field.Noise.Phases(tile.Time);
var (zm, h) = (slice.Ball.z, slice.AxisCamera.z);
// The arguments are built from the centred warp of the slice's own centre and reach, the detail
// from the unwarped set: no phase takes another's growth, nor the detail a warp.
var (moved0, grow0, moved1, grow1) = field.Noise.CentredWarp(slice.Centre, slice.Reach, tile.Time);
var (coverageFrequency, detailFrequency) = (field.Noise.F0, 4.0f * field.Noise.F0);
var expected = new[]
{
(slice.Coverage0, moved0 * coverageFrequency, (slice.Ball.w + grow0) * coverageFrequency),
(slice.Coverage1, moved1 * coverageFrequency, (slice.Ball.w + grow1) * coverageFrequency),
(slice.Detail, slice.Centre * detailFrequency + 17.0f, slice.Ball.w * detailFrequency),
};
foreach (var (arg, centre, radius) in expected)
{
Assert.True(length(arg.Centre - centre) <= 1.0e-6f * (1.0f + length(centre)) && Math.Abs(arg.Radius - radius) <= 1.0e-6f * radius, $"{field.Name}: argument ({arg.Centre}, {arg.Radius:R}) is not the documented ({centre}, {radius:R})");
}
var axisTolerance = 1.0e-6f * length(slice.Coverage0.Axis);
Assert.True(length(slice.Coverage1.Axis - slice.Coverage0.Axis) <= axisTolerance && length(slice.Detail.Axis - slice.Coverage0.Axis * (detailFrequency / coverageFrequency)) <= 4.0f * axisTolerance, $"{field.Name}: the arguments do not share one axis");
for (var i = 0; i < 64; i++)
{
var m = tile.PixelSlope(random.Next(tile.N), random.Next(tile.N), random.NextSingle(), random.NextSingle());
@ -849,14 +868,19 @@ public sealed class NoiseBoundTests
var p = tile.World(new float3(m.x * z, m.y * z, z));
var flow = field.Noise.Flow(p);
var eps = h == 0.0f ? 0.0f : clamp((z - zm) / h, -1.0f, 1.0f);
var phases = new[] { (shift0, slice.Centre0, slice.Grow0), (shift1, slice.Centre1, slice.Grow1) };
var phases = new[] { (shift0, slice.Coverage0, coverageFrequency), (shift1, slice.Coverage1, coverageFrequency) };
for (var k = 0; k < 2; k++)
{
var (shift, moved, grow) = phases[k];
var distance = length(p + flow * shift - (moved + slice.Axis * eps));
Assert.True(distance <= (slice.Ball.w + grow) * (1.0f + 1.0e-5f), $"{field.Name}: phase {k} warped ray point at depth {z:R} is {distance:R} from moved + axis eps, outside radius {slice.Ball.w:R} + growth {grow:R} (reach {slice.Reach:R}, slice [{z0:R}, {z1:R}])");
needing[k] += distance > slice.Ball.w ? 1 : 0;
var (shift, arg, frequency) = phases[k];
var point = (p + flow * shift) * frequency;
var distance = length(point - (arg.Centre + arg.Axis * eps));
Assert.True(distance <= arg.Radius * (1.0f + 1.0e-5f) + 1.0e-6f * length(point), $"{field.Name}: phase {k} warped ray point at depth {z:R} is {distance:R} from the argument's centre + axis eps, outside radius {arg.Radius:R} (reach {slice.Reach:R}, slice [{z0:R}, {z1:R}])");
needing[k] += distance > slice.Ball.w * frequency ? 1 : 0;
}
var detail = p * detailFrequency + 17.0f;
var detailDistance = length(detail - (slice.Detail.Centre + slice.Detail.Axis * eps));
Assert.True(detailDistance <= slice.Detail.Radius * (1.0f + 1.0e-5f) + 1.0e-6f * length(detail), $"{field.Name}: the detail ray point at depth {z:R} is {detailDistance:R} from the argument's centre + axis eps, outside radius {slice.Detail.Radius:R} (slice [{z0:R}, {z1:R}])");
}
}
@ -864,6 +888,44 @@ public sealed class NoiseBoundTests
Assert.True(needing[0] > 0 && needing[1] > 0, $"no ray needs phase growth ({needing[0]}, {needing[1]}): the check cannot tell a slice without it");
}
/// <summary>
/// The affine composition earns its place: over seeded slices of the deep well and the shallow wells
/// (warp 0 and the Aetheria warp, N = 8, slices of length log-uniform in [0.5, 100] from a drawn
/// depth), AffineBound is exactly the intersection of its two sound pieces (the affine arithmetic's
/// own, and the interval composition over the forms' ranges), and where the interval piece does not
/// already prove the slice empty the affine piece is the narrower of the two in at least
/// MinAffineWins of the slices and narrower in total width by at least MinWidthGain. The
/// cost contract (IntervalSkipHalvesEvaluations) sees only the sum, in which af_snoise's Hessian forms
/// already beat iv_snoise_ball; this is the test that sees af_add, af_mul and af_range, the shared
/// symbol, break.
/// </summary>
[Fact]
public void AffineCompositionIsTighterThanItsRanges()
{
var random = new System.Random(0xAC0F);
var (open, wins, affineWidth, intervalWidth) = (0, 0, 0.0, 0.0);
foreach (var warp in new[] { 0.0f, FogField.AetheriaWarp })
foreach (var field in new[] { FogField.DeepWell(warp, BoundMode.Affine), FogField.Wells(warp, BoundMode.Affine) })
for (var t = 0; t < 1500; t++)
{
var tile = field.DrawTile(random, 8);
var z0 = Uniform(random, field.Grid[0], field.Grid[^1] * 0.9f);
var z1 = MathF.Min(z0 + LogUniform(random, 0.5f, 100.0f), field.Grid[^1]);
var (affine, interval) = field.AffinePieces(tile, z0, z1, new Counts());
var bound = field.Bound(tile, z0, z1, false, new Counts());
Assert.True(bound.x == max(affine.x, interval.x) && bound.y == min(affine.y, interval.y), $"{field.Name}: Bound {bound} is not the intersection of the affine piece {affine} and the interval piece {interval}");
if (!(interval.y > 0.0f))
continue;
open++;
wins += affine.y - affine.x < interval.y - interval.x ? 1 : 0;
affineWidth += affine.y - affine.x;
intervalWidth += interval.y - interval.x;
}
output.WriteLine($"IV-REPORT affine composition: {open} open slices, affine narrower in {wins}, total width {affineWidth:F1} against {intervalWidth:F1} ({affineWidth / intervalWidth:F3})");
Assert.True(open > 0, "no slice is open");
}
// Why a field is not ten bands deep and wide, or null. The band G is the span of s where only the
// noise decides density: s in [F + A lo, F + A hi] for the free noise range [lo, hi], with the fade at 1.
private static string? TenBandsShortfall(FogField field, int cameras)
@ -1579,17 +1641,36 @@ public sealed class NoiseBoundTests
public override float2 Bound(Tile t, float z0, float z1, bool gated, Counts counts)
{
var tile = (FogTile)t;
var frame = FrameOf(tile, z0, z1, counts);
if (gated && frame.Envelope.y <= 0.0f)
return frame.Envelope;
return Mode == BoundMode.Affine
? AffineBound(tile, z0, z1, frame, gated, counts)
: IntervalBound(tile, z0, z1, frame.S, frame.Fade, frame.Envelope, gated, counts);
}
// The height interval, the density level s over the slice's box, its fade and the envelope.
private readonly record struct Frame(float2 Height, float2 S, float2 Fade, float2 Envelope);
private Frame FrameOf(FogTile tile, float z0, float z1, Counts counts)
{
var depth = new float2(z0, z1);
counts.Envelope++;
var height = HeightBound(tile.Centres, iv_mul(depth, tile.SlopesX), depth);
var s = iv_add(iv_add(iv_point(tile.Origin.y), iv_mul(depth, tile.SlopesY)), height);
var fade = iv_sub(iv_point(1.0f), iv_smoothstep(0.75f * Safety, Safety, s));
var envelope = Compose(s, fade, FreeNoise);
if (gated && envelope.y <= 0.0f)
return envelope;
return Mode == BoundMode.Affine
? AffineBound(tile, z0, z1, height, s, fade, envelope, gated, counts)
: IntervalBound(tile, z0, z1, s, fade, envelope, gated, counts);
return new Frame(height, s, fade, Compose(s, fade, FreeNoise));
}
/// <summary>
/// The two sound bounds AffineBound intersects, ungated: the affine arithmetic's own (the shared
/// symbol through af_add, af_mul and af_range), and the interval composition over the same forms'
/// ranges (NoiseBoundTests.AffineCompositionIsTighterThanItsRanges).
/// </summary>
internal (float2 Affine, float2 Interval) AffinePieces(Tile t, float z0, float z1, Counts counts)
{
var tile = (FogTile)t;
return AffinePieces(tile, z0, z1, FrameOf(tile, z0, z1, counts), counts);
}
private static bool Same(float3 a, float3 b, float ga, float gb) => all(a == b) && ga == gb;
@ -1627,11 +1708,12 @@ public sealed class NoiseBoundTests
/// <summary>
/// The affine slice of a tile over [z0, z1] in the world frame, with the centred warp applied: the
/// camera-frame axis and ball (af_frustum_axis, af_frustum_ball with no warp), the ball's world
/// centre, the world axis, the reach R = |axis| + radius of the whole set from that centre (the
/// distance the flow is varied over), and each phase's moved centre and growth
/// (WarpedNoise.CentredWarp over R). The rays at depth z are centre + axis eps(z) + w, and a phase's
/// warped point lies within radius + growth of moved + axis eps(z)
/// (NoiseBoundTests.AffineWarpStaysEnclosed). AffineBound reads its noise arguments from here.
/// centre, the world axis, and the reach R = |axis| + radius of the whole set from that centre (the
/// distance the flow is varied over). It returns the final af_snoise arguments, so a consumer has no
/// term left to wire: each phase's coverage argument is the set moved by the flow at its centre
/// (WarpedNoise.CentredWarp over R) and grown by what the flow can vary across it, and the detail
/// argument is the unwarped set. The rays at depth z lie within an argument's radius of its centre
/// + axis eps(z), eps(z) = clamp((z - z_m) / h, -1, 1) (NoiseBoundTests.AffineWarpStaysEnclosed).
/// </summary>
internal AffineSlice Slice(Tile t, float z0, float z1)
{
@ -1642,10 +1724,25 @@ public sealed class NoiseBoundTests
var axis = tile.Right * axisCamera.x + tile.Up * axisCamera.y + tile.Forward * axisCamera.z;
var reach = length(axis) + ball.w;
var (c0, g0, c1, g1) = Noise.CentredWarp(centre, reach, tile.Time);
return new AffineSlice(axisCamera, ball, centre, axis, reach, c0, g0, c1, g1);
var coverageFrequency = Noise.F0;
var detailFrequency = 4.0f * Noise.F0;
return new AffineSlice(
axisCamera, ball, centre, reach,
new NoiseArg(c0 * coverageFrequency, axis * coverageFrequency, (ball.w + g0) * coverageFrequency),
new NoiseArg(c1 * coverageFrequency, axis * coverageFrequency, (ball.w + g1) * coverageFrequency),
new NoiseArg(centre * detailFrequency + 17.0f, axis * detailFrequency, ball.w * detailFrequency));
}
internal readonly record struct AffineSlice(float3 AxisCamera, float4 Ball, float3 Centre, float3 Axis, float Reach, float3 Centre0, float Grow0, float3 Centre1, float Grow1);
/// <summary>One af_snoise argument in noise space: the centre, the axis along eps, and the radius.</summary>
internal readonly record struct NoiseArg(float3 Centre, float3 Axis, float Radius)
{
/// <summary>The distance of the whole set from its centre: the gate's reach.</summary>
public float Reach => length(Axis) + Radius;
public float3 Snoise() => af_snoise(Centre, Axis, Radius);
}
internal readonly record struct AffineSlice(float3 AxisCamera, float4 Ball, float3 Centre, float Reach, NoiseArg Coverage0, NoiseArg Coverage1, NoiseArg Detail);
// The best interval any centre value and gradient could give af_snoise over a set of reach R (the
// gradient 0, the value at the far end), toward low or high values: the affine gate.
@ -1669,29 +1766,37 @@ public sealed class NoiseBoundTests
/// over the same forms' ranges, each clipped to [-1, 1] (both sound, no further evaluation). The gate
/// is BestForm's.
/// </summary>
private float2 AffineBound(FogTile tile, float z0, float z1, float2 height, float2 s, float2 fade, float2 envelope, bool gated, Counts counts)
private float2 AffineBound(FogTile tile, float z0, float z1, Frame frame, bool gated, Counts counts)
{
var (axisCamera, ball, centre, axis, reach, c0, g0, c1, g1) = Slice(tile, z0, z1);
var coverageFrequency = Noise.F0;
var detailFrequency = 4.0f * Noise.F0;
if (gated)
{
var slice = Slice(tile, z0, z1);
var (s, fade) = (frame.S, frame.Fade);
var provable = false;
foreach (var low in new[] { true, false })
{
var coverage = Noise.CoverageBound(BestForm((reach + g0) * coverageFrequency, low), BestForm((reach + g1) * coverageFrequency, low), tile.Time);
provable |= Compose(s, fade, iv_add(coverage, iv_scale(BestForm(reach * detailFrequency, low), 0.5f))).y <= 0.0f;
var coverage = Noise.CoverageBound(BestForm(slice.Coverage0.Reach, low), BestForm(slice.Coverage1.Reach, low), tile.Time);
provable |= Compose(s, fade, iv_add(coverage, iv_scale(BestForm(slice.Detail.Reach, low), 0.5f))).y <= 0.0f;
}
if (!provable)
return envelope;
return frame.Envelope;
}
var shared = Same(c0, c1, g0, g1);
var (affine, interval) = AffinePieces(tile, z0, z1, frame, counts);
return new float2(max(affine.x, interval.x), min(affine.y, interval.y));
}
private (float2 Affine, float2 Interval) AffinePieces(FogTile tile, float z0, float z1, Frame frame, Counts counts)
{
var slice = Slice(tile, z0, z1);
var (ball, axisCamera) = (slice.Ball, slice.AxisCamera);
var (height, s, fade) = (frame.Height, frame.S, frame.Fade);
var shared = slice.Coverage0 == slice.Coverage1;
counts.Grad += shared ? 2 : 3;
var n0 = af_snoise(c0 * coverageFrequency, axis * coverageFrequency, (ball.w + g0) * coverageFrequency);
var n1 = shared ? n0 : af_snoise(c1 * coverageFrequency, axis * coverageFrequency, (ball.w + g1) * coverageFrequency);
var nd = af_snoise(centre * detailFrequency + 17.0f, axis * detailFrequency, ball.w * detailFrequency);
var n0 = slice.Coverage0.Snoise();
var n1 = shared ? n0 : slice.Coverage1.Snoise();
var nd = slice.Detail.Snoise();
var w0 = Noise.Phases(tile.Time).W0;
var noise = af_add(af_add(af_scale(n0, w0), af_scale(n1, 1.0f - w0)), af_scale(nd, 0.5f));
@ -1703,7 +1808,7 @@ public sealed class NoiseBoundTests
var displaced = af_range(af_add(level, af_scale(af_mul(af_from_iv(fade), noise), Amplitude)));
var affine = new float2(max(0.0f, (FloorOffset - displaced.y) / FloorBlend), max(0.0f, (FloorOffset - displaced.x) / FloorBlend));
var interval = Compose(s, fade, iv_add(Noise.CoverageBound(Clip(af_range(n0)), Clip(af_range(n1)), tile.Time), iv_scale(Clip(af_range(nd)), 0.5f)));
return new float2(max(affine.x, interval.x), min(affine.y, interval.y));
return (affine, interval);
}
}