From 5588e95fa23b4bcf398025aee972edd2c7c428d1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:26:51 +0200 Subject: [PATCH 01/24] cultmath: pin every shader constant to its C# twin (shader-third-constant-unpinned) EveryShaderConstantEqualsItsCSharpTwin reflects each CULTMATH_* literal of the compiled HLSL mirror and asserts it is bit-equal to the math constant of the same name, so CULTMATH_SNOISE_THIRD (and every other constant) edited in one runtime fails. Co-Authored-By: Claude Sonnet 5.5 --- .../HlslSourceCompatibilityTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index 4ca598ad..f1ecb780 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -48,6 +48,42 @@ public sealed class HlslSourceCompatibilityTests // Mirror internals with no public C# math counterpart. Any other cultmath_* function must match one. private static readonly string[] MirrorOnly = { "cultmath_snoise_mod289", "cultmath_snoise_permute" }; + // Shader constants with no C# twin of the same name (empty when every constant has one). + private static readonly string[] ConstantsWithoutTwin = Array.Empty(); + + /// + /// Every static const of the compiled HLSL mirror (a public static literal CULTMATH_* field) is the + /// same float32, bit for bit, as the math constant of the same name without the prefix (private ones + /// included), so a pin edited in one runtime and not the other fails here whichever constant it is. + /// A constant with neither a twin nor a listing in ConstantsWithoutTwin fails too. + /// + [Fact] + public void EveryShaderConstantEqualsItsCSharpTwin() + { + var (assembly, errors) = CompileShaderMirror(); + Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); + var shader = assembly!.GetType("CultMathHlsl.HlslShader")!; + var constants = shader.GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.IsLiteral && f.Name.StartsWith("CULTMATH_", StringComparison.Ordinal)) + .ToArray(); + Assert.True(constants.Length >= 10, $"only {constants.Length} shader constants were found by reflection"); + foreach (var constant in constants) + { + var name = constant.Name["CULTMATH_".Length..]; + var twin = typeof(math).GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (twin is null) + { + Assert.True(ConstantsWithoutTwin.Contains(constant.Name), $"{constant.Name} has no math.{name} and is not listed in ConstantsWithoutTwin"); + continue; + } + + var shaderValue = (float)constant.GetRawConstantValue()!; + var mathValue = (float)twin.GetValue(null)!; + Assert.True(BitConverter.SingleToInt32Bits(shaderValue) == BitConverter.SingleToInt32Bits(mathValue), + $"{constant.Name} is {shaderValue:R} in the shader and math.{name} is {mathValue:R}"); + } + } + private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN }; /// From 6e360cf56dc17969f27ee171643a48f90fe703cb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:37:35 +0200 Subject: [PATCH 02/24] cultmath: shader-constant pin reads the mirror's private const fields The compiled mirror declares its static consts without public access; the first version of EveryShaderConstantEqualsItsCSharpTwin found none and failed. Reflect NonPublic too. Co-Authored-By: Claude Sonnet 5.5 --- .../tests/CultMath.Tests/HlslSourceCompatibilityTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index f1ecb780..ea2fd145 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -52,7 +52,7 @@ public sealed class HlslSourceCompatibilityTests private static readonly string[] ConstantsWithoutTwin = Array.Empty(); /// - /// Every static const of the compiled HLSL mirror (a public static literal CULTMATH_* field) is the + /// Every static const of the compiled HLSL mirror (a static literal CULTMATH_* field, whatever its access) is the /// same float32, bit for bit, as the math constant of the same name without the prefix (private ones /// included), so a pin edited in one runtime and not the other fails here whichever constant it is. /// A constant with neither a twin nor a listing in ConstantsWithoutTwin fails too. @@ -63,7 +63,7 @@ public sealed class HlslSourceCompatibilityTests var (assembly, errors) = CompileShaderMirror(); Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); var shader = assembly!.GetType("CultMathHlsl.HlslShader")!; - var constants = shader.GetFields(BindingFlags.Public | BindingFlags.Static) + var constants = shader.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) .Where(f => f.IsLiteral && f.Name.StartsWith("CULTMATH_", StringComparison.Ordinal)) .ToArray(); Assert.True(constants.Length >= 10, $"only {constants.Length} shader constants were found by reflection"); From 38a2f50eb06588655be85c59e5855056b9852ea2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:37:36 +0200 Subject: [PATCH 03/24] cultmath: closed-form third tensor, SNOISE_THIRD re-pinned to 535.8009 (third-provenance-fd-noise) ThirdTensor is now snoise_hessian's per-corner derivation differentiated once more, in double, on the kernel's lattice, tie order and hash; central differences of the float Hessian are gone from the measurement and survive only as the oracle in ThirdTensorMatchesDifferencesOfTheHessian (Richardson extrapolated differences of the double Hessian). MeasureThird on the closed form: refined 487.0917, x 1.10 = 535.8009 (the differenced tensor read 488.1047, its roundoff). SNOISE_THIRD, ThirdWitness, CULTMATH_SNOISE_THIRD (both HLSL copies), the generated GLSL constant and the design.md provenance paragraph follow. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/docs/design.md | 15 +- .../cultmath/shaders/CultMath.Affine.hlsl | 2 +- packages/cultmath/shaders/CultMath.glsl | 2 +- packages/cultmath/src/CultMath/math.Span.cs | 21 +- .../tests/CultMath.Tests/NoiseBoundTests.cs | 188 ++++++++++++++++-- .../Shaders/CultMath.Affine.hlsl | 2 +- 6 files changed, 193 insertions(+), 37 deletions(-) diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index a4d4c9ba..5f84e734 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -777,11 +777,18 @@ differentiating each corner's gradient `-8 m^3 s x + m^4 p` once more. Each off-diagonal entry is computed once and placed twice, so the matrix equals its transpose bit for bit, and a clamped corner (`m = 0`) contributes nothing. Built from nine scalars, the matrix reads as columns in GLSL (`mat3`) and rows in HLSL, -which is the same matrix because it is symmetric. `SNOISE_THIRD = 536.9152` +which is the same matrix because it is symmetric. `SNOISE_THIRD = 535.8009` (`math.Span.cs`) is the next order: 1.10 times the maximum, over 1e6 seeded points, -of the operator norm of the third-derivative tensor of `snoise` (central -differences of `snoise_hessian` at h = 1e-3, then an ascent over unit vectors), -refined from the 1e4 largest; measured 488.1047 on the tied kernel. +of the operator norm of the third-derivative tensor of `snoise`, refined from the +1e4 largest; measured 487.0917 on the tied kernel. The tensor is closed form, in +double: `snoise_hessian`'s per-corner entry differentiated once more, on the +kernel's lattice, tie order and hash, and the test +`ThirdTensorMatchesDifferencesOfTheHessian` holds it against Richardson-extrapolated +differences of the double Hessian. The operator norm is a local ascent over unit +vectors (a dense sweep of 20,000 directions at the witness finds 99.9987% of it) and +the outer maximum a sampled search refined by ascent, so the constant is empirical +with a 10% margin, not a proof; an earlier pin from central differences of the +float Hessian read 488.1047, its roundoff. `MeasureThird` takes the measurement and `ThirdConstantPinsSampled` pins it. `integral_clamp_linear(a, b, lo, hi, t0, t1)` is the closed-form integral over `[t0, t1]` of `clamp(a + b t, lo, hi)`. All three have HLSL and GLSL lowerings and diff --git a/packages/cultmath/shaders/CultMath.Affine.hlsl b/packages/cultmath/shaders/CultMath.Affine.hlsl index 09616940..21360d07 100644 --- a/packages/cultmath/shaders/CultMath.Affine.hlsl +++ b/packages/cultmath/shaders/CultMath.Affine.hlsl @@ -14,7 +14,7 @@ static const float CULTMATH_SNOISE_HESSIAN = 55.642143; // An upper bound on the operator norm of the third-derivative tensor of cultmath_snoise(float3), the // next order after CULTMATH_SNOISE_HESSIAN; provenance in math.Span.cs (SNOISE_THIRD). -static const float CULTMATH_SNOISE_THIRD = 536.9152; +static const float CULTMATH_SNOISE_THIRD = 535.8009; float3 cultmath_af_point(float value) { return float3(value, 0.0, 0.0); } float3 cultmath_af_symbol(float x0, float a) { return float3(x0, a, 0.0); } diff --git a/packages/cultmath/shaders/CultMath.glsl b/packages/cultmath/shaders/CultMath.glsl index 323f1e64..3f090c7c 100644 --- a/packages/cultmath/shaders/CultMath.glsl +++ b/packages/cultmath/shaders/CultMath.glsl @@ -748,7 +748,7 @@ const float CULTMATH_SNOISE_HESSIAN = 55.642143; // An upper bound on the operator norm of the third-derivative tensor of cultmath_snoise(vec3), the // next order after CULTMATH_SNOISE_HESSIAN; provenance in math.Span.cs (SNOISE_THIRD). -const float CULTMATH_SNOISE_THIRD = 536.9152; +const float CULTMATH_SNOISE_THIRD = 535.8009; vec3 cultmath_af_point(float value) { return vec3(value, 0.0, 0.0); } vec3 cultmath_af_symbol(float x0, float a) { return vec3(x0, a, 0.0); } diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 364bd04b..6be788d0 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -5,15 +5,18 @@ public static partial class math // An upper bound on the operator norm of the third-derivative tensor of snoise(float3): // max over unit u of |D^3 snoise(p)[u, u, u]|, the next order after SNOISE_LIPSCHITZ (gradient) // and SNOISE_HESSIAN (curvature). Provenance: NoiseBoundTests.MeasureThird (slow, explicit) takes - // the largest such norm over 1e6 seeded points of [-256, 256]^3 (central differences of - // snoise_hessian at h = 1e-3, then an ascent over unit vectors for the tensor's operator norm), - // refines the 1e4 largest by ascent over the point, and multiplies the maximum by 1.10; it prints - // the witness start. NoiseBoundTests.ThirdConstantPinsSampled pins the value against a fast - // re-measure and the ascent from that witness. Measured on the tied kernel (snoise_grad's order), - // not on any pre-tie splice. Measured 488.1047 (a dense sweep of 20,000 directions at the witness - // found 99.9998% of the tensor norm there). Empirical with a margin, not a proof. A change to the snoise kernel - // must re-run MeasureThird and re-pin it. - public const float SNOISE_THIRD = 536.9152f; + // the largest such norm over 1e6 seeded points of [-256, 256]^3 of the closed-form tensor (snoise_hessian's + // per-corner derivation differentiated once more, in double, with the kernel's lattice, tie order and + // hash; NoiseBoundTests.ThirdTensorMatchesDifferencesOfTheHessian pins it against differences of the + // double Hessian), then an ascent over unit vectors for the tensor's operator norm, refines the 1e4 + // largest by ascent over the point, and multiplies the maximum by 1.10; it prints the witness start. + // NoiseBoundTests.ThirdConstantPinsSampled pins the value against a fast re-measure and the ascent + // from that witness. Measured on the tied kernel (snoise_grad's order), not on any pre-tie splice: + // refined maximum 487.0917, a dense sweep of 20,000 directions at the witness's ascent end found + // 99.9987% of the tensor norm there. The inner maximisation is a local ascent and the outer a sampled + // search refined by ascent: empirical with a margin, not a proof. A change to the snoise kernel must + // re-run MeasureThird and re-pin it. + public const float SNOISE_THIRD = 535.8009f; // Analytic Hessian of snoise(float3): the symmetric matrix of second partial derivatives, same // lattice, hash and tie order as snoise_grad, whose kernel it differentiates once more. Every local diff --git a/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs b/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs index 312770ba..8d61150a 100644 --- a/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs @@ -625,33 +625,179 @@ public sealed class NoiseBoundTests // ---- The third-derivative constant (math.Span.cs) ---- - // The symmetric third-derivative tensor of snoise at p, entry (i * 3 + j) * 3 + k: central differences - // of snoise_hessian along each axis at h = 1e-3 over the exact float32 steps, symmetrised over the six - // index orders (the Hessian is symmetric, so only the derivative index can differ). - private static double[] ThirdTensor(float3 p) + private static float3 CultMod289(float3 v) => v - floor(v * (1.0f / 289.0f)) * 289.0f; + private static float4 CultMod289(float4 v) => v - floor(v * (1.0f / 289.0f)) * 289.0f; + private static float4 CultPermute(float4 v) => CultMod289(((v * 34.0f) + 10.0f) * v); + + // The four simplex corners of snoise at (vx, vy, vz): each corner's offset x_c from the point, and its + // gradient direction p_c, in double, with snoise_hessian's lattice, step() tie order and (float) hash, + // so one corner table serves the Hessian and its derivative. Key names the lattice cell and traversal + // order: two points with the same key are differentiated through the same corners. + private static (double[][] X, double[][] P, double[] Key) Corners(double vx, double vy, double vz) { - const float h = 1.0e-3f; - var d = new double[27]; - for (var k = 0; k < 3; k++) + const double cx = 1.0 / 6.0; + const double cy = 1.0 / 3.0; + var skew = (vx + vy + vz) * cy; + double ix = Math.Floor(vx + skew), iy = Math.Floor(vy + skew), iz = Math.Floor(vz + skew); + var unskew = (ix + iy + iz) * cx; + double x0 = vx - ix + unskew, y0 = vy - iy + unskew, z0 = vz - iz + unskew; + double gx = x0 >= y0 ? 1.0 : 0.0, gy = y0 >= z0 ? 1.0 : 0.0, gz = 1.0 - (x0 >= z0 ? 1.0 : 0.0); + double lx = 1.0 - gx, ly = 1.0 - gy, lz = 1.0 - gz; + double i1x = Math.Min(gx, lz), i1y = Math.Min(gy, lx), i1z = Math.Min(gz, ly); + double i2x = Math.Max(gx, lz), i2y = Math.Max(gy, lx), i2z = Math.Max(gz, ly); + var x = new[] { - var e = new float3(k == 0 ? h : 0.0f, k == 1 ? h : 0.0f, k == 2 ? h : 0.0f); - var forward = Step(k == 0 ? p.x : k == 1 ? p.y : p.z, h, out var back); - var plus = snoise_hessian(p + e); - var minus = snoise_hessian(p - e); + new[] { x0, y0, z0 }, + new[] { x0 - i1x + cx, y0 - i1y + cx, z0 - i1z + cx }, + new[] { x0 - i2x + cy, y0 - i2y + cy, z0 - i2z + cy }, + new[] { x0 - 0.5, y0 - 0.5, z0 - 0.5 }, + }; + + var i = CultMod289(new float3((float)ix, (float)iy, (float)iz)); + var hash = CultPermute(CultPermute(CultPermute( + i.z + new float4(0.0f, (float)i1z, (float)i2z, 1.0f)) + i.y + new float4(0.0f, (float)i1y, (float)i2y, 1.0f)) + i.x + new float4(0.0f, (float)i1x, (float)i2x, 1.0f)); + const float n = 0.142857142857f; + var ns = new float3(2.0f * n, 0.5f * n - 1.0f, n); + var j = hash - 49.0f * floor(hash * ns.z * ns.z); + var xs = floor(j * ns.z); + var ys = floor(j - 7.0f * xs); + var gxs = xs * ns.x + ns.y; + var gys = ys * ns.x + ns.y; + var h = 1.0f - abs(gxs) - abs(gys); + var b0 = new float4(gxs.x, gxs.y, gys.x, gys.y); + var b1 = new float4(gxs.z, gxs.w, gys.z, gys.w); + var s0 = floor(b0) * 2.0f + 1.0f; + var s1 = floor(b1) * 2.0f + 1.0f; + var sh = -step(h, 0.0f); + var a0 = new float4(b0.x, b0.z, b0.y, b0.w) + new float4(s0.x, s0.z, s0.y, s0.w) * new float4(sh.x, sh.x, sh.y, sh.y); + var a1 = new float4(b1.x, b1.z, b1.y, b1.w) + new float4(s1.x, s1.z, s1.y, s1.w) * new float4(sh.z, sh.z, sh.w, sh.w); + var p0 = new float3(a0.x, a0.y, h.x); + var p1 = new float3(a0.z, a0.w, h.y); + var p2 = new float3(a1.x, a1.y, h.z); + var p3 = new float3(a1.z, a1.w, h.w); + var norm = 1.79284291400159f - 0.85373472095314f * new float4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + double[] Of(float3 v) => new double[] { v.x, v.y, v.z }; + return (x, new[] { Of(p0), Of(p1), Of(p2), Of(p3) }, new[] { ix, iy, iz, i1x, i1y, i1z, i2x, i2y, i2z }); + } + + // The Hessian of snoise at v in double, entry i * 3 + j: snoise_hessian's per-corner derivation, summed + // over Corners. A corner with m = 0.5 - |x|^2 <= 0 contributes nothing. + private static double[] HessianDouble(double vx, double vy, double vz) + { + var (x, p, _) = Corners(vx, vy, vz); + var hessian = new double[9]; + for (var c = 0; c < 4; c++) + { + var m = 0.5 - (x[c][0] * x[c][0] + x[c][1] * x[c][1] + x[c][2] * x[c][2]); + if (m <= 0.0) + continue; + var s = p[c][0] * x[c][0] + p[c][1] * x[c][1] + p[c][2] * x[c][2]; for (var i = 0; i < 3; i++) - for (var j = 0; j < 3; j++) - d[(i * 3 + j) * 3 + k] = (plus[i][j] - (double)minus[i][j]) / (forward + back); + for (var k = 0; k < 3; k++) + hessian[i * 3 + k] += 105.0 * (48.0 * m * m * s * x[c][i] * x[c][k] + - 8.0 * m * m * m * (p[c][k] * x[c][i] + p[c][i] * x[c][k] + (i == k ? s : 0.0))); } + return hessian; + } + + private static double Delta(int a, int b) => a == b ? 1.0 : 0.0; + + // The symmetric third-derivative tensor of snoise at p, entry (i * 3 + j) * 3 + k, in closed form: + // snoise_hessian's per-corner entry 105 (48 m^2 s x_i x_j - 8 m^3 (p_j x_i + p_i x_j + s delta_ij)) + // differentiated by x_k (dm = -2 x_k, ds = p_k, every corner's x moves one for one with p), which sums + // to 105 (-192 m s x_i x_j x_k + 48 m^2 (p_k x_i x_j + p_j x_i x_k + p_i x_j x_k) + // + 48 m^2 s (delta_ik x_j + delta_jk x_i + delta_ij x_k) - 8 m^3 (p_j delta_ik + p_i delta_jk + p_k delta_ij)). + // Symmetric in i, j and k by construction, so there is no symmetrising pass. + private static double[] ThirdTensor(float3 p) + { + var (x, g, _) = Corners(p.x, p.y, p.z); var t = new double[27]; - for (var i = 0; i < 3; i++) - for (var j = 0; j < 3; j++) - for (var k = 0; k < 3; k++) - t[(i * 3 + j) * 3 + k] = (d[(i * 3 + j) * 3 + k] + d[(i * 3 + k) * 3 + j] + d[(j * 3 + i) * 3 + k] - + d[(j * 3 + k) * 3 + i] + d[(k * 3 + i) * 3 + j] + d[(k * 3 + j) * 3 + i]) / 6.0; + for (var c = 0; c < 4; c++) + { + var m = 0.5 - (x[c][0] * x[c][0] + x[c][1] * x[c][1] + x[c][2] * x[c][2]); + if (m <= 0.0) + continue; + var s = g[c][0] * x[c][0] + g[c][1] * x[c][1] + g[c][2] * x[c][2]; + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + for (var k = 0; k < 3; k++) + t[(i * 3 + j) * 3 + k] += 105.0 * (-192.0 * m * s * x[c][i] * x[c][j] * x[c][k] + + 48.0 * m * m * (g[c][k] * x[c][i] * x[c][j] + g[c][j] * x[c][i] * x[c][k] + g[c][i] * x[c][j] * x[c][k]) + + 48.0 * m * m * s * (Delta(i, k) * x[c][j] + Delta(j, k) * x[c][i] + Delta(i, j) * x[c][k]) + - 8.0 * m * m * m * (g[c][j] * Delta(i, k) + g[c][i] * Delta(j, k) + g[c][k] * Delta(i, j))); + } + return t; } + /// + /// ThirdTensor is the third derivative of snoise: HessianDouble is the kernel's Hessian (within float + /// rounding of snoise_hessian over 200 seeded points, which proves Corners is the kernel's lattice), + /// and the closed form equals central differences of HessianDouble, Richardson-extrapolated from steps + /// h = 1e-3 and 2e-3 (error O(h^4)), at every one of 27 entries of every point whose neighbours share + /// its lattice cell and clear every corner's support edge. Dropping or mis-signing any term of the closed form moves some entry by far more + /// than the tolerance. + /// + [Fact] + public void ThirdTensorMatchesDifferencesOfTheHessian() + { + var random = new System.Random(0x7D1); + var compared = 0; + var largest = 0.0; + var worst = 0.0; + for (var n = 0; n < 400; n++) + { + var p = new float3(Uniform(random, -256.0f, 256.0f), Uniform(random, -256.0f, 256.0f), Uniform(random, -256.0f, 256.0f)); + var hd = HessianDouble(p.x, p.y, p.z); + var kernel = snoise_hessian(p); + if (n < 200) + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + Assert.True(Math.Abs(hd[i * 3 + j] - kernel[i][j]) <= 0.1, $"HessianDouble({p}) entry {i},{j} is {hd[i * 3 + j]:R}, snoise_hessian {kernel[i][j]:R}"); + + double[] At(int axis, double delta) => + HessianDouble(p.x + (axis == 0 ? delta : 0.0), p.y + (axis == 1 ? delta : 0.0), p.z + (axis == 2 ? delta : 0.0)); + var (corner, _, key) = Corners(p.x, p.y, p.z); + // A corner whose support edge (m = 0) lies within the differencing window has a kink in the + // Hessian's second derivative there, which the O(h^4) extrapolation does not cover. + if (corner.Any(c => Math.Abs(0.5 - (c[0] * c[0] + c[1] * c[1] + c[2] * c[2])) < 0.01)) + continue; + var sameCell = true; + for (var axis = 0; axis < 3; axis++) + foreach (var delta in new[] { -2.0e-3, 2.0e-3 }) + sameCell &= Corners(p.x + (axis == 0 ? delta : 0.0), p.y + (axis == 1 ? delta : 0.0), p.z + (axis == 2 ? delta : 0.0)).Key.SequenceEqual(key); + if (!sameCell) + continue; + + var closed = ThirdTensor(p); + compared++; + for (var k = 0; k < 3; k++) + { + var (plus1, minus1, plus2, minus2) = (At(k, 1.0e-3), At(k, -1.0e-3), At(k, 2.0e-3), At(k, -2.0e-3)); + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + { + var d1 = (plus1[i * 3 + j] - minus1[i * 3 + j]) / 2.0e-3; + var d2 = (plus2[i * 3 + j] - minus2[i * 3 + j]) / 4.0e-3; + var richardson = (4.0 * d1 - d2) / 3.0; + var entry = closed[(i * 3 + j) * 3 + k]; + largest = Math.Max(largest, Math.Abs(entry)); + worst = Math.Max(worst, Math.Abs(entry - richardson) / (1.0 + Math.Abs(entry))); + } + } + } + + output.WriteLine($"IV-REPORT third oracle: {compared} points, largest entry {largest:R}, worst relative difference from the Richardson differences {worst:R}"); + Assert.True(compared >= 250, $"only {compared} of 400 points kept their lattice cell and cleared the support edges"); + Assert.True(largest >= 200.0, $"the largest third-derivative entry compared is {largest:R}: the check has no teeth"); + Assert.True(worst <= 1.0e-6, $"the closed-form third tensor differs from the differenced Hessian by {worst:R} (relative)"); + } + // T(u, u, u) for a symmetric 3-tensor. private static double Cubic(double[] t, double[] u) { @@ -748,8 +894,8 @@ public sealed class NoiseBoundTests } /// - /// The provenance of SNOISE_THIRD: the largest operator norm of snoise's third-derivative tensor - /// (ThirdNorm) over 1e6 seeded points of [-256, 256]^3, the 1e4 largest refined by ascent over the point, + /// The provenance of SNOISE_THIRD: the largest operator norm of snoise's closed-form third-derivative + /// tensor (ThirdNorm, ThirdTensor) over 1e6 seeded points of [-256, 256]^3, the 1e4 largest refined by ascent over the point, /// times 1.10, on the tied kernel. Prints the start whose ascent reached the maximum, which /// ThirdConstantPinsSampled climbs from again, and a sweep of 20,000 directions at the point the /// ascent ends on (the tensor norm there against the largest cubic form the sweep finds, so the @@ -780,7 +926,7 @@ public sealed class NoiseBoundTests } // The start MeasureThird printed as its witness. - private static readonly float3 ThirdWitness = new(-168.3755f, -3.8205261f, -109.16948f); + private static readonly float3 ThirdWitness = new(-184.81363f, -34.250977f, 68.26865f); /// /// Pins SNOISE_THIRD: no third-derivative norm over 1e5 seeded points exceeds it, and the ascent from diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl index 09616940..21360d07 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl @@ -14,7 +14,7 @@ static const float CULTMATH_SNOISE_HESSIAN = 55.642143; // An upper bound on the operator norm of the third-derivative tensor of cultmath_snoise(float3), the // next order after CULTMATH_SNOISE_HESSIAN; provenance in math.Span.cs (SNOISE_THIRD). -static const float CULTMATH_SNOISE_THIRD = 536.9152; +static const float CULTMATH_SNOISE_THIRD = 535.8009; float3 cultmath_af_point(float value) { return float3(value, 0.0, 0.0); } float3 cultmath_af_symbol(float x0, float a) { return float3(x0, a, 0.0); } From b042bf19be10de134374f6b17339707cabf9f22a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:39:27 +0200 Subject: [PATCH 04/24] cultmath: integral_clamp_linear is finite for infinite clamp bounds (integral-infinite-bound-nan) Each held stretch contributes only when its length is positive, so an infinite bound times a zero length never forms: relu (hi = +inf) and its mirrors give the finite integral instead of NaN, in C#, HLSL (Unity copy byte-identical) and the generated GLSL. Finite cases are unchanged. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/shaders/CultMath.glsl | 4 ++- packages/cultmath/shaders/CultMath.hlsl | 4 ++- packages/cultmath/src/CultMath/math.Span.cs | 8 ++++-- .../CultMath.Tests/SpanPrimitiveTests.cs | 26 +++++++++++++++++++ .../Shaders/CultMath.hlsl | 4 ++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/cultmath/shaders/CultMath.glsl b/packages/cultmath/shaders/CultMath.glsl index 3f090c7c..3f6a1dd5 100644 --- a/packages/cultmath/shaders/CultMath.glsl +++ b/packages/cultmath/shaders/CultMath.glsl @@ -315,7 +315,9 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float float leave = clamp(max(atLo, atHi), start, stop); float before = b > 0.0 ? lo : hi; float after = b > 0.0 ? hi : lo; - float total = before * (enter - start) + (a + b * (0.5 * (enter + leave))) * (leave - enter) + after * (stop - leave); + float held = enter > start ? before * (enter - start) : 0.0; + float kept = stop > leave ? after * (stop - leave) : 0.0; + float total = held + (a + b * (0.5 * (enter + leave))) * (leave - enter) + kept; return direction * total; } diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index 8379b5f1..988e6a72 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -315,7 +315,9 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float float leave = clamp(max(atLo, atHi), start, stop); float before = b > 0.0 ? lo : hi; float after = b > 0.0 ? hi : lo; - float total = before * (enter - start) + (a + b * (0.5 * (enter + leave))) * (leave - enter) + after * (stop - leave); + float held = enter > start ? before * (enter - start) : 0.0; + float kept = stop > leave ? after * (stop - leave) : 0.0; + float total = held + (a + b * (0.5 * (enter + leave))) * (leave - enter) + kept; return direction * total; } diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 6be788d0..5b0fbbeb 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -104,7 +104,9 @@ public static partial class math // line's value at the stretch's midpoint), and the stretch held at the far bound. b = 0 is the // constant clamp(a, lo, hi); t1 < t0 is the negative of the integral over [t1, t0]; lo above hi // clamps to hi everywhere, as clamp does. A crossing that overflows (b tiny) lies past the span and - // cuts nothing. + // cuts nothing. A held stretch contributes only when its length is positive, so an infinite bound + // (relu has hi = +inf) times a zero length never forms: the integral of a ramp clamped on one side + // only is finite. public static float integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1) { if (b == 0.0f) @@ -120,7 +122,9 @@ public static partial class math var leave = clamp(max(atLo, atHi), start, stop); var before = b > 0.0f ? lo : hi; var after = b > 0.0f ? hi : lo; - var total = before * (enter - start) + (a + b * (0.5f * (enter + leave))) * (leave - enter) + after * (stop - leave); + var held = enter > start ? before * (enter - start) : 0.0f; + var kept = stop > leave ? after * (stop - leave) : 0.0f; + var total = held + (a + b * (0.5f * (enter + leave))) * (leave - enter) + kept; return direction * total; } } diff --git a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs index 44814195..738b268f 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs @@ -319,6 +319,32 @@ public sealed class SpanPrimitiveTests Assert.True(zeroSlopes > 10_000 && reversed > 40_000 && both > 2_000 && neither > 2_000, $"the sample meets a branch too rarely: {zeroSlopes} flat, {reversed} reversed, {both} both, {neither} neither"); } + /// + /// An infinite bound never reached by the line contributes nothing, not infinity times zero: relu + /// (hi = +inf) and its mirror (lo = -inf) give the finite integral, whether the line crosses the finite + /// bound inside the span or not. + /// + [Fact] + public void IntegralClampLinearIsFiniteForInfiniteBounds() + { + var inf = float.PositiveInfinity; + // t over [0, 1] clamped below at 0: all ramp, 1/2. + Assert.Equal(0.5f, integral_clamp_linear(0.0f, 1.0f, 0.0f, inf, 0.0f, 1.0f)); + // t - 1/2 over [0, 1] clamped below at 0: 0 until 1/2, then the ramp's 1/8. + Assert.Equal(0.125f, integral_clamp_linear(-0.5f, 1.0f, 0.0f, inf, 0.0f, 1.0f)); + // The falling line 1/2 - t clamped below at 0 is the mirror: 1/8 over [0, 1]. + Assert.Equal(0.125f, integral_clamp_linear(0.5f, -1.0f, 0.0f, inf, 0.0f, 1.0f)); + // lo = -inf: clamp(t, -inf, 1/2) over [0, 1] is t up to 1/2 then 1/2 (1/8 + 1/4), and 1 - t clamped + // above at 1/2 is 1/2 until t = 1/2 and then falls (1/4 + 1/8). + Assert.Equal(0.375f, integral_clamp_linear(0.0f, 1.0f, -inf, 0.5f, 0.0f, 1.0f)); + Assert.Equal(0.375f, integral_clamp_linear(1.0f, -1.0f, -inf, 0.5f, 0.0f, 1.0f)); + // Both infinite: the line itself, the trapezoid, in either direction. + Assert.Equal(7.5f, integral_clamp_linear(0.0f, 1.0f, -inf, inf, 1.0f, 4.0f)); + Assert.Equal(-7.5f, integral_clamp_linear(0.0f, 1.0f, -inf, inf, 4.0f, 1.0f)); + // A line starting on the finite bound and rising into the infinite one: all ramp, the integral of t over [2, 4]. + Assert.Equal(6.0f, integral_clamp_linear(0.0f, 1.0f, 2.0f, inf, 2.0f, 4.0f)); + } + [Fact] public void IntegralClampLinearHasExactValuesOnTheBinaryFloats() { diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index 8379b5f1..988e6a72 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -315,7 +315,9 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float float leave = clamp(max(atLo, atHi), start, stop); float before = b > 0.0 ? lo : hi; float after = b > 0.0 ? hi : lo; - float total = before * (enter - start) + (a + b * (0.5 * (enter + leave))) * (leave - enter) + after * (stop - leave); + float held = enter > start ? before * (enter - start) : 0.0; + float kept = stop > leave ? after * (stop - leave) : 0.0; + float total = held + (a + b * (0.5 * (enter + leave))) * (leave - enter) + kept; return direction * total; } From 6fd60a979f74b24f6ad2b998d83668c7dcd9b3fb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:42:04 +0200 Subject: [PATCH 05/24] cultmath: integral_clamp_linear normalises lo before the flat branch (gpu-clamp-lo-above-hi-undefined) lo = min(lo, hi) now precedes the b == 0 branch in C# and HLSL (Unity copy byte-identical, GLSL regenerated), so no clamp ever sees lo above hi; GLSL leaves that undefined. The b == 0 path used to call clamp(a, lo, hi) with the raw bounds. New tests: the flat and sloped lo > hi values, the statement-order check on the committed HLSL and GLSL, and an appended glsl-parity.json family of lo > hi and infinite-bound edge cases (every earlier case byte-identical). Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/shaders/CultMath.glsl | 2 +- packages/cultmath/shaders/CultMath.hlsl | 2 +- packages/cultmath/src/CultMath/math.Span.cs | 5 +- .../tests/CultMath.Tests/GlslMirrorTests.cs | 37 +++ .../CultMath.Tests/SpanPrimitiveTests.cs | 16 ++ .../CultMath.Tests/fixtures/glsl-parity.json | 262 ++++++++++++++++++ .../Shaders/CultMath.hlsl | 2 +- 7 files changed, 321 insertions(+), 5 deletions(-) diff --git a/packages/cultmath/shaders/CultMath.glsl b/packages/cultmath/shaders/CultMath.glsl index 3f6a1dd5..b3a8dd4a 100644 --- a/packages/cultmath/shaders/CultMath.glsl +++ b/packages/cultmath/shaders/CultMath.glsl @@ -302,10 +302,10 @@ mat3 cultmath_snoise_hessian(vec3 value) // The integral over [t0, t1] of clamp(a + b t, lo, hi); see the C# math.integral_clamp_linear comment. float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1) { + lo = min(lo, hi); if (b == 0.0) return clamp(a, lo, hi) * (t1 - t0); - lo = min(lo, hi); float direction = t1 < t0 ? -1.0 : 1.0; float start = min(t0, t1); float stop = max(t0, t1); diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index 988e6a72..c454773c 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -302,10 +302,10 @@ float3x3 cultmath_snoise_hessian(float3 value) // The integral over [t0, t1] of clamp(a + b t, lo, hi); see the C# math.integral_clamp_linear comment. float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1) { + lo = min(lo, hi); if (b == 0.0) return clamp(a, lo, hi) * (t1 - t0); - lo = min(lo, hi); float direction = t1 < t0 ? -1.0 : 1.0; float start = min(t0, t1); float stop = max(t0, t1); diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 5b0fbbeb..bfb0f373 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -103,16 +103,17 @@ public static partial class math // at the bound the line starts beyond, the stretch on the ramp (a trapezoid: its length times the // line's value at the stretch's midpoint), and the stretch held at the far bound. b = 0 is the // constant clamp(a, lo, hi); t1 < t0 is the negative of the integral over [t1, t0]; lo above hi - // clamps to hi everywhere, as clamp does. A crossing that overflows (b tiny) lies past the span and + // clamps to hi everywhere, as clamp does, and is normalised (lo = min(lo, hi)) before anything reads it, + // so no clamp ever sees lo above hi (GLSL leaves that undefined). A crossing that overflows (b tiny) lies past the span and // cuts nothing. A held stretch contributes only when its length is positive, so an infinite bound // (relu has hi = +inf) times a zero length never forms: the integral of a ramp clamped on one side // only is finite. public static float integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1) { + lo = min(lo, hi); if (b == 0.0f) return clamp(a, lo, hi) * (t1 - t0); - lo = min(lo, hi); var direction = t1 < t0 ? -1.0f : 1.0f; var start = min(t0, t1); var stop = max(t0, t1); diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs index 14ea07c0..9fb45199 100644 --- a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs @@ -45,6 +45,26 @@ public sealed class GlslMirrorTests } } + /// + /// The lowering keeps HLSL's statement order, and GLSL leaves clamp(x, minVal, maxVal) undefined for + /// minVal above maxVal, so integral_clamp_linear must normalise lo = min(lo, hi) before any clamp in + /// its body can see it: in the committed HLSL and the committed GLSL alike, the normalisation precedes + /// the first clamp. + /// + [Fact] + public void IntegralClampLinearNormalisesLoBeforeAnyClamp() + { + foreach (var (file, text) in Lowered.Where(l => l.File == "CultMath.glsl").Concat(new[] { ("CultMath.hlsl", Hlsl) })) + { + var start = text.IndexOf("cultmath_integral_clamp_linear(float a", StringComparison.Ordinal); + Assert.True(start >= 0, $"{file} has no cultmath_integral_clamp_linear"); + var body = text[start..text.IndexOf("}", start, StringComparison.Ordinal)]; + var normalise = body.IndexOf("lo = min(lo, hi);", StringComparison.Ordinal); + var firstClamp = body.IndexOf("clamp(", StringComparison.Ordinal); + Assert.True(normalise >= 0 && normalise < firstClamp, $"{file}: lo = min(lo, hi) must come before the first clamp in cultmath_integral_clamp_linear"); + } + } + // Identifier boundaries: cultmath_lerp is a CultMath function, lerp( is HLSL. private static readonly Regex HlslOnly = new( @"\b(float|u?int|bool)[234]\b|\bfloat3x3\b|\blerp\(|\bfrac\(|\bsaturate\(|\brsqrt\(|\basuint\(|\basfloat\(|\batan2\(|static const|\((float|int|uint)\)|Texture2D|SamplerState|#include|precision (high|medium|low)p"); @@ -638,6 +658,23 @@ public sealed class GlslMirrorTests var lo = Uniform(r, -2.0f, 0.5f); return new object[] { Uniform(r, -4.0f, 4.0f), r.Next(8) == 0 ? 0.0f : Uniform(r, -4.0f, 4.0f), lo, lo + Uniform(r, 0.0f, 3.0f), Uniform(r, -3.0f, 3.0f), Uniform(r, -3.0f, 3.0f) }; }), + // The edge cases the family above never draws: lo above hi (b = 0 and b != 0; the lowering must + // normalise before any clamp sees it) and infinite bounds (hi = +inf, lo = -inf, both). + new Family("float cultmath_integral_clamp_linear(float, float, float, float, float, float)", "ulp-bounded, lo above hi and infinite bounds", + r => + { + var a = Uniform(r, -4.0f, 4.0f); + var slope = Uniform(r, -4.0f, 4.0f); + var (t0, t1) = (Uniform(r, -3.0f, 3.0f), Uniform(r, -3.0f, 3.0f)); + return r.Next(5) switch + { + 0 => new object[] { a, 0.0f, Uniform(r, 0.5f, 2.0f), Uniform(r, -2.0f, 0.4f), t0, t1 }, + 1 => new object[] { a, slope, Uniform(r, 0.5f, 2.0f), Uniform(r, -2.0f, 0.4f), t0, t1 }, + 2 => new object[] { a, slope, Uniform(r, -2.0f, 1.0f), float.PositiveInfinity, t0, t1 }, + 3 => new object[] { a, slope, float.NegativeInfinity, Uniform(r, -1.0f, 2.0f), t0, t1 }, + _ => new object[] { a, slope, float.NegativeInfinity, float.PositiveInfinity, t0, t1 }, + }; + }), }).ToArray(); // The fixture text, each case's arguments given by family and point index. diff --git a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs index 738b268f..3b871896 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs @@ -345,6 +345,22 @@ public sealed class SpanPrimitiveTests Assert.Equal(6.0f, integral_clamp_linear(0.0f, 1.0f, 2.0f, inf, 2.0f, 4.0f)); } + /// + /// lo above hi is normalised before the flat branch reads it: with b = 0 the answer is hi times the + /// span (what clamp(a, lo, hi) gives with lo = min(lo, hi)), and with b != 0 the same. The GLSL + /// lowering would otherwise call clamp with minVal above maxVal, which is undefined there. + /// + [Fact] + public void IntegralClampLinearNormalisesBeforeTheFlatBranch() + { + // b = 0: a above both, below both and between them all give hi. + foreach (var a in new[] { 9.0f, -9.0f, 3.5f }) + Assert.Equal(2.0f * 3.0f, integral_clamp_linear(a, 0.0f, 5.0f, 2.0f, 0.0f, 3.0f)); + // b != 0: hi everywhere, whichever way the line runs. + Assert.Equal(2.0f * 3.0f, integral_clamp_linear(0.0f, 1.0f, 5.0f, 2.0f, 0.0f, 3.0f)); + Assert.Equal(2.0f * 3.0f, integral_clamp_linear(9.0f, -1.0f, 5.0f, 2.0f, 0.0f, 3.0f)); + } + [Fact] public void IntegralClampLinearHasExactValuesOnTheBinaryFloats() { diff --git a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json index 605a60a6..0aa34b7e 100644 --- a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json +++ b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json @@ -26478,6 +26478,268 @@ "bfdafc5c 404ff2aa bfb94d31 bfa4f005 bfc84ee5 c007567c -> 3f4bbcce", "c001df13 00000000 bfe57b78 bfe564b4 40132a80 3f82db94 -> 40128a76" ] + }, + { + "glsl": "float cultmath_integral_clamp_linear(float, float, float, float, float, float)", + "tolerance": "ulp-bounded, lo above hi and infinite bounds", + "cases": [ + "bf0b2a84 c0384244 ff800000 3f9a007c bfc80642 3f5f8200 -> be62fdf0", + "bf903e72 be013e00 ff800000 3f8f124e bfed81c9 4015f3f2 -> c09b895f", + "bffd4268 c0197f86 bfa70b86 7f800000 3ffe5ff8 403e73b0 -> bfa5200e", + "3fb686b4 3ff53690 3f75cfa4 7f800000 be0b6280 4027937e -> 4127a25f", + "bf6bb2f0 c0383fa6 ff800000 3e887584 3faa2b50 3fdde568 -> c009d85d", + "40753aee bf2c5544 bff5b17c 7f800000 3eb30210 402b63e8 -> 40d19059", + "3fd7e728 3f14eae8 3feba88f bff6ba08 bf9bd95e 3f98cd80 -> c094bc52", + "bf55b874 00000000 3f797988 bfe54df5 bfe638a0 3fe6ac28 -> c0ce6a80", + "3fefa58c 00000000 3f9f2a45 bee349b4 bf88b77e c008c9f7 -> 3ef305bd", + "bffd1d32 bff52532 3f14b7bc 7f800000 3f5aeb88 c03b152e -> c0937293", + "4009824e 4052b500 3fc0ea57 bd8e5390 bf7fd568 c0074df4 -> 3d9ea9bd", + "bfdc0802 4061136c be801d20 7f800000 4026e128 40380bc4 -> 4008013a", + "404ffab4 c04ecac4 3fc30ebc bf5da486 be657b80 bff22a7a -> 3fb8d46a", + "3e979c80 3fc0a8cc 3e84ffd0 7f800000 bff86de6 c01bf43d -> be03ea8c", + "c06f0bd2 bf78e25c ff800000 3f877bf0 be80f2f0 bf904c04 -> 402bb6a3", + "3fb318a4 00000000 3f1431a4 bf507b64 bfb71488 bdc34ac0 -> bf8b283f", + "bf87a14e 3dc402c0 ff800000 7f800000 3fe7c320 3fe21f08 -> 3d205f08", + "bfa11404 3f9901dc 3ebdb318 7f800000 3f28ace8 3f096210 -> bd39812e", + "c05920f3 bfc9fe26 3f1ee26e be71a1f0 bf82654b c01a834a -> 3ea89ad5", + "c00556d0 3feebf88 3ef66d70 7f800000 3ea319f0 3f547630 -> 3e7c0825", + "3f828c34 bfe13e0c bd879b50 7f800000 bf9ee05a 3ff55834 -> 40351a31", + "3f656160 bfe976f4 3ff8708d be711278 c00d46e0 4004c750 -> bf810990", + "4049950c 00000000 3f952f50 bc8be500 3fc0e688 3fcb9310 -> babaa746", + "3fabe24c c00711ba 3e068070 7f800000 bf63a110 bf5d45ac -> 3da264c8", + "c0233b98 3fff1294 ff800000 7f800000 402d3600 bf8aac5f -> 40626228", + "3feaba90 c053efca bfecf755 7f800000 40364634 bfffca21 -> c0e1bf93", + "4028f0cc c0612055 3f54dc54 7f800000 402ad238 40290ec6 -> bcbbaf79", + "beb04698 00000000 3f91dbd1 bf53ed66 c00b6465 3f074058 -> c00f62e0", + "406fa864 00000000 3f8b20ef 3e6939a0 bfea8224 3f191d90 -> 3f0db236", + "405fd6fe c0040094 ff800000 7f800000 4002336c be60d030 -> c06a8a31", + "bfb75c48 40201468 ff800000 7f800000 401b77ac 3f03cff8 -> c089cb06", + "40056938 3fe8379c ff800000 7f800000 3fa99ba0 3f9ad530 -> bf018a9f", + "c0556eb0 bc1a2300 3febcf2d bf9750a7 401435c0 c0068484 -> 40a71cf1", + "bef4a0d0 c0415561 bf7228f4 7f800000 403de0a8 bf882153 -> 3fc8f311", + "3e800c00 3e8da060 ff800000 3f7a9082 bda76760 3ff23f68 -> 3f7cfbd7", + "c0467619 00000000 3f4bbbfc bfd7b41d bfd61757 bde41880 -> c028612a", + "3ed56780 bf10add8 3dee9d20 7f800000 bfb672ae 3ffb8258 -> 3fbd0bef", + "4012d6b4 c06f1e30 ff800000 bf78a9fa bf853cf4 3f966510 -> c01493b8", + "c04d58e5 c06ee722 ff800000 3ed04950 3f141838 3fe7dd48 -> c1174a8e", + "3ea47f80 00000000 3f8a8816 3e8963e8 be6dc380 bf4225c8 -> be1096c4", + "4066fa60 c060a340 ff800000 7f800000 3f090cac bf6a8060 -> c0c6a530", + "c077191e 401e0304 3feefde4 bfc95e30 402dbf82 c0021a52 -> 40ef01d3", + "bfd20336 3e8d73f0 3f4e9f9a 3c550000 3f341614 3f8712b0 -> 3b95dd74", + "400b9982 401c9b94 3eb71990 7f800000 3f87b77c 3f41d668 -> bfaae6cb", + "3d15a000 c0799975 ff800000 beedd366 3e647880 bf775c28 -> 3f11e714", + "bf799da4 403f9dfa ff800000 bed08894 4012d1f4 3d588080 -> 3f70da61", + "3fbe1938 00000000 3ff40bd3 beaf5330 3f890abc 401b8f60 -> beee7080", + "3fbd70bc 3f7e1758 ff800000 7f800000 bef05ab8 4026cf76 -> 40fa0e87", + "c0229dc4 c036d32c ff800000 bf150af0 bffe5218 bfdccf14 -> be1c157f", + "bfe6ffe0 bf050038 3f01e041 bfac9dd0 c023de33 3ebccc20 -> c07cd000", + "3f687838 bd8e3b60 3f8f366c bff818ea c000ee81 3e4c9570 -> c08957e4", + "40722c5a 00000000 3fe691e1 be4fc540 3f4be2d8 3fb86d78 -> be05e3b8", + "3ff4b594 00000000 3f1dfd73 bf0697d8 3ffe2e30 c001ddfd -> 400718b2", + "c02fd649 3e5cc9a0 3fceabb6 bfb7fe6e c03c4bcd c0006605 -> bfac3327", + "c014e4f0 c0405a98 ff800000 3f66a948 c0105010 c016cee8 -> bdbb4834", + "bee72060 bedb74f0 3f0387d0 3e392a40 3e224820 c005bf10 -> bed02668", + "40665c3a 00000000 3fcf9ca4 bf880bb7 3d7a3a40 bedafe30 -> 3f05005c", + "3e912a30 bb7e1400 3ff1f167 bed87abc bfc5ea97 bf1a21fc -> becc6313", + "beb05d40 00000000 3fc08ab6 bdce7480 c004a2b4 bf4b4290 -> be03f88a", + "4007114a bf958182 bfa630e2 7f800000 3d523180 3f4c1688 -> 3f9a2e6d", + "bfc774c6 bfd46d30 ff800000 7f800000 bf0b93a0 3f89ac50 -> c04f49cb", + "c05a4f76 00000000 3faf89fd bfdd25e1 3f7c6fb0 be91f730 -> 400c8ee2", + "3db006c0 bf96efd4 ff800000 7f800000 3f98cc04 3fa107e0 -> bdb31de7", + "3fe5ebd4 402a5cb6 ff800000 7f800000 3fbd84a0 4036fb00 -> 412701cd", + "be130280 bf83421c 3f2d0fb4 bfc6d309 3fda2328 bfac537c -> 4097a0d3", + "bf998358 c07f2a7b bfdf42db 7f800000 3fe33530 3ec8fa80 -> 401a54fe", + "c003a422 c051b8fa 3fd169fb 3ec3f8b8 3ec1a460 3eb330b8 -> bc310236", + "c07db7cb 40652b7c bf8650a4 7f800000 3ee24aa0 bfbcc298 -> 4000b7e7", + "c050b426 c064494a ff800000 7f800000 be2c1ea0 3fda20ac -> c133bf2e", + "3ff5bf38 00000000 3febe24a bf246e24 bf8b52fe 3fee7658 -> bff2a785", + "3fef8560 00000000 3fd6bcb1 bf2f4aa8 be05c480 c018ada4 -> 3fc5a368", + "407e47fc bfee3d8c 3f3fd258 bef79ec8 be86f720 3f373aa8 -> bef28150", + "407a9322 4077a70c ff800000 3f16e5c0 4039278e 3e53b450 -> bfcaad7c", + "3ff74adc c074514c ff800000 7f800000 c036692f c00d50d4 -> 40ee1477", + "c0109599 407dcdfc ff800000 3e9826e8 3ff9ef34 bfbad3e8 -> 40f87d8c", + "c064d37e 3fabd540 ff800000 3f0b3400 403d2aee be4aadc0 -> 40ae0317", + "404dda66 405d6af2 bfecf98f 7f800000 3fdd9fe0 3f9e5abc -> c08419fa", + "3f8b94c0 00000000 3fd01cae bfabedbb c0053eb8 3f34f7f8 -> c06fbe1b", + "bfa782ce bfe55598 ff800000 7f800000 bfc85400 bf4eb930 -> 3f1e65b3", + "3fd827f8 c0721f90 ff800000 3faf1ee4 bf6c2dd0 bfd3bc9c -> bf801f26", + "3fe3ef4c 3ff5aa80 bd3e6540 7f800000 3b849000 c036e89e -> bf3e81f6", + "c02720c9 00000000 3fb39588 bffd0e72 bfa5ccee be3dcb80 -> c00c7148", + "4025b204 c0178ad4 ff800000 3f966f62 4035bc96 bf2b03dc -> 3fea7626", + "3f4c5f00 4027a106 bfc3f62d 7f800000 c02341c1 40397f36 -> 4127220a", + "4020b9d8 bfca59f4 ff800000 befd6a32 bffea84d 3fc48c50 -> bfdf5315", + "3fb6059c c03aeb32 ff800000 3f258852 403fb10c 4039b3a0 -> 3f2c3f36", + "3f66acd0 00000000 3f998fea bd8d6c10 3c347200 beab4b58 -> 3cc37c8b", + "bff98cd2 3fb23794 ff800000 3f9b5268 bfcfafe2 3e806180 -> c0ae1e58", + "3f8dc374 3f98a9f8 ff800000 7f800000 bb9b6000 bfe8226a -> bd2be404", + "3f974950 400380f8 ff800000 3d3dd240 c024f56a 400d0d90 -> c07f64b5", + "c07d5b40 bf3a9a3c bf194916 7f800000 3f22b430 bf990456 -> 3f8c5559", + "c07aaed0 402e17d8 ff800000 3fbe27ba bf0fb180 bee83ca0 -> bf121273", + "c00db16a 00000000 3f42cc80 bf57ab38 3ff8c090 3f2679ac -> 3f8b7054", + "3f2cbc60 00000000 3fa3b2db bfc92692 3e942528 c0390c40 -> 409ff36f", + "405fd66a c06cf16e 3e7196c0 7f800000 bf7c1f74 c0305c79 -> c193a29a", + "c044711a c031a2d6 ff800000 7f800000 bebb27d8 3f1155a0 -> c0481673", + "3cf92600 40353762 ff800000 beb7b1b2 bfe10d3c bfd7a9aa -> beb5ece8", + "bfbf100e 00000000 3f3505d8 bff3e8be 3ff63008 bfac1142 -> 40c7401d", + "3f207060 404afd72 bf21122c 7f800000 bf309db8 bfff02d4 -> 3f51c609", + "bf5fae3c 3f0789e0 3f6b63ec 7f800000 3fd8303c bfd41c44 -> c044e8b3", + "3d089500 bf897fbc ff800000 bf4a15df 402b3ed8 c0343436 -> 40c967b0", + "3fddab38 c00a9c2c 3f69bb9a bf8fd314 3fbe5748 3f862be0 -> 3efc747a", + "c061e050 bfa97d26 ff800000 3fda986a 3f9d338c bff7ceed -> 411af31e", + "404a3c54 404c5c1c bf6b4b74 7f800000 bf9c49de be6718b0 -> 3f58a2fb", + "406590ca 3f04dcb0 3fcaaecd bef9b444 4039241a bfe373d9 -> 4011c234", + "3fb551f8 c0020910 3fdfaf6f bfdeb6d5 bfe83ede 4005a37a -> c0d94966", + "c016a797 bfb2770e ff800000 7f800000 401cfafa bfa01d21 -> 413d2608", + "3f1c90a8 00000000 3fc3bac8 bf888bc0 3fe88cdc 3e344ba0 -> 3fe008e9", + "c0559cc6 bfb71176 3fed70d2 bf903083 3f4f3594 c023931e -> 40729e34", + "3fae840c 00000000 3f97115a 3e924de0 3d02eb80 3fb3f834 -> 3ec90777", + "3fc27378 3f494498 bf7d02b8 7f800000 c012410c 400ab734 -> 40d1dbb7", + "bf9e10e2 3f390bf0 ff800000 7f800000 3f972c38 3ed4cf28 -> 3f00e4ca", + "c07d7329 00000000 3f0da4a8 3d818360 c03ddfa2 bfbf8568 -> 3dbe7380", + "3e116060 00000000 3f420c52 3ec06c78 3f564cb8 bf92c392 -> bf3edb08", + "4063ad1e c07e35a3 ff800000 3f462e80 c00212a7 bf6ed200 -> 3f59e669", + "4044483e 402d13a2 3d50ef40 7f800000 402d46cc 40146fd2 -> c075046a", + "3fac2414 3f8b40ac be01e540 7f800000 bf682450 c02f685b -> 3dfedeea", + "c05e65ff c05526f6 3f909a1c bf042384 bfe455e8 bf6eb6bc -> bee10136", + "be776a10 403fd276 3f89268b bf2a53f8 3d3d1180 3e9358c0 -> be249f9f", + "4064ee2e 00000000 3f5e66be 3ec864c0 3e805e10 bf7f648c -> befa2911", + "4074ae32 00000000 3fcdee98 bf64354e bfffca31 3f252220 -> c016cfdc", + "bf87c254 00000000 3fc36d6f 3eaff918 bffe4cd0 400b1ff8 -> 3fb70933", + "c052c89e 4013111e ff800000 3f96e1b2 3f19be14 3f5ed320 -> bedd88e9", + "c009eb35 3fddee68 ff800000 3f044578 bf3ef864 bfa138ba -> 3ffff3a9", + "40691b74 00000000 3fe4e5aa bfb43ed8 3fe3f06c c01bd382 -> 40bdf578", + "3f830a4c c0531431 ff800000 be90363a 40370edc c03245db -> 4139b209", + "3fb8e818 c02ff12b 3fc3a58f bfcd1179 c018b019 3f67df5c -> c0a8bedc", + "3fc93094 3f22f3a0 ff800000 7f800000 4009c918 c01fb787 -> c0d99551", + "3fc13038 bee9dde8 3fcc6902 3dd5f6c0 3df0a1a0 3fcc1fc8 -> 3e1e094a", + "407033da 3f6849a8 bfce467d 7f800000 3fda1130 3fad13ac -> bfe73ad0", + "c031489e c07e39cd ff800000 7f800000 4019c98e c02a48fe -> 4136f1e3", + "c046dc6d 00000000 3fad6b49 bfd899d8 400b65bc bf10f6a4 -> 40949b6a", + "c05e533f c06d210a ff800000 7f800000 3f206210 be288850 -> 405b3391", + "bf7e85d0 40416500 3f8fdc10 be82a520 3fa35f7c c0056fbe -> 3f5b917e", + "40396ff6 402734d2 ff800000 3f60c942 bfc38654 c002fd20 -> 3f6b736f", + "404b1340 00000000 3fc2fb3c bfae7f84 3e430fe0 3e03e530 -> 3dac39e9", + "3f8f488c 00000000 3f6cafbe bf8aa584 3fbddcd4 bf809474 -> 402c76f4", + "40663df8 3f8cd5dc 3f9f57b4 bd8e8900 bfe9f0a6 3f955598 -> be556613", + "bf3bf700 4031fd74 ff800000 7f800000 40096f2c 40372942 -> 408e78dd", + "3f3342d0 c0272e74 ff800000 7f800000 bfeab9ec c0281aa2 -> c0a59170", + "3f3da9b8 00000000 3fb2875a bf319468 3fd6bb68 c000ec58 -> 4023e82d", + "40341d6e 40582d68 ff800000 bf1bc556 bfe0d240 bffe1562 -> 3f4d1a78", + "bf757580 c05a3300 ff800000 bea6f0e8 c02f51c4 c032f8fa -> 3c987746", + "3f86552c 3ed0c070 ff800000 7f800000 40146420 bfaf990a -> c092bb03", + "bdbde020 3f2abbb8 3f2459f1 bfe3ca0b 3f8583a4 3e88a890 -> 3fb0cd8b", + "bedefe38 3f839e38 ff800000 7f800000 40125092 3fb84be8 -> bfa05876", + "3ce15b00 c0108368 ff800000 3fc394ba 3feeb264 c0303e82 -> 3e29a2c0", + "bf995e24 bfd7be92 ff800000 3eddd4cc 3f4f9ff8 3fab76f4 -> bfcbaca5", + "404e6a02 401151c4 ff800000 7f800000 3ea4ac10 bf1f8304 -> c02e4f3d", + "bf0d3864 bfbd35da ff800000 7f800000 3f9e5764 3ff89eb8 -> c002f92a", + "3fb41270 3ee36b90 3ef72070 7f800000 40224ce2 403fee2c -> 3f9c3661", + "c044e618 3fe4f860 ff800000 bf3ab3d6 c02a551f bfc0667b -> c0fc1a14", + "3ea3d780 4021e21a ff800000 7f800000 403324b8 bff2fb70 -> c0db5ad7", + "3fd54e2c 3e944f40 ff800000 3fcca272 40391af2 bff0c14f -> c0e79db9", + "c01b99d7 3f90b97c ff800000 3edae39c 3ee34220 3e7c4700 -> 3ece6418", + "c01ae2cc 3f06c418 bebfa5e0 7f800000 c03305be c0029bb0 -> be90f9fd", + "bffa97b2 c0436485 bfbd3ae5 7f800000 3f685908 bf686f08 -> 3fe960f8", + "4052b922 3faad6ec ff800000 7f800000 be46c010 4001a130 -> 4120547c", + "c003292a 3ff31fd0 be378c10 7f800000 bed82bc8 3fdbb9f8 -> 3e008adc", + "c028fa52 402ef08c ff800000 3f970044 3fd06e10 401db5a4 -> 3f7c6de2", + "bf90e906 40570af6 3fe4ea7d beb7e66c bfa3f8a8 bef68628 -> be9308c8", + "3f59deb0 405c57b2 ff800000 bf4de4a9 403de08c bfad59ea -> 40993e1e", + "bfaf4754 bf8fbd24 3f25c0ac bf858e0c c03acd27 3fecabb0 -> c09f307e", + "bf90e182 40282582 3fc46df6 be554ce0 bf8271ba 3fe93108 -> bf177dcf", + "406af308 403a4800 ff800000 3f32aef0 bf8ba032 3fd28940 -> 3ff382a1", + "bf5d937c bfd430f6 ff800000 3f7dcd38 bedb1fd8 bfca9e2c -> bf3f7814", + "401da4d4 40116556 3f98ec71 be8d3d0c bfce7124 4012e7a8 -> bf89ff65", + "3fe228d8 00000000 3f9f5144 bd4de720 bfc2152a c003ee96 -> 3ce080a7", + "bfd65224 bf871dca bfc4fa72 7f800000 3e38c7c0 3f0a0458 -> bf0d4dd8", + "3eedae10 c0467403 ff800000 bead9056 3faca370 3fba4780 -> bed3e1b9", + "3f9285bc c035a27e ff800000 7f800000 3f8532a4 c0322863 -> c15d6539", + "400e0294 c0645562 ff800000 bf1e37fc 3f2ab790 bfa0f5e6 -> 3f983c47", + "406445f8 c056a1ed ff800000 7f800000 bf750180 3e926600 -> 40baa1c2", + "3f7ed1b0 3fd8acf4 3fdd35f6 bfab6e2e 3f9f492c bfa55d57 -> 405966f4", + "c073d5f9 4006818a ff800000 bf2ec36c 40021aee 3fc28b80 -> 3eb34fac", + "3fc831b0 00000000 3fdd735e bf1fdb88 bd8b8a80 bfb0546d -> 3f5152f5", + "c067b803 c053ee97 ff800000 7f800000 bf8d0f45 bf39c214 -> be65295f", + "40454632 3e33fe40 bef215e0 7f800000 3f873d84 bfc71e5b -> c0fdfdd3", + "c0371b4a 00000000 3f0d7f2c bf9b65f8 3f08eedc bd93ca60 -> 3f3cabee", + "4062a26e 4026084e 3f23621f bff89ca8 400e0fdc 3f65e738 -> 40244a41", + "bebe03d8 3d2fdd80 ff800000 3f835eb0 3f11c9b0 3eaa4138 -> 3daab3ea", + "c0657d7b 3e276860 bf506f04 7f800000 403635f6 4021d69e -> 3e84b269", + "bfafc7b8 bfc778be 3eb2bac8 7f800000 bf254a34 bf9c07fc -> be5744a1", + "c0249e78 bfae5444 bfb04f0c 7f800000 3f2a68e0 403dd784 -> c04acf36", + "c068736e bebe7aa8 ff800000 7f800000 402a1540 c01ed528 -> 4196ac89", + "3fd0127c bf5169a8 3fa4f77c bf8b48c4 3ff02d34 bfd3d383 -> 4075ecde", + "405aa754 c0423731 be096ae0 7f800000 3f1cc1f0 401cf950 -> 3e6661fd", + "3f86b240 3fabb848 ff800000 bf6ee45f 402ca76a 3f352178 -> 3fedb7fa", + "3ecdd950 00000000 3fd260dc bf811808 bfb00f52 400ccb1a -> c066c75e", + "bea6feb0 00000000 3fcca4b9 3ea249c0 bf2cfa20 3d67ae80 -> 3e6dac20", + "3e4a0840 00000000 3ffae0e6 be04b4c0 3e11a6d0 3fad4750 -> be20c604", + "c0799f76 bf85594e 3fe783e2 3ebf4b78 be03f160 bff50393 -> bf2ac2e6", + "bf8c7388 4032a74c ff800000 3fe1876e bfb9ecec bedcf780 -> c0738770", + "3e93efb0 be81bd00 ff800000 3ec55218 becf8b40 3e3f1150 -> 3e3fdd04", + "406241ca c05de18a ff800000 3e80529c bf271fe8 3e73c080 -> 3e64a338", + "c025c02c 00000000 3f5603c2 bf6d7634 c01aa5c0 bf56b0c0 -> bfbb5354", + "3fef160c c0039d40 ff800000 3f874ce4 3e683f80 c0037449 -> c01a4b84", + "40267ca2 00000000 3f31b7ee bfe701df bee92cf0 c01afc8e -> 40631bbc", + "befbf118 401fad5a ff800000 7f800000 bed93b70 c02a5de4 -> 411b762d", + "407c371e c07c0783 ff800000 3f5d20c4 3fa0bb70 c033ef8b -> c0447def", + "4029034e 403c0c0a 3feb36aa bfff3283 3fcad320 c01c200a -> 41005d70", + "3fd1dcc8 c06ea2f7 ff800000 3fff5a82 4038caf0 c03aedd4 -> 409ff98d", + "bfc1e3d2 3f8f931c bee4a428 7f800000 4004a430 4016cbd2 -> 3e8cbd70", + "4070c488 bffe9414 ff800000 7f800000 bf09ef98 3e37a380 -> 403d563f", + "bf8adb0a 00000000 3f530965 be6b63c8 c03047db 3e033610 -> bf29a108", + "3fceea28 3e77c340 ff800000 7f800000 401ff396 bf846d89 -> c0cad675", + "4057d9c8 00000000 3f937e61 bfe2a4ae 3d64f300 3e46d0e0 -> be7aafaf", + "c04cbc5e bf19041c ff800000 becedaf0 c00c7b5c bed40f78 -> c089dd29", + "c034a3ce c0451745 ff800000 3de07e90 c00b3cd8 3da72fe0 -> bfb2f3c5", + "bf2408a4 bfa6832c 3fe35a97 bf5850d4 3fdb6c9c 3fafe434 -> 3e93235a", + "3edb64e0 00000000 3f1f66ec 3eb80448 bfc4daee bf910b46 -> 3e14f87a", + "3fdc6cbc 40590126 ff800000 7f800000 c00231b2 c027f93b -> 406956a4", + "4040c116 40220f9e bfa796c8 7f800000 bf8e5c1a bed7c408 -> 3f3d3b10", + "3f86c7e4 00000000 3fb33aa0 bfc61af4 bf882bc6 c03e63cf -> 403d4a4a", + "40135a32 00000000 3f010b69 bfc53aac 401c8ada 3f067e58 -> 403d666e", + "3fb9ea54 3f667088 ff800000 7f800000 40126b4e bfaa9476 -> c0da143e", + "c0458ed9 40793090 bfaa3bb6 7f800000 bf67a36c 3fc9ac68 -> bf5677fd", + "40688054 c059af7c bfd79392 7f800000 3e2cf960 400c88b0 -> bddc33e8", + "4050b46a c008e7a1 bfc31ca2 7f800000 bbee3c00 3f8f245c -> 40154f21", + "3eca3830 3fddaec4 3fa6bd9f bfe8d46a 3f5c7528 c010e3f2 -> 40b5e713", + "c03e9e6b 40136fc6 3f655bb7 3e7a6900 c0033d1b 400e901c -> 3f85e95a", + "c06d2fc3 3f1626f8 3fa64533 3c6a4000 bebb86b0 bfc4f666 -> bc895477", + "400fca88 c034b1dd ff800000 7f800000 3e1b8dd0 3f763ae0 -> 3f0bd978", + "407c94a4 401ad33a bef86e10 7f800000 3f8a308c 401adac0 -> 412f5c0a", + "c03f7b9c bebc9828 3ffe5c68 befd807c bf8113ec 3ebca1e0 -> bf2e8422", + "4020cfe0 3fbfa9ec ff800000 bec74ee2 bf5da2c8 bec67c50 -> be3e93de", + "c00d2dee 3f50efe0 3fd8618f bfb19656 3f356f28 c025afbf -> 409266e9", + "bf44b364 c07447c3 bfd29b93 7f800000 bf8acc84 3fce3ab8 -> bf91d691", + "be9a1c78 befe9960 3f277228 7f800000 3e805e20 bfae4fde -> bf87017e", + "3f9e00fc bf88e102 ff800000 3eda1fc8 bff76283 3fee13f4 -> 3f76cb83", + "bf8ef440 bed711a0 ff800000 7f800000 3f32f3d4 be428170 -> 3f8b3ffe", + "be067930 bf9e417e bf853207 7f800000 bf648afc bf88e6d6 -> be43d727", + "bf7f9700 3fef7b1c 3f26c61f bfe0524f bf6d1eec c015e85e -> 401ed364", + "c03b0aad 3f01d8f0 bfb759c7 7f800000 403ced4e c01ba186 -> 40f6c684", + "bfe92a4c 00000000 3fe0c054 bff1c0f4 401ca664 3e9bbb10 -> 40818ca7", + "40158400 c07868c5 3fbaa390 3e7b5c90 3f0f2138 4035fb98 -> 3f0f8d26", + "c0252b55 402bdf3c ff800000 bf6bc7ac beec5098 bfd86ca4 -> 40d73441", + "c0439078 c02ab53c ff800000 3e5e0f60 3e709e60 c018c75b -> 401204ed", + "bff046aa c059b87e bf99fa11 7f800000 3f86a3f8 c030ae4e -> c0d2a314", + "40563928 bf14dc14 3ec4c038 7f800000 bfe97076 bed2e7d0 -> 40b49698", + "c014aa4e bdfe0340 3f3605dc bf8dcf77 bf297690 3e2aaf90 -> bf6b05c9", + "4006e3d2 3f612710 3f35e2c0 be124f38 c0008056 4023799c -> bf26dee6", + "bfce98fc 3cb4e000 ff800000 7f800000 c024ad6d c01767f0 -> beb1277d", + "401c9254 400d1d40 ff800000 7f800000 3fb53620 3edcd940 -> c08d3488", + "c0443ea5 40732a4e bfaecf9c 7f800000 c015d256 3f9feb58 -> c06ba7f1", + "3ff551d4 00000000 3f77cbec bef1ad68 bfb84901 3f451778 -> bf8580ed", + "3f38d8f0 00000000 3fd8c54d bfe14821 bf1befd8 3fb0f7f4 -> c06058a8", + "c033affa bf7d4dc8 ff800000 7f800000 40196ef2 3fa0ee0c -> 40a86678", + "bf2cc260 bf934a3e ff800000 bf714bc3 c01d504b 3ead2a20 -> c0291728", + "be5790e0 40534106 3f0bd833 bed0b664 befbb698 bf08982c -> 3c8c11c8", + "bfa83d6c 00000000 3fa24e71 be275838 bee3f830 bff605bb -> 3e77224e", + "4078f8f2 403b7202 bfe30e3b 7f800000 bdeac340 bf99fb7f -> c00891ab", + "400729d0 40616b48 ff800000 bf461e6f be2caae0 3ff7ba54 -> bfd06ba6", + "bf4c31ec 3ff690f8 ff800000 bedca326 c03b59a8 40086c3c -> c138ac1b", + "bda07260 3f5f9400 3f44ceb8 bf7d7ac4 3e564320 bf1a9360 -> 3f4e178c", + "bf878b18 c05db95e ff800000 3ff43ee2 3f871b38 bf1df154 -> 4042a94e" + ] } ] } diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index 988e6a72..c454773c 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -302,10 +302,10 @@ float3x3 cultmath_snoise_hessian(float3 value) // The integral over [t0, t1] of clamp(a + b t, lo, hi); see the C# math.integral_clamp_linear comment. float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1) { + lo = min(lo, hi); if (b == 0.0) return clamp(a, lo, hi) * (t1 - t0); - lo = min(lo, hi); float direction = t1 < t0 ? -1.0 : 1.0; float start = min(t0, t1); float stop = max(t0, t1); From c2722c651bd42f4ee5a5a97d903d92501d622b86 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:48:43 +0200 Subject: [PATCH 06/24] cultmath: span_fbm and span_ridged return SpanNoise (field, curvature, fold) The octave stages of a warp chain over a span, in C#: stretched, shifted, offset octave sums whose field equals fbm_grad and ridged_grad bit for bit at the identity transform and whose curvature and fold bounds derive from SNOISE_LIPSCHITZ, SNOISE_HESSIAN and SNOISE_THIRD only (local tier from the midpoint Hessian's Frobenius norm plus the third-derivative term, capped by the global pin). SpanStageTests encloses the Hessian norm and the branch departure over seeded spans in both regimes and checks the gradient against differences and the stretch/axis conventions. README and design.md state the span, the transform and the bound rules. No shader twin: stage-shaders is part 2. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/README.md | 4 +- packages/cultmath/docs/design.md | 32 ++ packages/cultmath/src/CultMath/SpanNoise.cs | 29 ++ .../cultmath/src/CultMath/SpanNoise.cs.meta | 2 + packages/cultmath/src/CultMath/math.Span.cs | 100 ++++ .../tests/CultMath.Tests/SpanStageTests.cs | 434 ++++++++++++++++++ 6 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 packages/cultmath/src/CultMath/SpanNoise.cs create mode 100644 packages/cultmath/src/CultMath/SpanNoise.cs.meta create mode 100644 packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md index 1a26571b..4f2018a3 100644 --- a/packages/cultmath/README.md +++ b/packages/cultmath/README.md @@ -82,7 +82,9 @@ shader semantics. the analytic Hessian of `snoise(float3)` on the same lattice, hash and tie order as `snoise_grad` (a symmetric `float3x3`; GLSL `mat3`), and `integral_clamp_linear(a, b, lo, hi, t0, t1)`, the closed-form integral over `[t0, t1]` of `clamp(a + b t, - lo, hi)`. Three measured bounds describe `snoise`: `SNOISE_LIPSCHITZ` (gradient + lo, hi)`; and, in C# for now (shaders follow), the octave stages `span_fbm` and + `span_ridged`, which return a `SpanNoise` (`field` as `float4(gradient, value)`, + `curvature`, `fold`) for a stretched, offset, shifted octave sum over a span. Three measured bounds describe `snoise`: `SNOISE_LIPSCHITZ` (gradient length, `NoiseBoundTests.MeasureLipschitz`), `SNOISE_HESSIAN` (Hessian spectral norm, `MeasureHessian`) and `SNOISE_THIRD` (third-derivative operator norm, `MeasureThird`); each is 1.10 times a seeded maximum refined by ascent, and a diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index 5f84e734..0d7cd738 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -806,6 +806,38 @@ constant of 40 misses there). The margin over the function itself: a scan of the independently over every hash configuration gives 52.22, so 55.64 sits 7% above the configuration bound. A change to the kernels must re-run the measurements. +### Span stages + +`span_fbm` and `span_ridged` (`math.Span.cs`, C# only so far) are the octave stages +of a warp chain, each returning one `SpanNoise { float4 field; float curvature; float +fold; }`. The span is the path `q(t)`, `|t| <= halfLength`, through `q` with the given +`velocity` at its midpoint, `|q'| <= speed` and `|q''| <= acceleration` along it. +Octave `o` samples `snoise_grad` at `x_o(q) = f_o (M q + shift) + offset + o +octaveStep`, with `f_o = lacunarity^o`, amplitude `gain^o`, and `M = frequency (I + +(1/stretch - 1) a a^T)`, `a = normalize(axis)`; a zero axis or `stretch` 1 means no +stretch, and `stretch` must be positive and finite (unguarded, like `lacunarity`). +`field` is the octave sum and its gradient in `q` (`f_o M grad n_o`); at frequency 1, +stretch 1 and zero shift, offset and octaveStep it equals `fbm_grad` and +`ridged_grad` bit for bit. `span_ridged` sums `gain^o (1 - |n_o|)`, `ridged_grad`'s +convention; the Span Lab's `1 - 2|n|` is twice this minus the gain sum, which the +consumer forms. + +Bounds come from the three pins only. With `|M| = |frequency| max(1, 1/stretch)` and +`nu = f_o |M| speed` the noise-space speed, the Hessian of the noise on the span has +operator norm at most `Hl = min(SNOISE_HESSIAN, ||snoise_hessian(x_mid)||_F + +SNOISE_THIRD nu halfLength)`, so `curvature = sum gain^o f_o^2 |M|^2 Hl` bounds the +operator norm of the field's Hessian in `q` at every point of the span (the pin takes +over once the third-derivative term exceeds it, and at `halfLength` 0 the local tier +alone remains). For `span_ridged`, an octave leaves the branch it is on at the +midpoint (`1 - s n`, `s` the midpoint's sign of `n`) only where `n` changes sign, and +there `|n| <= V - |n_mid|`, with `V = |grad n_mid . w| halfLength + (Hl nu |w| + Gl f_o +|M| acceleration) halfLength^2 / 2`, `w = f_o M velocity` and `Gl = min(SNOISE_LIPSCHITZ, +|grad n_mid| + Hl nu halfLength)`; `fold = sum 2 gain^o max(V - |n_mid|, 0)`, exactly 0 +for `span_fbm` and for an octave that cannot reach zero. Rounding is not widened: the +Frobenius norm exceeds the operator norm except on a rank-one Hessian, and the pins +carry a 10% margin. `SpanStageTests` encloses the Hessian norm and the branch +departure at 64 points of seeded spans, in the local and the global regime. + The slice. `af_frustum_ball(centreSlope, z0, z1, footprintPerDepth, warpVariation)` returns the centre `(m_c z_m, z_m)` and a radius `z1 footprintPerDepth + warpVariation`, with `af_frustum_axis` the axis `(m_c, 1) h`: diff --git a/packages/cultmath/src/CultMath/SpanNoise.cs b/packages/cultmath/src/CultMath/SpanNoise.cs new file mode 100644 index 00000000..dcdf8385 --- /dev/null +++ b/packages/cultmath/src/CultMath/SpanNoise.cs @@ -0,0 +1,29 @@ +namespace CultMath; + +/// +/// The result of and (invariant 8's one struct +/// return shape): an octave stage's value and gradient at the midpoint of a span, with the bounds that +/// hold over the whole span. +/// +public struct SpanNoise +{ + /// (∇, value): the stage's gradient in the span's space and its value, in the float4(gradient.xyz, value.w) layout of a bare value-and-gradient return. + public float4 field; + + /// An upper bound, at every point the span can reach, on the operator norm of the field's Hessian in the span's space. + public float curvature; + + /// + /// An upper bound on how far a ridged sum can leave its midpoint branch over the span (each octave's + /// 1 - |n| against the same octave continued with the midpoint's sign of n); 0 for + /// and for any octave that cannot cross zero over the span. + /// + public float fold; + + public SpanNoise(float4 field, float curvature, float fold) + { + this.field = field; + this.curvature = curvature; + this.fold = fold; + } +} diff --git a/packages/cultmath/src/CultMath/SpanNoise.cs.meta b/packages/cultmath/src/CultMath/SpanNoise.cs.meta new file mode 100644 index 00000000..d838deb2 --- /dev/null +++ b/packages/cultmath/src/CultMath/SpanNoise.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fe0da4c343e24c3781b076943f08ddbc diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index bfb0f373..4df9cd21 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -128,4 +128,104 @@ public static partial class math var total = held + (a + b * (0.5f * (enter + leave))) * (leave - enter) + kept; return direction * total; } + + private static float span_frobenius(float3x3 m) => + sqrt(m._m00 * m._m00 + m._m01 * m._m01 + m._m02 * m._m02 + m._m10 * m._m10 + m._m11 * m._m11 + m._m12 * m._m12 + + m._m20 * m._m20 + m._m21 * m._m21 + m._m22 * m._m22); + + // Plain fbm of snoise over a span: the octave stage of a warp chain, its value and gradient at the + // span's midpoint and the bounds that hold over the whole span. See span_stage for the span, the + // transform and the bound rules. At frequency 1, stretch 1 and zero shift, offset and octaveStep the + // field equals fbm_grad(q, octaves, lacunarity, gain) bit for bit. + public static SpanNoise span_fbm(float3 q, float3 velocity, float speed, float acceleration, float halfLength, + float frequency, float stretch, float3 axis, float3 shift, float3 offset, float3 octaveStep, int octaves, float lacunarity, float gain) => + span_stage(false, q, velocity, speed, acceleration, halfLength, frequency, stretch, axis, shift, offset, octaveStep, octaves, lacunarity, gain); + + // Ridged fbm over a span: octave o contributes gain^o (1 - |n_o|), ridged_grad's convention (the Span + // Lab's 1 - 2|n| is twice this minus the gain sum, the consumer's to form). fold bounds how far the + // sum can leave the branch it is on at the midpoint. At the identity transform the field equals + // ridged_grad bit for bit. + public static SpanNoise span_ridged(float3 q, float3 velocity, float speed, float acceleration, float halfLength, + float frequency, float stretch, float3 axis, float3 shift, float3 offset, float3 octaveStep, int octaves, float lacunarity, float gain) => + span_stage(true, q, velocity, speed, acceleration, halfLength, frequency, stretch, axis, shift, offset, octaveStep, octaves, lacunarity, gain); + + // The span is the path q(t), |t| <= halfLength, through q at t = 0 with the given velocity there, + // |q'| <= speed and |q''| <= acceleration along it (speed at least |velocity|; all three and halfLength + // non-negative). Octave o samples snoise_grad at x_o(q) = f_o (M q + shift) + offset + o octaveStep with + // f_o = lacunarity^o, amplitude gain^o and M = frequency (I + (1/stretch - 1) a a^T), a = normalize(axis); + // a zero axis, or stretch 1, means no stretch. stretch must be positive and finite, and nothing guards + // it, as nothing guards lacunarity. octaves clamps to [0, 16] as fbm_grad's does. The field is the + // octave sum in q: gradient f_o M grad n_o (M is symmetric), the same octave loop as fbm_grad and + // ridged_grad. + // + // Bounds, per octave, from SNOISE_LIPSCHITZ (L), SNOISE_HESSIAN (H) and SNOISE_THIRD (T) only. With + // |M| = |frequency| max(1, 1/stretch) (1 without stretch) and nu = f_o |M| speed, the noise-space speed: + // * |x(t) - x_mid| <= nu halfLength, so the Hessian of n at x(t) has operator norm at most + // Hl = min(H, ||hessian(x_mid)||_F + T nu halfLength) (the Hessian is T-Lipschitz in operator norm, + // and the Frobenius norm bounds the operator norm), and |grad n(x(t))| at most + // Gl = min(L, |grad n_mid| + Hl nu halfLength). + // * curvature = sum gain^o f_o^2 |M|^2 Hl: the Hessian in q of n(x_o(q)) is f_o^2 M H M. + // * fold, ridged only. Over the span the octave's value departs from its midpoint branch + // gain^o (1 - s n) (s the sign of n at the midpoint) only where n has changed sign, and there + // |n(t)| <= V - |n_mid|, V bounding |n(t) - n_mid|. Writing n(t) - n_mid as the integral of + // grad n . x', x' = w + rho with w = f_o M velocity and |rho| <= f_o |M| acceleration |t|, and + // grad n = grad n_mid + delta with |delta| <= Hl nu |t|: + // V = |grad n_mid . w| halfLength + (Hl nu |w| + Gl f_o |M| acceleration) halfLength^2 / 2, + // and the octave's fold is 2 gain^o max(V - |n_mid|, 0), zero when it cannot reach zero. + // Rounding is not widened: the Frobenius norm exceeds the operator norm except on a rank-one + // Hessian, and the pins carry a 10% margin. + private static SpanNoise span_stage(bool ridged, float3 q, float3 velocity, float speed, float acceleration, float halfLength, + float frequency, float stretch, float3 axis, float3 shift, float3 offset, float3 octaveStep, int octaves, float lacunarity, float gain) + { + octaves = clamp(octaves, 0, 16); + var stretched = dot(axis, axis) > 0.0f && stretch != 1.0f; + var direction = stretched ? normalize(axis) : new float3(0.0f, 0.0f, 0.0f); + var squeeze = stretched ? 1.0f / stretch - 1.0f : 0.0f; + var norm = abs(frequency) * (stretched ? max(1.0f, 1.0f / stretch) : 1.0f); + var mq = stretched ? frequency * (q + squeeze * dot(direction, q) * direction) : frequency * q; + var mv = stretched ? frequency * (velocity + squeeze * dot(direction, velocity) * direction) : frequency * velocity; + var amplitude = 1.0f; + var scale = 1.0f; + var value = 0.0f; + var gradient = new float3(0.0f, 0.0f, 0.0f); + var curvature = 0.0f; + var fold = 0.0f; + for (var i = 0; i < octaves; i++) + { + var x = scale * (mq + shift) + (offset + (float)i * octaveStep); + var n = snoise_grad(x); + var g = new float3(n.x, n.y, n.z); + var mg = stretched ? frequency * (g + squeeze * dot(direction, g) * direction) : frequency * g; + if (ridged) + { + var s = (float)sign(n.w); + value += amplitude * (1.0f - abs(n.w)); + gradient += -amplitude * scale * s * mg; + } + else + { + value += amplitude * n.w; + gradient += amplitude * scale * mg; + } + + var weight = abs(amplitude); + var factor = abs(scale) * norm; + var nu = factor * speed; + var local = min(SNOISE_HESSIAN, span_frobenius(snoise_hessian(x)) + SNOISE_THIRD * nu * halfLength); + curvature += weight * factor * factor * local; + if (ridged) + { + var w = scale * mv; + var reach = local * nu * halfLength; + var gl = min(SNOISE_LIPSCHITZ, length(g) + reach); + var travel = abs(dot(g, w)) * halfLength + (local * nu * length(w) + gl * factor * acceleration) * (0.5f * halfLength * halfLength); + fold += 2.0f * weight * max(travel - abs(n.w), 0.0f); + } + + scale *= lacunarity; + amplitude *= gain; + } + + return new SpanNoise(new float4(gradient, value), curvature, fold); + } } diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs new file mode 100644 index 00000000..bebed1cd --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -0,0 +1,434 @@ +using Xunit; +using static CultMath.math; + +namespace CultMath.Tests; + +/// +/// span_fbm and span_ridged (math.Span.cs): the octave stages of a warp chain over a span. At the identity +/// transform they are fbm_grad and ridged_grad; their gradient is the derivative of their value through the +/// stretch and the per-octave scaling; curvature bounds the Hessian of the field in q and fold the departure +/// of a ridged sum from its midpoint branch, at every point of every span the tests draw, in the regime +/// where the local third-derivative term is what holds the bound and in the one where the global pin is. +/// Lines starting IV-REPORT carry the numbers the cut report quotes. +/// +public sealed class SpanStageTests +{ + private readonly ITestOutputHelper output; + + public SpanStageTests(ITestOutputHelper output) => this.output = output; + + private static float Uniform(System.Random random, float lo, float hi) => lo + (hi - lo) * random.NextSingle(); + + private static float3 Vector(System.Random random, float lo, float hi) => new(Uniform(random, lo, hi), Uniform(random, lo, hi), Uniform(random, lo, hi)); + + private static float3 UnitVector(System.Random random) + { + float3 v; + do + { + v = Vector(random, -1.0f, 1.0f); + } + while (dot(v, v) is > 1.0f or < 0.01f); + return normalize(v); + } + + // One draw of the stage parameters: every one of them off its identity value. + private readonly record struct Stage(float Frequency, float Stretch, float3 Axis, float3 Shift, float3 Offset, float3 Step, int Octaves, float Lacunarity, float Gain) + { + public static Stage Draw(System.Random random, int octaves) => new( + Uniform(random, 0.3f, 1.6f), + random.Next(2) == 0 ? Uniform(random, 0.2f, 0.8f) : Uniform(random, 1.25f, 5.0f), + UnitVector(random) * Uniform(random, 0.5f, 3.0f), + Vector(random, -8.0f, 8.0f), + Vector(random, -8.0f, 8.0f), + Vector(random, -3.0f, 3.0f), + octaves, + Uniform(random, 1.5f, 2.3f), + Uniform(random, 0.35f, 0.7f)); + + public SpanNoise Fbm(float3 q, float3 velocity, float speed, float acceleration, float halfLength) => + span_fbm(q, velocity, speed, acceleration, halfLength, Frequency, Stretch, Axis, Shift, Offset, Step, Octaves, Lacunarity, Gain); + + public SpanNoise Ridged(float3 q, float3 velocity, float speed, float acceleration, float halfLength) => + span_ridged(q, velocity, speed, acceleration, halfLength, Frequency, Stretch, Axis, Shift, Offset, Step, Octaves, Lacunarity, Gain); + + // M = frequency (I + (1/stretch - 1) a a^T) in double, a the normalised axis (none for a zero axis). + public double[,] Matrix() + { + var m = new double[3, 3]; + var a = new double[] { Axis.x, Axis.y, Axis.z }; + var length = Math.Sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]); + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + m[i, j] = Frequency * ((i == j ? 1.0 : 0.0) + (length > 0.0 ? (1.0 / Stretch - 1.0) * a[i] * a[j] / (length * length) : 0.0)); + return m; + } + + // Octave o's noise-space point for the span point q. + public float3 Point(float3 q, int octave) + { + var m = Matrix(); + var scale = Math.Pow(Lacunarity, octave); + var x = new double[3]; + var qv = new double[] { q.x, q.y, q.z }; + var shift = new double[] { Shift.x, Shift.y, Shift.z }; + var offset = new double[] { Offset.x, Offset.y, Offset.z }; + var step = new double[] { Step.x, Step.y, Step.z }; + for (var i = 0; i < 3; i++) + { + var mq = 0.0; + for (var j = 0; j < 3; j++) + mq += m[i, j] * qv[j]; + x[i] = scale * (mq + shift[i]) + offset[i] + octave * step[i]; + } + + return new float3((float)x[0], (float)x[1], (float)x[2]); + } + + public double Amplitude(int octave) => Math.Pow(Gain, octave); + + public double Scale(int octave) => Math.Pow(Lacunarity, octave); + + // The spectral norm of M: frequency times the larger of 1 and 1/stretch. + public double Norm() => Math.Abs(Frequency) * Math.Max(1.0, 1.0 / Stretch); + } + + // A span through q: velocity v, constant acceleration c of norm `acceleration`, so q(t) = q + v t + c t^2 / 2; + // the speed bound is |v| plus what c can add over halfLength. + private readonly record struct Span(float3 Q, float3 Velocity, float3 Acceleration, float Speed, float AccelerationBound, float HalfLength) + { + public static Span Draw(System.Random random, float3 q, float halfLength, float acceleration) + { + var velocity = UnitVector(random) * Uniform(random, 0.2f, 3.0f); + return new Span(q, velocity, UnitVector(random) * acceleration, length(velocity) + acceleration * halfLength, acceleration, halfLength); + } + + public float3 At(float t) => Q + Velocity * t + Acceleration * (0.5f * t * t); + } + + // The operator norm of a symmetric 3x3 matrix (entries in row-major order): the larger |eigenvalue|. + private static double SymmetricNorm(double[] a) + { + var p1 = a[1] * a[1] + a[2] * a[2] + a[5] * a[5]; + var q = (a[0] + a[4] + a[8]) / 3.0; + if (p1 == 0.0) + return Math.Max(Math.Abs(a[0]), Math.Max(Math.Abs(a[4]), Math.Abs(a[8]))); + var p2 = (a[0] - q) * (a[0] - q) + (a[4] - q) * (a[4] - q) + (a[8] - q) * (a[8] - q) + 2.0 * p1; + var s = Math.Sqrt(p2 / 6.0); + var b = new[] { (a[0] - q) / s, a[1] / s, a[2] / s, a[3] / s, (a[4] - q) / s, a[5] / s, a[6] / s, a[7] / s, (a[8] - q) / s }; + var r = Math.Clamp((b[0] * (b[4] * b[8] - b[5] * b[7]) - b[1] * (b[3] * b[8] - b[5] * b[6]) + b[2] * (b[3] * b[7] - b[4] * b[6])) / 2.0, -1.0, 1.0); + var phi = Math.Acos(r) / 3.0; + return Math.Max(Math.Abs(q + 2.0 * s * Math.Cos(phi)), Math.Abs(q + 2.0 * s * Math.Cos(phi + 2.0 * Math.PI / 3.0))); + } + + // The Hessian in q of the stage field at span point q (signed per octave for a ridged sum), by the chain + // rule from snoise_hessian: sum over octaves of weight_o f_o^2 M H_o M. + private static double[] FieldHessian(Stage stage, float3 q, bool ridged) + { + var m = stage.Matrix(); + var total = new double[9]; + for (var o = 0; o < stage.Octaves; o++) + { + var x = stage.Point(q, o); + var h = snoise_hessian(x); + var weight = stage.Amplitude(o) * stage.Scale(o) * stage.Scale(o) * (ridged ? -Math.Sign(snoise(x)) : 1.0); + var hd = new double[3, 3]; + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + hd[i, j] = h[i][j]; + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + { + var sum = 0.0; + for (var k = 0; k < 3; k++) + for (var l = 0; l < 3; l++) + sum += m[i, k] * hd[k, l] * m[l, j]; + total[i * 3 + j] += weight * sum; + } + } + + return total; + } + + private static bool SameBits(float4 a, float4 b) => + BitConverter.SingleToInt32Bits(a.x) == BitConverter.SingleToInt32Bits(b.x) && BitConverter.SingleToInt32Bits(a.y) == BitConverter.SingleToInt32Bits(b.y) + && BitConverter.SingleToInt32Bits(a.z) == BitConverter.SingleToInt32Bits(b.z) && BitConverter.SingleToInt32Bits(a.w) == BitConverter.SingleToInt32Bits(b.w); + + private static IEnumerable Points(System.Random random) + { + for (var k = 0; k < 200; k++) + yield return Vector(random, -60.0f, 60.0f); + // The simplex tie set: x = y = z, and integer lattice points. + for (var k = 0; k < 30; k++) + { + var t = Uniform(random, -50.0f, 50.0f); + yield return new float3(t, t, t); + yield return new float3(random.Next(-40, 41), random.Next(-40, 41), random.Next(-40, 41)); + } + } + + /// + /// At frequency 1, stretch 1 (with and without an axis) and zero shift, offset and octaveStep, the field of + /// span_fbm and span_ridged is fbm_grad's and ridged_grad's, bit for bit, for 0 to 20 octaves (the clamp + /// included), every lacunarity and gain drawn, over random and simplex-tie points: any change to the octave + /// loop's order, scaling or accumulation fails. + /// + [Fact] + public void SpanStagesAtIdentityAreFbmAndRidged() + { + var random = new System.Random(0x57A6E1); + var none = new float3(0.0f, 0.0f, 0.0f); + foreach (var p in Points(random)) + { + var octaves = random.Next(0, 21); + var lacunarity = Uniform(random, 1.4f, 2.6f); + var gain = Uniform(random, 0.3f, 0.8f); + var axis = random.Next(2) == 0 ? none : UnitVector(random) * 2.0f; + var fbm = span_fbm(p, UnitVector(random), 1.0f, 0.5f, 0.25f, 1.0f, 1.0f, axis, none, none, none, octaves, lacunarity, gain); + var ridged = span_ridged(p, UnitVector(random), 1.0f, 0.5f, 0.25f, 1.0f, 1.0f, axis, none, none, none, octaves, lacunarity, gain); + Assert.True(SameBits(fbm.field, fbm_grad(p, octaves, lacunarity, gain)), $"span_fbm at {p}, {octaves} octaves: {fbm.field} is not fbm_grad {fbm_grad(p, octaves, lacunarity, gain)}"); + Assert.True(SameBits(ridged.field, ridged_grad(p, octaves, lacunarity, gain)), $"span_ridged at {p}, {octaves} octaves: {ridged.field} is not ridged_grad {ridged_grad(p, octaves, lacunarity, gain)}"); + Assert.Equal(0.0f, fbm.fold); + } + } + + /// + /// The gradient is the derivative of the value through the whole transform: with every parameter off its + /// identity (frequency, stretch both ways, axis, shift, offset, octaveStep, lacunarity, gain), field.xyz + /// equals the fourth-order central difference of field.w along each axis, within a tolerance built from the + /// third-derivative pin (truncation) and float32 rounding of the coordinates. A dropped M or lacunarity + /// factor, or M applied to the wrong side, moves it by the gradient itself. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpanStageGradientMatchesDifferences(bool ridged) + { + var random = new System.Random(ridged ? 0x6A1D : 0x6A1C); + var worst = 0.0; + var checkedCases = 0; + for (var k = 0; k < 400; k++) + { + var stage = Stage.Draw(random, ridged ? random.Next(1, 3) : random.Next(1, 4)); + var q = Vector(random, -3.0f, 3.0f); + // The fold's crease at n = 0 is not differentiable: keep every octave's stencil clear of it. + if (ridged && Enumerable.Range(0, stage.Octaves).Any(o => abs(snoise(stage.Point(q, o))) < 0.3f)) + continue; + 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); + // 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); + var scale = 0.0; + for (var o = 0; o < stage.Octaves; o++) + scale += stage.Amplitude(o) * stage.Scale(o) * stage.Norm(); + for (var axis = 0; axis < 3; axis++) + { + var e = new float3(axis == 0 ? h : 0.0f, axis == 1 ? h : 0.0f, axis == 2 ? h : 0.0f); + double F(float3 p) => At(p).field.w; + var step = (double)(q + e)[axis] - (q - e)[axis]; + var d1 = (F(q + e) - F(q - e)) / step; + var d2 = (F(q + e * 2.0f) - F(q - e * 2.0f)) / ((double)(q + e * 2.0f)[axis] - (q - e * 2.0f)[axis]); + var estimate = (4.0 * d1 - d2) / 3.0; + var tolerance = 4.0e-2 * scale; + var error = Math.Abs(estimate - centre.field[axis]); + worst = Math.Max(worst, error / scale); + Assert.True(error <= tolerance, $"stage {stage}: d/d{"xyz"[axis]} of the value is {estimate:R} by differences and {centre.field[axis]:R} by span_{(ridged ? "ridged" : "fbm")} (tolerance {tolerance:R})"); + checkedCases++; + } + } + + Assert.True(checkedCases >= 120, $"only {checkedCases} derivatives were compared"); + output.WriteLine($"IV-REPORT span gradient ({(ridged ? "ridged" : "fbm")}): {checkedCases} derivatives, worst error {worst:R} of the summed octave gain"); + } + + /// + /// stretch 1, or a zero axis, is the isotropic stage bit for bit whatever the other of the two is; a non-unit + /// axis is its normalisation (the tested axes normalise exactly); a stretch that does apply changes the + /// result. + /// + [Fact] + public void SpanStageStretchAndAxis() + { + var random = new System.Random(0x57E7C4); + for (var k = 0; k < 60; k++) + { + var stage = Stage.Draw(random, 3); + var q = Vector(random, -5.0f, 5.0f); + var span = Span.Draw(random, q, 0.2f, 0.3f); + var plain = stage with { Stretch = 1.0f }; + var noAxis = stage with { Axis = new float3(0.0f, 0.0f, 0.0f), Stretch = 3.0f }; + var reference = plain.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + var isotropic = (plain with { Axis = new float3(0.0f, 0.0f, 0.0f) }).Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + var viaNoAxis = noAxis.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + Assert.True(SameBits(reference.field, isotropic.field) && reference.curvature == isotropic.curvature && reference.fold == isotropic.fold, "stretch 1 with an axis differs from no axis"); + Assert.True(SameBits(isotropic.field, viaNoAxis.field) && isotropic.curvature == viaNoAxis.curvature && isotropic.fold == viaNoAxis.fold, "a zero axis with stretch 3 differs from the isotropic stage"); + + var unit = stage with { Axis = new float3(0.0f, 1.0f, 0.0f) }; + var scaled = stage with { Axis = new float3(0.0f, 2.0f, 0.0f) }; + var a = unit.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + var b = scaled.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + Assert.True(SameBits(a.field, b.field) && a.curvature == b.curvature, "a non-unit axis differs from its normalisation"); + var stretched = unit.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + Assert.False(SameBits(stretched.field, (unit with { Stretch = 1.0f }).Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength).field), "a stretch of " + unit.Stretch + " did not change the field"); + } + } + + // The stage parameters and per-octave bookkeeping the curvature checks share. + private static void CheckCurvature(Stage stage, bool ridged, Span span, SpanNoise result, int samples, ref double tightest, ref int checkedPoints) + { + for (var s = 0; s < samples; s++) + { + var t = span.HalfLength * (2.0f * s / (samples - 1) - 1.0f); + var norm = SymmetricNorm(FieldHessian(stage, span.At(t), ridged)); + Assert.True(norm <= result.curvature * (1.0 + 1.0e-3), $"stage {stage}, span {span}: the Hessian norm at t = {t:R} is {norm:R}, above curvature {result.curvature:R}"); + tightest = Math.Min(tightest, result.curvature > 0.0 ? 1.0 - norm / result.curvature : 1.0); + checkedPoints++; + } + } + + /// + /// span-bounds-enclose, curvature: over seeded spans the operator norm of the field's q-Hessian (chain rule + /// from snoise_hessian, M and the octave scales) at 64 points per span never exceeds curvature, in the + /// local regime (short spans, where the third-derivative term is the bound) and the global one (long spans, + /// where SNOISE_HESSIAN is); at halfLength 0 curvature is the local tier, the Frobenius norm of the + /// midpoint Hessians scaled by amplitude, f_o^2 and |M|^2, to within float rounding; and a span through + /// MeasureHessian's witness stays enclosed in 200 directions, which the Frobenius norm at the midpoint alone + /// does not guarantee. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpanCurvatureEnclosesTheHessianAlongTheSpan(bool ridged) + { + var random = new System.Random(ridged ? 0xC0DE2 : 0xC0DE1); + var tightest = 1.0; + var checkedPoints = 0; + foreach (var halfLength in new[] { 0.002f, 0.02f, 0.2f, 1.0f, 8.0f }) + for (var k = 0; k < 24; k++) + { + var stage = Stage.Draw(random, random.Next(1, 4)); + var span = Span.Draw(random, Vector(random, -30.0f, 30.0f), halfLength, Uniform(random, 0.0f, 2.0f)); + var result = ridged ? stage.Ridged(span.Q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength) : stage.Fbm(span.Q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + CheckCurvature(stage, ridged, span, result, 64, ref tightest, ref checkedPoints); + } + + // halfLength 0: the local tier exactly. + for (var k = 0; k < 40; k++) + { + var stage = Stage.Draw(random, random.Next(1, 4)); + var q = Vector(random, -30.0f, 30.0f); + var span = Span.Draw(random, q, 0.0f, 1.0f); + var result = ridged ? stage.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, 0.0f) : stage.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, 0.0f); + var expected = 0.0; + for (var o = 0; o < stage.Octaves; o++) + { + var h = snoise_hessian(stage.Point(q, o)); + var frobenius = Math.Sqrt(Enumerable.Range(0, 9).Sum(i => (double)h[i / 3][i % 3] * h[i / 3][i % 3])); + expected += stage.Amplitude(o) * stage.Scale(o) * stage.Scale(o) * stage.Norm() * stage.Norm() * Math.Min(SNOISE_HESSIAN, frobenius); + } + + Assert.InRange(result.curvature, expected * (1.0 - 1.0e-3), expected * (1.0 + 1.0e-3)); + } + + // Through the Hessian witness: one octave at the identity transform, the span's midpoint 0.1 to 0.5 away, + // moving toward the witness at speed 1, so the witness lies on the span. + var witness = new float3(-13.7920685f, -9.672134f, 61.563934f); + var identity = new Stage(1.0f, 1.0f, new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), 1, 2.0f, 0.5f); + var through = 0.0; + for (var k = 0; k < 200; k++) + { + var u = UnitVector(random); + var distance = Uniform(random, 0.1f, 0.5f); + var span = new Span(witness + u * distance, -u, new float3(0.0f, 0.0f, 0.0f), 1.0f, 0.0f, 0.6f); + var result = ridged ? identity.Ridged(span.Q, span.Velocity, 1.0f, 0.0f, 0.6f) : identity.Fbm(span.Q, span.Velocity, 1.0f, 0.0f, 0.6f); + CheckCurvature(identity, ridged, span, result, 64, ref tightest, ref checkedPoints); + through = Math.Max(through, SymmetricNorm(FieldHessian(identity, witness, ridged))); + } + + Assert.True(through > 40.0, $"the witness Hessian norm is {through:R}: the through-the-witness spans do not reach the curvature"); + output.WriteLine($"IV-REPORT span curvature ({(ridged ? "ridged" : "fbm")}): {checkedPoints} points enclosed, least relative slack {tightest:R}, witness norm {through:R}"); + } + + /// + /// span-bounds-enclose, fold: for span_ridged, the value's departure from the midpoint branch (each + /// octave's gain^o (s n - |n|), s the sign of n at the midpoint, summed) never exceeds fold at 64 points of + /// every span; fold is positive on spans that do cross zero (and the departure there is not trivially + /// zero), exactly 0 when no octave can reach zero and for span_fbm, and 0 at halfLength 0. + /// + [Fact] + public void SpanFoldEnclosesTheBranchDeviation() + { + var random = new System.Random(0xF01D); + var crossing = 0; + var largest = 0.0; + var tightest = 1.0; + for (var k = 0; k < 400; k++) + { + var stage = Stage.Draw(random, random.Next(1, 4)); + var halfLength = new[] { 0.02f, 0.1f, 0.5f, 2.0f }[random.Next(4)]; + // Draw midpoints with small noise values so spans cross zero, as well as arbitrary ones. + var q = Vector(random, -30.0f, 30.0f); + if (k % 2 == 0) + { + var best = q; + var smallest = float.MaxValue; + for (var trial = 0; trial < 40; trial++) + { + var candidate = Vector(random, -30.0f, 30.0f); + var value = abs(snoise(stage.Point(candidate, 0))); + if (value < smallest) + (smallest, best) = (value, candidate); + } + + q = best; + } + + var span = Span.Draw(random, q, halfLength, Uniform(random, 0.0f, 2.0f)); + var fold = stage.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, halfLength).fold; + Assert.Equal(0.0f, stage.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, halfLength).fold); + Assert.Equal(0.0f, stage.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, 0.0f).fold); + var signs = Enumerable.Range(0, stage.Octaves).Select(o => Math.Sign(snoise(stage.Point(q, o)))).ToArray(); + var peak = 0.0; + for (var s = 0; s < 64; s++) + { + var t = halfLength * (2.0f * s / 63 - 1.0f); + var point = span.At(t); + var departure = 0.0; + for (var o = 0; o < stage.Octaves; o++) + { + var n = (double)snoise(stage.Point(point, o)); + departure += stage.Amplitude(o) * (signs[o] * n - Math.Abs(n)); + } + + peak = Math.Max(peak, Math.Abs(departure)); + Assert.True(Math.Abs(departure) <= fold * (1.0 + 1.0e-3), $"stage {stage}, span {span}: the branch departure at t = {t:R} is {departure:R}, above fold {fold:R}"); + } + + if (fold > 0.0) + crossing++; + largest = Math.Max(largest, peak); + if (fold > 0.0 && peak > 0.0) + tightest = Math.Min(tightest, peak / fold); + } + + Assert.True(crossing >= 100, $"only {crossing} of 400 spans had a positive fold"); + Assert.True(largest > 0.1, $"the largest branch departure drawn is {largest:R}: the check has no teeth"); + output.WriteLine($"IV-REPORT span fold: {crossing} spans with fold > 0, largest departure {largest:R}, tightest departure / fold {tightest:R}"); + + // An octave that cannot reach zero: a midpoint where |n| is large and a short span give fold exactly 0. + var calm = new Stage(1.0f, 1.0f, new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), 1, 2.0f, 0.5f); + var found = 0; + for (var k = 0; k < 400 && found < 20; k++) + { + var q = Vector(random, -30.0f, 30.0f); + if (abs(snoise(q)) < 0.6f) + continue; + found++; + Assert.Equal(0.0f, calm.Ridged(q, new float3(1.0f, 0.0f, 0.0f), 1.0f, 0.0f, 0.01f).fold); + } + + Assert.True(found >= 5, $"only {found} calm midpoints found"); + } +} From 427d81feda30ec07b661f7d390462efc1151cc4c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:48:44 +0200 Subject: [PATCH 07/24] GameCult.Caching: SpanNoise MessagePack and JSON shapes MessagePack [field, curvature, fold] beside CultPhasor in CultMathResolver, JSON {field, curvature, fold} in CultMathJson; round-trip, hex and JSON golden cases beside CultPhasor's. Co-Authored-By: Claude Sonnet 5.5 --- .../CultMathJson.cs | 11 +++++++++-- .../CultMathResolver.cs | 18 +++++++++++++++++- .../CultMathJsonTests.cs | 3 +++ .../CultMathSerializationTests.cs | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/GameCult.Caching.MessagePack/CultMathJson.cs b/src/GameCult.Caching.MessagePack/CultMathJson.cs index 0e65bf22..29c312da 100644 --- a/src/GameCult.Caching.MessagePack/CultMathJson.cs +++ b/src/GameCult.Caching.MessagePack/CultMathJson.cs @@ -14,7 +14,7 @@ namespace GameCult.Caching.MessagePack; /// The canonical System.Text.Json shape of every public CultMath value type, the JSON twin of /// . Vectors, quaternions and colors are objects keyed by their /// field names ({"x":1,"y":2}), matrices are arrays of row objects, rect is -/// {"min":..,"max":..}, CultCellular is {"nearest":..,"edge":..,"id":..}, and CultPhasor is {"cos":..,"sin":..}. +/// {"min":..,"max":..}, CultCellular is {"nearest":..,"edge":..,"id":..}, CultPhasor is {"cos":..,"sin":..}, and SpanNoise is {"field":..,"curvature":..,"fold":..}. /// Property names are fixed: PropertyNamingPolicy does not rename them, and /// PropertyNameCaseInsensitive makes reading case-insensitive. /// @@ -481,7 +481,7 @@ public static class CultMathJson Add(UIntScalar.Instance, new Fields(("state", Val())), a => new Random { state = a[0] }, (v, a) => a[0] = v.state), - // Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}. + // Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}, SpanNoise is {field, curvature, fold}. Add(f, new Rows(2, Xy()), a => new float2x2(a[0], a[1], a[2], a[3]), (v, a) => { a[0] = v[0].x; a[1] = v[0].y; a[2] = v[1].x; a[3] = v[1].y; }), @@ -514,6 +514,13 @@ public static class CultMathJson a[0] = v.cos.x; a[1] = v.cos.y; a[2] = v.cos.z; a[3] = v.cos.w; a[4] = v.sin.x; a[5] = v.sin.y; a[6] = v.sin.z; a[7] = v.sin.w; }), + Add(f, new Fields(("field", Xyzw()), ("curvature", Val()), ("fold", Val())), + a => new SpanNoise(new float4(a[0], a[1], a[2], a[3]), a[4], a[5]), + (v, a) => + { + a[0] = v.field.x; a[1] = v.field.y; a[2] = v.field.z; a[3] = v.field.w; + a[4] = v.curvature; a[5] = v.fold; + }), }; var map = new Dictionary(); diff --git a/src/GameCult.Caching.MessagePack/CultMathResolver.cs b/src/GameCult.Caching.MessagePack/CultMathResolver.cs index 8383a464..529ed2e7 100644 --- a/src/GameCult.Caching.MessagePack/CultMathResolver.cs +++ b/src/GameCult.Caching.MessagePack/CultMathResolver.cs @@ -180,7 +180,7 @@ public sealed class CultMathResolver : IFormatterResolver return value; }), - // Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin]. + // Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin], SpanNoise is [field, curvature, fold]. Shape((ref MessagePackWriter w, float2x2 v) => { w.WriteArrayHeader(2); PutFloat2(ref w, v[0]); PutFloat2(ref w, v[1]); }, (ref MessagePackReader r) => { @@ -236,6 +236,22 @@ public sealed class CultMathResolver : IFormatterResolver } return value; }), + Shape((ref MessagePackWriter w, SpanNoise v) => { w.WriteArrayHeader(3); PutFloat4(ref w, v.field); w.Write(v.curvature); w.Write(v.fold); }, + (ref MessagePackReader r) => + { + var value = default(SpanNoise); + for (int i = 0, n = r.ReadArrayHeader(); i < n; i++) + { + switch (i) + { + case 0: value.field = GetFloat4(ref r); break; + case 1: value.curvature = r.ReadSingle(); break; + case 2: value.fold = r.ReadSingle(); break; + default: r.Skip(); break; + } + } + return value; + }), }; var map = new Dictionary(); diff --git a/tests/GameCult.Caching.Tests/CultMathJsonTests.cs b/tests/GameCult.Caching.Tests/CultMathJsonTests.cs index 90d739be..26474907 100644 --- a/tests/GameCult.Caching.Tests/CultMathJsonTests.cs +++ b/tests/GameCult.Caching.Tests/CultMathJsonTests.cs @@ -45,6 +45,8 @@ namespace GameCult.Caching.Tests """{"nearest":{"x":1,"y":2,"z":3,"w":4},"edge":{"x":5,"y":6,"z":7,"w":8},"id":0.25}"""); Json(new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f))).Should().Be( """{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}}"""); + Json(new SpanNoise(new float4(1f, 2f, 3f, 4f), 0.25f, 0.5f)).Should().Be( + """{"field":{"x":1,"y":2,"z":3,"w":4},"curvature":0.25,"fold":0.5}"""); } [Test] @@ -69,6 +71,7 @@ namespace GameCult.Caching.Tests RoundTrip(new rect(-1f, -2f, 5f, 7f)); RoundTrip(new CultCellular(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8), 0.25f)); RoundTrip(new CultPhasor(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8))); + RoundTrip(new SpanNoise(new float4(1, 2, 3, 4), 0.25f, 0.5f)); } [Test] diff --git a/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs b/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs index 98f126af..7836663f 100644 --- a/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs +++ b/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs @@ -102,6 +102,7 @@ namespace GameCult.Caching.Tests RoundTrip(new rect(-1f, -2f, 5f, 7f)); RoundTrip(new CultCellular(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8), 0.25f)); RoundTrip(new CultPhasor(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8))); + RoundTrip(new SpanNoise(new float4(1, 2, 3, 4), 0.25f, 0.5f)); } [Test] @@ -137,6 +138,11 @@ namespace GameCult.Caching.Tests "92" + "94CA3F800000CA40000000CA40400000CA40800000" + "94CA40A00000CA40C00000CA40E00000CA41000000"); + Hex(new SpanNoise(new float4(1f, 2f, 3f, 4f), 0.25f, 0.5f)).Should().Be( + "93" + + "94CA3F800000CA40000000CA40400000CA40800000" + + "CA3E800000" + + "CA3F000000"); } [Test] From 6a3132f1399e91e57dbbf375926336c19cfe63a9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:53:37 +0200 Subject: [PATCH 08/24] 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); From f722f7c177edd5551bc79dda3c5c5f676ad6f3e7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 21:03:52 +0200 Subject: [PATCH 09/24] cultmath: span fold test for the acceleration term alone SpanFoldCoversAccelerationAlone draws spans with zero midpoint velocity and an acceleration along the gradient that carries n across zero, so the fold's acceleration term and its gradient bound are the only thing holding the branch departure under fold; two hand mutants survived without it. Co-Authored-By: Claude Sonnet 5.5 --- .../tests/CultMath.Tests/SpanStageTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index 7ec26cbc..bc6c6eeb 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -453,4 +453,51 @@ public sealed class SpanStageTests Assert.True(found >= 5, $"only {found} calm midpoints found"); } + + /// + /// The fold's acceleration term, alone: the midpoint velocity is zero and the acceleration points along + /// the gradient, against the sign of n, so n reaches zero and crosses it on the way to the span's ends + /// by acceleration only. Without the acceleration term (or with the gradient bound under the gradient + /// the path reaches) the departure from the midpoint branch exceeds fold. + /// + [Fact] + public void SpanFoldCoversAccelerationAlone() + { + var random = new System.Random(0xACCE1); + var identity = new Stage(1.0f, 1.0f, new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), 1, 2.0f, 0.5f); + var still = new float3(0.0f, 0.0f, 0.0f); + var cases = 0; + var crossings = 0; + var tightest = double.MaxValue; + for (var trial = 0; trial < 200_000 && cases < 150; trial++) + { + var q = Vector(random, -60.0f, 60.0f); + var n = snoise_grad(q); + var gradient = new float3(n.x, n.y, n.z); + var halfLength = Uniform(random, 0.05f, 0.15f); + var acceleration = Uniform(random, 4.0f, 12.0f); + // n at the midpoint close enough to zero that the acceleration alone carries it across. + if (length(gradient) < 1.5f || abs(n.w) > 0.4f * length(gradient) * acceleration * halfLength * halfLength * 0.5f) + continue; + var c = -(float)Math.Sign(n.w) * acceleration * normalize(gradient); + var span = new Span(q, still, c, acceleration * halfLength, acceleration, halfLength); + var fold = identity.Ridged(q, still, span.Speed, acceleration, halfLength).fold; + var departure = 0.0; + for (var s = 0; s < 64; s++) + { + var value = (double)snoise(span.At(halfLength * (2.0f * s / 63 - 1.0f))); + departure = Math.Max(departure, Math.Abs(Math.Sign(n.w) * value - Math.Abs(value))); + } + + if (departure > 0.0) + crossings++; + Assert.True(departure <= fold * (1.0 + 1.0e-3), $"q {q}, acceleration {acceleration:R}, half-length {halfLength:R}: the branch departure is {departure:R}, above fold {fold:R}"); + if (departure > 0.0) + tightest = Math.Min(tightest, departure / fold); + cases++; + } + + Assert.True(crossings >= 100, $"only {crossings} of {cases} paths crossed zero by acceleration alone"); + output.WriteLine($"IV-REPORT span fold, acceleration only: {crossings} of {cases} paths cross zero, tightest departure / fold {tightest:R}"); + } } From 0a1b49a23163456b6c8c1becd0754b4eb72af9d4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 20:49:30 +0200 Subject: [PATCH 10/24] cultmath: span stages take SpanPath and SpanStage and widen curvature and fold by float rounding allowances span_fbm/span_ridged/span_stage lose the 14-argument signatures for SpanPath, SpanStage, octaves, lacunarity, gain. Per octave S_o and e_n, e_g, e_h (af_snoise's derivation plus the Hessian's 2^-10) widen the local Hessian tier, the gradient bound and the fold travel. Tests compare against the exact double kernel in coordinate bands, add the pinned rank-one span, widen the stage parameter ranges, and state the lab-form mapping. Not built or run yet. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/README.md | 5 +- packages/cultmath/docs/design.md | 68 ++- packages/cultmath/src/CultMath/SpanPath.cs | 26 ++ .../cultmath/src/CultMath/SpanPath.cs.meta | 2 + packages/cultmath/src/CultMath/SpanStage.cs | 33 ++ .../cultmath/src/CultMath/SpanStage.cs.meta | 2 + packages/cultmath/src/CultMath/math.Span.cs | 108 +++-- .../tests/CultMath.Tests/NoiseBoundTests.cs | 21 +- .../tests/CultMath.Tests/SpanStageTests.cs | 439 ++++++++++++------ 9 files changed, 490 insertions(+), 214 deletions(-) create mode 100644 packages/cultmath/src/CultMath/SpanPath.cs create mode 100644 packages/cultmath/src/CultMath/SpanPath.cs.meta create mode 100644 packages/cultmath/src/CultMath/SpanStage.cs create mode 100644 packages/cultmath/src/CultMath/SpanStage.cs.meta diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md index 4f2018a3..61351da3 100644 --- a/packages/cultmath/README.md +++ b/packages/cultmath/README.md @@ -82,8 +82,9 @@ shader semantics. the analytic Hessian of `snoise(float3)` on the same lattice, hash and tie order as `snoise_grad` (a symmetric `float3x3`; GLSL `mat3`), and `integral_clamp_linear(a, b, lo, hi, t0, t1)`, the closed-form integral over `[t0, t1]` of `clamp(a + b t, - lo, hi)`; and, in C# for now (shaders follow), the octave stages `span_fbm` and - `span_ridged`, which return a `SpanNoise` (`field` as `float4(gradient, value)`, + lo, hi)`; and, in C# for now (shaders follow), the octave stages `span_fbm(path, stage, + octaves, lacunarity, gain)` and `span_ridged`, over a `SpanPath` and a `SpanStage` (`SpanStage.Identity` + is the plain fbm), which return a `SpanNoise` (`field` as `float4(gradient, value)`, `curvature`, `fold`) for a stretched, offset, shifted octave sum over a span. Three measured bounds describe `snoise`: `SNOISE_LIPSCHITZ` (gradient length, `NoiseBoundTests.MeasureLipschitz`), `SNOISE_HESSIAN` (Hessian spectral norm, `MeasureHessian`) and `SNOISE_THIRD` (third-derivative operator norm, diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index 0d7cd738..b7edc78b 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -809,34 +809,62 @@ configuration bound. A change to the kernels must re-run the measurements. ### Span stages `span_fbm` and `span_ridged` (`math.Span.cs`, C# only so far) are the octave stages -of a warp chain, each returning one `SpanNoise { float4 field; float curvature; float -fold; }`. The span is the path `q(t)`, `|t| <= halfLength`, through `q` with the given -`velocity` at its midpoint, `|q'| <= speed` and `|q''| <= acceleration` along it. -Octave `o` samples `snoise_grad` at `x_o(q) = f_o (M q + shift) + offset + o -octaveStep`, with `f_o = lacunarity^o`, amplitude `gain^o`, and `M = frequency (I + -(1/stretch - 1) a a^T)`, `a = normalize(axis)`; a zero axis or `stretch` 1 means no -stretch, and `stretch` must be positive and finite (unguarded, like `lacunarity`). -`field` is the octave sum and its gradient in `q` (`f_o M grad n_o`); at frequency 1, -stretch 1 and zero shift, offset and octaveStep it equals `fbm_grad` and -`ridged_grad` bit for bit. `span_ridged` sums `gain^o (1 - |n_o|)`, `ridged_grad`'s -convention; the Span Lab's `1 - 2|n|` is twice this minus the gain sum, which the -consumer forms. +of a warp chain, `span_fbm(path, stage, octaves, lacunarity, gain)`, each returning +one `SpanNoise { float4 field; float curvature; float fold; }`. The inputs are two +structs with named public fields and no positional constructor, so the three floats +of the span and the six fields of the transform cannot be swapped silently. +`SpanPath { midpoint, velocity, speed, acceleration, halfLength }` is the path +`q(t)`, `|t| <= halfLength`, through `midpoint` with `velocity` at `t = 0`, +`|q'| <= speed` and `|q''| <= acceleration` along it. `SpanStage { frequency, +stretch, axis, shift, offset, octaveStep }` makes octave `o` sample `snoise_grad` +at `x_o(q) = f_o (M q + shift) + offset + o octaveStep`, with `f_o = +lacunarity^o`, amplitude `gain^o`, and `M = frequency (I + (1/stretch - 1) a a^T)`, +`a = normalize(axis)`; a zero axis or `stretch` 1 means no stretch, and `stretch` +must be positive and finite (unguarded, like `lacunarity`). `default(SpanStage)` +has frequency 0; `SpanStage.Identity` is the stage whose field equals `fbm_grad` +and `ridged_grad` bit for bit. `octaves`, `lacunarity` and `gain` stay positional: +they are `fbm_grad`'s own trailing triple, and `octaves` is the one int among the +scalars. `field` is the octave sum and its gradient in `q` (`f_o M grad n_o`). +`span_ridged` sums `gain^o (1 - |n_o|)`, `ridged_grad`'s convention. The Span +Lab's stage is `1 - 2|n|` per octave: with `G` the sum of `gain^o` over the +octaves (signed, so the alternating sum for a negative gain), its value is `2 value +- G`, and its gradient, curvature and fold are twice `span_ridged`'s. Bounds come from the three pins only. With `|M| = |frequency| max(1, 1/stretch)` and `nu = f_o |M| speed` the noise-space speed, the Hessian of the noise on the span has -operator norm at most `Hl = min(SNOISE_HESSIAN, ||snoise_hessian(x_mid)||_F + +operator norm at most `Hl = min(SNOISE_HESSIAN, ||snoise_hessian(x_mid)||_F + e_h + SNOISE_THIRD nu halfLength)`, so `curvature = sum gain^o f_o^2 |M|^2 Hl` bounds the operator norm of the field's Hessian in `q` at every point of the span (the pin takes over once the third-derivative term exceeds it, and at `halfLength` 0 the local tier alone remains). For `span_ridged`, an octave leaves the branch it is on at the midpoint (`1 - s n`, `s` the midpoint's sign of `n`) only where `n` changes sign, and -there `|n| <= V - |n_mid|`, with `V = |grad n_mid . w| halfLength + (Hl nu |w| + Gl f_o -|M| acceleration) halfLength^2 / 2`, `w = f_o M velocity` and `Gl = min(SNOISE_LIPSCHITZ, -|grad n_mid| + Hl nu halfLength)`; `fold = sum 2 gain^o max(V - |n_mid|, 0)`, exactly 0 -for `span_fbm` and for an octave that cannot reach zero. Rounding is not widened: the -Frobenius norm exceeds the operator norm except on a rank-one Hessian, and the pins -carry a 10% margin. `SpanStageTests` encloses the Hessian norm and the branch -departure at 64 points of seeded spans, in the local and the global regime. +there `|n| <= V - |n_mid|`, with `V = (|grad n_mid . w| + e_g |w|) halfLength + (Hl nu |w| ++ Gl f_o |M| acceleration) halfLength^2 / 2 + 2 e_n`, `w = f_o M velocity` and `Gl = +min(SNOISE_LIPSCHITZ, |grad n_mid| + e_g + Hl nu halfLength)`; `fold = sum 2 gain^o +max(V - |n_mid|, 0)`, exactly 0 for `span_fbm` and for an octave that cannot reach zero. + +The float32 `snoise`, `snoise_grad` and `snoise_hessian` are not the exact functions, so +the quantities read at the midpoint carry allowances, by the derivation of `af_snoise`'s +comment. `S_o = |f_o| (|M q|_1 + |shift|_1) + |offset|_1 + |o octaveStep|_1 + sqrt(3) nu +halfLength` is the sum of the magnitudes the octave coordinate's roundings are relative +to, and `e_n = 2^-14 + L 2^-20 S_o` (value), `e_g = 2^-12 + H 2^-20 S_o` (gradient +length) and `e_h = 2^-10 + T 2^-20 S_o` (Frobenius norm of the Hessian), with `L`, `H`, +`T` the three pins; they add no pin. Measured against a double kernel with the float +hash (6000 points per band, `|x|` from 2^-2 to 2^23), the error is at most 0.057 of +`e_n`, 0.046 of `e_g` and 0.075 of `e_h` (the last at the smallest band, where the +absolute Hessian error is 2.3e-5 and the absolute term carries it). Errors grow +linearly in `|x|`: the value's is 2e-4 at 2^8, 1.5e-2 at 2^14 and 0.94 at 2^20. +Rounding in the Frobenius-to-operator-norm step needs no allowance: the Frobenius norm +exceeds the operator norm. + +Domain. No coordinate range is checked, clamped or refused. The allowances grow with +`S_o`, so `curvature` saturates at `SNOISE_HESSIAN` (true everywhere) by `S_o` of about +1.1e5, and `fold` turns positive for every octave whose `V + 2 e_n` reaches `|n_mid|`: +the bounds stay sound at every magnitude and stop being informative where float32 noise +is (see "Precision domain" above). `SpanStageTests` encloses the exact (double) Hessian +norm and both the float and the exact branch departure at 64 points of seeded spans in +bands of `S_o` from under 1e2 to over 1e6, and in a fixed span where the local tier +with its third-derivative term halved fails. The slice. `af_frustum_ball(centreSlope, z0, z1, footprintPerDepth, warpVariation)` returns the centre `(m_c z_m, z_m)` and a radius `z1 diff --git a/packages/cultmath/src/CultMath/SpanPath.cs b/packages/cultmath/src/CultMath/SpanPath.cs new file mode 100644 index 00000000..cc6524d9 --- /dev/null +++ b/packages/cultmath/src/CultMath/SpanPath.cs @@ -0,0 +1,26 @@ +namespace CultMath; + +/// +/// The span and bound over: the path q(t), +/// |t| <= , through at t = 0 with there, +/// |q'| <= and |q''| <= along it. speed is at least +/// |velocity|; speed, acceleration and halfLength are non-negative. Name every field: the three floats +/// have no order a caller could mistake for a convention. +/// +public struct SpanPath +{ + /// q(0), the point the field and its gradient are evaluated at. + public float3 midpoint; + + /// q'(0). + public float3 velocity; + + /// An upper bound on |q'(t)| over the span; at least |velocity|. + public float speed; + + /// An upper bound on |q''(t)| over the span. + public float acceleration; + + /// The span covers |t| <= halfLength; 0 is the midpoint alone. + public float halfLength; +} diff --git a/packages/cultmath/src/CultMath/SpanPath.cs.meta b/packages/cultmath/src/CultMath/SpanPath.cs.meta new file mode 100644 index 00000000..3c58b0e9 --- /dev/null +++ b/packages/cultmath/src/CultMath/SpanPath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3a8c1d52e7b94f06a1d5c2b9e04f7a31 diff --git a/packages/cultmath/src/CultMath/SpanStage.cs b/packages/cultmath/src/CultMath/SpanStage.cs new file mode 100644 index 00000000..faeccd3e --- /dev/null +++ b/packages/cultmath/src/CultMath/SpanStage.cs @@ -0,0 +1,33 @@ +namespace CultMath; + +/// +/// The transform of one octave stage of a warp chain, as and +/// read it. Octave o samples snoise at x_o = f_o (M q + shift) + offset + +/// o octaveStep, with f_o = lacunarity^o and M = (I + (1/ - 1) a a^T), +/// a = normalize(); a zero axis, or a stretch of 1, means no stretch. stretch must be +/// positive and finite, and nothing guards it. default(SpanStage) has frequency 0, which samples +/// one point: start from . +/// +public struct SpanStage +{ + /// The stage whose span_fbm/span_ridged field equals fbm_grad/ridged_grad: frequency 1, stretch 1, every vector zero. + public static readonly SpanStage Identity = new SpanStage { frequency = 1.0f, stretch = 1.0f }; + + /// The scale of M; its sign is kept. + public float frequency; + + /// The factor the axis direction is stretched by (M scales it by 1/stretch); 1 is none. + public float stretch; + + /// The direction stretch applies along; any length, zero for none. + public float3 axis; + + /// Added to M q before the per-octave scaling. + public float3 shift; + + /// Added after the per-octave scaling. + public float3 offset; + + /// Added to the octave coordinate once per octave index: o octaveStep. + public float3 octaveStep; +} diff --git a/packages/cultmath/src/CultMath/SpanStage.cs.meta b/packages/cultmath/src/CultMath/SpanStage.cs.meta new file mode 100644 index 00000000..7edaee93 --- /dev/null +++ b/packages/cultmath/src/CultMath/SpanStage.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9d46b0f27c1e4a85b3e8f1a60c2d5b97 diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 4df9cd21..73bf199e 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -135,55 +135,78 @@ public static partial class math // Plain fbm of snoise over a span: the octave stage of a warp chain, its value and gradient at the // span's midpoint and the bounds that hold over the whole span. See span_stage for the span, the - // transform and the bound rules. At frequency 1, stretch 1 and zero shift, offset and octaveStep the - // field equals fbm_grad(q, octaves, lacunarity, gain) bit for bit. - public static SpanNoise span_fbm(float3 q, float3 velocity, float speed, float acceleration, float halfLength, - float frequency, float stretch, float3 axis, float3 shift, float3 offset, float3 octaveStep, int octaves, float lacunarity, float gain) => - span_stage(false, q, velocity, speed, acceleration, halfLength, frequency, stretch, axis, shift, offset, octaveStep, octaves, lacunarity, gain); + // transform and the bound rules. At SpanStage.Identity the field equals fbm_grad(path.midpoint, + // octaves, lacunarity, gain) bit for bit. + public static SpanNoise span_fbm(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) => + span_stage(false, path, stage, octaves, lacunarity, gain); - // Ridged fbm over a span: octave o contributes gain^o (1 - |n_o|), ridged_grad's convention (the Span - // Lab's 1 - 2|n| is twice this minus the gain sum, the consumer's to form). fold bounds how far the - // sum can leave the branch it is on at the midpoint. At the identity transform the field equals - // ridged_grad bit for bit. - public static SpanNoise span_ridged(float3 q, float3 velocity, float speed, float acceleration, float halfLength, - float frequency, float stretch, float3 axis, float3 shift, float3 offset, float3 octaveStep, int octaves, float lacunarity, float gain) => - span_stage(true, q, velocity, speed, acceleration, halfLength, frequency, stretch, axis, shift, offset, octaveStep, octaves, lacunarity, gain); + // Ridged fbm over a span: octave o contributes gain^o (1 - |n_o|), ridged_grad's convention. fold + // bounds how far the sum can leave the branch it is on at the midpoint. At SpanStage.Identity the field + // equals ridged_grad bit for bit. + // + // The Span Lab's stage is 1 - 2|n| per octave. With G the sum of gain^o over the octaves (signed: a + // negative gain gives its alternating sum), the lab's value is 2 value - G, and its gradient, + // curvature and fold are twice this one's. A consumer in the lab form applies that mapping and does + // not derive its own bounds. + public static SpanNoise span_ridged(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) => + span_stage(true, path, stage, octaves, lacunarity, gain); - // The span is the path q(t), |t| <= halfLength, through q at t = 0 with the given velocity there, - // |q'| <= speed and |q''| <= acceleration along it (speed at least |velocity|; all three and halfLength - // non-negative). Octave o samples snoise_grad at x_o(q) = f_o (M q + shift) + offset + o octaveStep with - // f_o = lacunarity^o, amplitude gain^o and M = frequency (I + (1/stretch - 1) a a^T), a = normalize(axis); - // a zero axis, or stretch 1, means no stretch. stretch must be positive and finite, and nothing guards - // it, as nothing guards lacunarity. octaves clamps to [0, 16] as fbm_grad's does. The field is the - // octave sum in q: gradient f_o M grad n_o (M is symmetric), the same octave loop as fbm_grad and - // ridged_grad. + private static float span_l1(float3 v) => abs(v.x) + abs(v.y) + abs(v.z); + + // The span is SpanPath: q(t), |t| <= halfLength, through midpoint with velocity at t = 0, |q'| <= speed + // and |q''| <= acceleration along it (speed at least |velocity|; all three and halfLength non-negative). + // The transform is SpanStage: octave o samples snoise_grad at x_o(q) = f_o (M q + shift) + offset + + // o octaveStep with f_o = lacunarity^o, amplitude gain^o and M = frequency (I + (1/stretch - 1) a a^T), + // a = normalize(axis); a zero axis, or stretch 1, means no stretch. stretch must be positive and + // finite, and nothing guards it, as nothing guards lacunarity. octaves clamps to [0, 16] as fbm_grad's + // does. The field is the octave sum in q: gradient f_o M grad n_o (M is symmetric), the same octave + // loop as fbm_grad and ridged_grad. // // Bounds, per octave, from SNOISE_LIPSCHITZ (L), SNOISE_HESSIAN (H) and SNOISE_THIRD (T) only. With // |M| = |frequency| max(1, 1/stretch) (1 without stretch) and nu = f_o |M| speed, the noise-space speed: // * |x(t) - x_mid| <= nu halfLength, so the Hessian of n at x(t) has operator norm at most - // Hl = min(H, ||hessian(x_mid)||_F + T nu halfLength) (the Hessian is T-Lipschitz in operator norm, - // and the Frobenius norm bounds the operator norm), and |grad n(x(t))| at most - // Gl = min(L, |grad n_mid| + Hl nu halfLength). + // Hl = min(H, ||hessian(x_mid)||_F + e_h + T nu halfLength) (the Hessian is T-Lipschitz in operator + // norm, and the Frobenius norm bounds the operator norm), and |grad n(x(t))| at most + // Gl = min(L, |grad n_mid| + e_g + Hl nu halfLength). // * curvature = sum gain^o f_o^2 |M|^2 Hl: the Hessian in q of n(x_o(q)) is f_o^2 M H M. // * fold, ridged only. Over the span the octave's value departs from its midpoint branch // gain^o (1 - s n) (s the sign of n at the midpoint) only where n has changed sign, and there // |n(t)| <= V - |n_mid|, V bounding |n(t) - n_mid|. Writing n(t) - n_mid as the integral of // grad n . x', x' = w + rho with w = f_o M velocity and |rho| <= f_o |M| acceleration |t|, and // grad n = grad n_mid + delta with |delta| <= Hl nu |t|: - // V = |grad n_mid . w| halfLength + (Hl nu |w| + Gl f_o |M| acceleration) halfLength^2 / 2, + // V = (|grad n_mid . w| + e_g |w|) halfLength + (Hl nu |w| + Gl f_o |M| acceleration) halfLength^2 / 2 + // + 2 e_n, // and the octave's fold is 2 gain^o max(V - |n_mid|, 0), zero when it cannot reach zero. - // Rounding is not widened: the Frobenius norm exceeds the operator norm except on a rank-one - // Hessian, and the pins carry a 10% margin. - private static SpanNoise span_stage(bool ridged, float3 q, float3 velocity, float speed, float acceleration, float halfLength, - float frequency, float stretch, float3 axis, float3 shift, float3 offset, float3 octaveStep, int octaves, float lacunarity, float gain) + // + // The float32 snoise, snoise_grad and snoise_hessian are not the exact functions, and the field a + // consumer reads is the float one, so what is read at the midpoint carries an allowance, by the + // derivation af_snoise's comment gives (math.Affine.cs): the skew and the corner offsets round the + // coordinate by about 2u of the magnitudes it is formed from, which moves a result by its derivative + // times that, and the caller's scaling of the point and a GPU's evaluation take 2u to 16u = 2^-20. + // S_o is the sum of those magnitudes, |f_o| (|M q|_1 + |shift|_1) + |offset|_1 + |o octaveStep|_1, plus + // the span's reach sqrt(3) nu halfLength (the coordinate of a path point). The allowances are the + // value's e_n = 2^-14 + L 2^-20 S_o, the gradient length's e_g = 2^-12 + H 2^-20 S_o (H is the + // gradient's own derivative's pin) and the Hessian's Frobenius norm's e_h = 2^-10 + T 2^-20 S_o (T is + // the Hessian's derivative's pin): af_snoise's absolute terms for the first two, and 2^-10 for the + // Hessian's, which the kernel's rounding at small coordinates (2.3e-5 absolute, measured against a + // double kernel) sits well under. The same probe (6000 points per band, |x| from 2^-2 to 2^23) puts + // the error at most 0.057, 0.046 and 0.075 of e_n, e_g and e_h. No new pin enters. + // + // Domain: no coordinate range is checked, clamped or refused. The allowances grow with S_o, so + // curvature saturates at H (true everywhere) by S_o of about 1.1e5, and fold turns positive for every + // octave whose V + 2 e_n reaches |n_mid|: the bounds stay sound at every magnitude and stop being + // informative where float32 noise is (design.md, Precision domain). + private static SpanNoise span_stage(bool ridged, SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) { + var q = path.midpoint; + var halfLength = path.halfLength; octaves = clamp(octaves, 0, 16); - var stretched = dot(axis, axis) > 0.0f && stretch != 1.0f; - var direction = stretched ? normalize(axis) : new float3(0.0f, 0.0f, 0.0f); - var squeeze = stretched ? 1.0f / stretch - 1.0f : 0.0f; - var norm = abs(frequency) * (stretched ? max(1.0f, 1.0f / stretch) : 1.0f); - var mq = stretched ? frequency * (q + squeeze * dot(direction, q) * direction) : frequency * q; - var mv = stretched ? frequency * (velocity + squeeze * dot(direction, velocity) * direction) : frequency * velocity; + var stretched = dot(stage.axis, stage.axis) > 0.0f && stage.stretch != 1.0f; + var direction = stretched ? normalize(stage.axis) : new float3(0.0f, 0.0f, 0.0f); + var squeeze = stretched ? 1.0f / stage.stretch - 1.0f : 0.0f; + var norm = abs(stage.frequency) * (stretched ? max(1.0f, 1.0f / stage.stretch) : 1.0f); + var mq = stretched ? stage.frequency * (q + squeeze * dot(direction, q) * direction) : stage.frequency * q; + var mv = stretched ? stage.frequency * (path.velocity + squeeze * dot(direction, path.velocity) * direction) : stage.frequency * path.velocity; var amplitude = 1.0f; var scale = 1.0f; var value = 0.0f; @@ -192,10 +215,11 @@ public static partial class math var fold = 0.0f; for (var i = 0; i < octaves; i++) { - var x = scale * (mq + shift) + (offset + (float)i * octaveStep); + var step = (float)i * stage.octaveStep; + var x = scale * (mq + stage.shift) + (stage.offset + step); var n = snoise_grad(x); var g = new float3(n.x, n.y, n.z); - var mg = stretched ? frequency * (g + squeeze * dot(direction, g) * direction) : frequency * g; + var mg = stretched ? stage.frequency * (g + squeeze * dot(direction, g) * direction) : stage.frequency * g; if (ridged) { var s = (float)sign(n.w); @@ -210,15 +234,19 @@ public static partial class math var weight = abs(amplitude); var factor = abs(scale) * norm; - var nu = factor * speed; - var local = min(SNOISE_HESSIAN, span_frobenius(snoise_hessian(x)) + SNOISE_THIRD * nu * halfLength); + var nu = factor * path.speed; + var rounding = 9.5367431640625e-7f * (abs(scale) * (span_l1(mq) + span_l1(stage.shift)) + span_l1(stage.offset) + span_l1(step) + 1.7320508f * nu * halfLength); + var valueSlack = 6.103515625e-5f + SNOISE_LIPSCHITZ * rounding; + var gradientSlack = 2.44140625e-4f + SNOISE_HESSIAN * rounding; + var hessianSlack = 9.765625e-4f + SNOISE_THIRD * rounding; + var local = min(SNOISE_HESSIAN, span_frobenius(snoise_hessian(x)) + hessianSlack + SNOISE_THIRD * nu * halfLength); curvature += weight * factor * factor * local; if (ridged) { var w = scale * mv; var reach = local * nu * halfLength; - var gl = min(SNOISE_LIPSCHITZ, length(g) + reach); - var travel = abs(dot(g, w)) * halfLength + (local * nu * length(w) + gl * factor * acceleration) * (0.5f * halfLength * halfLength); + var gl = min(SNOISE_LIPSCHITZ, length(g) + gradientSlack + reach); + var travel = (abs(dot(g, w)) + gradientSlack * length(w)) * halfLength + (local * nu * length(w) + gl * factor * path.acceleration) * (0.5f * halfLength * halfLength) + 2.0f * valueSlack; fold += 2.0f * weight * max(travel - abs(n.w), 0.0f); } diff --git a/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs b/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs index 8d61150a..0962a357 100644 --- a/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs @@ -633,7 +633,7 @@ public sealed class NoiseBoundTests // gradient direction p_c, in double, with snoise_hessian's lattice, step() tie order and (float) hash, // so one corner table serves the Hessian and its derivative. Key names the lattice cell and traversal // order: two points with the same key are differentiated through the same corners. - private static (double[][] X, double[][] P, double[] Key) Corners(double vx, double vy, double vz) + internal static (double[][] X, double[][] P, double[] Key) Corners(double vx, double vy, double vz) { const double cx = 1.0 / 6.0; const double cy = 1.0 / 3.0; @@ -686,7 +686,7 @@ public sealed class NoiseBoundTests // The Hessian of snoise at v in double, entry i * 3 + j: snoise_hessian's per-corner derivation, summed // over Corners. A corner with m = 0.5 - |x|^2 <= 0 contributes nothing. - private static double[] HessianDouble(double vx, double vy, double vz) + internal static double[] HessianDouble(double vx, double vy, double vz) { var (x, p, _) = Corners(vx, vy, vz); var hessian = new double[9]; @@ -705,6 +705,23 @@ public sealed class NoiseBoundTests return hessian; } + // The value of snoise at v in double: 105 sum m^4 s over Corners, the kernel's exact value (the tied + // kernel, as HessianDouble is its exact Hessian). + internal static double ValueDouble(double vx, double vy, double vz) + { + var (x, p, _) = Corners(vx, vy, vz); + var total = 0.0; + for (var c = 0; c < 4; c++) + { + var m = 0.5 - (x[c][0] * x[c][0] + x[c][1] * x[c][1] + x[c][2] * x[c][2]); + if (m <= 0.0) + continue; + total += 105.0 * m * m * m * m * (p[c][0] * x[c][0] + p[c][1] * x[c][1] + p[c][2] * x[c][2]); + } + + return total; + } + private static double Delta(int a, int b) => a == b ? 1.0 : 0.0; // The symmetric third-derivative tensor of snoise at p, entry (i * 3 + j) * 3 + k, in closed form: diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index bc6c6eeb..8bd68193 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -32,48 +32,76 @@ public sealed class SpanStageTests return normalize(v); } - // One draw of the stage parameters: every one of them off its identity value. - private readonly record struct Stage(float Frequency, float Stretch, float3 Axis, float3 Shift, float3 Offset, float3 Step, int Octaves, float Lacunarity, float Gain) + private static float LogUniform(System.Random random, float lo, float hi) => MathF.Exp(Uniform(random, MathF.Log(lo), MathF.Log(hi))); + + private static float Signed(System.Random random, float magnitude) => random.Next(2) == 0 ? magnitude : -magnitude; + + private static readonly float3 None = new(0.0f, 0.0f, 0.0f); + + private static double L1(double[] v) => Math.Abs(v[0]) + Math.Abs(v[1]) + Math.Abs(v[2]); + + // One draw of the stage parameters: every one of them off its identity value, over the ranges the stage + // takes in use (stage-parameter-range): |frequency| 1e-3..10, stretch 1e-2..1e2, |lacunarity| 0.5..3, + // |gain| 0.1..1.2, the last three and frequency with random signs where a sign is meaningful, + // octaveStep nonzero in every component. + private readonly record struct Stage(SpanStage S, int Octaves, float Lacunarity, float Gain) { - public static Stage Draw(System.Random random, int octaves) => new( - Uniform(random, 0.3f, 1.6f), - random.Next(2) == 0 ? Uniform(random, 0.2f, 0.8f) : Uniform(random, 1.25f, 5.0f), - UnitVector(random) * Uniform(random, 0.5f, 3.0f), - Vector(random, -8.0f, 8.0f), - Vector(random, -8.0f, 8.0f), - Vector(random, -3.0f, 3.0f), - octaves, - Uniform(random, 1.5f, 2.3f), - Uniform(random, 0.35f, 0.7f)); + public static Stage Draw(System.Random random, int maxOctaves = 8, int minOctaves = 1) => new( + new SpanStage + { + frequency = Signed(random, LogUniform(random, 1.0e-3f, 10.0f)), + stretch = LogUniform(random, 1.0e-2f, 1.0e2f), + axis = UnitVector(random) * Uniform(random, 0.5f, 3.0f), + shift = Vector(random, -8.0f, 8.0f), + offset = Vector(random, -8.0f, 8.0f), + octaveStep = new float3(Signed(random, Uniform(random, 0.1f, 3.0f)), Signed(random, Uniform(random, 0.1f, 3.0f)), Signed(random, Uniform(random, 0.1f, 3.0f))), + }, + random.Next(minOctaves, maxOctaves + 1), + Signed(random, Uniform(random, 0.5f, 3.0f)), + Signed(random, Uniform(random, 0.1f, 1.2f))); - public SpanNoise Fbm(float3 q, float3 velocity, float speed, float acceleration, float halfLength) => - span_fbm(q, velocity, speed, acceleration, halfLength, Frequency, Stretch, Axis, Shift, Offset, Step, Octaves, Lacunarity, Gain); + // A draw whose octave coordinates stay under `limit` for every q with |q|_1 <= qL1: the fastest + // octave's |f_o| |M| is capped by rejection. For the derivative checks, where float rounding of the + // coordinate must not swamp a difference quotient. + public static Stage DrawSmooth(System.Random random, float limit, float qL1, int maxOctaves = 8, int minOctaves = 1) + { + while (true) + { + var stage = Draw(random, maxOctaves, minOctaves); + var worst = 0.0; + for (var o = 0; o < stage.Octaves; o++) + worst = Math.Max(worst, Math.Abs(stage.Scale(o)) * (stage.Norm() * qL1 + L1(Components(stage.S.shift))) + L1(Components(stage.S.offset)) + o * L1(Components(stage.S.octaveStep))); + if (worst < limit) + return stage; + } + } - public SpanNoise Ridged(float3 q, float3 velocity, float speed, float acceleration, float halfLength) => - span_ridged(q, velocity, speed, acceleration, halfLength, Frequency, Stretch, Axis, Shift, Offset, Step, Octaves, Lacunarity, Gain); + public SpanNoise Fbm(SpanPath path) => span_fbm(path, S, Octaves, Lacunarity, Gain); + + public SpanNoise Ridged(SpanPath path) => span_ridged(path, S, Octaves, Lacunarity, Gain); // M = frequency (I + (1/stretch - 1) a a^T) in double, a the normalised axis (none for a zero axis). public double[,] Matrix() { var m = new double[3, 3]; - var a = new double[] { Axis.x, Axis.y, Axis.z }; + var a = Components(S.axis); var length = Math.Sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]); for (var i = 0; i < 3; i++) for (var j = 0; j < 3; j++) - m[i, j] = Frequency * ((i == j ? 1.0 : 0.0) + (length > 0.0 ? (1.0 / Stretch - 1.0) * a[i] * a[j] / (length * length) : 0.0)); + m[i, j] = S.frequency * ((i == j ? 1.0 : 0.0) + (length > 0.0 && S.stretch != 1.0f ? (1.0 / S.stretch - 1.0) * a[i] * a[j] / (length * length) : 0.0)); return m; } - // Octave o's noise-space point for the span point q. - public float3 Point(float3 q, int octave) + // Octave o's noise-space point for the span point q, in double from the same inputs. + public double[] PointD(float3 q, int octave) { var m = Matrix(); var scale = Math.Pow(Lacunarity, octave); var x = new double[3]; - var qv = new double[] { q.x, q.y, q.z }; - var shift = new double[] { Shift.x, Shift.y, Shift.z }; - var offset = new double[] { Offset.x, Offset.y, Offset.z }; - var step = new double[] { Step.x, Step.y, Step.z }; + var qv = Components(q); + var shift = Components(S.shift); + var offset = Components(S.offset); + var step = Components(S.octaveStep); for (var i = 0; i < 3; i++) { var mq = 0.0; @@ -82,28 +110,59 @@ public sealed class SpanStageTests x[i] = scale * (mq + shift[i]) + offset[i] + octave * step[i]; } + return x; + } + + public float3 Point(float3 q, int octave) + { + var x = PointD(q, octave); return new float3((float)x[0], (float)x[1], (float)x[2]); } + // S_o: the magnitudes octave o's coordinate roundings are relative to, plus the span's reach. + public double Magnitude(SpanPath path, int octave) + { + var m = Matrix(); + var qv = Components(path.midpoint); + var mq = new double[3]; + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + mq[i] += m[i, j] * qv[j]; + var scale = Math.Abs(Scale(octave)); + return scale * (L1(mq) + L1(Components(S.shift))) + L1(Components(S.offset)) + octave * L1(Components(S.octaveStep)) + + Math.Sqrt(3.0) * scale * Norm() * path.speed * path.halfLength; + } + + public double MaxMagnitude(SpanPath path) => Enumerable.Range(0, Octaves).Max(o => Magnitude(path, o)); + + // Octave o as a stage of its own: one octave at the scaled frequency, shift and offset. + public Stage OneOctave(int octave) => new( + new SpanStage { frequency = S.frequency * (float)Scale(octave), stretch = S.stretch, axis = S.axis, shift = S.shift * (float)Scale(octave), offset = S.offset + (float)octave * S.octaveStep, octaveStep = None }, + 1, 1.0f, 1.0f); + public double Amplitude(int octave) => Math.Pow(Gain, octave); public double Scale(int octave) => Math.Pow(Lacunarity, octave); // The spectral norm of M: frequency times the larger of 1 and 1/stretch. - public double Norm() => Math.Abs(Frequency) * Math.Max(1.0, 1.0 / Stretch); + public double Norm() => Math.Abs(S.frequency) * Math.Max(1.0, 1.0 / S.stretch); } + private static double[] Components(float3 v) => new double[] { v.x, v.y, v.z }; + + private static SpanPath PathAt(float3 q) => new() { midpoint = q, velocity = None, speed = 0.0f, acceleration = 0.0f, halfLength = 0.0f }; + // A span through q: velocity v, constant acceleration c of norm `acceleration`, so q(t) = q + v t + c t^2 / 2; // the speed bound is |v| plus what c can add over halfLength. - private readonly record struct Span(float3 Q, float3 Velocity, float3 Acceleration, float Speed, float AccelerationBound, float HalfLength) + private readonly record struct Span(SpanPath Path, float3 Acceleration) { public static Span Draw(System.Random random, float3 q, float halfLength, float acceleration) { var velocity = UnitVector(random) * Uniform(random, 0.2f, 3.0f); - return new Span(q, velocity, UnitVector(random) * acceleration, length(velocity) + acceleration * halfLength, acceleration, halfLength); + return new Span(new SpanPath { midpoint = q, velocity = velocity, speed = length(velocity) + acceleration * halfLength, acceleration = acceleration, halfLength = halfLength }, UnitVector(random) * acceleration); } - public float3 At(float t) => Q + Velocity * t + Acceleration * (0.5f * t * t); + public float3 At(float t) => Path.midpoint + Path.velocity * t + Acceleration * (0.5f * t * t); } // The operator norm of a symmetric 3x3 matrix (entries in row-major order): the larger |eigenvalue|. @@ -121,28 +180,25 @@ public sealed class SpanStageTests return Math.Max(Math.Abs(q + 2.0 * s * Math.Cos(phi)), Math.Abs(q + 2.0 * s * Math.Cos(phi + 2.0 * Math.PI / 3.0))); } - // The Hessian in q of the stage field at span point q (signed per octave for a ridged sum), by the chain - // rule from snoise_hessian: sum over octaves of weight_o f_o^2 M H_o M. + // The exact Hessian in q of the stage field at span point q (signed per octave for a ridged sum), by the + // chain rule from the double kernel's Hessian: sum over octaves of weight_o f_o^2 M H_o M, with every + // octave point computed in double from the same SpanStage. private static double[] FieldHessian(Stage stage, float3 q, bool ridged) { var m = stage.Matrix(); var total = new double[9]; for (var o = 0; o < stage.Octaves; o++) { - var x = stage.Point(q, o); - var h = snoise_hessian(x); - var weight = stage.Amplitude(o) * stage.Scale(o) * stage.Scale(o) * (ridged ? -Math.Sign(snoise(x)) : 1.0); - var hd = new double[3, 3]; - for (var i = 0; i < 3; i++) - for (var j = 0; j < 3; j++) - hd[i, j] = h[i][j]; + var x = stage.PointD(q, o); + var h = NoiseBoundTests.HessianDouble(x[0], x[1], x[2]); + var weight = stage.Amplitude(o) * stage.Scale(o) * stage.Scale(o) * (ridged ? -Math.Sign(NoiseBoundTests.ValueDouble(x[0], x[1], x[2])) : 1.0); for (var i = 0; i < 3; i++) for (var j = 0; j < 3; j++) { var sum = 0.0; for (var k = 0; k < 3; k++) for (var l = 0; l < 3; l++) - sum += m[i, k] * hd[k, l] * m[l, j]; + sum += m[i, k] * h[k * 3 + l] * m[l, j]; total[i * 3 + j] += weight * sum; } } @@ -168,24 +224,41 @@ public sealed class SpanStageTests } /// - /// At frequency 1, stretch 1 (with and without an axis) and zero shift, offset and octaveStep, the field of + /// The exact reference the enclosure tests lean on is the kernel: the double ValueDouble is snoise to + /// within float rounding at small coordinates, so a sign taken from it is the kernel's sign. + /// + [Fact] + public void ExactReferenceIsTheKernel() + { + var random = new System.Random(0xE4AC7); + for (var k = 0; k < 300; k++) + { + var p = Vector(random, -20.0f, 20.0f); + Assert.InRange(NoiseBoundTests.ValueDouble(p.x, p.y, p.z) - snoise(p), -2.0e-4, 2.0e-4); + } + } + + /// + /// At SpanStage.Identity (also with an axis, stretch 1) and zero shift, offset and octaveStep, the field of /// span_fbm and span_ridged is fbm_grad's and ridged_grad's, bit for bit, for 0 to 20 octaves (the clamp /// included), every lacunarity and gain drawn, over random and simplex-tie points: any change to the octave - /// loop's order, scaling or accumulation fails. + /// loop's order, scaling or accumulation, and any bit the rounding allowances put in the field, fails. /// [Fact] public void SpanStagesAtIdentityAreFbmAndRidged() { var random = new System.Random(0x57A6E1); - var none = new float3(0.0f, 0.0f, 0.0f); foreach (var p in Points(random)) { var octaves = random.Next(0, 21); var lacunarity = Uniform(random, 1.4f, 2.6f); var gain = Uniform(random, 0.3f, 0.8f); - var axis = random.Next(2) == 0 ? none : UnitVector(random) * 2.0f; - var fbm = span_fbm(p, UnitVector(random), 1.0f, 0.5f, 0.25f, 1.0f, 1.0f, axis, none, none, none, octaves, lacunarity, gain); - var ridged = span_ridged(p, UnitVector(random), 1.0f, 0.5f, 0.25f, 1.0f, 1.0f, axis, none, none, none, octaves, lacunarity, gain); + var stage = SpanStage.Identity; + if (random.Next(2) == 1) + stage.axis = UnitVector(random) * 2.0f; + var path = new SpanPath { midpoint = p, velocity = UnitVector(random), speed = 1.0f, acceleration = 0.5f, halfLength = 0.25f }; + var fbm = span_fbm(path, stage, octaves, lacunarity, gain); + var ridged = span_ridged(path, stage, octaves, lacunarity, gain); Assert.True(SameBits(fbm.field, fbm_grad(p, octaves, lacunarity, gain)), $"span_fbm at {p}, {octaves} octaves: {fbm.field} is not fbm_grad {fbm_grad(p, octaves, lacunarity, gain)}"); Assert.True(SameBits(ridged.field, ridged_grad(p, octaves, lacunarity, gain)), $"span_ridged at {p}, {octaves} octaves: {ridged.field} is not ridged_grad {ridged_grad(p, octaves, lacunarity, gain)}"); Assert.Equal(0.0f, fbm.fold); @@ -194,10 +267,12 @@ public sealed class SpanStageTests /// /// The gradient is the derivative of the value through the whole transform: with every parameter off its - /// identity (frequency, stretch both ways, axis, shift, offset, octaveStep, lacunarity, gain), field.xyz - /// equals the fourth-order central difference of field.w along each axis, within a tolerance built from the - /// third-derivative pin (truncation) and float32 rounding of the coordinates. A dropped M or lacunarity - /// factor, or M applied to the wrong side, moves it by the gradient itself. + /// identity (frequency of either sign, stretch both ways, axis, shift, offset, octaveStep, lacunarity and + /// gain of either sign, 4 to 8 octaves) field.xyz equals the fourth-order central difference of field.w + /// along each axis, within a tolerance built from the third-derivative pin (truncation) and float32 + /// rounding of the coordinates, and both agree with an independent octave sum. A dropped M or lacunarity + /// factor, M applied to the wrong side, a clamp on frequency, stretch or the octaveStep index, or a dropped + /// sign (abs of the scale or the amplitude) moves the field by the gradient itself. /// [Theory] [InlineData(false)] @@ -207,15 +282,23 @@ public sealed class SpanStageTests var random = new System.Random(ridged ? 0x6A1D : 0x6A1C); var worst = 0.0; var checkedCases = 0; - for (var k = 0; k < 400; k++) + var spans = 0; + for (var k = 0; k < 6000 && spans < 80; k++) { - var stage = Stage.Draw(random, ridged ? random.Next(1, 3) : random.Next(1, 4)); + var stage = Stage.DrawSmooth(random, 300.0f, 9.0f, 8, 4); var q = Vector(random, -3.0f, 3.0f); // The fold's crease at n = 0 is not differentiable: keep every octave's stencil clear of it. - if (ridged && Enumerable.Range(0, stage.Octaves).Any(o => abs(snoise(stage.Point(q, o))) < 0.3f)) + if (ridged && Enumerable.Range(0, stage.Octaves).Any(o => abs(snoise(stage.Point(q, o))) < 0.15f)) continue; + spans++; 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); + SpanNoise At(float3 p) + { + var path = span.Path; + path.midpoint = p; + return ridged ? stage.Ridged(path) : stage.Fbm(path); + } + 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. @@ -233,18 +316,16 @@ public sealed class SpanStageTests 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); var scale = 0.0; for (var o = 0; o < stage.Octaves; o++) - scale += stage.Amplitude(o) * stage.Scale(o) * stage.Norm(); + scale += Math.Abs(stage.Amplitude(o) * stage.Scale(o)) * stage.Norm(); + var amplitudes = Enumerable.Range(0, stage.Octaves).Sum(o => Math.Abs(stage.Amplitude(o))); + Assert.True(Math.Abs(referenceValue - centre.field.w) <= 2.0e-3 * (1.0 + amplitudes), $"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]) <= 1.0e-2 * (1.0 + scale), $"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() * Enumerable.Range(0, stage.Octaves).Max(o => Math.Abs(stage.Scale(o))); + var h = (float)(0.02 / fastest); for (var axis = 0; axis < 3; axis++) { var e = new float3(axis == 0 ? h : 0.0f, axis == 1 ? h : 0.0f, axis == 2 ? h : 0.0f); @@ -276,33 +357,46 @@ public sealed class SpanStageTests var random = new System.Random(0x57E7C4); for (var k = 0; k < 60; k++) { - var stage = Stage.Draw(random, 3); + var stage = Stage.DrawSmooth(random, 300.0f, 15.0f, 3, 3); var q = Vector(random, -5.0f, 5.0f); var span = Span.Draw(random, q, 0.2f, 0.3f); - var plain = stage with { Stretch = 1.0f }; - var noAxis = stage with { Axis = new float3(0.0f, 0.0f, 0.0f), Stretch = 3.0f }; - var reference = plain.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); - var isotropic = (plain with { Axis = new float3(0.0f, 0.0f, 0.0f) }).Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); - var viaNoAxis = noAxis.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + var plain = stage with { S = stage.S with { stretch = 1.0f } }; + var noAxis = stage with { S = stage.S with { axis = None, stretch = 3.0f } }; + var reference = plain.Ridged(span.Path); + var isotropic = (plain with { S = plain.S with { axis = None } }).Ridged(span.Path); + var viaNoAxis = noAxis.Ridged(span.Path); Assert.True(SameBits(reference.field, isotropic.field) && reference.curvature == isotropic.curvature && reference.fold == isotropic.fold, "stretch 1 with an axis differs from no axis"); Assert.True(SameBits(isotropic.field, viaNoAxis.field) && isotropic.curvature == viaNoAxis.curvature && isotropic.fold == viaNoAxis.fold, "a zero axis with stretch 3 differs from the isotropic stage"); - var unit = stage with { Axis = new float3(0.0f, 1.0f, 0.0f) }; - var scaled = stage with { Axis = new float3(0.0f, 2.0f, 0.0f) }; - var a = unit.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); - var b = scaled.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); + var unit = stage with { S = stage.S with { axis = new float3(0.0f, 1.0f, 0.0f) } }; + var scaled = stage with { S = stage.S with { axis = new float3(0.0f, 2.0f, 0.0f) } }; + var a = unit.Fbm(span.Path); + var b = scaled.Fbm(span.Path); Assert.True(SameBits(a.field, b.field) && a.curvature == b.curvature, "a non-unit axis differs from its normalisation"); - var stretched = unit.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); - Assert.False(SameBits(stretched.field, (unit with { Stretch = 1.0f }).Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength).field), "a stretch of " + unit.Stretch + " did not change the field"); + var stretched = unit.Fbm(span.Path); + Assert.False(SameBits(stretched.field, (unit with { S = unit.S with { stretch = 1.0f } }).Fbm(span.Path).field), "a stretch of " + unit.S.stretch + " did not change the field"); } } - // The stage parameters and per-octave bookkeeping the curvature checks share. + // The coordinate bands the enclosure tests draw: a large offset (0 keeps the drawn one) lifts S_o through + // the decades; Bucket names the decade a span's largest S_o is in. + private static readonly float[] OffsetScales = { 0.0f, 40.0f, 400.0f, 4.0e3f, 4.0e4f, 4.0e5f, 4.0e6f }; + + private static int Bucket(double magnitude) => magnitude < 1.0e2 ? 0 : magnitude < 1.0e3 ? 1 : magnitude < 1.0e4 ? 2 : magnitude < 1.0e5 ? 3 : magnitude < 1.0e6 ? 4 : 5; + + private static Stage Banded(System.Random random, float offsetScale, int octaves) + { + var stage = Stage.Draw(random, octaves); + if (offsetScale > 0.0f) + stage = stage with { S = stage.S with { offset = Vector(random, -offsetScale, offsetScale) } }; + return stage; + } + private static void CheckCurvature(Stage stage, bool ridged, Span span, SpanNoise result, int samples, ref double tightest, ref int checkedPoints) { for (var s = 0; s < samples; s++) { - var t = span.HalfLength * (2.0f * s / (samples - 1) - 1.0f); + var t = span.Path.halfLength * (2.0f * s / (samples - 1) - 1.0f); var norm = SymmetricNorm(FieldHessian(stage, span.At(t), ridged)); Assert.True(norm <= result.curvature * (1.0 + 1.0e-3), $"stage {stage}, span {span}: the Hessian norm at t = {t:R} is {norm:R}, above curvature {result.curvature:R}"); tightest = Math.Min(tightest, result.curvature > 0.0 ? 1.0 - norm / result.curvature : 1.0); @@ -311,13 +405,15 @@ public sealed class SpanStageTests } /// - /// span-bounds-enclose, curvature: over seeded spans the operator norm of the field's q-Hessian (chain rule - /// from snoise_hessian, M and the octave scales) at 64 points per span never exceeds curvature, in the - /// local regime (short spans, where the third-derivative term is the bound) and the global one (long spans, - /// where SNOISE_HESSIAN is); at halfLength 0 curvature is the local tier, the Frobenius norm of the - /// midpoint Hessians scaled by amplitude, f_o^2 and |M|^2, to within float rounding; and a span through - /// MeasureHessian's witness stays enclosed in 200 directions, which the Frobenius norm at the midpoint alone - /// does not guarantee. + /// span-bounds-enclose, curvature: over seeded spans the operator norm of the exact field q-Hessian (chain + /// rule from the double kernel's Hessian, every octave point in double, M and the octave scales) at 64 + /// points per span never exceeds curvature, in the local regime (short spans, where the third-derivative + /// term is the bound) and the global one (long spans, where SNOISE_HESSIAN is), and in every coordinate + /// band from S_o under 1e2 to over 1e6, where the float32 kernel is no longer the exact one; at + /// halfLength 0 curvature is the widened local tier, the Frobenius norm of the midpoint Hessians plus + /// e_h, capped at the pin, scaled by amplitude, f_o^2 and |M|^2; and a span through MeasureHessian's + /// witness stays enclosed in 200 directions, which the Frobenius norm at the midpoint alone does not + /// guarantee. /// [Theory] [InlineData(false)] @@ -327,28 +423,34 @@ public sealed class SpanStageTests var random = new System.Random(ridged ? 0xC0DE2 : 0xC0DE1); var tightest = 1.0; var checkedPoints = 0; - foreach (var halfLength in new[] { 0.002f, 0.02f, 0.2f, 1.0f, 8.0f }) - for (var k = 0; k < 24; k++) - { - var stage = Stage.Draw(random, random.Next(1, 4)); - var span = Span.Draw(random, Vector(random, -30.0f, 30.0f), halfLength, Uniform(random, 0.0f, 2.0f)); - var result = ridged ? stage.Ridged(span.Q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength) : stage.Fbm(span.Q, span.Velocity, span.Speed, span.AccelerationBound, span.HalfLength); - CheckCurvature(stage, ridged, span, result, 64, ref tightest, ref checkedPoints); - } + var buckets = new int[6]; + foreach (var offsetScale in OffsetScales) + foreach (var halfLength in new[] { 0.02f, 0.5f, 8.0f }) + for (var k = 0; k < 8; k++) + { + var stage = Banded(random, offsetScale, 3); + var span = Span.Draw(random, Vector(random, -30.0f, 30.0f), halfLength, Uniform(random, 0.0f, 2.0f)); + var result = ridged ? stage.Ridged(span.Path) : stage.Fbm(span.Path); + buckets[Bucket(stage.MaxMagnitude(span.Path))]++; + CheckCurvature(stage, ridged, span, result, 64, ref tightest, ref checkedPoints); + } - // halfLength 0: the local tier exactly. + Assert.True(buckets.All(count => count >= 3), $"the coordinate bands drawn are {string.Join(", ", buckets)} spans: a band is not reached"); + + // halfLength 0: the widened local tier. for (var k = 0; k < 40; k++) { - var stage = Stage.Draw(random, random.Next(1, 4)); + var stage = Stage.DrawSmooth(random, 100.0f, 90.0f, 3); var q = Vector(random, -30.0f, 30.0f); var span = Span.Draw(random, q, 0.0f, 1.0f); - var result = ridged ? stage.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, 0.0f) : stage.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, 0.0f); + var result = ridged ? stage.Ridged(span.Path) : stage.Fbm(span.Path); var expected = 0.0; for (var o = 0; o < stage.Octaves; o++) { var h = snoise_hessian(stage.Point(q, o)); var frobenius = Math.Sqrt(Enumerable.Range(0, 9).Sum(i => (double)h[i / 3][i % 3] * h[i / 3][i % 3])); - expected += stage.Amplitude(o) * stage.Scale(o) * stage.Scale(o) * stage.Norm() * stage.Norm() * Math.Min(SNOISE_HESSIAN, frobenius); + var slack = Math.Pow(2.0, -10) + SNOISE_THIRD * Math.Pow(2.0, -20) * stage.Magnitude(span.Path, o); + expected += Math.Abs(stage.Amplitude(o)) * stage.Scale(o) * stage.Scale(o) * stage.Norm() * stage.Norm() * Math.Min(SNOISE_HESSIAN, frobenius + slack); } Assert.InRange(result.curvature, expected * (1.0 - 1.0e-3), expected * (1.0 + 1.0e-3)); @@ -357,27 +459,50 @@ public sealed class SpanStageTests // Through the Hessian witness: one octave at the identity transform, the span's midpoint 0.1 to 0.5 away, // moving toward the witness at speed 1, so the witness lies on the span. var witness = new float3(-13.7920685f, -9.672134f, 61.563934f); - var identity = new Stage(1.0f, 1.0f, new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), 1, 2.0f, 0.5f); + var identity = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); var through = 0.0; for (var k = 0; k < 200; k++) { var u = UnitVector(random); var distance = Uniform(random, 0.1f, 0.5f); - var span = new Span(witness + u * distance, -u, new float3(0.0f, 0.0f, 0.0f), 1.0f, 0.0f, 0.6f); - var result = ridged ? identity.Ridged(span.Q, span.Velocity, 1.0f, 0.0f, 0.6f) : identity.Fbm(span.Q, span.Velocity, 1.0f, 0.0f, 0.6f); + var span = new Span(new SpanPath { midpoint = witness + u * distance, velocity = -u, speed = 1.0f, acceleration = 0.0f, halfLength = 0.6f }, None); + var result = ridged ? identity.Ridged(span.Path) : identity.Fbm(span.Path); CheckCurvature(identity, ridged, span, result, 64, ref tightest, ref checkedPoints); through = Math.Max(through, SymmetricNorm(FieldHessian(identity, witness, ridged))); } Assert.True(through > 40.0, $"the witness Hessian norm is {through:R}: the through-the-witness spans do not reach the curvature"); - output.WriteLine($"IV-REPORT span curvature ({(ridged ? "ridged" : "fbm")}): {checkedPoints} points enclosed, least relative slack {tightest:R}, witness norm {through:R}"); + output.WriteLine($"IV-REPORT span curvature ({(ridged ? "ridged" : "fbm")}): {checkedPoints} points enclosed, bands {string.Join("/", buckets)}, least relative slack {tightest:R}, witness norm {through:R}"); + } + + /// + /// local-tier-coefficients-unpinned: a fixed span (found by an audit, kept) at SpanStage.Identity, one + /// octave, where the Hessian's operator norm at the end of the span (11.768) exceeds the local tier built + /// with half of SNOISE_THIRD's term (about 10.1) and the exact norm along the 64 sampled points stays + /// under curvature (12.526 before the allowance). + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpanCurvatureEnclosesThePinnedRankOneSpan(bool ridged) + { + var stage = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); + var span = new Span(new SpanPath { midpoint = new float3(144.93297f, -286.08800f, -53.59360f), velocity = new float3(-0.02139f, 0.17122f, -0.98500f), speed = 1.0f, acceleration = 0.0f, halfLength = 0.01f }, None); + var result = ridged ? stage.Ridged(span.Path) : stage.Fbm(span.Path); + var tightest = 1.0; + var points = 0; + CheckCurvature(stage, ridged, span, result, 64, ref tightest, ref points); + var end = SymmetricNorm(FieldHessian(stage, span.At(span.Path.halfLength), ridged)); + Assert.True(end > 10.5 && result.curvature >= end, $"the end-of-span norm is {end:R} and curvature {result.curvature:R}: the span no longer pins the local tier"); } /// /// span-bounds-enclose, fold: for span_ridged, the value's departure from the midpoint branch (each /// octave's gain^o (s n - |n|), s the sign of n at the midpoint, summed) never exceeds fold at 64 points of - /// every span; fold is positive on spans that do cross zero (and the departure there is not trivially - /// zero), exactly 0 when no octave can reach zero and for span_fbm, and 0 at halfLength 0. + /// every span, measured twice: for the float field span_ridged itself evaluates at the path point, and for + /// the exact octave sum (double kernel); in every coordinate band from S_o under 1e2 to over 1e6. fold is + /// positive on spans that do cross zero (and the departure there is not trivially zero), exactly 0 for + /// span_fbm and when no octave can reach zero. /// [Fact] public void SpanFoldEnclosesTheBranchDeviation() @@ -385,62 +510,77 @@ public sealed class SpanStageTests var random = new System.Random(0xF01D); var crossing = 0; var largest = 0.0; - var tightest = 1.0; - for (var k = 0; k < 400; k++) - { - var stage = Stage.Draw(random, random.Next(1, 4)); - var halfLength = new[] { 0.02f, 0.1f, 0.5f, 2.0f }[random.Next(4)]; - // Draw midpoints with small noise values so spans cross zero, as well as arbitrary ones. - var q = Vector(random, -30.0f, 30.0f); - if (k % 2 == 0) + var tightestFloat = 1.0; + var tightestExact = 1.0; + var buckets = new int[6]; + foreach (var offsetScale in OffsetScales) + for (var k = 0; k < 60; k++) { - var best = q; - var smallest = float.MaxValue; - for (var trial = 0; trial < 40; trial++) + var stage = Banded(random, offsetScale, 3); + var halfLength = new[] { 0.02f, 0.1f, 0.5f, 2.0f }[random.Next(4)]; + // Draw midpoints with small noise values so spans cross zero, as well as arbitrary ones. + var q = Vector(random, -30.0f, 30.0f); + if (k % 2 == 0) { - var candidate = Vector(random, -30.0f, 30.0f); - var value = abs(snoise(stage.Point(candidate, 0))); - if (value < smallest) - (smallest, best) = (value, candidate); + var best = q; + var smallest = float.MaxValue; + for (var trial = 0; trial < 40; trial++) + { + var candidate = Vector(random, -30.0f, 30.0f); + var value = abs(snoise(stage.Point(candidate, 0))); + if (value < smallest) + (smallest, best) = (value, candidate); + } + + q = best; } - q = best; - } - - var span = Span.Draw(random, q, halfLength, Uniform(random, 0.0f, 2.0f)); - var fold = stage.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, halfLength).fold; - Assert.Equal(0.0f, stage.Fbm(q, span.Velocity, span.Speed, span.AccelerationBound, halfLength).fold); - Assert.Equal(0.0f, stage.Ridged(q, span.Velocity, span.Speed, span.AccelerationBound, 0.0f).fold); - var signs = Enumerable.Range(0, stage.Octaves).Select(o => Math.Sign(snoise(stage.Point(q, o)))).ToArray(); - var peak = 0.0; - for (var s = 0; s < 64; s++) - { - var t = halfLength * (2.0f * s / 63 - 1.0f); - var point = span.At(t); - var departure = 0.0; - for (var o = 0; o < stage.Octaves; o++) + var span = Span.Draw(random, q, halfLength, Uniform(random, 0.0f, 2.0f)); + buckets[Bucket(stage.MaxMagnitude(span.Path))]++; + var fold = stage.Ridged(span.Path).fold; + Assert.Equal(0.0f, stage.Fbm(span.Path).fold); + var signs = Enumerable.Range(0, stage.Octaves).Select(o => Math.Sign(stage.OneOctave(o).Fbm(PathAt(q)).field.w)).ToArray(); + var peakFloat = 0.0; + var peakExact = 0.0; + for (var s = 0; s < 64; s++) { - var n = (double)snoise(stage.Point(point, o)); - departure += stage.Amplitude(o) * (signs[o] * n - Math.Abs(n)); + var t = halfLength * (2.0f * s / 63 - 1.0f); + var point = span.At(t); + var departureFloat = 0.0; + var departureExact = 0.0; + for (var o = 0; o < stage.Octaves; o++) + { + var octave = stage.OneOctave(o); + var nFloat = (double)octave.Fbm(PathAt(point)).field.w; + var absFloat = 1.0 - octave.Ridged(PathAt(point)).field.w; + departureFloat += stage.Amplitude(o) * (signs[o] * nFloat - absFloat); + var x = stage.PointD(point, o); + var nExact = NoiseBoundTests.ValueDouble(x[0], x[1], x[2]); + departureExact += stage.Amplitude(o) * (signs[o] * nExact - Math.Abs(nExact)); + } + + peakFloat = Math.Max(peakFloat, Math.Abs(departureFloat)); + peakExact = Math.Max(peakExact, Math.Abs(departureExact)); + Assert.True(Math.Abs(departureFloat) <= fold * (1.0 + 1.0e-3) + 1.0e-6, $"stage {stage}, span {span}: the float branch departure at t = {t:R} is {departureFloat:R}, above fold {fold:R}"); + Assert.True(Math.Abs(departureExact) <= fold * (1.0 + 1.0e-3) + 1.0e-6, $"stage {stage}, span {span}: the exact branch departure at t = {t:R} is {departureExact:R}, above fold {fold:R}"); } - peak = Math.Max(peak, Math.Abs(departure)); - Assert.True(Math.Abs(departure) <= fold * (1.0 + 1.0e-3), $"stage {stage}, span {span}: the branch departure at t = {t:R} is {departure:R}, above fold {fold:R}"); + if (fold > 0.0) + crossing++; + largest = Math.Max(largest, Math.Max(peakFloat, peakExact)); + if (fold > 0.0 && peakFloat > 0.0) + tightestFloat = Math.Min(tightestFloat, peakFloat / fold); + if (fold > 0.0 && peakExact > 0.0) + tightestExact = Math.Min(tightestExact, peakExact / fold); } - if (fold > 0.0) - crossing++; - largest = Math.Max(largest, peak); - if (fold > 0.0 && peak > 0.0) - tightest = Math.Min(tightest, peak / fold); - } - - Assert.True(crossing >= 100, $"only {crossing} of 400 spans had a positive fold"); + Assert.True(buckets.All(count => count >= 3), $"the coordinate bands drawn are {string.Join(", ", buckets)} spans: a band is not reached"); + Assert.True(crossing >= 100, $"only {crossing} of {OffsetScales.Length * 60} spans had a positive fold"); Assert.True(largest > 0.1, $"the largest branch departure drawn is {largest:R}: the check has no teeth"); - output.WriteLine($"IV-REPORT span fold: {crossing} spans with fold > 0, largest departure {largest:R}, tightest departure / fold {tightest:R}"); + output.WriteLine($"IV-REPORT span fold: {crossing} spans with fold > 0, bands {string.Join("/", buckets)}, largest departure {largest:R}, tightest departure / fold {tightestFloat:R} (float) {tightestExact:R} (exact)"); // An octave that cannot reach zero: a midpoint where |n| is large and a short span give fold exactly 0. - var calm = new Stage(1.0f, 1.0f, new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), 1, 2.0f, 0.5f); + var calm = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); var found = 0; for (var k = 0; k < 400 && found < 20; k++) { @@ -448,7 +588,7 @@ public sealed class SpanStageTests if (abs(snoise(q)) < 0.6f) continue; found++; - Assert.Equal(0.0f, calm.Ridged(q, new float3(1.0f, 0.0f, 0.0f), 1.0f, 0.0f, 0.01f).fold); + Assert.Equal(0.0f, calm.Ridged(new SpanPath { midpoint = q, velocity = new float3(1.0f, 0.0f, 0.0f), speed = 1.0f, acceleration = 0.0f, halfLength = 0.01f }).fold); } Assert.True(found >= 5, $"only {found} calm midpoints found"); @@ -464,8 +604,7 @@ public sealed class SpanStageTests public void SpanFoldCoversAccelerationAlone() { var random = new System.Random(0xACCE1); - var identity = new Stage(1.0f, 1.0f, new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), new float3(0.0f, 0.0f, 0.0f), 1, 2.0f, 0.5f); - var still = new float3(0.0f, 0.0f, 0.0f); + var identity = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); var cases = 0; var crossings = 0; var tightest = double.MaxValue; @@ -480,8 +619,8 @@ public sealed class SpanStageTests if (length(gradient) < 1.5f || abs(n.w) > 0.4f * length(gradient) * acceleration * halfLength * halfLength * 0.5f) continue; var c = -(float)Math.Sign(n.w) * acceleration * normalize(gradient); - var span = new Span(q, still, c, acceleration * halfLength, acceleration, halfLength); - var fold = identity.Ridged(q, still, span.Speed, acceleration, halfLength).fold; + var span = new Span(new SpanPath { midpoint = q, velocity = None, speed = acceleration * halfLength, acceleration = acceleration, halfLength = halfLength }, c); + var fold = identity.Ridged(span.Path).fold; var departure = 0.0; for (var s = 0; s < 64; s++) { From 52d25740ac8764261df0e219752440fc47222716 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 20:49:31 +0200 Subject: [PATCH 11/24] caching: MessagePack and JSON shapes for SpanPath and SpanStage Co-Authored-By: Claude Opus 5.5 --- .../CultMathJson.cs | 22 ++++++++++- .../CultMathResolver.cs | 39 ++++++++++++++++++- .../CultMathJsonTests.cs | 6 +++ .../CultMathSerializationTests.cs | 15 +++++++ 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/GameCult.Caching.MessagePack/CultMathJson.cs b/src/GameCult.Caching.MessagePack/CultMathJson.cs index 29c312da..dfd15c7f 100644 --- a/src/GameCult.Caching.MessagePack/CultMathJson.cs +++ b/src/GameCult.Caching.MessagePack/CultMathJson.cs @@ -14,7 +14,7 @@ namespace GameCult.Caching.MessagePack; /// The canonical System.Text.Json shape of every public CultMath value type, the JSON twin of /// . Vectors, quaternions and colors are objects keyed by their /// field names ({"x":1,"y":2}), matrices are arrays of row objects, rect is -/// {"min":..,"max":..}, CultCellular is {"nearest":..,"edge":..,"id":..}, CultPhasor is {"cos":..,"sin":..}, and SpanNoise is {"field":..,"curvature":..,"fold":..}. +/// {"min":..,"max":..}, CultCellular is {"nearest":..,"edge":..,"id":..}, CultPhasor is {"cos":..,"sin":..}, SpanNoise is {"field":..,"curvature":..,"fold":..}, SpanPath is {"midpoint":..,"velocity":..,"speed":..,"acceleration":..,"halfLength":..}, and SpanStage is {"frequency":..,"stretch":..,"axis":..,"shift":..,"offset":..,"octaveStep":..}. /// Property names are fixed: PropertyNamingPolicy does not rename them, and /// PropertyNameCaseInsensitive makes reading case-insensitive. /// @@ -481,7 +481,7 @@ public static class CultMathJson Add(UIntScalar.Instance, new Fields(("state", Val())), a => new Random { state = a[0] }, (v, a) => a[0] = v.state), - // Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}, SpanNoise is {field, curvature, fold}. + // Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}, SpanNoise is {field, curvature, fold}, SpanPath is {midpoint, velocity, speed, acceleration, halfLength}, SpanStage is {frequency, stretch, axis, shift, offset, octaveStep}. Add(f, new Rows(2, Xy()), a => new float2x2(a[0], a[1], a[2], a[3]), (v, a) => { a[0] = v[0].x; a[1] = v[0].y; a[2] = v[1].x; a[3] = v[1].y; }), @@ -521,6 +521,24 @@ public static class CultMathJson a[0] = v.field.x; a[1] = v.field.y; a[2] = v.field.z; a[3] = v.field.w; a[4] = v.curvature; a[5] = v.fold; }), + Add(f, new Fields(("midpoint", Xyz()), ("velocity", Xyz()), ("speed", Val()), ("acceleration", Val()), ("halfLength", Val())), + a => new SpanPath { midpoint = new float3(a[0], a[1], a[2]), velocity = new float3(a[3], a[4], a[5]), speed = a[6], acceleration = a[7], halfLength = a[8] }, + (v, a) => + { + a[0] = v.midpoint.x; a[1] = v.midpoint.y; a[2] = v.midpoint.z; + a[3] = v.velocity.x; a[4] = v.velocity.y; a[5] = v.velocity.z; + a[6] = v.speed; a[7] = v.acceleration; a[8] = v.halfLength; + }), + Add(f, new Fields(("frequency", Val()), ("stretch", Val()), ("axis", Xyz()), ("shift", Xyz()), ("offset", Xyz()), ("octaveStep", Xyz())), + a => new SpanStage { frequency = a[0], stretch = a[1], axis = new float3(a[2], a[3], a[4]), shift = new float3(a[5], a[6], a[7]), offset = new float3(a[8], a[9], a[10]), octaveStep = new float3(a[11], a[12], a[13]) }, + (v, a) => + { + a[0] = v.frequency; a[1] = v.stretch; + a[2] = v.axis.x; a[3] = v.axis.y; a[4] = v.axis.z; + a[5] = v.shift.x; a[6] = v.shift.y; a[7] = v.shift.z; + a[8] = v.offset.x; a[9] = v.offset.y; a[10] = v.offset.z; + a[11] = v.octaveStep.x; a[12] = v.octaveStep.y; a[13] = v.octaveStep.z; + }), }; var map = new Dictionary(); diff --git a/src/GameCult.Caching.MessagePack/CultMathResolver.cs b/src/GameCult.Caching.MessagePack/CultMathResolver.cs index 529ed2e7..8f287a5c 100644 --- a/src/GameCult.Caching.MessagePack/CultMathResolver.cs +++ b/src/GameCult.Caching.MessagePack/CultMathResolver.cs @@ -180,7 +180,7 @@ public sealed class CultMathResolver : IFormatterResolver return value; }), - // Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin], SpanNoise is [field, curvature, fold]. + // Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin], SpanNoise is [field, curvature, fold], SpanPath is [midpoint, velocity, speed, acceleration, halfLength], SpanStage is [frequency, stretch, axis, shift, offset, octaveStep]. Shape((ref MessagePackWriter w, float2x2 v) => { w.WriteArrayHeader(2); PutFloat2(ref w, v[0]); PutFloat2(ref w, v[1]); }, (ref MessagePackReader r) => { @@ -252,6 +252,43 @@ public sealed class CultMathResolver : IFormatterResolver } return value; }), + Shape((ref MessagePackWriter w, SpanPath v) => { w.WriteArrayHeader(5); PutFloat3(ref w, v.midpoint); PutFloat3(ref w, v.velocity); w.Write(v.speed); w.Write(v.acceleration); w.Write(v.halfLength); }, + (ref MessagePackReader r) => + { + var value = default(SpanPath); + for (int i = 0, n = r.ReadArrayHeader(); i < n; i++) + { + switch (i) + { + case 0: value.midpoint = GetFloat3(ref r); break; + case 1: value.velocity = GetFloat3(ref r); break; + case 2: value.speed = r.ReadSingle(); break; + case 3: value.acceleration = r.ReadSingle(); break; + case 4: value.halfLength = r.ReadSingle(); break; + default: r.Skip(); break; + } + } + return value; + }), + Shape((ref MessagePackWriter w, SpanStage v) => { w.WriteArrayHeader(6); w.Write(v.frequency); w.Write(v.stretch); PutFloat3(ref w, v.axis); PutFloat3(ref w, v.shift); PutFloat3(ref w, v.offset); PutFloat3(ref w, v.octaveStep); }, + (ref MessagePackReader r) => + { + var value = default(SpanStage); + for (int i = 0, n = r.ReadArrayHeader(); i < n; i++) + { + switch (i) + { + case 0: value.frequency = r.ReadSingle(); break; + case 1: value.stretch = r.ReadSingle(); break; + case 2: value.axis = GetFloat3(ref r); break; + case 3: value.shift = GetFloat3(ref r); break; + case 4: value.offset = GetFloat3(ref r); break; + case 5: value.octaveStep = GetFloat3(ref r); break; + default: r.Skip(); break; + } + } + return value; + }), }; var map = new Dictionary(); diff --git a/tests/GameCult.Caching.Tests/CultMathJsonTests.cs b/tests/GameCult.Caching.Tests/CultMathJsonTests.cs index 26474907..2920c2f3 100644 --- a/tests/GameCult.Caching.Tests/CultMathJsonTests.cs +++ b/tests/GameCult.Caching.Tests/CultMathJsonTests.cs @@ -47,6 +47,10 @@ namespace GameCult.Caching.Tests """{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}}"""); Json(new SpanNoise(new float4(1f, 2f, 3f, 4f), 0.25f, 0.5f)).Should().Be( """{"field":{"x":1,"y":2,"z":3,"w":4},"curvature":0.25,"fold":0.5}"""); + Json(new SpanPath { midpoint = new float3(1f, 2f, 3f), velocity = new float3(4f, 5f, 6f), speed = 7f, acceleration = 8f, halfLength = 9f }).Should().Be( + """{"midpoint":{"x":1,"y":2,"z":3},"velocity":{"x":4,"y":5,"z":6},"speed":7,"acceleration":8,"halfLength":9}"""); + Json(new SpanStage { frequency = 1f, stretch = 2f, axis = new float3(3f, 4f, 5f), shift = new float3(6f, 7f, 8f), offset = new float3(9f, 10f, 11f), octaveStep = new float3(12f, 13f, 14f) }).Should().Be( + """{"frequency":1,"stretch":2,"axis":{"x":3,"y":4,"z":5},"shift":{"x":6,"y":7,"z":8},"offset":{"x":9,"y":10,"z":11},"octaveStep":{"x":12,"y":13,"z":14}}"""); } [Test] @@ -72,6 +76,8 @@ namespace GameCult.Caching.Tests RoundTrip(new CultCellular(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8), 0.25f)); RoundTrip(new CultPhasor(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8))); RoundTrip(new SpanNoise(new float4(1, 2, 3, 4), 0.25f, 0.5f)); + RoundTrip(new SpanPath { midpoint = new float3(1f, 2f, 3f), velocity = new float3(4f, 5f, 6f), speed = 7f, acceleration = 8f, halfLength = 9f }); + RoundTrip(new SpanStage { frequency = 1f, stretch = 2f, axis = new float3(3f, 4f, 5f), shift = new float3(6f, 7f, 8f), offset = new float3(9f, 10f, 11f), octaveStep = new float3(12f, 13f, 14f) }); } [Test] diff --git a/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs b/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs index 7836663f..1882bb80 100644 --- a/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs +++ b/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs @@ -103,6 +103,8 @@ namespace GameCult.Caching.Tests RoundTrip(new CultCellular(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8), 0.25f)); RoundTrip(new CultPhasor(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8))); RoundTrip(new SpanNoise(new float4(1, 2, 3, 4), 0.25f, 0.5f)); + RoundTrip(new SpanPath { midpoint = new float3(1f, 2f, 3f), velocity = new float3(4f, 5f, 6f), speed = 7f, acceleration = 8f, halfLength = 9f }); + RoundTrip(new SpanStage { frequency = 1f, stretch = 2f, axis = new float3(3f, 4f, 5f), shift = new float3(6f, 7f, 8f), offset = new float3(9f, 10f, 11f), octaveStep = new float3(12f, 13f, 14f) }); } [Test] @@ -143,6 +145,19 @@ namespace GameCult.Caching.Tests "94CA3F800000CA40000000CA40400000CA40800000" + "CA3E800000" + "CA3F000000"); + Hex(new SpanPath { midpoint = new float3(1f, 2f, 3f), velocity = new float3(4f, 5f, 6f), speed = 7f, acceleration = 8f, halfLength = 9f }).Should().Be( + "95" + + "93CA3F800000CA40000000CA40400000" + + "93CA40800000CA40A00000CA40C00000" + + "CA40E00000CA41000000CA41100000"); + Hex(new SpanStage { frequency = 1f, stretch = 2f, axis = new float3(3f, 4f, 5f), shift = new float3(6f, 7f, 8f), offset = new float3(9f, 10f, 11f), octaveStep = new float3(12f, 13f, 14f) }).Should().Be( + "96" + + "CA3F800000" + + "CA40000000" + + "93CA40400000CA40800000CA40A00000" + + "93CA40C00000CA40E00000CA41000000" + + "93CA41100000CA41200000CA41300000" + + "93CA41400000CA41500000CA41600000"); } [Test] From ffb25b6657bdce713244ea8089c25aaba75b7f04 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 20:51:58 +0200 Subject: [PATCH 12/24] cultmath: span tests: MaxMagnitude as a loop (a lambda cannot capture a struct's this); previous commit did not build the test project Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index 8bd68193..f7db96c1 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -133,7 +133,13 @@ public sealed class SpanStageTests + Math.Sqrt(3.0) * scale * Norm() * path.speed * path.halfLength; } - public double MaxMagnitude(SpanPath path) => Enumerable.Range(0, Octaves).Max(o => Magnitude(path, o)); + public double MaxMagnitude(SpanPath path) + { + var largest = 0.0; + for (var o = 0; o < Octaves; o++) + largest = Math.Max(largest, Magnitude(path, o)); + return largest; + } // Octave o as a stage of its own: one octave at the scaled frequency, shift and offset. public Stage OneOctave(int octave) => new( From 3de67b9f9a9d95e74823c2cf548beaef833914f8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 21:25:22 +0200 Subject: [PATCH 13/24] cultmath: span tests: octave-sum identity, stretched-velocity and transformed-acceleration folds, zero axis with stretch below 1, bands lifted through octaveStep Kills the Stryker survivors of the first allowance commit: the velocity stretch sign, the norm at a zero axis, gain-weight and acceleration-factor arithmetic, the octaveStep term of S_o. Not yet built or run. Co-Authored-By: Claude Opus 5.5 --- .../tests/CultMath.Tests/SpanStageTests.cs | 132 +++++++++++++++--- 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index f7db96c1..815dc998 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -371,8 +371,10 @@ public sealed class SpanStageTests var reference = plain.Ridged(span.Path); var isotropic = (plain with { S = plain.S with { axis = None } }).Ridged(span.Path); var viaNoAxis = noAxis.Ridged(span.Path); + var narrow = (stage with { S = stage.S with { axis = None, stretch = 0.25f } }).Ridged(span.Path); Assert.True(SameBits(reference.field, isotropic.field) && reference.curvature == isotropic.curvature && reference.fold == isotropic.fold, "stretch 1 with an axis differs from no axis"); Assert.True(SameBits(isotropic.field, viaNoAxis.field) && isotropic.curvature == viaNoAxis.curvature && isotropic.fold == viaNoAxis.fold, "a zero axis with stretch 3 differs from the isotropic stage"); + Assert.True(SameBits(isotropic.field, narrow.field) && isotropic.curvature == narrow.curvature && isotropic.fold == narrow.fold, "a zero axis with stretch 0.25 differs from the isotropic stage"); var unit = stage with { S = stage.S with { axis = new float3(0.0f, 1.0f, 0.0f) } }; var scaled = stage with { S = stage.S with { axis = new float3(0.0f, 2.0f, 0.0f) } }; @@ -390,11 +392,12 @@ public sealed class SpanStageTests private static int Bucket(double magnitude) => magnitude < 1.0e2 ? 0 : magnitude < 1.0e3 ? 1 : magnitude < 1.0e4 ? 2 : magnitude < 1.0e5 ? 3 : magnitude < 1.0e6 ? 4 : 5; - private static Stage Banded(System.Random random, float offsetScale, int octaves) + // byStep lifts the coordinate through octaveStep (octave o adds o octaveStep) instead of offset. + private static Stage Banded(System.Random random, float offsetScale, int octaves, bool byStep) { var stage = Stage.Draw(random, octaves); if (offsetScale > 0.0f) - stage = stage with { S = stage.S with { offset = Vector(random, -offsetScale, offsetScale) } }; + stage = stage with { S = byStep ? stage.S with { octaveStep = Vector(random, -offsetScale, offsetScale) } : stage.S with { offset = Vector(random, -offsetScale, offsetScale) } }; return stage; } @@ -434,7 +437,7 @@ public sealed class SpanStageTests foreach (var halfLength in new[] { 0.02f, 0.5f, 8.0f }) for (var k = 0; k < 8; k++) { - var stage = Banded(random, offsetScale, 3); + var stage = Banded(random, offsetScale, 3, k % 2 == 1); var span = Span.Draw(random, Vector(random, -30.0f, 30.0f), halfLength, Uniform(random, 0.0f, 2.0f)); var result = ridged ? stage.Ridged(span.Path) : stage.Fbm(span.Path); buckets[Bucket(stage.MaxMagnitude(span.Path))]++; @@ -522,7 +525,7 @@ public sealed class SpanStageTests foreach (var offsetScale in OffsetScales) for (var k = 0; k < 60; k++) { - var stage = Banded(random, offsetScale, 3); + var stage = Banded(random, offsetScale, 3, k / 2 % 2 == 1); var halfLength = new[] { 0.02f, 0.1f, 0.5f, 2.0f }[random.Next(4)]; // Draw midpoints with small noise values so spans cross zero, as well as arbitrary ones. var q = Vector(random, -30.0f, 30.0f); @@ -601,24 +604,31 @@ public sealed class SpanStageTests } /// - /// The fold's acceleration term, alone: the midpoint velocity is zero and the acceleration points along - /// the gradient, against the sign of n, so n reaches zero and crosses it on the way to the span's ends - /// by acceleration only. Without the acceleration term (or with the gradient bound under the gradient - /// the path reaches) the departure from the midpoint branch exceeds fold. + /// The fold's acceleration term, alone, at the identity stage and under a transform (frequency 2.5, stretch + /// 0.5, shift and offset): the midpoint velocity is zero and the acceleration points along the gradient + /// (in q), against the sign of n, so n reaches zero and crosses it on the way to the span's ends by + /// acceleration only. Without the acceleration term, with the gradient bound under the gradient the path + /// reaches, or with the transform's norm f_o |M| misplaced in it, the departure from the midpoint branch + /// exceeds fold. /// - [Fact] - public void SpanFoldCoversAccelerationAlone() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpanFoldCoversAccelerationAlone(bool transformed) { - var random = new System.Random(0xACCE1); - var identity = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); + var random = new System.Random(transformed ? 0xACCE2 : 0xACCE1); + var stage = transformed + ? new Stage(new SpanStage { frequency = 2.5f, stretch = 0.5f, axis = new float3(0.0f, 1.0f, 0.0f), shift = new float3(0.3f, -1.2f, 2.1f), offset = new float3(5.0f, -3.0f, 7.0f), octaveStep = None }, 1, 2.0f, 0.5f) + : new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); + var m = stage.Matrix(); var cases = 0; var crossings = 0; var tightest = double.MaxValue; - for (var trial = 0; trial < 200_000 && cases < 150; trial++) + for (var trial = 0; trial < 400_000 && cases < 150; trial++) { var q = Vector(random, -60.0f, 60.0f); - var n = snoise_grad(q); - var gradient = new float3(n.x, n.y, n.z); + var n = snoise_grad(stage.Point(q, 0)); + var gradient = new float3((float)(m[0, 0] * n.x + m[0, 1] * n.y + m[0, 2] * n.z), (float)(m[1, 0] * n.x + m[1, 1] * n.y + m[1, 2] * n.z), (float)(m[2, 0] * n.x + m[2, 1] * n.y + m[2, 2] * n.z)); var halfLength = Uniform(random, 0.05f, 0.15f); var acceleration = Uniform(random, 4.0f, 12.0f); // n at the midpoint close enough to zero that the acceleration alone carries it across. @@ -626,11 +636,11 @@ public sealed class SpanStageTests continue; var c = -(float)Math.Sign(n.w) * acceleration * normalize(gradient); var span = new Span(new SpanPath { midpoint = q, velocity = None, speed = acceleration * halfLength, acceleration = acceleration, halfLength = halfLength }, c); - var fold = identity.Ridged(span.Path).fold; + var fold = stage.Ridged(span.Path).fold; var departure = 0.0; for (var s = 0; s < 64; s++) { - var value = (double)snoise(span.At(halfLength * (2.0f * s / 63 - 1.0f))); + var value = (double)snoise(stage.Point(span.At(halfLength * (2.0f * s / 63 - 1.0f)), 0)); departure = Math.Max(departure, Math.Abs(Math.Sign(n.w) * value - Math.Abs(value))); } @@ -643,6 +653,92 @@ public sealed class SpanStageTests } Assert.True(crossings >= 100, $"only {crossings} of {cases} paths crossed zero by acceleration alone"); - output.WriteLine($"IV-REPORT span fold, acceleration only: {crossings} of {cases} paths cross zero, tightest departure / fold {tightest:R}"); + output.WriteLine($"IV-REPORT span fold, acceleration only ({(transformed ? "transformed" : "identity")}): {crossings} of {cases} paths cross zero, tightest departure / fold {tightest:R}"); + } + + /// + /// The fold's velocity term under a stretch: the velocity runs along the stretched axis (M scales it by + /// 1/stretch), the midpoint is where n reaches zero inside the span, so the departure is about twice + /// |grad n . w| halfLength and a velocity stretched the wrong way (or not at all) puts fold under it. + /// + [Theory] + [InlineData(0.25f)] + [InlineData(0.5f)] + [InlineData(4.0f)] + public void SpanFoldCoversTheStretchedVelocity(float stretch) + { + var random = new System.Random(0x57E1C + (int)(stretch * 8.0f)); + var cases = 0; + var crossings = 0; + var tightest = double.MaxValue; + for (var trial = 0; trial < 400_000 && cases < 60; trial++) + { + var axis = UnitVector(random); + var stage = new Stage(new SpanStage { frequency = Uniform(random, 0.5f, 2.0f), stretch = stretch, axis = axis, shift = Vector(random, -4.0f, 4.0f), offset = Vector(random, -4.0f, 4.0f), octaveStep = None }, 1, 2.0f, 0.5f); + var q = Vector(random, -30.0f, 30.0f); + var velocity = axis * Uniform(random, 0.5f, 2.0f); + var halfLength = 0.05f; + var path = new SpanPath { midpoint = q, velocity = velocity, speed = length(velocity), acceleration = 0.0f, halfLength = halfLength }; + var n = snoise_grad(stage.Point(q, 0)); + var m = stage.Matrix(); + var w = new double[3]; + for (var i = 0; i < 3; i++) + w[i] = m[i, 0] * velocity.x + m[i, 1] * velocity.y + m[i, 2] * velocity.z; + var along = Math.Abs(n.x * w[0] + n.y * w[1] + n.z * w[2]); + if (along < 2.0 || Math.Abs(n.w) > 0.5 * along * halfLength) + continue; + var fold = stage.Ridged(path).fold; + var departure = 0.0; + for (var s = 0; s < 64; s++) + { + var t = halfLength * (2.0f * s / 63 - 1.0f); + var value = (double)snoise(stage.Point(q + velocity * t, 0)); + departure = Math.Max(departure, Math.Abs(Math.Sign(n.w) * value - Math.Abs(value))); + } + + if (departure > 0.0) + crossings++; + Assert.True(departure <= fold * (1.0 + 1.0e-3), $"stage {stage}, q {q}: the branch departure is {departure:R}, above fold {fold:R}"); + if (departure > 0.0) + tightest = Math.Min(tightest, departure / fold); + cases++; + } + + Assert.True(crossings >= 30, $"only {crossings} of {cases} paths crossed zero along the stretched velocity"); + output.WriteLine($"IV-REPORT span fold, stretched velocity {stretch}: {crossings} of {cases} paths cross zero, tightest departure / fold {tightest:R}"); + } + + /// + /// Both bounds are sums over the octaves, weighted by |gain|^o: for a stage of several octaves, + /// curvature and fold equal the weighted sum of the bounds of each octave taken as a stage of its own, + /// to within the rounding of forming the octave's coordinate another way. A weight misplaced (gain + /// instead of |gain|, a division for a product), a dropped octave or a mis-scaled octave fails. + /// + [Fact] + public void SpanBoundsAreSumsOverTheOctaves() + { + var random = new System.Random(0x0C7A5); + var compared = 0; + for (var k = 0; k < 80; k++) + { + var stage = Stage.DrawSmooth(random, 300.0f, 90.0f, 5, 2); + var span = Span.Draw(random, Vector(random, -30.0f, 30.0f), new[] { 0.02f, 0.1f, 0.5f }[k % 3], Uniform(random, 0.0f, 2.0f)); + var whole = stage.Ridged(span.Path); + double curvature = 0.0, fold = 0.0; + for (var o = 0; o < stage.Octaves; o++) + { + var one = stage.OneOctave(o).Ridged(span.Path); + var weight = Math.Abs(stage.Amplitude(o)); + curvature += weight * one.curvature; + fold += weight * one.fold; + } + + Assert.InRange(whole.curvature, curvature * (1.0 - 5.0e-3) - 1.0e-4, curvature * (1.0 + 5.0e-3) + 1.0e-4); + Assert.InRange(whole.fold, fold * (1.0 - 5.0e-3) - 1.0e-4, fold * (1.0 + 5.0e-3) + 1.0e-4); + if (fold > 1.0e-3) + compared++; + } + + Assert.True(compared >= 20, $"only {compared} of 80 stages had a fold to compare"); } } From 8939af134fc3e60bef759cbcf15fdf7607837ad5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 23:12:41 +0200 Subject: [PATCH 14/24] cultmath: span tests: long path from rest, near-zero midpoint under a point span and a loose speed bound, fold's float arm read from span_ridged's own field The fold test's float arm no longer re-spells the octave coordinate (OneOctave): each octave's float value is the difference of span_fbm / span_ridged at o + 1 and o octaves, with a stated bound on what the accumulating float sum loses. New fixed span at halfLength 6 pins the Gl reach operator (nu / halfLength fails); a near-zero midpoint on a stationary path (halfLength 0, and halfLength 1 under a loose speed bound) pins the halfLength-0 fold rule and S_o's reach term. Builds: yes (to be confirmed by the Yggdrasil job). Co-Authored-By: Claude Sonnet 5.5 --- .../tests/CultMath.Tests/SpanStageTests.cs | 164 +++++++++++++++--- 1 file changed, 138 insertions(+), 26 deletions(-) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index 815dc998..684e7789 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -80,6 +80,10 @@ public sealed class SpanStageTests public SpanNoise Ridged(SpanPath path) => span_ridged(path, S, Octaves, Lacunarity, Gain); + public SpanNoise Fbm(SpanPath path, int octaves) => span_fbm(path, S, octaves, Lacunarity, Gain); + + public SpanNoise Ridged(SpanPath path, int octaves) => span_ridged(path, S, octaves, Lacunarity, Gain); + // M = frequency (I + (1/stretch - 1) a a^T) in double, a the normalised axis (none for a zero axis). public double[,] Matrix() { @@ -505,6 +509,82 @@ public sealed class SpanStageTests Assert.True(end > 10.5 && result.curvature >= end, $"the end-of-span norm is {end:R} and curvature {result.curvature:R}: the span no longer pins the local tier"); } + // Octave o's amplitude-weighted value (n_o gain^o, or gain^o (1 - |n_o|) for ridged) at `point`, read from + // production: the difference of span_fbm's (span_ridged's) field at o + 1 and at o octaves. + private static double[] OctaveValues(Stage stage, float3 point, bool ridged) + { + var path = PathAt(point); + var values = new double[stage.Octaves]; + var previous = 0.0; + for (var k = 1; k <= stage.Octaves; k++) + { + var total = ridged ? stage.Ridged(path, k).field.w : stage.Fbm(path, k).field.w; + values[k - 1] = total - previous; + previous = total; + } + + return values; + } + + // The departure of the octave sum from its midpoint branch, sum_o gain^o (s_o n_o - |n_o|) with s_o the + // sign of n_o at the midpoint, peaked over `samples` points of the span: Float for the field span_ridged + // and span_fbm themselves evaluate (read octave by octave through OctaveValues), Exact for the double + // kernel. Rounding bounds what the float differences of the accumulating sums can lose. + private static (double Float, double Exact, double Rounding) BranchDeparture(Stage stage, Span span, int samples) + { + var amplitudes = new float[stage.Octaves]; + var weight = 1.0f; + var sum = 0.0; + for (var o = 0; o < stage.Octaves; o++) + { + amplitudes[o] = weight; + sum += Math.Abs(weight); + weight *= stage.Gain; + } + + var middle = OctaveValues(stage, span.Path.midpoint, false); + var signs = Enumerable.Range(0, stage.Octaves).Select(o => Math.Sign(middle[o]) * Math.Sign(amplitudes[o])).ToArray(); + var peakFloat = 0.0; + var peakExact = 0.0; + for (var s = 0; s < samples; s++) + { + var t = span.Path.halfLength * (2.0f * s / (samples - 1) - 1.0f); + var point = span.At(t); + var n = OctaveValues(stage, point, false); + var r = OctaveValues(stage, point, true); + var departureFloat = 0.0; + var departureExact = 0.0; + for (var o = 0; o < stage.Octaves; o++) + { + departureFloat += signs[o] * n[o] - (amplitudes[o] - r[o]); + var x = stage.PointD(point, o); + var exact = NoiseBoundTests.ValueDouble(x[0], x[1], x[2]); + departureExact += stage.Amplitude(o) * (signs[o] * exact - Math.Abs(exact)); + } + + peakFloat = Math.Max(peakFloat, Math.Abs(departureFloat)); + peakExact = Math.Max(peakExact, Math.Abs(departureExact)); + } + + return (peakFloat, peakExact, stage.Octaves * Math.Pow(2.0, -22) * sum); + } + + // Newton steps from q to the exact kernel's n = 0 (the float gradient steers, the double value decides). + private static float3 ProjectToZero(float3 q) + { + for (var iteration = 0; iteration < 4; iteration++) + { + var n = snoise_grad(q); + var gg = (double)n.x * n.x + (double)n.y * n.y + (double)n.z * n.z; + if (gg < 1.0e-12) + break; + var v = NoiseBoundTests.ValueDouble(q.x, q.y, q.z); + q = new float3((float)(q.x - v * n.x / gg), (float)(q.y - v * n.y / gg), (float)(q.z - v * n.z / gg)); + } + + return q; + } + /// /// span-bounds-enclose, fold: for span_ridged, the value's departure from the midpoint branch (each /// octave's gain^o (s n - |n|), s the sign of n at the midpoint, summed) never exceeds fold at 64 points of @@ -548,32 +628,9 @@ public sealed class SpanStageTests buckets[Bucket(stage.MaxMagnitude(span.Path))]++; var fold = stage.Ridged(span.Path).fold; Assert.Equal(0.0f, stage.Fbm(span.Path).fold); - var signs = Enumerable.Range(0, stage.Octaves).Select(o => Math.Sign(stage.OneOctave(o).Fbm(PathAt(q)).field.w)).ToArray(); - var peakFloat = 0.0; - var peakExact = 0.0; - for (var s = 0; s < 64; s++) - { - var t = halfLength * (2.0f * s / 63 - 1.0f); - var point = span.At(t); - var departureFloat = 0.0; - var departureExact = 0.0; - for (var o = 0; o < stage.Octaves; o++) - { - var octave = stage.OneOctave(o); - var nFloat = (double)octave.Fbm(PathAt(point)).field.w; - var absFloat = 1.0 - octave.Ridged(PathAt(point)).field.w; - departureFloat += stage.Amplitude(o) * (signs[o] * nFloat - absFloat); - var x = stage.PointD(point, o); - var nExact = NoiseBoundTests.ValueDouble(x[0], x[1], x[2]); - departureExact += stage.Amplitude(o) * (signs[o] * nExact - Math.Abs(nExact)); - } - - peakFloat = Math.Max(peakFloat, Math.Abs(departureFloat)); - peakExact = Math.Max(peakExact, Math.Abs(departureExact)); - Assert.True(Math.Abs(departureFloat) <= fold * (1.0 + 1.0e-3) + 1.0e-6, $"stage {stage}, span {span}: the float branch departure at t = {t:R} is {departureFloat:R}, above fold {fold:R}"); - Assert.True(Math.Abs(departureExact) <= fold * (1.0 + 1.0e-3) + 1.0e-6, $"stage {stage}, span {span}: the exact branch departure at t = {t:R} is {departureExact:R}, above fold {fold:R}"); - } - + var (peakFloat, peakExact, rounding) = BranchDeparture(stage, span, 64); + Assert.True(peakFloat <= fold * (1.0 + 1.0e-3) + 1.0e-6 + rounding, $"stage {stage}, span {span}: the float branch departure is {peakFloat:R}, above fold {fold:R}"); + Assert.True(peakExact <= fold * (1.0 + 1.0e-3) + 1.0e-6, $"stage {stage}, span {span}: the exact branch departure is {peakExact:R}, above fold {fold:R}"); if (fold > 0.0) crossing++; largest = Math.Max(largest, Math.Max(peakFloat, peakExact)); @@ -603,6 +660,61 @@ public sealed class SpanStageTests Assert.True(found >= 5, $"only {found} calm midpoints found"); } + /// + /// The Gl reach term over a long span: a straight path from rest along -s grad n (s the sign of n at the + /// midpoint), identity stage, one octave, half-length 6, found by an audit and kept. The path reaches + /// n = 0 and the branch departure (about 0.387) is covered only if the gradient bound grows by + /// Hl nu halfLength over the span; a reach that shrinks with halfLength (nu / halfLength) puts fold at + /// 0.384 and fails here, as does any reach right only for half-lengths up to sqrt 2. + /// + [Fact] + public void SpanFoldEnclosesTheLongPathFromRest() + { + var stage = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); + const float halfLength = 6.0f; + const float acceleration = 0.0055555557f; + var span = new Span(new SpanPath { midpoint = new float3(7.634474f, 24.228996f, -3.7595012f), velocity = None, speed = acceleration * halfLength, acceleration = acceleration, halfLength = halfLength }, new float3(-0.9375727f, -0.07116994f, 0.3404294f) * acceleration); + var fold = stage.Ridged(span.Path).fold; + var (departureFloat, departureExact, rounding) = BranchDeparture(stage, span, 129); + Assert.True(Math.Max(departureFloat, departureExact) > 0.3, $"the branch departure is {Math.Max(departureFloat, departureExact):R}: the span no longer crosses zero"); + Assert.True(departureFloat <= fold * (1.0 + 1.0e-3) + 1.0e-6 + rounding, $"the float branch departure is {departureFloat:R}, above fold {fold:R}"); + Assert.True(departureExact <= fold * (1.0 + 1.0e-3) + 1.0e-6, $"the exact branch departure is {departureExact:R}, above fold {fold:R}"); + } + + /// + /// A midpoint within float noise of n = 0, on a path that does not move: halfLength 0 (a point), and + /// halfLength 1 under a speed bound far above the path's motion (speed is an upper bound, so that is a + /// legal SpanPath, and composed chains pass loose ones). The exact kernel's sign at such a midpoint is + /// opposite to the float one in about a fifth of the draws, so fold must still cover the exact + /// departure there: a fold forced to 0 at halfLength 0, or an S_o whose reach term drives e_n below + /// zero under the loose bound, leaves it 0 and fails. span_fbm's fold is 0 throughout. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpanFoldEnclosesTheExactBranchAtANearZeroMidpoint(bool loose) + { + var random = new System.Random(loose ? 0x100E : 0x4A11); + var stage = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); + var crossing = 0; + foreach (var scale in new[] { 30.0f, 300.0f, 3000.0f }) + for (var k = 0; k < 300; k++) + { + var q = ProjectToZero(Vector(random, -scale, scale)); + var speed = loose ? 4.0f * (abs(q.x) + abs(q.y) + abs(q.z) + 20.0f) : 0.0f; + var span = new Span(new SpanPath { midpoint = q, velocity = None, speed = speed, acceleration = 0.0f, halfLength = loose ? 1.0f : 0.0f }, None); + var fold = stage.Ridged(span.Path).fold; + var (departureFloat, departureExact, _) = BranchDeparture(stage, span, 2); + Assert.Equal(0.0f, stage.Fbm(span.Path).fold); + Assert.True(departureExact <= fold * (1.0 + 1.0e-3) + 1.0e-9, $"midpoint {q}, speed {speed:R}: the exact branch departure is {departureExact:R}, above fold {fold:R}"); + Assert.True(departureFloat <= fold * (1.0 + 1.0e-3) + 1.0e-9, $"midpoint {q}, speed {speed:R}: the float branch departure is {departureFloat:R}, above fold {fold:R}"); + if (departureExact > 0.0) + crossing++; + } + + Assert.True(crossing >= 50, $"only {crossing} of 900 midpoints had an exact sign opposite to the float one: the check has no teeth"); + } + /// /// The fold's acceleration term, alone, at the identity stage and under a transform (frequency 2.5, stretch /// 0.5, shift and offset): the midpoint velocity is zero and the acceleration points along the gradient From 15730d4e995ae323fe8bf84a691855cafe359b5b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 23:13:30 +0200 Subject: [PATCH 15/24] cultmath: span tests: the float arm of the near-zero midpoint test carries the accumulation bound of its octave reads; previous commit's new test failed on 7e-9 of float noise (builds, one new test red) Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index 684e7789..a7ed48b3 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -704,10 +704,10 @@ public sealed class SpanStageTests var speed = loose ? 4.0f * (abs(q.x) + abs(q.y) + abs(q.z) + 20.0f) : 0.0f; var span = new Span(new SpanPath { midpoint = q, velocity = None, speed = speed, acceleration = 0.0f, halfLength = loose ? 1.0f : 0.0f }, None); var fold = stage.Ridged(span.Path).fold; - var (departureFloat, departureExact, _) = BranchDeparture(stage, span, 2); + var (departureFloat, departureExact, rounding) = BranchDeparture(stage, span, 2); Assert.Equal(0.0f, stage.Fbm(span.Path).fold); Assert.True(departureExact <= fold * (1.0 + 1.0e-3) + 1.0e-9, $"midpoint {q}, speed {speed:R}: the exact branch departure is {departureExact:R}, above fold {fold:R}"); - Assert.True(departureFloat <= fold * (1.0 + 1.0e-3) + 1.0e-9, $"midpoint {q}, speed {speed:R}: the float branch departure is {departureFloat:R}, above fold {fold:R}"); + Assert.True(departureFloat <= fold * (1.0 + 1.0e-3) + 1.0e-9 + rounding, $"midpoint {q}, speed {speed:R}: the float branch departure is {departureFloat:R}, above fold {fold:R}"); if (departureExact > 0.0) crossing++; } From 98f4c44170e49af5460f3b5703601221d1fbdb18 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 23:15:33 +0200 Subject: [PATCH 16/24] cultmath: span rounding margins stated as observed samples with seeds, not "at most" bounds (comment and design.md); no allowance changed Two samples of the probe: the cut's own (seed not recorded) and an audit's (seed 0x501) give 0.057/0.046/0.075 and 0.064/0.0485/0.057 of e_n/e_g/e_h; a new seed moves them. Comment only in math.Span.cs: builds. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/docs/design.md | 11 +++++++---- packages/cultmath/src/CultMath/math.Span.cs | 8 ++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index b7edc78b..103cfa5b 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -850,10 +850,13 @@ halfLength` is the sum of the magnitudes the octave coordinate's roundings are r to, and `e_n = 2^-14 + L 2^-20 S_o` (value), `e_g = 2^-12 + H 2^-20 S_o` (gradient length) and `e_h = 2^-10 + T 2^-20 S_o` (Frobenius norm of the Hessian), with `L`, `H`, `T` the three pins; they add no pin. Measured against a double kernel with the float -hash (6000 points per band, `|x|` from 2^-2 to 2^23), the error is at most 0.057 of -`e_n`, 0.046 of `e_g` and 0.075 of `e_h` (the last at the smallest band, where the -absolute Hessian error is 2.3e-5 and the absolute term carries it). Errors grow -linearly in `|x|`: the value's is 2e-4 at 2^8, 1.5e-2 at 2^14 and 0.94 at 2^20. +hash (6000 points per band, `|x|` from 2^-2 to 2^23), two samples observed the error's +maxima as fractions of `e_n`, `e_g` and `e_h`: 0.057, 0.046 and 0.075 in the cut's own +sample (seed not recorded; the 0.075 is against `T 2^-20 |x|_1` alone, at the smallest +band, where the absolute Hessian error is 2.3e-5 and the absolute term carries it), and +0.064, 0.0485 and 0.057 in an audit's (seed 0x501). They are samples, not bounds: a new +seed moves them. They say the allowances enclose the error with a margin of about 15. +Errors grow linearly in `|x|`: the value's is 2e-4 at 2^8, 1.5e-2 at 2^14 and 0.94 at 2^20. Rounding in the Frobenius-to-operator-norm step needs no allowance: the Frobenius norm exceeds the operator norm. diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 73bf199e..6dcdd180 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -189,8 +189,12 @@ public static partial class math // gradient's own derivative's pin) and the Hessian's Frobenius norm's e_h = 2^-10 + T 2^-20 S_o (T is // the Hessian's derivative's pin): af_snoise's absolute terms for the first two, and 2^-10 for the // Hessian's, which the kernel's rounding at small coordinates (2.3e-5 absolute, measured against a - // double kernel) sits well under. The same probe (6000 points per band, |x| from 2^-2 to 2^23) puts - // the error at most 0.057, 0.046 and 0.075 of e_n, e_g and e_h. No new pin enters. + // double kernel) sits well under. Two samples of that probe (6000 points per band, |x| from 2^-2 to + // 2^23) observed the error's maxima, as fractions of e_n, e_g and e_h: 0.057, 0.046 and 0.075 in the + // cut's own sample (seed not recorded; the 0.075 is against T 2^-20 |x|_1 alone, at the smallest + // band), and 0.064, 0.0485 and 0.057 in an audit's (seed 0x501). They are samples, not bounds: a new + // seed moves them. What they say is that the allowances enclose the error with a margin of about 15. + // No new pin enters. // // Domain: no coordinate range is checked, clamped or refused. The allowances grow with S_o, so // curvature saturates at H (true everywhere) by S_o of about 1.1e5, and fold turns positive for every From b2852304b1ba0e3fc7c077188feffde414f38182 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 01:01:54 +0200 Subject: [PATCH 17/24] cultmath: flat integral_clamp_linear returns +0 for a zero-length span; HLSL guard, lo-normalise and pin tests behavioural; owned dxc compile script The b == 0 branch now guards t1 != t0 like the sloped branch (C# and both HLSL copies), so an infinite bound times a zero length no longer forms NaN. Edge-grid mirror test and a counting-clamp shadow test replace the text-order check; the four pin tests assert equality with their measured expression; tools/compile-hlsl.ps1 is the one dxc compile verdict for the HLSL copies. Builds. CultMath.glsl and glsl-parity.json are not yet regenerated, so CommittedGlslEqualsLowering and GoldenFixtureMatchesCSharp are red at this commit; the next commit regenerates them. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/README.md | 3 + packages/cultmath/docs/design.md | 5 +- packages/cultmath/shaders/CultMath.hlsl | 2 +- packages/cultmath/src/CultMath/math.Span.cs | 2 +- .../tests/CultMath.Tests/GlslMirrorTests.cs | 63 +++++++++++++---- .../HlslSourceCompatibilityTests.cs | 68 +++++++++++++++++-- .../tests/CultMath.Tests/NoiseBoundTests.cs | 30 ++++---- .../CultMath.Tests/SpanPrimitiveTests.cs | 11 ++- packages/cultmath/tools/compile-hlsl.ps1 | 39 +++++++++++ packages/cultmath/tools/compile-hlsl.ps1.meta | 7 ++ .../Shaders/CultMath.hlsl | 2 +- 11 files changed, 192 insertions(+), 40 deletions(-) create mode 100644 packages/cultmath/tools/compile-hlsl.ps1 create mode 100644 packages/cultmath/tools/compile-hlsl.ps1.meta diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md index 61351da3..0111949c 100644 --- a/packages/cultmath/README.md +++ b/packages/cultmath/README.md @@ -167,6 +167,7 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`: ```powershell .\packages\cultmath\tools\get-dxc.ps1 +.\packages\cultmath\tools\compile-hlsl.ps1 .\packages\cultmath\tools\get-spirv-cross.ps1 .\packages\cultmath\tools\compile-hlsl-spirv.ps1 ` -ShaderPath E:\Projects\Odin\crates\muninn-move-tracker\shaders\MoveSphereCandidate.comp.hlsl ` @@ -177,6 +178,8 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`: -OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.metal ``` +`compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC. + DXC emits Vulkan SPIR-V with `-spirv`. `-TargetEnv vulkan1.2` is exposed by the script, but the current Windows DXC release compiled this shader successfully with DXC's default Vulkan target while rejecting the dotted target-env value. diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index 103cfa5b..d11e1518 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -112,8 +112,9 @@ local) and writes through a swizzle component (`v.xy.x = s`, which C# rejects as modifying a temporary). The HLSL side is checked too: `tools/get-dxc.ps1` fetches Microsoft's -DirectXShaderCompiler, and -`dxc -T lib_6_3 -HV 2021 shaders/CultMath.hlsl` must compile cleanly. +DirectXShaderCompiler (x64), and `tools/compile-hlsl.ps1` compiles both copies of +`CultMath.hlsl` with it (`dxc -T lib_6_3 -HV 2021`); the script throws when dxc cannot +launch or exits non-zero, so a stale exit code never reads as a pass. The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from 0). It proves the text of `CultMath.hlsl` computes the same float32 results as diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index c454773c..c480acc7 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -304,7 +304,7 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float { lo = min(lo, hi); if (b == 0.0) - return clamp(a, lo, hi) * (t1 - t0); + return t1 != t0 ? clamp(a, lo, hi) * (t1 - t0) : 0.0; float direction = t1 < t0 ? -1.0 : 1.0; float start = min(t0, t1); diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 6dcdd180..068dfd0c 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -112,7 +112,7 @@ public static partial class math { lo = min(lo, hi); if (b == 0.0f) - return clamp(a, lo, hi) * (t1 - t0); + return t1 != t0 ? clamp(a, lo, hi) * (t1 - t0) : 0.0f; var direction = t1 < t0 ? -1.0f : 1.0f; var start = min(t0, t1); diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs index 9fb45199..33665d75 100644 --- a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs @@ -45,24 +45,50 @@ public sealed class GlslMirrorTests } } + // Counting stand-ins for the mirror's clamp, one per overload, declared in the mirror's class so they + // hide math.clamp for every function in it. Each counts a call whose minimum exceeds its maximum in any + // component and forwards to math.clamp. (The int vector overloads only forward: no mirror function + // clamps integer vectors with swapped bounds, and counting them would be unreachable.) + private const string CountingClampShadows = @" +public static int ClampMinAboveMax; +static float clamp(float x, float a, float b) { if (a > b) ClampMinAboveMax++; return math.clamp(x, a, b); } +static float2 clamp(float2 x, float2 a, float2 b) { if (a.x > b.x || a.y > b.y) ClampMinAboveMax++; return math.clamp(x, a, b); } +static float3 clamp(float3 x, float3 a, float3 b) { if (a.x > b.x || a.y > b.y || a.z > b.z) ClampMinAboveMax++; return math.clamp(x, a, b); } +static float4 clamp(float4 x, float4 a, float4 b) { if (a.x > b.x || a.y > b.y || a.z > b.z || a.w > b.w) ClampMinAboveMax++; return math.clamp(x, a, b); } +static int clamp(int x, int a, int b) { if (a > b) ClampMinAboveMax++; return math.clamp(x, a, b); } +static int2 clamp(int2 x, int2 a, int2 b) { if (a.x > b.x || a.y > b.y) ClampMinAboveMax++; return math.clamp(x, a, b); } +static int3 clamp(int3 x, int3 a, int3 b) { if (a.x > b.x || a.y > b.y || a.z > b.z) ClampMinAboveMax++; return math.clamp(x, a, b); } +static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > b.z || a.w > b.w) ClampMinAboveMax++; return math.clamp(x, a, b); } +"; + /// - /// The lowering keeps HLSL's statement order, and GLSL leaves clamp(x, minVal, maxVal) undefined for - /// minVal above maxVal, so integral_clamp_linear must normalise lo = min(lo, hi) before any clamp in - /// its body can see it: in the committed HLSL and the committed GLSL alike, the normalisation precedes - /// the first clamp. + /// The lowering keeps HLSL's statement order (CommittedGlslEqualsLowering pins the GLSL as that + /// lowering), and GLSL leaves clamp(x, minVal, maxVal) undefined for minVal above maxVal, so no clamp + /// reached by cultmath_integral_clamp_linear may see lo above hi, on any path and for any spelling of the + /// normalisation. The mirror is compiled with counting clamps and the function is run over the edge grid + /// (lo above hi, infinite bounds, zero-length spans, b = 0 and b != 0): the count of such clamp calls + /// stays 0 after every call. /// [Fact] - public void IntegralClampLinearNormalisesLoBeforeAnyClamp() + public void IntegralClampLinearNeverClampsWithMinAboveMax() { - foreach (var (file, text) in Lowered.Where(l => l.File == "CultMath.glsl").Concat(new[] { ("CultMath.hlsl", Hlsl) })) + var (assembly, errors) = HlslSourceCompatibilityTests.CompileShaderMirror(CountingClampShadows); + Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); + var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!; + var shader = Activator.CreateInstance(shaderType); + var count = shaderType.GetField("ClampMinAboveMax")!; + var function = shaderType.GetMethod("cultmath_integral_clamp_linear", BindingFlags.Instance | BindingFlags.NonPublic)!; + + var bad = new List(); + foreach (var c in HlslSourceCompatibilityTests.IntegralClampLinearEdgeGrid) { - var start = text.IndexOf("cultmath_integral_clamp_linear(float a", StringComparison.Ordinal); - Assert.True(start >= 0, $"{file} has no cultmath_integral_clamp_linear"); - var body = text[start..text.IndexOf("}", start, StringComparison.Ordinal)]; - var normalise = body.IndexOf("lo = min(lo, hi);", StringComparison.Ordinal); - var firstClamp = body.IndexOf("clamp(", StringComparison.Ordinal); - Assert.True(normalise >= 0 && normalise < firstClamp, $"{file}: lo = min(lo, hi) must come before the first clamp in cultmath_integral_clamp_linear"); + count.SetValue(null, 0); + function.Invoke(shader, c.Cast().ToArray()); + if ((int)count.GetValue(null)! > 0) + bad.Add($"({string.Join(", ", c)})"); } + + Assert.True(bad.Count == 0, $"{bad.Count} cases clamp with the minimum above the maximum:{Environment.NewLine}{string.Join(Environment.NewLine, bad.Take(12))}"); } // Identifier boundaries: cultmath_lerp is a CultMath function, lerp( is HLSL. @@ -675,6 +701,19 @@ public sealed class GlslMirrorTests _ => new object[] { a, slope, float.NegativeInfinity, float.PositiveInfinity, t0, t1 }, }; }), + // Zero-length spans (t1 = t0 three times in four) with a flat or sloped line, and infinite clamp bounds + // on either side or both: an infinite bound times a zero length must give +0, and a flat line clamped + // to an infinite bound over a nonzero span is infinite, never NaN. + new Family("float cultmath_integral_clamp_linear(float, float, float, float, float, float)", "ulp-bounded, zero-length spans and flat infinite clamps", + r => + { + float Line() => r.Next(3) switch { 0 => 0.0f, 1 => -0.0f, _ => Uniform(r, -4.0f, 4.0f) }; + float Bound() => r.Next(3) switch { 0 => float.NegativeInfinity, 1 => float.PositiveInfinity, _ => Uniform(r, -2.0f, 2.0f) }; + var (a, b, lo, hi) = (Line(), Line(), Bound(), Bound()); + var t0 = Uniform(r, -3.0f, 3.0f); + var t1 = r.Next(4) != 0 ? t0 : Uniform(r, -3.0f, 3.0f); + return new object[] { a, b, lo, hi, t0, t1 }; + }), }).ToArray(); // The fixture text, each case's arguments given by family and point index. diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index ea2fd145..68cfce95 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -84,12 +84,13 @@ public sealed class HlslSourceCompatibilityTests } } - private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN }; + // Special values every mirror argument takes: zeros, ties, negatives, large values, NaN and both infinities. + private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN, float.PositiveInfinity, float.NegativeInfinity }; /// /// Every mirror function with a C# math counterpart returns the same bits (any NaN equals any NaN; /// -0 differs from 0) over random inputs, every special value in every argument (ties, zeros, - /// negatives, NaN, large values, integer lattice points), special pairs, and vectors whose first two + /// negatives, NaN, infinities, large values, integer lattice points), special pairs, and vectors whose first two /// components tie (x0.x == x0.y in snoise). This proves the text of CultMath.hlsl computes the same /// float32 results as C# math on the CPU. It does not prove GPU agreement: driver sin precision alone /// makes cultmath_hash and value noise differ bit for bit on hardware. The mirror's intrinsics are C# @@ -178,6 +179,56 @@ public sealed class HlslSourceCompatibilityTests Assert.Equal(120, compared.Count); } + /// + /// The 1500 arguments (a, b, lo, hi, t0, t1) at which integral_clamp_linear's zero-length rule, its lo + /// normalisation and its infinite bounds all bite: a in {-2.5, 0, 1.5}, b in {-1.5, -0, 0, 2}, lo in + /// {-inf, -1, 0.5, 3, +inf}, hi in {-inf, -0.5, 1, 4, +inf}, and (t0, t1) in {(0, 0), (1.25, 1.25), (-1, 2), + /// (2, -1), (0, 1e-30)}. One spelling, read by the mirror grid test and by the GLSL clamp shadow test. + /// + internal static IReadOnlyList IntegralClampLinearEdgeGrid { get; } = BuildIntegralClampLinearEdgeGrid(); + + private static List BuildIntegralClampLinearEdgeGrid() + { + var inf = float.PositiveInfinity; + var grid = new List(); + foreach (var a in new[] { -2.5f, 0.0f, 1.5f }) + foreach (var b in new[] { -1.5f, -0.0f, 0.0f, 2.0f }) + foreach (var lo in new[] { -inf, -1.0f, 0.5f, 3.0f, inf }) + foreach (var hi in new[] { -inf, -0.5f, 1.0f, 4.0f, inf }) + foreach (var (t0, t1) in new[] { (0.0f, 0.0f), (1.25f, 1.25f), (-1.0f, 2.0f), (2.0f, -1.0f), (0.0f, 1.0e-30f) }) + grid.Add(new[] { a, b, lo, hi, t0, t1 }); + return grid; + } + + /// + /// The compiled mirror's cultmath_integral_clamp_linear returns C#'s bits on every edge-grid case, and C#'s + /// result is never NaN there. The generic case set catches each of the function's guards (the held and + /// kept stretch guards, the flat branch's zero-length guard) only by its rotation: with the kept guard + /// removed it finds 0 mismatches, with the held guard removed 1 once infinities join the specials, and the + /// edge grid finds 246 for either every time. + /// + [Fact] + public void IntegralClampLinearMirrorMatchesCSharpOnTheEdgeGrid() + { + var (assembly, errors) = CompileShaderMirror(); + Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); + var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!; + var shader = Activator.CreateInstance(shaderType); + var mirror = ShaderFunction(shaderType, "cultmath_integral_clamp_linear", Enumerable.Repeat(typeof(float), 6).ToArray())!; + + var mismatches = new List(); + foreach (var c in IntegralClampLinearEdgeGrid) + { + var expected = integral_clamp_linear(c[0], c[1], c[2], c[3], c[4], c[5]); + var actual = (float)mirror.Invoke(shader, c.Cast().ToArray())!; + Assert.False(float.IsNaN(expected), $"C# returns NaN at ({string.Join(", ", c)})"); + if (BitConverter.SingleToInt32Bits(expected) != BitConverter.SingleToInt32Bits(actual)) + mismatches.Add($"({string.Join(", ", c)}): C# {expected:R}, mirror {actual:R}"); + } + + Assert.True(mismatches.Count == 0, $"{mismatches.Count} of {IntegralClampLinearEdgeGrid.Count} cases differ:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}"); + } + /// /// The generic comparison above passes about a hundred inputs through cultmath_phacelle, and /// its prune only changes an output for the rare cell whose weight is nonzero but whose lower @@ -448,8 +499,11 @@ public sealed class HlslSourceCompatibilityTests : string.Empty); } - /// The documented HLSL-to-C# transformations, and nothing else. - internal static string TransformHlslToCSharp(string hlsl) + /// + /// The documented HLSL-to-C# transformations, and nothing else. is C# source a + /// test supplies, placed first in the class body: it is not a transformation of the HLSL. + /// + internal static string TransformHlslToCSharp(string hlsl, string members = "") { // 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include. var source = Regex.Replace(hlsl, @"(?m)^\s*#(ifndef|define|endif)\b.*$", string.Empty); @@ -471,13 +525,13 @@ public sealed class HlslSourceCompatibilityTests m.Groups[1].Value + Regex.Replace(m.Groups[2].Value, @"(?m)^(\s*)(\w+\s+\w+;)", "$1public $2") + m.Groups[3].Value); // 6. The file body is wrapped in a class; C# has no free functions. - return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + source + "\n}\n"; + return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + members + source + "\n}\n"; } - internal static (Assembly? Assembly, IReadOnlyList Errors) CompileShaderMirror() + internal static (Assembly? Assembly, IReadOnlyList Errors) CompileShaderMirror(string members = "") { var hlsl = ReadShaderSource(FindCultMathRoot()); - var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl)); + var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl, members)); var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) .Split(Path.PathSeparator) .Append(typeof(math).Assembly.Location) diff --git a/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs b/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs index 0962a357..f97d5bfe 100644 --- a/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs @@ -469,8 +469,8 @@ public sealed class NoiseBoundTests /// /// Pins SNOISE_LIPSCHITZ: no |snoise_grad| over 1e5 seeded points exceeds it, and the ascent from - /// MeasureLipschitz's witness reaches a maximum that, times 1.10, is the constant to within half a - /// percent, so a constant lowered (or raised) by 1% fails here. + /// MeasureLipschitz's witness reaches a maximum that, times 1.10, equals the constant bit for bit + /// (the expression MeasureLipschitz prints), so any moved constant fails here. /// [Fact] public void LipschitzConstantPinsSampledGradients() @@ -479,8 +479,8 @@ public sealed class NoiseBoundTests Assert.True(sampled <= SNOISE_LIPSCHITZ, $"|snoise_grad| reaches {sampled:R}, above SNOISE_LIPSCHITZ {SNOISE_LIPSCHITZ:R}"); var refined = AscendGradientNorm(LipschitzWitness); Assert.True(refined <= SNOISE_LIPSCHITZ, $"|snoise_grad| reaches {refined:R}, above SNOISE_LIPSCHITZ {SNOISE_LIPSCHITZ:R}"); - var ratio = SNOISE_LIPSCHITZ / (1.10f * refined); - Assert.True(MathF.Abs(ratio - 1.0f) <= 0.005f, $"SNOISE_LIPSCHITZ is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureLipschitz and re-pin"); + var measured = 1.10f * refined; + Assert.True(SNOISE_LIPSCHITZ == measured, $"SNOISE_LIPSCHITZ is {SNOISE_LIPSCHITZ:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureLipschitz and re-pin"); } // ---- The curvature and 2D Lipschitz constants (math.Affine.cs, math.Interval.cs) ---- @@ -609,8 +609,8 @@ public sealed class NoiseBoundTests /// /// Pins SNOISE_HESSIAN: no Hessian norm over 1e5 seeded points exceeds it, and the ascent from - /// MeasureHessian's witness reaches a maximum that, times 1.10, is the constant to within half a - /// percent, so a constant moved by 1% fails here. + /// MeasureHessian's witness reaches a maximum that, times 1.10, equals the constant bit for bit + /// (the expression MeasureHessian prints), so any moved constant fails here. /// [Fact] public void HessianConstantPinsSampledCurvature() @@ -619,8 +619,8 @@ public sealed class NoiseBoundTests Assert.True(sampled <= SNOISE_HESSIAN, $"the Hessian norm reaches {sampled:R}, above SNOISE_HESSIAN {SNOISE_HESSIAN:R}"); var refined = Ascend(HessianNorm, HessianWitness); Assert.True(refined <= SNOISE_HESSIAN, $"the Hessian norm reaches {refined:R}, above SNOISE_HESSIAN {SNOISE_HESSIAN:R}"); - var ratio = SNOISE_HESSIAN / (1.10 * refined); - Assert.True(Math.Abs(ratio - 1.0) <= 0.005, $"SNOISE_HESSIAN is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureHessian and re-pin"); + var measured = (float)(refined * 1.10); + Assert.True(SNOISE_HESSIAN == measured, $"SNOISE_HESSIAN is {SNOISE_HESSIAN:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureHessian and re-pin"); } // ---- The third-derivative constant (math.Span.cs) ---- @@ -947,8 +947,8 @@ public sealed class NoiseBoundTests /// /// Pins SNOISE_THIRD: no third-derivative norm over 1e5 seeded points exceeds it, and the ascent from - /// MeasureThird's witness reaches a maximum that, times 1.10, is the constant to within half a - /// percent, so a constant moved by 1% fails here. + /// MeasureThird's witness reaches a maximum that, times 1.10, equals the constant bit for bit + /// (the expression MeasureThird prints), so any moved constant fails here. /// [Fact] public void ThirdConstantPinsSampled() @@ -957,8 +957,8 @@ public sealed class NoiseBoundTests Assert.True(sampled <= SNOISE_THIRD, $"the third-derivative norm reaches {sampled:R}, above SNOISE_THIRD {SNOISE_THIRD:R}"); var refined = Ascend(ThirdNorm, ThirdWitness); Assert.True(refined <= SNOISE_THIRD, $"the third-derivative norm reaches {refined:R}, above SNOISE_THIRD {SNOISE_THIRD:R}"); - var ratio = SNOISE_THIRD / (1.10 * refined); - Assert.True(Math.Abs(ratio - 1.0) <= 0.005, $"SNOISE_THIRD is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureThird and re-pin"); + var measured = (float)(refined * 1.10); + Assert.True(SNOISE_THIRD == measured, $"SNOISE_THIRD is {SNOISE_THIRD:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureThird and re-pin"); } /// @@ -1019,7 +1019,7 @@ public sealed class NoiseBoundTests /// /// Pins SNOISE2_LIPSCHITZ as LipschitzConstantPinsSampledGradients pins SNOISE_LIPSCHITZ: 1e5 seeded - /// points under it, and the ascent from MeasureLipschitz2's witness within half a percent of it / 1.10. + /// points under it, and the ascent from MeasureLipschitz2's witness, times 1.10, equals it bit for bit (the expression MeasureLipschitz2 prints). /// [Fact] public void Lipschitz2ConstantPins() @@ -1028,8 +1028,8 @@ public sealed class NoiseBoundTests Assert.True(sampled <= SNOISE2_LIPSCHITZ, $"|grad snoise2| reaches {sampled:R}, above SNOISE2_LIPSCHITZ {SNOISE2_LIPSCHITZ:R}"); var refined = Ascend2(Gradient2Norm, Lipschitz2Witness); Assert.True(refined <= SNOISE2_LIPSCHITZ, $"|grad snoise2| reaches {refined:R}, above SNOISE2_LIPSCHITZ {SNOISE2_LIPSCHITZ:R}"); - var ratio = SNOISE2_LIPSCHITZ / (1.10 * refined); - Assert.True(Math.Abs(ratio - 1.0) <= 0.005, $"SNOISE2_LIPSCHITZ is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureLipschitz2 and re-pin"); + var measured = (float)(refined * 1.10); + Assert.True(SNOISE2_LIPSCHITZ == measured, $"SNOISE2_LIPSCHITZ is {SNOISE2_LIPSCHITZ:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureLipschitz2 and re-pin"); } /// diff --git a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs index 3b871896..022a17cb 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs @@ -322,7 +322,7 @@ public sealed class SpanPrimitiveTests /// /// An infinite bound never reached by the line contributes nothing, not infinity times zero: relu /// (hi = +inf) and its mirror (lo = -inf) give the finite integral, whether the line crosses the finite - /// bound inside the span or not. + /// bound inside the span or not. The flat branch (b = 0) obeys the same zero-length rule. /// [Fact] public void IntegralClampLinearIsFiniteForInfiniteBounds() @@ -343,6 +343,15 @@ public sealed class SpanPrimitiveTests Assert.Equal(-7.5f, integral_clamp_linear(0.0f, 1.0f, -inf, inf, 4.0f, 1.0f)); // A line starting on the finite bound and rising into the infinite one: all ramp, the integral of t over [2, 4]. Assert.Equal(6.0f, integral_clamp_linear(0.0f, 1.0f, 2.0f, inf, 2.0f, 4.0f)); + + // The flat branch obeys the same zero-length rule: a stretch of zero length contributes +0 however + // infinite the bound it would be clamped to, and a nonzero length of an infinite constant is infinite. + // The bit patterns are compared because Assert.Equal treats -0 as 0. + Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(-2.3244874f, -0.0f, 0.0f, -inf, 1.429815f, 1.429815f))); + Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(1.5f, 0.0f, inf, inf, 2.0f, 2.0f))); + Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(1.5f, -0.0f, -inf, -inf, 0.0f, 0.0f))); + Assert.Equal(inf, integral_clamp_linear(1.5f, 0.0f, inf, inf, 0.0f, 2.0f)); + Assert.Equal(inf, integral_clamp_linear(1.5f, 0.0f, -inf, -inf, 2.0f, 0.0f)); } /// diff --git a/packages/cultmath/tools/compile-hlsl.ps1 b/packages/cultmath/tools/compile-hlsl.ps1 new file mode 100644 index 00000000..6b768aa9 --- /dev/null +++ b/packages/cultmath/tools/compile-hlsl.ps1 @@ -0,0 +1,39 @@ +param( + # The HLSL files to compile, each as a lib_6_3 library with its own directory as the include path. + # Default: both copies of CultMath.hlsl (the canonical one and the Unity package's byte copy). + # tools\compile-hlsl.ps1 -ShaderPath is the negative control. + [string[]]$ShaderPath = @(), + # The dxc to run. Default: the x64 install get-dxc.ps1 makes; it is never searched for, so a dxc + # that cannot launch (the arm64 one on x64) is an error, not a skipped check. + [string]$DxcPath = "" +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +if (-not $DxcPath) { + $DxcPath = Join-Path $repoRoot ".tools\dxc\current\bin\x64\dxc.exe" +} +if (-not $ShaderPath) { + $ShaderPath = @( + (Join-Path $repoRoot "shaders\CultMath.hlsl"), + (Join-Path $repoRoot "unity\org.gamecult.cultmath\Shaders\CultMath.hlsl")) +} +if (-not (Test-Path -LiteralPath $DxcPath)) { + throw "dxc not found (parameter DxcPath). Run tools\get-dxc.ps1 first or pass -DxcPath." +} + +$outputDirectory = Join-Path $repoRoot ".tools\hlsl" +New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null + +$index = 0 +foreach ($shader in $ShaderPath) { + $index++ + $shaderFile = Get-Item -LiteralPath $shader + $output = Join-Path $outputDirectory "$index-$($shaderFile.BaseName).dxil" + & $DxcPath -T lib_6_3 -HV 2021 -I $shaderFile.DirectoryName $shaderFile.FullName -Fo $output + if ($LASTEXITCODE -ne 0) { + throw "dxc failed with exit code $LASTEXITCODE (parameter ShaderPath, entry $index)." + } + Write-Host "compiled shader $index of $(@($ShaderPath).Count) (lib_6_3, -HV 2021)" +} diff --git a/packages/cultmath/tools/compile-hlsl.ps1.meta b/packages/cultmath/tools/compile-hlsl.ps1.meta new file mode 100644 index 00000000..d59a4297 --- /dev/null +++ b/packages/cultmath/tools/compile-hlsl.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3f6c1e7a9b2d4c58a0e4d7b1c95f2a86 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index c454773c..c480acc7 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -304,7 +304,7 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float { lo = min(lo, hi); if (b == 0.0) - return clamp(a, lo, hi) * (t1 - t0); + return t1 != t0 ? clamp(a, lo, hi) * (t1 - t0) : 0.0; float direction = t1 < t0 ? -1.0 : 1.0; float start = min(t0, t1); From bf337f013504daa5a2889761f410e9b74e680844 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 01:03:57 +0200 Subject: [PATCH 18/24] cultmath: regenerate CultMath.glsl and glsl-parity.json (CULTMATH_WRITE_GLSL=1, Yggdrasil) glsl: line 307 only (the flat-branch guard). Fixture: one family appended ('zero-length spans and flat infinite clamps'), earlier families byte-identical. Builds. Together with the previous commit the lowering and fixture tests pass. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/shaders/CultMath.glsl | 2 +- .../CultMath.Tests/fixtures/glsl-parity.json | 262 ++++++++++++++++++ 2 files changed, 263 insertions(+), 1 deletion(-) diff --git a/packages/cultmath/shaders/CultMath.glsl b/packages/cultmath/shaders/CultMath.glsl index b3a8dd4a..09770947 100644 --- a/packages/cultmath/shaders/CultMath.glsl +++ b/packages/cultmath/shaders/CultMath.glsl @@ -304,7 +304,7 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float { lo = min(lo, hi); if (b == 0.0) - return clamp(a, lo, hi) * (t1 - t0); + return t1 != t0 ? clamp(a, lo, hi) * (t1 - t0) : 0.0; float direction = t1 < t0 ? -1.0 : 1.0; float start = min(t0, t1); diff --git a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json index 0aa34b7e..91abcd17 100644 --- a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json +++ b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json @@ -26740,6 +26740,268 @@ "bda07260 3f5f9400 3f44ceb8 bf7d7ac4 3e564320 bf1a9360 -> 3f4e178c", "bf878b18 c05db95e ff800000 3ff43ee2 3f871b38 bf1df154 -> 4042a94e" ] + }, + { + "glsl": "float cultmath_integral_clamp_linear(float, float, float, float, float, float)", + "tolerance": "ulp-bounded, zero-length spans and flat infinite clamps", + "cases": [ + "80000000 80000000 3ff65b76 7f800000 40101a44 40101a44 -> 00000000", + "00000000 bffab8a4 7f800000 7f800000 3f12b010 3f12b010 -> 00000000", + "00000000 80000000 bf34a3ac 7f800000 3fd46400 3fd46400 -> 00000000", + "80000000 c00a0d51 7f800000 7f800000 bf9849a6 bf9849a6 -> 00000000", + "00000000 00000000 ff800000 7f800000 3fe4a0ec 3fe4a0ec -> 00000000", + "80000000 beb441d8 be195228 3fb78208 c015d454 c015d454 -> 00000000", + "80000000 00000000 3efc18a0 7f800000 c0157e7c c0157e7c -> 00000000", + "00000000 80000000 3fcb9924 3f93c620 bdbbe200 bdbbe200 -> 00000000", + "80000000 3f16a5d0 3ff1278c bfaa73fc 403c47f4 403c47f4 -> 00000000", + "bfeded14 80000000 ff800000 ff800000 400aa252 400aa252 -> 00000000", + "4042a300 3ec80180 ff800000 7f800000 c0369fbd c0369fbd -> 00000000", + "bf834bb8 3ff1675c 3fccd224 3eea4e38 bfe4046f bfe4046f -> 00000000", + "80000000 80000000 7f800000 bfb8ed52 c015deb4 c03bb881 -> 3f5abd11", + "00000000 c004ae4d 7f800000 7f800000 4038c578 4038c578 -> 00000000", + "00000000 c04d2c62 ff800000 bd754b20 3fcc4a60 3fcc4a60 -> 00000000", + "c0363eaa 80000000 be8c9ea8 ff800000 3fba9ac0 3fba9ac0 -> 00000000", + "80000000 80000000 7f800000 bf4c4cd0 c003ca24 c003ca24 -> 00000000", + "3f92b974 80000000 ff800000 ff800000 c008d69e c008d69e -> 00000000", + "00000000 40441954 ff800000 7f800000 40217b3a 40217b3a -> 00000000", + "80000000 00000000 ff800000 7f800000 3f8c7c70 3f8c7c70 -> 00000000", + "3ea22a30 00000000 3f5708fc 7f800000 c03b518b c03b518b -> 00000000", + "00000000 00000000 ff800000 ff800000 c00c95a0 bf5a3618 -> ff800000", + "80000000 80000000 ff800000 7f800000 3fb3af0c 400bf204 -> 80000000", + "4060b036 00000000 3fa51834 ff800000 c023687e bff93c2a -> ff800000", + "00000000 80000000 7f800000 ff800000 bfe50f6f bfe50f6f -> 00000000", + "80000000 80000000 bfbcb16e bf944da4 3f89a670 3f89a670 -> 00000000", + "00000000 00000000 beaae9fc ff800000 3f9468b8 3f9468b8 -> 00000000", + "00000000 bf50c0d8 3f7f9d38 7f800000 3ed30c90 3ed30c90 -> 00000000", + "405610da 80000000 3fe8ba90 3e9602a0 3f1e7c70 3f1e7c70 -> 00000000", + "00000000 00000000 3f84dca4 3ed1edd8 bfb5cbe0 bfb5cbe0 -> 00000000", + "00000000 00000000 ff800000 7f800000 401d4be0 401d4be0 -> 00000000", + "bea92ba8 00000000 ff800000 bfbc161a bfce0954 bfce0954 -> 00000000", + "00000000 00000000 3ffabe24 ff800000 be94c538 be94c538 -> 00000000", + "3fdb9278 80000000 7f800000 ff800000 bfb80c20 3fad3c54 -> ff800000", + "00000000 80000000 7f800000 ff800000 bfa61865 bfa61865 -> 00000000", + "00000000 80000000 ff800000 bf84d6bd 3f403a2c 3f403a2c -> 00000000", + "405d8f82 be715bf0 7f800000 bff060d1 40252552 3fabcf50 -> 4014cf8e", + "80000000 bf8e4188 7f800000 7f800000 3f6acd98 3f6acd98 -> 00000000", + "bf2996a4 3feeb9fc 3ef574c0 ff800000 403f2da4 403f2da4 -> 00000000", + "404b1a90 80000000 7f800000 ff800000 40358f58 40358f58 -> 00000000", + "406964ea 3f1b1f88 ff800000 7f800000 3eace890 3fe2f1b0 -> 40c4dd90", + "00000000 406a2510 ff800000 bf7d18c8 3f024fac 3f024fac -> 00000000", + "00000000 00000000 3fc5fa3a 3fa07496 bf8fb485 bf8fb485 -> 00000000", + "80000000 80000000 bfbb7b60 ff800000 3e2af490 3e2af490 -> 00000000", + "80000000 3e5aa380 7f800000 bfd8835b 3fdb8594 3fdb8594 -> 00000000", + "3f74a1e0 4011863a bf0a69ae bff30675 3eb98b78 3eb98b78 -> 00000000", + "bfa52d40 00000000 ff800000 bfc83f64 c0094bc0 c0094bc0 -> 00000000", + "3fdf6ba8 3fa96b98 be04bc30 7f800000 c00c72cf c00c72cf -> 00000000", + "80000000 80000000 3faeecdc bebf17e8 3f5565c8 3f5565c8 -> 00000000", + "00000000 c07111a4 7f800000 ff800000 3fe4c6ec 3fe4c6ec -> 00000000", + "00000000 3d8861c0 ff800000 7f800000 bfb30a60 befc8658 -> bd69a824", + "00000000 00000000 ff800000 ff800000 3eaec780 3f4e3528 -> ff800000", + "80000000 00000000 3fa5cf60 7f800000 3f46b9c4 3f46b9c4 -> 00000000", + "80000000 00000000 3c95e600 ff800000 3f847fa4 3f847fa4 -> 00000000", + "00000000 00000000 3fd73136 ff800000 bf7d6bc8 40305c10 -> ff800000", + "80000000 bfe74ff0 ff800000 3e0ee410 3ecea760 3ecea760 -> 00000000", + "80000000 00000000 3f082ac0 ff800000 40151e54 40151e54 -> 00000000", + "80000000 80000000 ff800000 7f800000 bf63ab8c bf63ab8c -> 00000000", + "00000000 c0571bc6 7f800000 ff800000 bffa05e1 bf93391b -> ff800000", + "00000000 80000000 bded5bc0 7f800000 3f8f48f0 3e98cc20 -> 80000000", + "00000000 80000000 ff800000 bcf42a40 3f0ec9a8 3f0ec9a8 -> 00000000", + "80000000 00000000 ff800000 ff800000 beb280f8 beb280f8 -> 00000000", + "80000000 00000000 3f9f75f8 7f800000 bfe89986 bfe89986 -> 00000000", + "00000000 404345ca 7f800000 7f800000 bf8f67d5 3f9b0790 -> 7f800000", + "c02d14e5 c018b2de 7f800000 beabd00c bf628f58 bf628f58 -> 00000000", + "80000000 3fa81b84 7f800000 7f800000 bf7fcb58 402c99e8 -> 7f800000", + "405a6490 c015edd8 ff800000 7f800000 402b25a8 402b25a8 -> 00000000", + "00000000 80000000 ff800000 3faf5518 3e8458b0 bf17a4b0 -> 80000000", + "3ddcc380 c01b944b ff800000 3e1a4ed0 c02f5e1e c02f5e1e -> 00000000", + "80000000 80000000 7f800000 7f800000 bfae87ca bfae87ca -> 00000000", + "80000000 bf365dc8 7f800000 7f800000 3cb87a00 3cb87a00 -> 00000000", + "3c9b6800 3fd96c50 ff800000 7f800000 bfa0d940 bfa0d940 -> 00000000", + "80000000 3fe3b634 ff800000 bfb50e83 be879260 be879260 -> 00000000", + "40400d36 80000000 7f800000 7f800000 3fcbf0b0 3fcbf0b0 -> 00000000", + "00000000 c073a1d2 3e22f910 be8c4dd8 402c5370 402c5370 -> 00000000", + "c00c4bae bf7f88c8 7f800000 ff800000 3f50b1bc 3fc21d00 -> ff800000", + "00000000 beb0d330 bf6b718a 7f800000 bf2bffdc 4015e728 -> bf5e9010", + "80000000 407ee62e bfc8b0ac ff800000 bfe242a3 bfe242a3 -> 00000000", + "c00a085f 4000655e 7f800000 ff800000 402f7d2a 402f7d2a -> 00000000", + "80000000 80000000 7f800000 ff800000 3ec08940 3ec08940 -> 00000000", + "00000000 80000000 bfb3cb9a 3fae284c bf73e094 bf73e094 -> 00000000", + "80000000 80000000 7f800000 ff800000 402b9396 3fe25440 -> 7f800000", + "00000000 00000000 bfa90b1a 3e615980 bf113528 bf113528 -> 00000000", + "00000000 80000000 ba34e800 ff800000 401b67ce 401b67ce -> 00000000", + "c066c90e 00000000 bda3fa30 7f800000 40336df4 40336df4 -> 00000000", + "00000000 80000000 ff800000 ff800000 3f965088 bf746b10 -> 7f800000", + "80000000 00000000 bfbad0bc ff800000 beba6078 beba6078 -> 00000000", + "401d2f02 00000000 3fea9790 3d3f6080 bfafe5e0 bfafe5e0 -> 00000000", + "00000000 3f51ea80 7f800000 7f800000 3fcab42c 3fbfaad4 -> ff800000", + "00000000 00000000 ff800000 7f800000 3fe9ee70 3fe9ee70 -> 00000000", + "80000000 c034a2be 7f800000 ff800000 3df9d3c0 3df9d3c0 -> 00000000", + "3f1f29e8 80000000 ff800000 ff800000 3dfe57a0 3dfe57a0 -> 00000000", + "80000000 c058626d 7f800000 bff33e4d 401c9e9a 401c9e9a -> 00000000", + "00000000 4075df8a bfd1267c 3fff39cc bfbb7f91 bfbb7f91 -> 00000000", + "00000000 00000000 ff800000 7f800000 c0105faf c0105faf -> 00000000", + "00000000 80000000 3e8b28a0 7f800000 bfe3598a bfd7dee1 -> 3cc7ad83", + "00000000 00000000 ff800000 ff800000 bec241f0 bec241f0 -> 00000000", + "40322ac4 3f612ac8 ff800000 7f800000 bf3990c4 bf3990c4 -> 00000000", + "80000000 80000000 3fc155d6 ff800000 bfdcf5b0 c0339ef0 -> 7f800000", + "00000000 00000000 7f800000 3fb27a36 c027718a be8432c0 -> 40526f29", + "80000000 00000000 3e4910d0 3bf3c600 3ff7f7b0 3ff7f7b0 -> 00000000", + "80000000 00000000 7f800000 ff800000 bf853af2 bf853af2 -> 00000000", + "80000000 80000000 7f800000 7f800000 c03fcb21 c03fcb21 -> 00000000", + "3f1c4320 404b71ae ff800000 ff800000 c03b7ec3 3ef955b0 -> ff800000", + "80000000 00000000 7f800000 bf3589e8 c004199e 3f4c3114 -> c001e073", + "405882aa 3f58fe18 bf3c8892 ff800000 3f5ba17c 3f5ba17c -> 00000000", + "3ed60fd0 c0787bf7 7f800000 ff800000 c027a669 c027a669 -> 00000000", + "40415c10 00000000 ff800000 7f800000 3ed104e0 3ed104e0 -> 00000000", + "bfd396d0 00000000 ff800000 be9a61e8 c03e89c2 c03e89c2 -> 00000000", + "00000000 3f504908 7f800000 ff800000 3fa5c6a4 3fa5c6a4 -> 00000000", + "00000000 bffb884a ff800000 7f800000 be7903f0 3ecc3d80 -> bdc934ef", + "00000000 3f2bd2f8 ff800000 7f800000 bfaa3d52 bfaa3d52 -> 00000000", + "80000000 00000000 7f800000 7f800000 3ff3f280 3ff3f280 -> 00000000", + "00000000 3fe24c9c 7f800000 bff12655 3f102eb0 40087702 -> c03d302a", + "406358a8 00000000 3ef01bf8 7f800000 3ffedf74 3ffedf74 -> 00000000", + "00000000 00000000 7f800000 3fd89804 bfaebcbc bfaebcbc -> 00000000", + "80000000 80000000 ff800000 7f800000 bf843386 bf843386 -> 00000000", + "00000000 404be2c4 ff800000 3fc25892 3e4d0290 3e4d0290 -> 00000000", + "bf0e67fc 80000000 7f800000 ff800000 bffe5952 bffe5952 -> 00000000", + "00000000 3fa4f594 7f800000 3fe5d4dc bfafd589 bfafd589 -> 00000000", + "00000000 bf77c8f0 beefd1e4 ff800000 bf79e4d0 bf79e4d0 -> 00000000", + "00000000 3f2f9d20 7f800000 ff800000 c017a5d0 c017a5d0 -> 00000000", + "80000000 80000000 ff800000 bf4b5558 3f19e55c bf993596 -> 3fb6cea8", + "c04aecf2 bffdc53c 7f800000 7f800000 3ff42e04 3ff42e04 -> 00000000", + "00000000 bf8733ce bf30ee0c 7f800000 bfdf4843 bedbf9d0 -> 3fc139ba", + "80000000 80000000 7f800000 7f800000 3f65e7e8 3f65e7e8 -> 00000000", + "80000000 c01677e0 ff800000 ff800000 bf007e88 4034553c -> ff800000", + "c023ebaf 80000000 3e888168 3fd868b0 3fd88290 bee63c60 -> bf1223eb", + "00000000 00000000 7f800000 beb09d98 bfac21b0 bfa0ead4 -> bcf793d2", + "00000000 80000000 3fe85996 3f3ed978 c0316f65 c0316f65 -> 00000000", + "00000000 00000000 7f800000 7f800000 3fdab088 c005b692 -> ff800000", + "00000000 80000000 7f800000 3fc6a1dc 3f5b0e68 3f5b0e68 -> 00000000", + "80000000 80000000 7f800000 beae44f8 bf31265c bf31265c -> 00000000", + "80000000 80000000 3fd86ce6 bf6d3f26 3ff3c428 3feb4ba0 -> 3d7b359a", + "80000000 3f9d2860 ff800000 7f800000 3f665fd8 3f665fd8 -> 00000000", + "80000000 80000000 ff800000 ff800000 bf3d4470 401a79f4 -> ff800000", + "80000000 00000000 7f800000 3fb11030 bfeb92e0 bfeb92e0 -> 00000000", + "bfea040a 80000000 3f4a5d74 bea5d2c4 3f741340 40247da0 -> bf060c1d", + "80000000 401a5258 ff800000 bf87f253 bf009dc4 40089a4c -> c033879b", + "00000000 bfe60b72 7f800000 ff800000 40386c38 40386c38 -> 00000000", + "80000000 80000000 bfcf6e6c 7f800000 403515f0 403515f0 -> 00000000", + "00000000 3f90bd0c ff800000 ff800000 3fa3921c 3fa3921c -> 00000000", + "00000000 3f494080 ff800000 be21e630 bebcd298 bebcd298 -> 00000000", + "406a06a6 00000000 7f800000 3fecf07a 3f8a6220 3f8a6220 -> 00000000", + "bf720b18 00000000 7f800000 ff800000 c031ef1a c031ef1a -> 00000000", + "bfa18170 80000000 3c31b200 ff800000 bffa78f4 bffb00c0 -> 7f800000", + "80000000 c010f43c 3f35544c 7f800000 bd35a040 bd35a040 -> 00000000", + "400eec30 80000000 3fb9fe58 3ff68576 402f8fb0 402f8fb0 -> 00000000", + "00000000 3f727ac8 ff800000 7f800000 40196800 40196800 -> 00000000", + "00000000 80000000 beabf8b8 7f800000 40275b20 40275b20 -> 00000000", + "80000000 c06216f0 ff800000 ff800000 3fbcd710 3fbcd710 -> 00000000", + "3e2b8e20 be09a8c0 7f800000 ff800000 bf2dc6fc c01fc7d8 -> 7f800000", + "80000000 80000000 ff800000 bfdd501a bf26753c bf26753c -> 00000000", + "407b0f90 00000000 ff800000 bff2b216 3f233758 c02aa030 -> 40c87116", + "c01eb335 00000000 ff800000 3f3265c0 401b2e1e 3feb10c0 -> 3fbab530", + "80000000 00000000 3f838946 3f0a5c88 3f0a68f0 3f0a68f0 -> 00000000", + "80000000 80000000 ff800000 7f800000 c02c2d43 c02c2d43 -> 00000000", + "00000000 00000000 ff800000 ff800000 c0275de0 c0275de0 -> 00000000", + "4015a6ce 40201064 7f800000 7f800000 c023744b c023744b -> 00000000", + "00000000 00000000 bdbabbb0 bfd7fffc 3f7393f8 40147bb2 -> c013ce4d", + "00000000 00000000 7f800000 7f800000 402fac7a 402fac7a -> 00000000", + "00000000 00000000 7f800000 3f2cba98 4022c284 4022c284 -> 00000000", + "bf7649a8 3fd2428c 3eef5f18 7f800000 401ff64e 401ff64e -> 00000000", + "80000000 80000000 ff800000 bf58f31e 3f568188 3f568188 -> 00000000", + "80000000 4039be2c ff800000 ff800000 402c7c98 3f8ee1f0 -> 7f800000", + "c0762d7d 80000000 7f800000 3f97fcf0 3f0b0374 3f0b0374 -> 00000000", + "80000000 00000000 7f800000 ff800000 3f52ccd8 bfc0fc02 -> 7f800000", + "00000000 4070b1da 3f5b1dc4 bf13e846 bff47ea1 3f35263c -> bfc19734", + "80000000 00000000 3f9c9f20 3f65673c 3fb0e250 c004e4f9 -> c046574d", + "401ed582 00000000 bfb557e0 7f800000 3fe43a6c 402222b6 -> 3fee5b90", + "80000000 bfc22d2e bdcfeaa0 3fa464a0 bfbda1fd bfbda1fd -> 00000000", + "00000000 80000000 3b208000 bfaca132 c030f6ab c030f6ab -> 00000000", + "3f9d2ae4 80000000 7f800000 7f800000 400dc94c bdccfa00 -> ff800000", + "00000000 80000000 bfcfc459 bf3e3016 40200df4 40200df4 -> 00000000", + "80000000 80000000 7f800000 ff800000 3ec4ecd8 3ec4ecd8 -> 00000000", + "00000000 3f892e30 7f800000 7f800000 3f9fcfc8 3f9fcfc8 -> 00000000", + "80000000 c042469e 7f800000 3f89d6f0 c008d2e1 c008d2e1 -> 00000000", + "80000000 00000000 bfa9f914 7f800000 bfe56d87 bfe56d87 -> 00000000", + "00000000 00000000 7f800000 3e7e1130 bfe16440 bfade4e4 -> 3dcc6f4a", + "3fed278c c0175c36 7f800000 7f800000 4006c8da 4006c8da -> 00000000", + "00000000 80000000 ff800000 be0c0b68 3f5397e8 3f5397e8 -> 00000000", + "3eb88820 c0494d42 bef4f1f4 7f800000 c00a6ab0 bf5aa13c -> 40d5c9c0", + "4058173a 80000000 ff800000 bfb537a2 c021c9a4 c021c9a4 -> 00000000", + "bef693e8 c039c856 7f800000 3fe18074 3f047ec0 3eefba60 -> bdb20561", + "c005b8ad 00000000 be9992cc be1fb720 bfc55229 bfc55229 -> 00000000", + "00000000 00000000 bff6c59b 7f800000 3f121cd0 3f121cd0 -> 00000000", + "80000000 bf7e2114 bec9b580 ff800000 c0122268 c0122268 -> 00000000", + "c01aa58a 80000000 3ff5ddde ff800000 c0001a48 c0001a48 -> 00000000", + "00000000 00000000 7f800000 bfd66c9d c0177ec3 c0177ec3 -> 00000000", + "3f322310 80000000 7f800000 bf947393 3feaf4e8 3feaf4e8 -> 00000000", + "00000000 80000000 bfa02d07 ff800000 bfde7439 bfde7439 -> 00000000", + "00000000 80000000 ff800000 7f800000 c02b599c bfee1d64 -> 00000000", + "80000000 00000000 3fddd822 7f800000 3fb54f5c 3fb54f5c -> 00000000", + "00000000 4007f0d0 7f800000 ff800000 3f31f590 3f31f590 -> 00000000", + "405cb2f2 00000000 7f800000 3f8d6758 bfe7a6d8 bfe7a6d8 -> 00000000", + "00000000 40567352 ff800000 3fd0fb4e 4030ba98 4030ba98 -> 00000000", + "80000000 00000000 ff800000 7f800000 402e382c 402e382c -> 00000000", + "80000000 00000000 7f800000 ff800000 bd8df4c0 4000ba00 -> ff800000", + "400e00d0 00000000 7f800000 bf0354a0 3db22980 3db22980 -> 00000000", + "00000000 00000000 7f800000 bf1c13f0 c03fb2ac c03fb2ac -> 00000000", + "00000000 80000000 bff31dff ff800000 402821c6 402821c6 -> 00000000", + "80000000 c0689249 7f800000 7f800000 bf9fa480 4013c62c -> 7f800000", + "bfd95cea 00000000 7f800000 ff800000 c0173b18 c0173b18 -> 00000000", + "80000000 3e952d00 7f800000 bf8097d4 40302390 40302390 -> 00000000", + "80000000 80000000 ff800000 bfeffee4 c01eb36c be619270 -> c0879000", + "404b75a4 80000000 7f800000 ff800000 3e95b1b8 3e95b1b8 -> 00000000", + "c00e25f0 bfbdd6ae ff800000 ff800000 4026857e 4026857e -> 00000000", + "3fea06f4 00000000 bff0ce8a 7f800000 3fd74714 c025bd4f -> c0f9e9f2", + "80000000 00000000 7f800000 bfb96dd4 bd946640 3fe4f32c -> c02c8dcd", + "40261dbe 00000000 7f800000 3f3f6830 3fb95060 bfc2eb22 -> c00e2593", + "80000000 00000000 3fde5382 3f1fb348 3f5e8390 3f5e8390 -> 00000000", + "80000000 00000000 bf8a5d94 ff800000 40363990 3fbfa8c4 -> 7f800000", + "80000000 00000000 ff800000 ff800000 4005e244 4005e244 -> 00000000", + "4024309a 80000000 7f800000 7f800000 3fdecd88 3fdecd88 -> 00000000", + "00000000 00000000 7f800000 3f91238c 40079ca8 40079ca8 -> 00000000", + "00000000 00000000 7f800000 7f800000 3ff7960c bcc05800 -> ff800000", + "80000000 00000000 7f800000 7f800000 40065cc0 3fe91e88 -> ff800000", + "00000000 3fe389a0 bed1d474 bd357fc0 c01f4cc3 c01f4cc3 -> 00000000", + "00000000 00000000 bf0a3aa4 7f800000 c032ed34 c032ed34 -> 00000000", + "80000000 80000000 ff800000 7f800000 3d3a0e40 3d3a0e40 -> 00000000", + "80000000 80000000 ff800000 bf6eae48 bf888f74 bf888f74 -> 00000000", + "405c38fa 80000000 3e25bd20 bf0077c0 3f8d28cc 3f8d28cc -> 00000000", + "80000000 c05d2c4a ff800000 befb6b5c c00cb828 c00cb828 -> 00000000", + "403e43e6 80000000 bfb8ab82 ff800000 3faf7fb0 3faf7fb0 -> 00000000", + "00000000 bfd6fecc ff800000 3ed7c3c0 3f225020 beac0090 -> 3e7ee0dc", + "80000000 80000000 bfda0e7f 3f5b65d0 3f74fed0 3f74fed0 -> 00000000", + "80000000 c037e6c4 7f800000 7f800000 3fa63768 3fa63768 -> 00000000", + "80000000 00000000 ff800000 7f800000 402a7c9a 402a7c9a -> 00000000", + "3ff192cc bf9b4ede ff800000 ff800000 c01f8d5b c01f8d5b -> 00000000", + "00000000 bf801ad0 3f59ed4c ff800000 c02ff429 c02ff429 -> 00000000", + "403bdb5a 00000000 3fd09b08 7f800000 bfdc1271 bfdc1271 -> 00000000", + "4063f4fa bfd6c9cc be9b4e08 7f800000 bfde6f62 3cda9100 -> 410d15b2", + "80000000 80000000 ff800000 3ea3f328 be05b570 be05b570 -> 00000000", + "00000000 80000000 ff800000 3edcd728 3f53d948 3f53d948 -> 00000000", + "c03d9d24 80000000 ff800000 ff800000 bfd2feb9 bfaea521 -> ff800000", + "80000000 00000000 7f800000 ff800000 bfaa4ef0 bfaa4ef0 -> 00000000", + "3fbe989c 00000000 ff800000 bf955032 3f043390 3f043390 -> 00000000", + "00000000 00000000 ff800000 bfb66c20 3f5be7f0 c03ae854 -> 40ac5d0f", + "bfbd1070 00000000 7f800000 ff800000 3f097bf4 bfb0083c -> 7f800000", + "407eb752 80000000 7f800000 ff800000 4017e772 4017e772 -> 00000000", + "00000000 00000000 be3e4598 3fd93476 3f1bbd64 3f1bbd64 -> 00000000", + "80000000 3e011e80 bf8a1de8 3e70f440 c020ae44 c020ae44 -> 00000000", + "00000000 80000000 7f800000 ff800000 403ce988 403ce988 -> 00000000", + "80000000 00000000 7f800000 ff800000 bfec7761 bfec7761 -> 00000000", + "c01aacb0 80000000 7f800000 bffce4f3 be0040d0 be0040d0 -> 00000000", + "00000000 00000000 ff800000 ff800000 bf2f0254 bf2f0254 -> 00000000", + "80000000 3fc97bcc bf3468ec 7f800000 401fb7c2 c002f127 -> c06786d0", + "80000000 80000000 ff800000 ff800000 bf0a598c bf0a598c -> 00000000", + "00000000 00000000 beb9c524 ff800000 3f949a78 3f949a78 -> 00000000", + "00000000 80000000 7f800000 bfd73167 3fa7e328 403d13e6 -> c030c039", + "80000000 00000000 bfc58738 ff800000 3f5f8ea8 3f5f8ea8 -> 00000000", + "3fb9b348 4007bb94 7f800000 ff800000 3fe45408 3fe45408 -> 00000000", + "3fde732c 00000000 ff800000 bfd1e56f bffd7643 bffd7643 -> 00000000", + "00000000 00000000 3f29de30 ff800000 bece6e60 bece6e60 -> 00000000", + "80000000 00000000 bfaab168 ff800000 bf0391a0 bf0391a0 -> 00000000", + "00000000 00000000 3f54e0a8 ff800000 bfc96f89 bfc96f89 -> 00000000" + ] } ] } From e4c1ffa4f12215954f8d83a0e230eb4b0276d0f2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 01:24:20 +0200 Subject: [PATCH 19/24] cultmath: span tests: a zero-length span through the sloped branch is +0 by bit pattern Stryker survivor on the previous head: direction t1 <= t0 changes a zero-length sloped span from +0 to -0, which no test observed. Builds; test-only. Co-Authored-By: Claude Sonnet 5.5 --- .../cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs index 022a17cb..dea5ab0c 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs @@ -322,7 +322,7 @@ public sealed class SpanPrimitiveTests /// /// An infinite bound never reached by the line contributes nothing, not infinity times zero: relu /// (hi = +inf) and its mirror (lo = -inf) give the finite integral, whether the line crosses the finite - /// bound inside the span or not. The flat branch (b = 0) obeys the same zero-length rule. + /// bound inside the span or not. Both branches give +0 for a zero-length span. /// [Fact] public void IntegralClampLinearIsFiniteForInfiniteBounds() @@ -352,6 +352,12 @@ public sealed class SpanPrimitiveTests Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(1.5f, -0.0f, -inf, -inf, 0.0f, 0.0f))); Assert.Equal(inf, integral_clamp_linear(1.5f, 0.0f, inf, inf, 0.0f, 2.0f)); Assert.Equal(inf, integral_clamp_linear(1.5f, 0.0f, -inf, -inf, 2.0f, 0.0f)); + + // The sloped branch gives +0 for a zero-length span too, not -0: the direction sign never meets a + // zero total, whichever way the line leans and whatever the bounds. + Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(1.0f, 2.0f, -inf, inf, 1.0f, 1.0f))); + Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(-3.0f, -2.0f, 0.0f, 5.0f, 1.0f, 1.0f))); + Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(0.5f, 1.0f, 0.0f, inf, 0.0f, 0.0f))); } /// From 5b9c77480b36516fc4976aa90abb4c17f6264ba2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 01:30:23 +0200 Subject: [PATCH 20/24] cultmath: compile-hlsl.ps1 comment no longer names a dxc architecture directory The spec's negative check greps the script for arm64; the comment matched. Comment only; script rerun on Starfire (compiles both copies, exit 0; arm64 dxc control throws). Builds. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/compile-hlsl.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cultmath/tools/compile-hlsl.ps1 b/packages/cultmath/tools/compile-hlsl.ps1 index 6b768aa9..6742503c 100644 --- a/packages/cultmath/tools/compile-hlsl.ps1 +++ b/packages/cultmath/tools/compile-hlsl.ps1 @@ -4,7 +4,7 @@ param( # tools\compile-hlsl.ps1 -ShaderPath is the negative control. [string[]]$ShaderPath = @(), # The dxc to run. Default: the x64 install get-dxc.ps1 makes; it is never searched for, so a dxc - # that cannot launch (the arm64 one on x64) is an error, not a skipped check. + # that cannot launch (one built for another architecture) is an error, not a skipped check. [string]$DxcPath = "" ) From 9548e1d0e64a1d825c45928139307ae5a44754ea Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 02:58:05 +0200 Subject: [PATCH 21/24] cultmath: HLSL span stage twins over struct arguments; mirror twin rule; span_l1 becomes csum(abs(..)) cultmath_span_stage, cultmath_span_fbm and cultmath_span_ridged with SpanPath, SpanStage and SpanNoise, statement for statement from math.span_stage (no shader identity). They follow the includes, not cultmath_ridged_grad: the bounds read the SNOISE_* constants the includes define. The mirror comparison matches every mirror function to the math function of the same name, public or private, with struct parameters mapped by name and fields; MirrorOnly is gone. New tests: identity and realistic-span mirror sweeps, a large-octave-step fold test; the near-origin fold test gains scales 1e-4 and 1e-2; the integral_clamp_linear tests also read the committed fixture's cases. The GLSL, the fixture families and the docs follow: CommittedGlslEqualsLowering and GoldenFixtureMatchesCSharp are red at this commit. It builds. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/shaders/CultMath.hlsl | 113 ++++++++++ packages/cultmath/src/CultMath/math.Span.cs | 4 +- .../tests/CultMath.Tests/GlslMirrorTests.cs | 37 +++- .../HlslSourceCompatibilityTests.cs | 206 ++++++++++++++++-- .../tests/CultMath.Tests/SpanStageTests.cs | 43 +++- .../Shaders/CultMath.hlsl | 113 ++++++++++ 6 files changed, 482 insertions(+), 34 deletions(-) diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index c480acc7..b3e8f915 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -605,6 +605,119 @@ CultCellular cultmath_cellular(float3 p) #include "CultMath.Cycles.hlsl" #include "CultMath.Affine.hlsl" +// Span stages (invariant 8): the SpanPath, SpanStage and SpanNoise of math.Span.cs, field for field in +// C#'s order, and cultmath_span_stage, its octave stage, statement for statement so the text is a bit-for-bit +// mirror. They follow the includes because the bounds read CULTMATH_SNOISE_LIPSCHITZ, _HESSIAN and _THIRD. +// A shader caller builds a SpanStage itself (SpanStage(1.0, 1.0, vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0)) +// is the identity of math.SpanStage.Identity, which has no shader form); see math.span_stage's comments for +// the span, the transform, the bounds and the allowances. +struct SpanPath +{ + float3 midpoint; + float3 velocity; + float speed; + float acceleration; + float halfLength; +}; + +struct SpanStage +{ + float frequency; + float stretch; + float3 axis; + float3 shift; + float3 offset; + float3 octaveStep; +}; + +struct SpanNoise +{ + float4 field; + float curvature; + float fold; +}; + +// The octave stage behind cultmath_span_fbm and cultmath_span_ridged, as math.span_stage is behind span_fbm +// and span_ridged. The Frobenius norm of the Hessian is written out in C#'s order (GLSL has no _mRC member +// access); the C# local named step is octaveShift here (it would shadow GLSL's step()). +SpanNoise cultmath_span_stage(bool ridged, SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + float3 q = path.midpoint; + float halfLength = path.halfLength; + octaves = clamp(octaves, 0, 16); + bool stretched = dot(stage.axis, stage.axis) > 0.0 && stage.stretch != 1.0; + float3 direction = stretched ? normalize(stage.axis) : float3(0.0, 0.0, 0.0); + float squeeze = stretched ? 1.0 / stage.stretch - 1.0 : 0.0; + float norm = abs(stage.frequency) * (stretched ? max(1.0, 1.0 / stage.stretch) : 1.0); + float3 mq = stretched ? stage.frequency * (q + squeeze * dot(direction, q) * direction) : stage.frequency * q; + float3 mv = stretched ? stage.frequency * (path.velocity + squeeze * dot(direction, path.velocity) * direction) : stage.frequency * path.velocity; + float amplitude = 1.0; + float scale = 1.0; + float value = 0.0; + float3 gradient = float3(0.0, 0.0, 0.0); + float curvature = 0.0; + float fold = 0.0; + for (int i = 0; i < octaves; i++) + { + float3 octaveShift = (float)i * stage.octaveStep; + float3 x = scale * (mq + stage.shift) + (stage.offset + octaveShift); + float4 n = cultmath_snoise_grad(x); + float3 g = float3(n.x, n.y, n.z); + float3 mg = stretched ? stage.frequency * (g + squeeze * dot(direction, g) * direction) : stage.frequency * g; + if (ridged) + { + float s = sign(n.w); + value += amplitude * (1.0 - abs(n.w)); + gradient += -amplitude * scale * s * mg; + } + else + { + value += amplitude * n.w; + gradient += amplitude * scale * mg; + } + + float weight = abs(amplitude); + float factor = abs(scale) * norm; + float nu = factor * path.speed; + float rounding = 9.5367431640625e-7 * (abs(scale) * (cultmath_csum(abs(mq)) + cultmath_csum(abs(stage.shift))) + cultmath_csum(abs(stage.offset)) + cultmath_csum(abs(octaveShift)) + 1.7320508 * nu * halfLength); + float valueSlack = 6.103515625e-5 + CULTMATH_SNOISE_LIPSCHITZ * rounding; + float gradientSlack = 2.44140625e-4 + CULTMATH_SNOISE_HESSIAN * rounding; + float hessianSlack = 9.765625e-4 + CULTMATH_SNOISE_THIRD * rounding; + float3x3 hm = cultmath_snoise_hessian(x); + float frobenius = sqrt(hm[0][0] * hm[0][0] + hm[0][1] * hm[0][1] + hm[0][2] * hm[0][2] + hm[1][0] * hm[1][0] + hm[1][1] * hm[1][1] + hm[1][2] * hm[1][2] + + hm[2][0] * hm[2][0] + hm[2][1] * hm[2][1] + hm[2][2] * hm[2][2]); + float hessianBound = min(CULTMATH_SNOISE_HESSIAN, frobenius + hessianSlack + CULTMATH_SNOISE_THIRD * nu * halfLength); + curvature += weight * factor * factor * hessianBound; + if (ridged) + { + float3 w = scale * mv; + float reach = hessianBound * nu * halfLength; + float gl = min(CULTMATH_SNOISE_LIPSCHITZ, length(g) + gradientSlack + reach); + float travel = (abs(dot(g, w)) + gradientSlack * length(w)) * halfLength + (hessianBound * nu * length(w) + gl * factor * path.acceleration) * (0.5 * halfLength * halfLength) + 2.0 * valueSlack; + fold += 2.0 * weight * max(travel - abs(n.w), 0.0); + } + + scale *= lacunarity; + amplitude *= gain; + } + + SpanNoise result; + result.field = float4(gradient, value); + result.curvature = curvature; + result.fold = fold; + return result; +} + +SpanNoise cultmath_span_fbm(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + return cultmath_span_stage(false, path, stage, octaves, lacunarity, gain); +} + +SpanNoise cultmath_span_ridged(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + return cultmath_span_stage(true, path, stage, octaves, lacunarity, gain); +} + float cultmath_value_noise(float2 position) { float2 cell = floor(position); diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 068dfd0c..3e898818 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -151,8 +151,6 @@ public static partial class math public static SpanNoise span_ridged(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) => span_stage(true, path, stage, octaves, lacunarity, gain); - private static float span_l1(float3 v) => abs(v.x) + abs(v.y) + abs(v.z); - // The span is SpanPath: q(t), |t| <= halfLength, through midpoint with velocity at t = 0, |q'| <= speed // and |q''| <= acceleration along it (speed at least |velocity|; all three and halfLength non-negative). // The transform is SpanStage: octave o samples snoise_grad at x_o(q) = f_o (M q + shift) + offset + @@ -239,7 +237,7 @@ public static partial class math var weight = abs(amplitude); var factor = abs(scale) * norm; var nu = factor * path.speed; - var rounding = 9.5367431640625e-7f * (abs(scale) * (span_l1(mq) + span_l1(stage.shift)) + span_l1(stage.offset) + span_l1(step) + 1.7320508f * nu * halfLength); + var rounding = 9.5367431640625e-7f * (abs(scale) * (csum(abs(mq)) + csum(abs(stage.shift))) + csum(abs(stage.offset)) + csum(abs(step)) + 1.7320508f * nu * halfLength); var valueSlack = 6.103515625e-5f + SNOISE_LIPSCHITZ * rounding; var gradientSlack = 2.44140625e-4f + SNOISE_HESSIAN * rounding; var hessianSlack = 9.765625e-4f + SNOISE_THIRD * rounding; diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs index 33665d75..f643d2c3 100644 --- a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs @@ -67,7 +67,9 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > /// reached by cultmath_integral_clamp_linear may see lo above hi, on any path and for any spelling of the /// normalisation. The mirror is compiled with counting clamps and the function is run over the edge grid /// (lo above hi, infinite bounds, zero-length spans, b = 0 and b != 0): the count of such clamp calls - /// stays 0 after every call. + /// stays 0 after every call. The cases are HlslSourceCompatibilityTests.IntegralClampLinearCases: the grid fixes + /// a in {-2.5, 0, 1.5}, so a normalisation keyed on a past 1.75 never clamps wrongly there; the committed + /// fixture's random cases reach it. /// [Fact] public void IntegralClampLinearNeverClampsWithMinAboveMax() @@ -80,7 +82,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > var function = shaderType.GetMethod("cultmath_integral_clamp_linear", BindingFlags.Instance | BindingFlags.NonPublic)!; var bad = new List(); - foreach (var c in HlslSourceCompatibilityTests.IntegralClampLinearEdgeGrid) + foreach (var c in HlslSourceCompatibilityTests.IntegralClampLinearCases) { count.SetValue(null, 0); function.Invoke(shader, c.Cast().ToArray()); @@ -714,8 +716,38 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > var t1 = r.Next(4) != 0 ? t0 : Uniform(r, -3.0f, 3.0f); return new object[] { a, b, lo, hi, t0, t1 }; }), + // The span stages over their struct arguments: realistic stages and spans (SpanStageTests' own draw), the + // midpoint a Point3, one span in four a point. + new Family("SpanNoise cultmath_span_fbm(SpanPath, SpanStage, int, float, float)", "ulp-bounded", SpanArguments), + new Family("SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float)", "ulp-bounded", SpanArguments), }).ToArray(); + private static object[] SpanArguments(System.Random r) + { + var stage = SpanStageTests.Stage.Draw(r); + var halfLength = r.Next(4) == 0 ? 0.0f : new[] { 0.02f, 0.1f, 0.5f }[r.Next(3)]; + var span = SpanStageTests.Span.Draw(r, Point3(r), halfLength, Uniform(r, 0.0f, 2.0f)); + return new object[] { span.Path, stage.S, stage.Octaves, stage.Lacunarity, stage.Gain }; + } + + /// + /// The committed fixture's cases of the three cultmath_integral_clamp_linear families, decoded into the + /// C# arguments (a, b, lo, hi, t0, t1): the random cases the clamp-normalisation tests also run. + /// + internal static IReadOnlyList IntegralClampLinearFixtureCases + { + get + { + const string clamp = "float cultmath_integral_clamp_linear(float, float, float, float, float, float)"; + var method = Resolve(clamp); + return JsonNode.Parse(File.ReadAllText(FixturePath))!["functions"]!.AsArray() + .Where(family => (string)family!["glsl"]! == clamp) + .SelectMany(family => family!["cases"]!.AsArray()) + .Select(entry => Arguments(method, ((string)entry!).Split(" -> ")[0]).Select(a => (float)a).ToArray()) + .ToArray(); + } + } + // The fixture text, each case's arguments given by family and point index. private static string GoldenFixture(Func arguments) { @@ -758,6 +790,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > { ["float"] = typeof(float), ["int"] = typeof(int), ["vec2"] = typeof(float2), ["vec3"] = typeof(float3), ["vec4"] = typeof(float4), ["ivec3"] = typeof(int3), ["ivec4"] = typeof(int4), + ["SpanPath"] = typeof(SpanPath), ["SpanStage"] = typeof(SpanStage), }; // The C# math method a GLSL signature names: cultmath_ dropped, parameter types mapped. diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index 68cfce95..1e01ba43 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -39,15 +39,20 @@ public sealed class HlslSourceCompatibilityTests Assert.NotNull(ShaderFunction(shader, "cultmath_af_snoise", typeof(float3), typeof(float3), typeof(float))); Assert.NotNull(ShaderFunction(shader, "cultmath_af_fbm", typeof(float3), typeof(float3), typeof(float), typeof(int), typeof(float), typeof(float))); Assert.NotNull(ShaderFunction(shader, "cultmath_af_frustum_ball", typeof(float2), typeof(float), typeof(float), typeof(float), typeof(float))); + var (spanPath, spanStage) = SpanTypes(shader); + Assert.NotNull(ShaderFunction(shader, "cultmath_span_fbm", spanPath, spanStage, typeof(int), typeof(float), typeof(float))); + Assert.NotNull(ShaderFunction(shader, "cultmath_span_ridged", spanPath, spanStage, typeof(int), typeof(float), typeof(float))); + Assert.NotNull(ShaderFunction(shader, "cultmath_span_stage", typeof(bool), spanPath, spanStage, typeof(int), typeof(float), typeof(float))); } + // The mirror's struct types are private nested types of HlslShader (a struct without a modifier). + private static (Type Path, Type Stage) SpanTypes(Type shader) => + (shader.GetNestedType("SpanPath", BindingFlags.Public | BindingFlags.NonPublic)!, shader.GetNestedType("SpanStage", BindingFlags.Public | BindingFlags.NonPublic)!); + // HLSL functions carry no access modifier, so they compile as private instance methods. private static MethodInfo? ShaderFunction(Type shader, string name, params Type[] parameters) => shader.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null); - // Mirror internals with no public C# math counterpart. Any other cultmath_* function must match one. - private static readonly string[] MirrorOnly = { "cultmath_snoise_mod289", "cultmath_snoise_permute" }; - // Shader constants with no C# twin of the same name (empty when every constant has one). private static readonly string[] ConstantsWithoutTwin = Array.Empty(); @@ -88,7 +93,9 @@ public sealed class HlslSourceCompatibilityTests private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN, float.PositiveInfinity, float.NegativeInfinity }; /// - /// Every mirror function with a C# math counterpart returns the same bits (any NaN equals any NaN; + /// Every mirror function matches the math function of the same name, public or private (a function with no + /// twin fails, and nothing is exempt), with a struct parameter mapped by name and fields to the CultMath + /// type of the same name, and returns the same bits (any NaN equals any NaN; /// -0 differs from 0) over random inputs, every special value in every argument (ties, zeros, /// negatives, NaN, infinities, large values, integer lattice points), special pairs, and vectors whose first two /// components tie (x0.x == x0.y in snoise). This proves the text of CultMath.hlsl computes the same @@ -153,20 +160,15 @@ public sealed class HlslSourceCompatibilityTests foreach (var mirror in shaderType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Where(m => m.Name.StartsWith("cultmath_"))) { var types = mirror.GetParameters().Select(p => p.ParameterType).ToArray(); - var counterpart = typeof(math).GetMethod(mirror.Name["cultmath_".Length..], BindingFlags.Public | BindingFlags.Static, null, types, null); - if (counterpart is null) - { - Assert.Contains(mirror.Name, MirrorOnly); - continue; - } + var counterpart = Twin(shaderType, mirror.Name["cultmath_".Length..], types); + Assert.True(counterpart is not null, $"{mirror.Name} has no C# math function of the same name and parameter types, public or private"); compared.Add(mirror.Name); foreach (var input in cases) { var slot = 0; - var args = types.Select(t => Scalars.Contains(t) ? Scalar(t, input(slot++)) - : Activator.CreateInstance(t, Components(t).Select(f => Scalar(f.FieldType, input(slot++))).ToArray())!).ToArray(); - var expected = Values(counterpart.Invoke(null, args)!); + var args = types.Select(t => Build(t, () => input(slot++))).ToArray(); + var expected = Values(counterpart!.Invoke(null, args.Select(a => CopyFields(a, TwinType(shaderType, a.GetType())!)).ToArray())!); var actual = Values(mirror.Invoke(shader, args)!); if (!BitwiseEqual(expected, actual)) { @@ -176,7 +178,7 @@ public sealed class HlslSourceCompatibilityTests } Assert.True(mismatches.Count == 0, $"{mismatches.Count} mismatches:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}"); - Assert.Equal(120, compared.Count); + Assert.Equal(125, compared.Count); } /// @@ -201,14 +203,22 @@ public sealed class HlslSourceCompatibilityTests } /// - /// The compiled mirror's cultmath_integral_clamp_linear returns C#'s bits on every edge-grid case, and C#'s - /// result is never NaN there. The generic case set catches each of the function's guards (the held and + /// The edge grid followed by the committed GLSL fixture's cases of the three integral_clamp_linear families + /// (GlslMirrorTests.IntegralClampLinearFixtureCases, decoded): one list, read by the mirror test and by the + /// clamp-shadow test. The grid fixes a in {-2.5, 0, 1.5}, so a guard keyed on a past 1.75 hides from it + /// (Soul s4); the fixture's random a in [-4, 4] closes that. + /// + internal static IReadOnlyList IntegralClampLinearCases => IntegralClampLinearEdgeGrid.Concat(GlslMirrorTests.IntegralClampLinearFixtureCases).ToArray(); + + /// + /// The compiled mirror's cultmath_integral_clamp_linear returns C#'s bits on every edge case (the grid and + /// the fixture's cases), and C#'s result is never NaN there. The generic case set catches each of the function's guards (the held and /// kept stretch guards, the flat branch's zero-length guard) only by its rotation: with the kept guard /// removed it finds 0 mismatches, with the held guard removed 1 once infinities join the specials, and the /// edge grid finds 246 for either every time. /// [Fact] - public void IntegralClampLinearMirrorMatchesCSharpOnTheEdgeGrid() + public void IntegralClampLinearMirrorMatchesCSharpOnTheEdgeCases() { var (assembly, errors) = CompileShaderMirror(); Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); @@ -217,7 +227,8 @@ public sealed class HlslSourceCompatibilityTests var mirror = ShaderFunction(shaderType, "cultmath_integral_clamp_linear", Enumerable.Repeat(typeof(float), 6).ToArray())!; var mismatches = new List(); - foreach (var c in IntegralClampLinearEdgeGrid) + var cases = IntegralClampLinearCases; + foreach (var c in cases) { var expected = integral_clamp_linear(c[0], c[1], c[2], c[3], c[4], c[5]); var actual = (float)mirror.Invoke(shader, c.Cast().ToArray())!; @@ -226,7 +237,101 @@ public sealed class HlslSourceCompatibilityTests mismatches.Add($"({string.Join(", ", c)}): C# {expected:R}, mirror {actual:R}"); } - Assert.True(mismatches.Count == 0, $"{mismatches.Count} of {IntegralClampLinearEdgeGrid.Count} cases differ:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}"); + Assert.True(mismatches.Count == 0, $"{mismatches.Count} of {cases.Count} cases differ:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}"); + } + + /// + /// At a SpanStage the shader has no identity constant for, so C#'s identity (copied field by field) is the + /// argument: the mirror's cultmath_span_fbm and cultmath_span_ridged return the field of the mirror's own + /// cultmath_fbm_grad and cultmath_ridged_grad, bit for bit, for 0 to 20 octaves over random and simplex-tie + /// points. The shader-side statement of math.SpanStage.Identity (design decision a: no shader identity). + /// + [Fact] + public void SpanStagesAtIdentityAreFbmAndRidgedInTheMirror() + { + var (assembly, errors) = CompileShaderMirror(); + Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); + var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!; + var shader = Activator.CreateInstance(shaderType); + var (pathType, stageType) = SpanTypes(shaderType); + var spanArguments = new[] { pathType, stageType, typeof(int), typeof(float), typeof(float) }; + var pairs = new[] + { + (Span: ShaderFunction(shaderType, "cultmath_span_fbm", spanArguments)!, Plain: ShaderFunction(shaderType, "cultmath_fbm_grad", typeof(float3), typeof(int), typeof(float), typeof(float))!), + (Span: ShaderFunction(shaderType, "cultmath_span_ridged", spanArguments)!, Plain: ShaderFunction(shaderType, "cultmath_ridged_grad", typeof(float3), typeof(int), typeof(float), typeof(float))!), + }; + var identity = CopyFields(SpanStage.Identity, stageType); + + var random = new System.Random(0x57A6E2); + var compared = 0; + foreach (var p in SpanStageTests.Points(random)) + { + var octaves = random.Next(0, 21); + var lacunarity = 1.4f + 1.2f * random.NextSingle(); + var gain = 0.3f + 0.5f * random.NextSingle(); + var path = CopyFields(new SpanPath { midpoint = p, velocity = new float3(0.6f, 0.0f, 0.8f), speed = 1.0f, acceleration = 0.5f, halfLength = 0.25f }, pathType); + foreach (var (span, plain) in pairs) + { + var field = FieldOf(span.Invoke(shader, new object[] { path, identity, octaves, lacunarity, gain })!, "field"); + var expected = plain.Invoke(shader, new object[] { p, octaves, lacunarity, gain })!; + Assert.True(BitwiseEqual(Values(expected), Values(field)), $"{span.Name} at {p}, {octaves} octaves: {string.Join(" ", Values(field).Select(v => v.Value))} is not {plain.Name} {string.Join(" ", Values(expected).Select(v => v.Value))}"); + compared++; + } + } + + Assert.True(compared >= 460, $"only {compared} identity comparisons ran"); + } + + /// + /// The generic comparison draws every argument from [-100, 100] (and specials), where the octave loop's + /// frequency, stretch and span terms saturate: 0 or 16 octaves, a Hessian bound pinned at its global value. + /// This sweep draws SpanStageTests' realistic stages and spans (4000, one in four with a zero axis, one in + /// four with a point span, halfLength 0), both stages, and the mirror must return C#'s bits for the whole + /// SpanNoise. Each of stretched and unstretched stages, and of positive and zero fold, must be met at least + /// 500 times, or the sweep proves nothing about that branch. + /// + [Fact] + public void SpanStageMirrorMatchesCSharpBitForBitOnRealisticSpans() + { + var (assembly, errors) = CompileShaderMirror(); + Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); + var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!; + var shader = Activator.CreateInstance(shaderType); + var (pathType, stageType) = SpanTypes(shaderType); + var spanArguments = new[] { pathType, stageType, typeof(int), typeof(float), typeof(float) }; + var mirrors = new[] { ShaderFunction(shaderType, "cultmath_span_fbm", spanArguments)!, ShaderFunction(shaderType, "cultmath_span_ridged", spanArguments)! }; + + var random = new System.Random(0x5EED4); + float Next(float extent) => (random.NextSingle() * 2.0f - 1.0f) * extent; + int stretched = 0, unstretched = 0, folded = 0, flat = 0; + for (var i = 0; i < 4000; i++) + { + var drawn = SpanStageTests.Stage.Draw(random); + var stage = drawn.S; + if (i % 4 == 1) + stage.axis = new float3(0.0f, 0.0f, 0.0f); + var halfLength = i % 4 == 2 ? 0.0f : new[] { 0.02f, 0.1f, 0.5f, 2.0f }[random.Next(4)]; + var span = SpanStageTests.Span.Draw(random, new float3(Next(30.0f), Next(30.0f), Next(30.0f)), halfLength, random.NextSingle() * 2.0f); + var arguments = new object[] { CopyFields(span.Path, pathType), CopyFields(stage, stageType), drawn.Octaves, drawn.Lacunarity, drawn.Gain }; + if (dot(stage.axis, stage.axis) > 0.0f && stage.stretch != 1.0f) + stretched++; + else + unstretched++; + for (var m = 0; m < 2; m++) + { + var expected = m == 0 ? span_fbm(span.Path, stage, drawn.Octaves, drawn.Lacunarity, drawn.Gain) : span_ridged(span.Path, stage, drawn.Octaves, drawn.Lacunarity, drawn.Gain); + var actual = mirrors[m].Invoke(shader, arguments)!; + Assert.True(BitwiseEqual(Values(expected), Values(actual)), + $"{mirrors[m].Name} differs from C# at midpoint {span.Path.midpoint}, halfLength {halfLength:R}, {drawn.Octaves} octaves: C# {string.Join(" ", Values(expected).Select(v => v.Value))}, mirror {string.Join(" ", Values(actual).Select(v => v.Value))}"); + if (m == 1 && expected.fold > 0.0f) + folded++; + else if (m == 1) + flat++; + } + } + + Assert.True(stretched >= 500 && unstretched >= 500 && folded >= 500 && flat >= 500, + $"the sweep meets a branch too rarely: stretched {stretched}, unstretched {unstretched}, fold > 0 {folded}, fold = 0 {flat} of 4000"); } /// @@ -439,14 +544,66 @@ public sealed class HlslSourceCompatibilityTests } // Integer arguments take the bit pattern of the float input, so specials become 0, 0x80000000, NaN bits, ... - private static readonly Type[] Scalars = { typeof(float), typeof(int), typeof(uint) }; + // A bool argument is true when its float input is positive. + private static readonly Type[] Scalars = { typeof(float), typeof(int), typeof(uint), typeof(bool) }; private static object Scalar(Type t, float f) => t == typeof(float) ? f + : t == typeof(bool) ? f > 0.0f : t == typeof(int) ? BitConverter.SingleToInt32Bits(f) : (object)(uint)BitConverter.SingleToInt32Bits(f); - // Scoped to scalar-typed fields: this only ever builds function ARGUMENTS (float2/3/4, int2/3/4, ...), - // never a struct-of-struct return, so it must not recurse. - private static FieldInfo[] Components(Type t) => t.GetFields().Where(f => !f.IsStatic && Scalars.Contains(f.FieldType)).ToArray(); + private static FieldInfo[] InstanceFields(Type t) => t.GetFields(BindingFlags.Public | BindingFlags.Instance); + + // One function ARGUMENT from the case's scalars, in field order: a scalar from its float, a vector by the + // constructor taking its scalar fields, a struct field by field, recursively. + private static object Build(Type t, Func next) + { + if (Scalars.Contains(t)) + return Scalar(t, next()); + var fields = InstanceFields(t); + var constructor = t.GetConstructor(fields.Select(f => f.FieldType).ToArray()); + if (constructor is not null) + return constructor.Invoke(fields.Select(f => Build(f.FieldType, next)).ToArray()); + var value = Activator.CreateInstance(t)!; + foreach (var field in fields) + field.SetValue(value, Build(field.FieldType, next)); + return value; + } + + // The CultMath type a mirror parameter type stands for: itself, or for a type nested in HlslShader the + // CultMath type of the same name, only if both have the same instance fields by name, type (mapped + // likewise) and order. Null when there is no such twin. + private static Type? TwinType(Type shaderType, Type mirror) + { + if (mirror.DeclaringType != shaderType) + return mirror; + var twin = typeof(math).Assembly.GetType("CultMath." + mirror.Name); + if (twin is null) + return null; + var mine = InstanceFields(mirror); + var theirs = InstanceFields(twin); + return mine.Length == theirs.Length && mine.Zip(theirs).All(p => p.First.Name == p.Second.Name && TwinType(shaderType, p.First.FieldType) == p.Second.FieldType) ? twin : null; + } + + // The math function of the mirror function's name, with its parameter types mapped, public or private. + private static MethodInfo? Twin(Type shaderType, string name, Type[] mirrorTypes) + { + var types = mirrorTypes.Select(t => TwinType(shaderType, t)).ToArray(); + return types.Any(t => t is null) ? null + : typeof(math).GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, types!, null); + } + + // A struct value as another struct type of the same fields by name: itself when the type is the same. + private static object CopyFields(object source, Type target) + { + if (source.GetType() == target) + return source; + var copy = Activator.CreateInstance(target)!; + foreach (var field in InstanceFields(target)) + field.SetValue(copy, CopyFields(source.GetType().GetField(field.Name)!.GetValue(source)!, field.FieldType)); + return copy; + } + + private static object FieldOf(object value, string name) => value.GetType().GetField(name)!.GetValue(value)!; /// /// One flattened scalar leaf of a (possibly nested) return value: its declaration path (field @@ -520,7 +677,8 @@ public sealed class HlslSourceCompatibilityTests // 5. Struct fields gain `public`; HLSL has no field access-modifier concept, and a C# struct's // fields default to private, which would hide them from the mirror test's reflection-based - // field comparison (invariant 8's one struct return shape, e.g. CultCellular). + // field comparison (invariant 8's one struct return shape, e.g. CultCellular) and the struct + // arguments' copies (SpanPath, SpanStage), which reflection reads and writes by field name. source = Regex.Replace(source, @"(?ms)(struct\s+\w+\s*\{)(.*?)(\};)", m => m.Groups[1].Value + Regex.Replace(m.Groups[2].Value, @"(?m)^(\s*)(\w+\s+\w+;)", "$1public $2") + m.Groups[3].Value); diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index a7ed48b3..1d1b4b87 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -44,7 +44,7 @@ public sealed class SpanStageTests // takes in use (stage-parameter-range): |frequency| 1e-3..10, stretch 1e-2..1e2, |lacunarity| 0.5..3, // |gain| 0.1..1.2, the last three and frequency with random signs where a sign is meaningful, // octaveStep nonzero in every component. - private readonly record struct Stage(SpanStage S, int Octaves, float Lacunarity, float Gain) + internal readonly record struct Stage(SpanStage S, int Octaves, float Lacunarity, float Gain) { public static Stage Draw(System.Random random, int maxOctaves = 8, int minOctaves = 1) => new( new SpanStage @@ -164,7 +164,7 @@ public sealed class SpanStageTests // A span through q: velocity v, constant acceleration c of norm `acceleration`, so q(t) = q + v t + c t^2 / 2; // the speed bound is |v| plus what c can add over halfLength. - private readonly record struct Span(SpanPath Path, float3 Acceleration) + internal readonly record struct Span(SpanPath Path, float3 Acceleration) { public static Span Draw(System.Random random, float3 q, float halfLength, float acceleration) { @@ -220,7 +220,7 @@ public sealed class SpanStageTests BitConverter.SingleToInt32Bits(a.x) == BitConverter.SingleToInt32Bits(b.x) && BitConverter.SingleToInt32Bits(a.y) == BitConverter.SingleToInt32Bits(b.y) && BitConverter.SingleToInt32Bits(a.z) == BitConverter.SingleToInt32Bits(b.z) && BitConverter.SingleToInt32Bits(a.w) == BitConverter.SingleToInt32Bits(b.w); - private static IEnumerable Points(System.Random random) + internal static IEnumerable Points(System.Random random) { for (var k = 0; k < 200; k++) yield return Vector(random, -60.0f, 60.0f); @@ -697,7 +697,7 @@ public sealed class SpanStageTests var random = new System.Random(loose ? 0x100E : 0x4A11); var stage = new Stage(SpanStage.Identity, 1, 2.0f, 0.5f); var crossing = 0; - foreach (var scale in new[] { 30.0f, 300.0f, 3000.0f }) + foreach (var scale in new[] { 1.0e-4f, 1.0e-2f, 30.0f, 300.0f, 3000.0f }) for (var k = 0; k < 300; k++) { var q = ProjectToZero(Vector(random, -scale, scale)); @@ -712,7 +712,40 @@ public sealed class SpanStageTests crossing++; } - Assert.True(crossing >= 50, $"only {crossing} of 900 midpoints had an exact sign opposite to the float one: the check has no teeth"); + Assert.True(crossing >= 80, $"only {crossing} of 1500 midpoints had an exact sign opposite to the float one: the check has no teeth"); + } + + /// + /// The step term of S_o, alone: an octaveStep of random direction and magnitude 1e3 to 1e5 (log-uniform) at + /// the identity stage, two octaves, the midpoint moved until octave 1's coordinate (2 q + step) sits on + /// the kernel's n = 0 (a Newton projection of that coordinate, then q = (x - step) / 2). There the + /// coordinate's float rounding (ulp 0.004 to 0.008) is what separates the float kernel from the exact one, + /// and only |o octaveStep|_1 in S_o carries it: the exact branch departure stays within fold, span_fbm's + /// fold is 0, and at least 100 draws have an exact sign opposite to the float one. Without the step term + /// fold is the 2^-14 and the small-coordinate rounding alone, and the departure passes it. + /// + [Fact] + public void SpanFoldEnclosesTheExactBranchAtLargeOctaveSteps() + { + var random = new System.Random(0x57E9); + var crossing = 0; + for (var k = 0; k < 600; k++) + { + var step = UnitVector(random) * LogUniform(random, 1.0e3f, 1.0e5f); + var transform = SpanStage.Identity; + transform.octaveStep = step; + var stage = new Stage(transform, 2, 2.0f, 0.5f); + var q = (ProjectToZero(Vector(random, -30.0f, 30.0f) + step) - step) * 0.5f; + var span = new Span(new SpanPath { midpoint = q, velocity = None, speed = 0.0f, acceleration = 0.0f, halfLength = 0.0f }, None); + var fold = stage.Ridged(span.Path).fold; + var (_, departureExact, _) = BranchDeparture(stage, span, 2); + Assert.Equal(0.0f, stage.Fbm(span.Path).fold); + Assert.True(departureExact <= fold * (1.0 + 1.0e-3) + 1.0e-9, $"octaveStep {step}, midpoint {q}: the exact branch departure is {departureExact:R}, above fold {fold:R}"); + if (departureExact > 0.0) + crossing++; + } + + Assert.True(crossing >= 100, $"only {crossing} of 600 midpoints had an exact sign opposite to the float one: the check has no teeth"); } /// diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index c480acc7..b3e8f915 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -605,6 +605,119 @@ CultCellular cultmath_cellular(float3 p) #include "CultMath.Cycles.hlsl" #include "CultMath.Affine.hlsl" +// Span stages (invariant 8): the SpanPath, SpanStage and SpanNoise of math.Span.cs, field for field in +// C#'s order, and cultmath_span_stage, its octave stage, statement for statement so the text is a bit-for-bit +// mirror. They follow the includes because the bounds read CULTMATH_SNOISE_LIPSCHITZ, _HESSIAN and _THIRD. +// A shader caller builds a SpanStage itself (SpanStage(1.0, 1.0, vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0)) +// is the identity of math.SpanStage.Identity, which has no shader form); see math.span_stage's comments for +// the span, the transform, the bounds and the allowances. +struct SpanPath +{ + float3 midpoint; + float3 velocity; + float speed; + float acceleration; + float halfLength; +}; + +struct SpanStage +{ + float frequency; + float stretch; + float3 axis; + float3 shift; + float3 offset; + float3 octaveStep; +}; + +struct SpanNoise +{ + float4 field; + float curvature; + float fold; +}; + +// The octave stage behind cultmath_span_fbm and cultmath_span_ridged, as math.span_stage is behind span_fbm +// and span_ridged. The Frobenius norm of the Hessian is written out in C#'s order (GLSL has no _mRC member +// access); the C# local named step is octaveShift here (it would shadow GLSL's step()). +SpanNoise cultmath_span_stage(bool ridged, SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + float3 q = path.midpoint; + float halfLength = path.halfLength; + octaves = clamp(octaves, 0, 16); + bool stretched = dot(stage.axis, stage.axis) > 0.0 && stage.stretch != 1.0; + float3 direction = stretched ? normalize(stage.axis) : float3(0.0, 0.0, 0.0); + float squeeze = stretched ? 1.0 / stage.stretch - 1.0 : 0.0; + float norm = abs(stage.frequency) * (stretched ? max(1.0, 1.0 / stage.stretch) : 1.0); + float3 mq = stretched ? stage.frequency * (q + squeeze * dot(direction, q) * direction) : stage.frequency * q; + float3 mv = stretched ? stage.frequency * (path.velocity + squeeze * dot(direction, path.velocity) * direction) : stage.frequency * path.velocity; + float amplitude = 1.0; + float scale = 1.0; + float value = 0.0; + float3 gradient = float3(0.0, 0.0, 0.0); + float curvature = 0.0; + float fold = 0.0; + for (int i = 0; i < octaves; i++) + { + float3 octaveShift = (float)i * stage.octaveStep; + float3 x = scale * (mq + stage.shift) + (stage.offset + octaveShift); + float4 n = cultmath_snoise_grad(x); + float3 g = float3(n.x, n.y, n.z); + float3 mg = stretched ? stage.frequency * (g + squeeze * dot(direction, g) * direction) : stage.frequency * g; + if (ridged) + { + float s = sign(n.w); + value += amplitude * (1.0 - abs(n.w)); + gradient += -amplitude * scale * s * mg; + } + else + { + value += amplitude * n.w; + gradient += amplitude * scale * mg; + } + + float weight = abs(amplitude); + float factor = abs(scale) * norm; + float nu = factor * path.speed; + float rounding = 9.5367431640625e-7 * (abs(scale) * (cultmath_csum(abs(mq)) + cultmath_csum(abs(stage.shift))) + cultmath_csum(abs(stage.offset)) + cultmath_csum(abs(octaveShift)) + 1.7320508 * nu * halfLength); + float valueSlack = 6.103515625e-5 + CULTMATH_SNOISE_LIPSCHITZ * rounding; + float gradientSlack = 2.44140625e-4 + CULTMATH_SNOISE_HESSIAN * rounding; + float hessianSlack = 9.765625e-4 + CULTMATH_SNOISE_THIRD * rounding; + float3x3 hm = cultmath_snoise_hessian(x); + float frobenius = sqrt(hm[0][0] * hm[0][0] + hm[0][1] * hm[0][1] + hm[0][2] * hm[0][2] + hm[1][0] * hm[1][0] + hm[1][1] * hm[1][1] + hm[1][2] * hm[1][2] + + hm[2][0] * hm[2][0] + hm[2][1] * hm[2][1] + hm[2][2] * hm[2][2]); + float hessianBound = min(CULTMATH_SNOISE_HESSIAN, frobenius + hessianSlack + CULTMATH_SNOISE_THIRD * nu * halfLength); + curvature += weight * factor * factor * hessianBound; + if (ridged) + { + float3 w = scale * mv; + float reach = hessianBound * nu * halfLength; + float gl = min(CULTMATH_SNOISE_LIPSCHITZ, length(g) + gradientSlack + reach); + float travel = (abs(dot(g, w)) + gradientSlack * length(w)) * halfLength + (hessianBound * nu * length(w) + gl * factor * path.acceleration) * (0.5 * halfLength * halfLength) + 2.0 * valueSlack; + fold += 2.0 * weight * max(travel - abs(n.w), 0.0); + } + + scale *= lacunarity; + amplitude *= gain; + } + + SpanNoise result; + result.field = float4(gradient, value); + result.curvature = curvature; + result.fold = fold; + return result; +} + +SpanNoise cultmath_span_fbm(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + return cultmath_span_stage(false, path, stage, octaves, lacunarity, gain); +} + +SpanNoise cultmath_span_ridged(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + return cultmath_span_stage(true, path, stage, octaves, lacunarity, gain); +} + float cultmath_value_noise(float2 position) { float2 cell = floor(position); From d172e7f71ff70a6893b1f2def21f78abb326d20c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 03:02:12 +0200 Subject: [PATCH 22/24] cultmath: regenerate CultMath.glsl and glsl-parity.json for the span stage twins (CULTMATH_WRITE_GLSL=1, Yggdrasil) CultMath.glsl gains the three structs and cultmath_span_stage/fbm/ridged by the lowering; the fixture gains the two span families after the last one, earlier families byte-identical (0 deleted lines). Builds; CultMath.Tests 452 passed at this tree. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/shaders/CultMath.glsl | 113 ++++ .../CultMath.Tests/fixtures/glsl-parity.json | 524 ++++++++++++++++++ 2 files changed, 637 insertions(+) diff --git a/packages/cultmath/shaders/CultMath.glsl b/packages/cultmath/shaders/CultMath.glsl index 09770947..a9ed32ac 100644 --- a/packages/cultmath/shaders/CultMath.glsl +++ b/packages/cultmath/shaders/CultMath.glsl @@ -872,6 +872,119 @@ vec4 cultmath_af_frustum_ball(vec2 centreSlope, float z0, float z1, float footpr +// Span stages (invariant 8): the SpanPath, SpanStage and SpanNoise of math.Span.cs, field for field in +// C#'s order, and cultmath_span_stage, its octave stage, statement for statement so the text is a bit-for-bit +// mirror. They follow the includes because the bounds read CULTMATH_SNOISE_LIPSCHITZ, _HESSIAN and _THIRD. +// A shader caller builds a SpanStage itself (SpanStage(1.0, 1.0, vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0)) +// is the identity of math.SpanStage.Identity, which has no shader form); see math.span_stage's comments for +// the span, the transform, the bounds and the allowances. +struct SpanPath +{ + vec3 midpoint; + vec3 velocity; + float speed; + float acceleration; + float halfLength; +}; + +struct SpanStage +{ + float frequency; + float stretch; + vec3 axis; + vec3 shift; + vec3 offset; + vec3 octaveStep; +}; + +struct SpanNoise +{ + vec4 field; + float curvature; + float fold; +}; + +// The octave stage behind cultmath_span_fbm and cultmath_span_ridged, as math.span_stage is behind span_fbm +// and span_ridged. The Frobenius norm of the Hessian is written out in C#'s order (GLSL has no _mRC member +// access); the C# local named step is octaveShift here (it would shadow GLSL's step()). +SpanNoise cultmath_span_stage(bool ridged, SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + vec3 q = path.midpoint; + float halfLength = path.halfLength; + octaves = clamp(octaves, 0, 16); + bool stretched = dot(stage.axis, stage.axis) > 0.0 && stage.stretch != 1.0; + vec3 direction = stretched ? normalize(stage.axis) : vec3(0.0, 0.0, 0.0); + float squeeze = stretched ? 1.0 / stage.stretch - 1.0 : 0.0; + float norm = abs(stage.frequency) * (stretched ? max(1.0, 1.0 / stage.stretch) : 1.0); + vec3 mq = stretched ? stage.frequency * (q + squeeze * dot(direction, q) * direction) : stage.frequency * q; + vec3 mv = stretched ? stage.frequency * (path.velocity + squeeze * dot(direction, path.velocity) * direction) : stage.frequency * path.velocity; + float amplitude = 1.0; + float scale = 1.0; + float value = 0.0; + vec3 gradient = vec3(0.0, 0.0, 0.0); + float curvature = 0.0; + float fold = 0.0; + for (int i = 0; i < octaves; i++) + { + vec3 octaveShift = float(i) * stage.octaveStep; + vec3 x = scale * (mq + stage.shift) + (stage.offset + octaveShift); + vec4 n = cultmath_snoise_grad(x); + vec3 g = vec3(n.x, n.y, n.z); + vec3 mg = stretched ? stage.frequency * (g + squeeze * dot(direction, g) * direction) : stage.frequency * g; + if (ridged) + { + float s = sign(n.w); + value += amplitude * (1.0 - abs(n.w)); + gradient += -amplitude * scale * s * mg; + } + else + { + value += amplitude * n.w; + gradient += amplitude * scale * mg; + } + + float weight = abs(amplitude); + float factor = abs(scale) * norm; + float nu = factor * path.speed; + float rounding = 9.5367431640625e-7 * (abs(scale) * (cultmath_csum(abs(mq)) + cultmath_csum(abs(stage.shift))) + cultmath_csum(abs(stage.offset)) + cultmath_csum(abs(octaveShift)) + 1.7320508 * nu * halfLength); + float valueSlack = 6.103515625e-5 + CULTMATH_SNOISE_LIPSCHITZ * rounding; + float gradientSlack = 2.44140625e-4 + CULTMATH_SNOISE_HESSIAN * rounding; + float hessianSlack = 9.765625e-4 + CULTMATH_SNOISE_THIRD * rounding; + mat3 hm = cultmath_snoise_hessian(x); + float frobenius = sqrt(hm[0][0] * hm[0][0] + hm[0][1] * hm[0][1] + hm[0][2] * hm[0][2] + hm[1][0] * hm[1][0] + hm[1][1] * hm[1][1] + hm[1][2] * hm[1][2] + + hm[2][0] * hm[2][0] + hm[2][1] * hm[2][1] + hm[2][2] * hm[2][2]); + float hessianBound = min(CULTMATH_SNOISE_HESSIAN, frobenius + hessianSlack + CULTMATH_SNOISE_THIRD * nu * halfLength); + curvature += weight * factor * factor * hessianBound; + if (ridged) + { + vec3 w = scale * mv; + float reach = hessianBound * nu * halfLength; + float gl = min(CULTMATH_SNOISE_LIPSCHITZ, length(g) + gradientSlack + reach); + float travel = (abs(dot(g, w)) + gradientSlack * length(w)) * halfLength + (hessianBound * nu * length(w) + gl * factor * path.acceleration) * (0.5 * halfLength * halfLength) + 2.0 * valueSlack; + fold += 2.0 * weight * max(travel - abs(n.w), 0.0); + } + + scale *= lacunarity; + amplitude *= gain; + } + + SpanNoise result; + result.field = vec4(gradient, value); + result.curvature = curvature; + result.fold = fold; + return result; +} + +SpanNoise cultmath_span_fbm(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + return cultmath_span_stage(false, path, stage, octaves, lacunarity, gain); +} + +SpanNoise cultmath_span_ridged(SpanPath path, SpanStage stage, int octaves, float lacunarity, float gain) +{ + return cultmath_span_stage(true, path, stage, octaves, lacunarity, gain); +} + float cultmath_value_noise(vec2 position) { vec2 cell = floor(position); diff --git a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json index 91abcd17..32b0fcce 100644 --- a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json +++ b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json @@ -27002,6 +27002,530 @@ "80000000 00000000 bfaab168 ff800000 bf0391a0 bf0391a0 -> 00000000", "00000000 00000000 3f54e0a8 ff800000 bfc96f89 bfc96f89 -> 00000000" ] + }, + { + "glsl": "SpanNoise cultmath_span_fbm(SpanPath, SpanStage, int, float, float)", + "tolerance": "ulp-bounded", + "cases": [ + "c1e8c486 408859a0 4209df1c bfd405da bfae7f45 bf4bf283 40173577 3f3df92e 3dcccccd 3daf634f 40773997 bee454c8 3f4650cf 3f117634 c0fa9f30 40328fb0 c084111c 40cdfff8 c07fcc48 c038ffca bfe64a6f bf5f5c56 3f9985f8 00000003 bfce4600 bf721003 -> beabe5a4 3e0539c6 bec85e69 3f290ddc 4052379f 00000000", + "c189c6d0 c1a1a95d 421a9dca bb872588 3e566bda bf6d4d57 3f7a38ae 3fad719d 3ca3d70a 3ebd46b1 421ba090 3fee0ccb bf94d83b be4eaf17 c0ab18ec c04962ae 40fc3e12 c0cf5763 c03a6dfc c0b8d06c c0333f47 c03ee6a2 c03de320 00000002 3f4d9338 be75267e -> be3bfd40 bee86efe 3ed91f5c 3e81cb36 4084a651 00000000", + "4171abd0 4100b594 41e79c38 be3ab428 3e2e3b8c 3eef71ee 3f391044 3ec58756 3f000000 3eca832b 40052ea3 be9184d6 bf096b92 3e9cac7c c0b8afb0 c0333dc6 c0b5cd4a c09daa9c 40d488ba bd778080 be8986c5 bfa7b7ac c039408d 00000004 3fcc6857 3eedc0c3 -> 3f1c45fe bf4ac425 3e59bc0e bd0437f6 4236b592 00000000", + "413fd3c4 c1a2b8dc c0912300 bfe8af9f 3f1b8111 3e81713d 400477b9 3faec659 3dcccccd bd984a9a 3e99c3d4 3f8ea27a 3f81ec15 3f8b2e04 c012a21a 40f315bc be0985a0 c019d566 c0bc628e c09185dc 3f84d85e 3f13f3e2 3e584f9a 00000005 3f36a156 3f8b8bfb -> 3e0e7221 3ed5cfce 3eab833a bfb45f1d 40b42f1f 00000000", + "40cc6a60 4133d034 41fba404 4019b988 3f139385 bdf9342c 402025a7 3fba886c 3ca3d70a bc858213 3e57ffb3 be856941 3ec0c011 c01f90c2 4089161c c0398198 40b8e87e c0882fa9 c0b1f685 c0819f02 402dd736 3f69e33e 3ea75944 00000005 bf53e0b0 bf2cc0e8 -> 3b852f1f bcbbec36 3d84e78b beaff143 3e6d6e6a 00000000", + "4231d2d0 c1aba9ae c2443623 3fab6f26 3f949615 bfaa0f42 4024a6c8 3f371a21 3f000000 3fd49533 3c766fa8 bd74c1ac 3f46c890 bf22300b 40e8094c 3fb8b970 c0de5299 c04379f4 40ef200a c048773a 4011b36b bfa17c11 bfea954b 00000004 40268527 bf27efb8 -> 4192a37c c39cd16a 438302f6 3e4b2811 4c91e2f6 00000000", + "c1d5f064 c194386a 41126b84 3f14e7f8 bf8c110b c02d2ab0 406c4b7c 3fb75103 3f000000 bb1e41ea 3e8cb0c3 400e1b01 bf966cf6 3fb3d0d9 400a57b8 c08c3d74 c0ffb8de 40bcd070 c0b325de bfc386ac 3fff3828 3ff71900 3f9aaaed 00000005 bff19635 bf845b60 -> bd324040 3c409948 bd7ead2c bf4ae73a 3f8949ed 00000000", + "3f802d20 c1067134 403266e0 bef9b9e5 3fbf0341 400ab782 402b653c 3db224a6 3ca3d70a bf0a21be 40077d71 3f09df29 c00b90e7 3eb0cc37 c0d9333b c0bab75f 3fd6ec60 40517608 c0487e0e c0d3c302 401c3cff c0359246 4024ee5a 00000001 c0285ac4 3f0860c1 -> 3effe827 be8c1b9f bf47ac03 3ea47b0c 413d149f 00000000", + "c0836d58 c2303c12 40913c88 3f02a951 3e1f7eb6 3ed1d98a 3f32319f 3f947d04 3ca3d70a bcb992d2 3c348184 3c81fbb2 3fa2796b bf490d24 3f9b5978 c0b9b5f2 c0748930 3fd20130 c00b19d6 409533f4 3ed6e69c 4030b114 3e2fb80c 00000005 3f3ac510 bf575170 -> 3c9899c7 3ee5208d be555174 bf2747fe 4389f73a 00000000", + "41a3f724 41bf57d8 c13ed9ac beb5332d 3f9d9383 3f746b6b 3fcc7e55 3fcb0906 00000000 3f9961ef 3c829ece bf112939 3f0a57af be552572 bfa9bc30 3ffe5758 40d50412 40cee2b6 c00b50ca 3ff7abc8 bea63a7d 3f7418a4 bfd66bc1 00000001 3febb7c6 3f8c99db -> c2a5b4e1 4299db1e c202831a bed784d7 4814f9ee 00000000", + "41466c9c c1fa6241 c1f201bf 40135b9c 3db49ce0 3fe964c4 404b110d 3ef0334e 3f000000 be55c93d 3e6684ec 3efca50f 3edf817e bd858552 c089c9da 40e68558 c0c5c125 c09327a4 4091b76c c0ce1070 be672ed8 3e103ee6 3fe589bd 00000003 3f5eb2fe be83c4b9 -> 401d2baa 40155d06 be090d41 bdecadcc 426bf920 00000000", + "41ac8848 c1c8d5d4 419ee46c bf8c9a83 402d8df3 be472928 403e36fb 3ffee66a 3ca3d70a 3c0d46fd 3c9bf162 be45aca4 3e744a01 beffecc5 bdfa11c0 40e8be3e c05e040c 407fbba8 40c4bafc 40515ef8 bfdadd3c 40084c64 3fde692e 00000006 3fae2c08 bf96b088 -> 3fbcc2f2 c010e5d0 40891e28 3fb3f9b5 447c6731 00000000", + "419b84c0 c15badb0 c214e7f1 bb8b29e7 3daa323b 3f925209 3f9ead5f 3e3f838b 3f000000 bd8e1057 3e268108 bd159118 3f7093f8 4016ad5e 4084b7f4 bfb191ec c085323e c0dd4ed7 40b8b6ea c0aad83f 3f9b3a24 bfbea860 bfa670af 00000007 40396b71 3f10163d -> 3ebada3d 4055ed07 beae3ae8 bdfa7d0a 480b76e5 00000000", + "c143b798 bfb00160 4223b0d8 becb8c2b bfc907e3 c0163973 403861ba 3fb8ee45 3ca3d70a 3e148220 3e216a44 be96defc 3fa0263b 3e72019d 4054ad64 408aa782 c0503aaa bf3e6958 c06b28a6 3fc7e108 3de9226d bfb45af6 bf8380ea 00000005 bf0b7977 3f868b0a -> beabf28f 402103cc 3ea1bc39 3f395fb0 42751101 00000000", + "422a9ee6 c2326299 41f372d4 3fec7421 bf9372e2 3f2fc27c 4012182c 3f2e6e91 00000000 bbfe1a2f 3e296a80 bf0b2f4a 3f5a96a5 bf6c5440 c0847ef4 40dc54be 40c57ee4 40f1ed84 40b3d6a4 3e9905e0 400ce8a4 c021a8db be7632d0 00000001 bf0e10b9 3e183bcc -> 3b849332 bc153149 3cd46fda 3d19da97 3d3162ff 00000000", + "4227afd4 40ce3ae0 42073478 bff05385 bf94e2c8 bfb6d1dc 402855c6 3f4a0183 00000000 3d700ca9 3cf17e9b bf354721 c0104a71 3e96ed08 c0099b4a c0273c84 3e9904e0 bfef5e4c bfdb0ae8 c05e8768 bf72a154 3ffcb970 40045238 00000005 403015c7 3f497076 -> c23f9b32 c30a23fc 4190ca7e 3f7a647d 480bab76 00000000", + "41cd4e28 4123e7e8 419d3464 3d855d67 be8d5546 bbaa051a 3e913ca1 3f753fb6 00000000 bd6960a2 3f133dcd 3f01e8d0 bff5a312 c00aec5c 40d4420a 40f130d8 c0f8accf 3fb379f0 3feb0910 c07b13a8 bf6076c5 3e1b28ba bfaf18e8 00000007 bfdd6d30 bf26cf77 -> 3ded1a42 bf9eb62b bf0098b8 be2800c0 419aaf14 00000000", + "c0ce3178 c14c8710 422d6d32 3fbaec37 3f78eaf5 c01a82de 403f3ec3 3e439da5 3ca3d70a bdf42534 3f83d100 bd18b614 3d502dcc bf018def 3f023660 c0b0a519 4013d810 c0cedc18 c0e6f551 c094e390 bf8b784a c01bb2f7 c030fa23 00000002 bfd5b3fc bf21b657 -> 3e29ca4c bd2d2070 3e557ed5 3efe5d6d 3fa3f121 00000000", + "407c8a80 41976410 c1e42d6d 3f3e9daf 3f028cb3 bfa84f50 3fcc2381 3fd7b1da 00000000 c09b3fd5 41013bb7 bf2eacc7 3fbf8072 3eff3d88 c0b977dc 40df71c2 c0650a84 40fb91da c0c74236 40802768 400d565b 4036d0e7 c039a62d 00000003 c0104194 3f420f50 -> 40b6c1c0 41915625 c1f443d2 bdfc5598 463d0826 00000000", + "c101faa0 c2246379 419e1838 c009e8c5 becee55d bf7fcdae 401b62ed 3f6d5d9b 3ca3d70a bda14697 405922db 401a0b3b 3ec73f7a 3fcdcf23 401ff42c c0f26032 401cde2c c05766ba c003a452 402b5ad4 3f951e18 4026d012 3f74c1a3 00000004 4038df8e 3e8bae39 -> 3e05ec4a bd530b80 be5d051a 3e0e2536 40c738a6 00000000", + "c13346b0 41f6276c 420e4546 40178adc 3d688fb8 3f90314d 4027e8db 3ad4cb50 3f000000 c0b826b7 3d0395ab bf4c86f9 3eedefd0 3f208905 3ea5cc20 c0feaa41 40f98f92 c07ead52 c0b3dc38 40ffa62e 3ffdc042 3ec711bf bf69aa23 00000008 3ff4295f 3e4fcef2 -> c377f82c 4317cbf4 4357025f bee87499 4abde401 00000000", + "c2474822 c1897a80 41c5fa74 bfc081f2 be8f0bb5 3f30bf8f 3fe57cf7 3f92ac00 3dcccccd c0aed12c 3e5ce478 bde110f5 3f7dc9b0 be623f80 40423ec0 409cfa26 3fb06088 c0d4ed0c 40c858e4 c03b1ff0 c02515ed 3e2a6b6e be706064 00000008 400f667e 3f8c96aa -> 45e681de c680d80e 44b7fa2f bf7222fa 4fc96248 00000000", + "42372ab8 c0123920 4225584e 3e46a4a2 3f38d0c9 bfbe3847 3fdfdb66 3f5a88a6 3dcccccd bddbec60 41c5aab5 3e062c2f bf9aa084 bf989938 c0b0ce54 40a33b98 3ed8fe40 3ff775f0 40c03150 bee58900 c01eec70 3e095f6b bfa10dce 00000008 3f8ac558 3f513c21 -> 3c7c6a40 3ecbdd24 bee2f328 3e845d37 4035459e 00000000", + "408af178 41a23fa8 418c8748 c000fb6e bf820795 bfc09004 402fad5c 3fd0bde9 3ca3d70a 40860c6d 40827671 bc4a5e83 bef90d91 be202b0e 4098db26 402fe65c 4079706c 401b5a20 4045b8d4 407f17f4 401a1d80 3de32362 bf9e4e89 00000006 c0332eb7 be62299c -> c1188c68 c0127f11 40c75df0 bea077a4 47073620 00000000", + "41d25c84 c23c0cdf c20f60c1 3f8d74e3 3ff473f5 bdec94f5 4013ae54 3f7ae2a2 3dcccccd 410e0e65 420ae7ae bf185aa3 be33ad5a 3efe88a7 3ff42b10 403612ec c0fc7bfa c09fa6fe 40cf909e c04b20a6 bee9a968 bfee04e9 bfccb1d8 00000004 403a3287 3f80ce75 -> c3982d36 41ae3ea8 c3b0ac48 bf2e824c 4a3b65e2 00000000", + "c00eed60 c1f9c353 c1743468 be872098 3e9191d8 3f36174c 3f4f6966 3f533a57 00000000 be0116b2 3ed38680 bf8a2323 3ec91869 bf80b05d 40cbad9e be41b320 403de714 c0ee90b1 c0f14314 c00dbc3e 3fc8fa5b c01450e8 3e25ee7c 00000003 c002c1cb bf74062e -> 3f1683cd bb33fd00 3efa9cfc bea2e09e 422f39f5 00000000", + "c22ebbe2 bfbcbe00 c22d8ece bf82a489 3e172181 3e1d9f3d 3f8576bf 3e4fe027 00000000 3bdf54db 3e859a53 40187857 bf088117 bf10aa56 40e9dd06 406a649c c0ff713c 408f580a 40f7545a c01cd20a be0709c8 4007a19c bf6d7d6a 00000005 403d378e 3e631b0d -> 3b144e5d bb8aa09e bca31034 3dbcd6fd 3ec6ccc9 00000000", + "c22ef0d5 c226860f 40940d30 be5895d2 3ebd7d13 3ec06e4e 3f483a4b 3edaf920 3f000000 c0abe0a3 415e681d bfd20629 4006573e bf62800f 4071e64c 40b5e4fc bfc2d310 c09e99ba 409f7b52 40b652c0 4019908d 3febfe1f 403da491 00000004 3fd500b8 3ea02514 -> 4148d96f 4191920d 41827b8a 3ea70d30 45a3d818 00000000", + "c22ab0a6 c1efed8a c1640d50 3d9fca6a bd106bd7 3f13aed1 3f16996a 3e81dec4 3ca3d70a 3c6476c4 428f10ed 3f4db2b6 bf56e31f bfd534e0 3fa52e48 c0b55d62 409371a6 c0f16f5d c0598a22 40adb1e4 bf26a117 3fd32243 bffa4352 00000001 400b9c55 bf3e0c90 -> bc754ac8 bc1b4f65 bb250a07 bd0923d3 3b1e516a 00000000", + "40a078c0 c0d7f3c0 41a1e938 bd491ae4 bcdeb047 3f396332 3f39f17f 3f8ff087 00000000 bc7043bf 3c46f52c 3f6a46d5 bf2adf15 bf93bbd4 c0985fb4 c0360fe0 c04b17d0 3fadf058 c081cb7e 40aadfd8 3f288b0d 3fb367a9 c02c1802 00000003 3f8efdcf 3eb0e626 -> 3fc03631 bf8d1a48 bfedaf3f bed4d5e6 42531e27 00000000", + "c17b4514 41f29130 4239f0f2 3f66df55 bf0bb35d 3f2d47f0 3fa93fca 3f320b23 3dcccccd 404fd5ed 3fbc4642 bfe53418 3fa6f122 3fbba932 c0a46ade c0d6c057 3e3efe80 bf1f6658 bf3b1750 408c68ca c0123148 4039d763 c0216e01 00000006 40066bc4 be93f058 -> c0919b2b 3ff84acf 40ef8ea0 3ea0f90d 45db941c 00000000", + "c1e4c8de 41ba4210 41f58304 3fe0355b bf17bd14 bfd1b808 401e1eaf 3e2e0e8b 00000000 c0b58068 4140629c 3fbf9144 bf037b40 bfa5f2f9 3f72a940 bf9dbf00 4060d6d4 c0cea58c bdee2180 3e8c8d60 3fca08ba bfe5f40b bf44e87c 00000008 c03c5ddf 3edec443 -> 4218ab66 42b82b40 41e54382 beba9708 4bc3a406 00000000", + "c23b8ec2 c0f67120 bfb7ea00 3ea23f8e bd1489e8 be295efe 3eb81125 3c2cdef8 3ca3d70a bb23ab4d 3dd9ea35 bf2b45e5 be3d11d8 3f98b414 40950a00 40799824 c0b5a5d8 40452128 c05dd8d8 bb7d3000 bdf10c64 bfb40370 3f175799 00000007 bfd7c2f4 bee82815 -> bc60f16e 3c000a2c 3c48dc08 3e9cc376 3e617d62 00000000", + "c1ffb6ae 40d16fe8 c22cb319 3fa6d768 3f04eb30 3e987b4e 3fc095f9 3f33dfaf 3dcccccd 4111ef02 3e537bea bf28fb62 3f8f9cec 3e1391ff c04dc29c c0062e84 4003c4ac bfa336b4 409d1ad4 40dbb1d2 bf7c5fa8 bf18b4ab c0373f54 00000001 4032abc0 3e09e1d9 -> c2804c8f 4306791d 3f1d2285 bebaaa4b 47d3f531 00000000", + "c16393fc 40498970 418ac5ac 3cdecc73 3eb869ec 3eae8151 3efe45b3 3fe94915 00000000 3b465572 3cb84973 bed59b57 bf05e65f bf7860fe c0d0820b 403cbdc4 408f244a bfea3be8 c018d5b4 40845102 3f44f44e 3fbcec2d bf51f3fa 00000004 40293138 3e04c2c2 -> 3c20814b 3b0055d2 3c93ddf8 3f165b4c 3fb9d308 00000000", + "c20aefe4 42215ed0 c22c52b6 be905088 bef9e826 3e38e733 3f1783e3 3e286dc0 00000000 bbd33a6c 3d847d7c 3fb8294f 3f4ef38d 3daae83e 40983458 40a30048 c0945b14 bfffa0f0 c0cf7349 40c2d8ea 3feac2c6 3ff5a60e 3fdabe8f 00000005 bf8e354f bf8ad0dd -> 3b89aa80 3c7cdbc0 bd88b2db 3dcafe80 40082b55 00000000", + "415f15a4 c1083a08 bfab6ae0 3f3cca0b c02193d4 bf828f21 403dd42f 3fb9b05a 3dcccccd 3f1024d9 3d813dc8 3fba2622 3f82d30c 3f93d455 40dccd36 bfdc5f94 40d1ea54 c0a68a5e 408063e2 c0b073b1 c007d2bc 3ff18ff1 c00b1c3a 00000001 bfb90b0a bf276008 -> 411c24cb 40ff3ad8 40eefa13 3f4dee18 458a6d8e 00000000", + "c1dc0226 409882d0 40d35ff8 bfd2b98d 3e110a75 bf4489fe 3fee0283 3ebfae1b 3dcccccd 3de8bab1 3c906ea5 be987339 4034f869 3e08b5d4 40e7e21a 3fd059c8 c0e35bcd bfd884b4 bed42740 c0f4d7e6 bf83deb9 3f963d16 bf912118 00000001 4037422a bf16cfe5 -> 3fb1c646 c13a594c bebfe100 3ee903cb 45107853 00000000", + "c1c0eeb4 c1001f50 c24268b7 bd6375b0 bf1083b8 3ee0d78b 3f48d88e 3f2c0586 3dcccccd 3d584989 40072769 3f8b5bef be0d90e0 3efb19c9 3ff7db08 bfd805fc 3fe62820 bf1d1598 c057497e bfdf69c4 3f9c6557 3f9845f5 bf5f475b 00000007 bfe2eef0 bec6a711 -> bd023ce4 bc81719e 3985b870 3e82d970 3fd8d2dc 00000000", + "41fe142c 4129e8c8 420722a2 3fb719a3 3f07aec7 3ea19020 3fcd0f34 3ee27565 3dcccccd 3bd63e83 3ec92df7 401e93bf bf080be8 bfbb02e3 4095043c c0f4f28a be930370 c0f67ad4 c00bc180 40b8c902 3fee22ab bf8a91a3 bf61e0d4 00000005 3f99ab82 bee0349e -> 3d3ceb30 bc446e5b bce1c991 3eb49e6a 3c743699 00000000", + "41c572a4 422ec68c c1f32288 3fae2a74 bf3eb647 bfd111ec 40102ad0 3fa1aa0d 00000000 3d18a6b0 3d0300e0 3ed211fd 3f994298 c00638a2 3f718700 4094f6d2 bf8dfcbc 40c5d076 bf272e50 c00b6948 c03750d3 bf7b4183 bfa2e751 00000006 3ffc6326 bebc2c1a -> 3ebca933 3f63b3e6 bfc73e20 be48025e 440aa932 00000000", + "41c56454 41b4e2f4 42198eb4 3e80372a 400ec263 bfa44ab5 4025ff63 3ecd5750 3ca3d70a bcb58cc2 3dbcf515 beca9e70 be9efd37 3e82ffaa beec75a0 bfe12304 c062b746 c00359dc 40e5788c 4054deec 3fda2ca6 bfd453dd 3f1f991d 00000004 403b5f56 bf3dfad3 -> bfbc94b3 c00c4c14 3fd87bc6 bf994f8c 4472e934 00000000", + "c1968389 419c5290 41574878 3e8b71ee bf1638e3 bd5997cf 3f262bd8 3faa8f72 00000000 40bc546b 403bf13a bfcce6f8 bf9f7101 bf8bdb46 3fc392a0 40f77fe4 c0a56074 c0b626e2 40efa8be c04d2c92 3fe6c8be 3f66379d c0227eb6 00000005 c03ad71c 3f245067 -> 4226c89a 43521ddf c30fb345 bf3ce258 49df6efc 00000000", + "421bd68a 411fdd98 423cbf8e be548efe bf6338f9 bfa53c2a 3fca46fa 3fa6de72 00000000 406ebc6d 41f8ff42 be3ca009 3e3b8fa7 3eeb10e4 c0d58352 40f07626 c02f4e7e c0d59f52 3f619880 bf742230 c0359918 3fe22855 4032de16 00000001 c00c4f9a bf874eac -> bf96a4ae c101f10b 402567b6 3df261c9 438c40e2 00000000", + "41e52f20 c07fa660 c2125a47 bfb0a809 3eaee9b7 c01f78c2 4037bf99 3de3ff52 3ca3d70a 3bbddb34 41d9ae3e 3dcf8a72 becacbeb 3f1f5cbd c0c0c100 4057584c bf88d944 3f8adea0 409e9c82 40cf49c2 bfc7bdae bf72da19 c0152ff4 00000004 c0370912 3e06e652 -> bcb5b4f3 bc49d780 bb831683 bee32ef3 3b906ea9 00000000", + "c20d4088 c05ff650 c115b880 bf6affcf 3e4a16b5 3e857026 3f947bb7 3fed8cc3 3dcccccd bde039a2 3e3fef7e 4005c75e 3ee3191e 3ff8cdd5 c0c707b2 40d29c46 c0017c5e c0831230 bdb07400 c00a977a 4009f3be 3fb8f755 bea76d8b 00000003 c01d34eb bf862c5c -> c066d8c1 be715548 c09cbad0 3e4d8508 4460432b 00000000", + "419ab084 422b18a0 c224c589 3ed10fdb 3df3cccc 3e03d732 3ee38635 3f1934a8 00000000 40a28ea6 3f91d12a bf632bce 3f6c874e bfa3d715 40ca143a 408e86b2 c0a980d0 40d0505e 4079a004 be84c250 4001f40d 3e51007a 3f704b1b 00000008 3fcbaa7c bf601596 -> 4180b5e2 42057e34 41a5ea06 bf2036f1 48bd05d3 00000000", + "422cbd06 422a39b8 c1bae1a6 bfa99e27 3e1f7832 bf0a02bd 3fb832f3 3f42a27f 00000000 bb060bc9 3df1378b bff8e65c beecdc30 3f317d70 bfde1830 40960284 c0afbce1 c0b7298a 403446a8 c0421dd6 3fb22f59 bf2f7f36 401b3611 00000007 bfd58e15 bf3f4c6c -> 3e0ec8b3 3d41a222 bd37b7d0 3f0dc343 3f923c60 00000000", + "41c16600 c227f41b 42164b02 3e91f581 3f181509 3ec51185 3f435b26 3f9c159f 00000000 3d4e4a31 3f855e77 3d2c150f 3ea5edb0 befcab4c c0aaa9ae 401f8c98 c0b7778e 3ee1a280 4086e1e0 3ff8aff0 400eae78 c008adae beb2f91f 00000008 3f764865 3ea55a29 -> bd050a99 3c9e8e5b bdab7c49 3d01541f 3dbb79cd 00000000", + "41793ac0 c21b90c0 c24495cb 3df01ec4 bfaa85b9 3f90337a 3fdfd2de 3ea4a7a4 00000000 3c5a32d5 3fb87386 bf57545c 3f98a3f8 bf02afbb 40309794 bacbd000 40a73724 c049af8a 40eb94ee 3e032440 3faf67f9 3e0826c9 bfdb8c09 00000008 3fed06b5 3f773189 -> bf003889 be754bba bf6481ed c00ec382 42003844 00000000", + "4143e7a4 c067f020 c03a20e0 3ebdfdc4 bf0271f8 bf0baf33 3f758f84 3fa0a8bd 3dcccccd bdc025c0 3e394828 bee6e00c 3d880df4 bedccc9a 40a68ac2 c09f66b3 bee7f380 4022e6bc 402785e4 40a67ce0 3f1de3c6 3fce9d3d 3e97e9e0 00000004 bfe3bafb bf5664ad -> bfda0186 3e02adf4 c004277d 3f51c105 43da7a98 00000000", + "c2018b5b 414ce190 424261b8 c0155a97 bf08d91e bf7b1a69 402a7479 3f42dd79 3dcccccd 3a9b29b4 4196096f bf234a68 bebbd73b 3f24b59a c026670e 40cd86aa 40aafb56 c0ee3ca0 4004f840 3fb2a5c8 bf9056a3 401b0863 bfa6c9b3 00000002 c002183a bf30ad28 -> 3a417bd2 b8014a30 3a8553dd be124e4b 38d31734 00000000", + "bf277000 c201e667 bfc54760 beeea1ff 3e13e117 3f1a3ce1 3f483b8e 3eafba17 3ca3d70a 40c70c84 3da8b07a 3fd3ea87 3f8372ca bde015a8 bfc9adfc 3f6bf200 c0ccc83a 40365568 40bc71fc 4087ed02 c028cf87 3fddc284 3fce2112 00000006 40072c76 3f8bc4a2 -> 466aa96c 4613c14e c440b022 bef3290f 4e828636 00000000", + "c22f76a6 40bbdb50 409d6fd8 bd99697f be006249 be83c4b2 3f35829e 3f53835d 3f000000 bcd2b064 42ad700d 3ee70e84 bedf6418 3e932622 c0d7595c 400799f8 40deeece bfb8ce70 40f65dec bfc201c8 40309555 402a80cd bdd1465f 00000007 bfca1d28 bebf92ba -> bd735611 bd1003e1 3d2573fb be58623f 3e2cfcb2 00000000", + "41412ad0 c19c89ce 420bf800 bf4f1f57 3a8d57d5 bfeb1743 400071f7 3e894229 00000000 3a8a5569 3d948b2c bf79ff44 3f80c270 bf68becc 40146b98 40f7b15a 4075c1c0 c07ca6f0 c009d330 bedd2500 40226efb 3f91e81f bfced978 00000005 bf862e03 bf050c7e -> bbdf40a4 3bc686b1 bc0b0ec5 3f0efa17 3c0bdc50 00000000", + "41dd4500 c214cee9 c1996f6e beeede01 3ff1fffe bf8f302c 4010e151 3f6615c7 3ca3d70a 3be11e39 413d25a9 3eb785f6 3e1ada51 bf2318f3 be3dea80 3faedf60 40fddc08 c09c1185 40b3dcc8 3ee2c380 3f805874 be6c7114 3fca7239 00000007 3fd524a2 bf95c3ba -> 3f149a8f 3d33ba70 3ec5e2b6 c0644e78 40057d69 00000000", + "423db370 c22f3b46 4238826c bf92af59 be4d049e 40163097 4029c5cb 3eab4c59 3dcccccd 3f0b7aca 3e7ec0a4 bfb54f6c be229752 bf83053e be573d20 40694388 40ed4946 40b36d24 be8e4c90 c0dabb68 3fb40c04 403a8be0 befd22cb 00000002 401944c3 3eda83a1 -> 4011c48e bd84d96c 40350927 bed28986 4466069c 00000000", + "40c1a468 41678558 c13ca070 bf1ee3c4 3f6370cc bfb99cce 3ff76088 3f9c890e 3dcccccd bb771cd6 3d7441f8 bd281043 3edc4d63 3ee876be 40e4e86c 3fa55438 c0e84dbf c0d3ebf2 c0cb454a 40bd2836 bee346e1 c038877f 3e51a89a 00000001 3f406b20 3f946247 -> bb8f742d 3d5eba5a 3d53ca4e bc52783a 3e14fd49 00000000", + "4219e07c 422893ca 41db3958 3fb96177 3d4f921d bf0a9997 40162c22 3fcca806 3f000000 bd84fa3f 428903b3 3d6245d0 c000bb60 400706fc bf7ab710 4036a6dc 40ec6ed4 c0f3ab55 40d25b8c c0e67bcd 3eab56eb bfa38661 c03a4b78 00000001 bf8fb148 3e43505c -> 3c5da552 be6120d2 be5af53b 3e6de0f3 3e703713 00000000", + "4183cee4 421792a6 c1ff9628 3e2bddc1 3f38ab1d 3fc596f3 3fdb279e 3f83a55a 00000000 4047a6a1 3eaaeace bf2ccd03 bdf52839 3c98cec0 c0c00f8a bf75f628 4027a7b8 c0dbe5a2 3ebe7d60 40ae3968 400f0bb3 bff15000 400a2812 00000008 bf991a8b bead8475 -> bf569345 3fb688b4 40497ffc 3f136092 458b9880 00000000", + "c0f53c70 424147c6 c1684a8c bfffe56f 3f4a34bc 3f9f59b9 401f17f8 3db76e5b 3ca3d70a 3d895acb 41ad3a24 bf7fa4b7 bf1574da 3f72a54d 40de1440 c0dfa418 c01a23dc 40c9d96a 3fd707b0 408ad7d8 be212d0e bffb2917 3f333241 00000008 c0181047 bf8aa59a -> c0f97648 c3020c9c c28f1d1a 3fcba7d5 47baccec 00000000", + "424440fe c242d229 c1a21949 3e9d4867 c0127d2b 3f3859fd 401cdbeb 3fcbc712 3ca3d70a 3a9c120b 3cb0bc8b 3d9d13b1 bf79816f 3f07f7ac c093b382 bfdf9c74 bfae1a20 3fa1a630 c0f691e3 bf181aa0 c00edd46 c02c8ae2 bfe6e7ca 00000002 3f9ec80a 3eed332b -> bb6b48c3 3cb91fca bc257000 3f8ef87a 3e2cc81e 00000000", + "420d252a 3f1ef800 c0e0e3e0 3d3699bf bec8e55b 3ee8515d 3f452ef2 3fd7f779 3dcccccd bdb36116 410f8916 bedbdbdb c02127b3 3f8e79a1 3fc30b88 c062b7ce c0cb7371 c0e5a5e9 4079c1d4 bf811d44 c0130396 3fe08c3a 40275f1f 00000006 bf43c50f bf08d1c7 -> 3db11a11 3d3adb04 3e175b8a 3d041bfe 3eaa9013 00000000", + "41f99554 c0e978f8 c1a5566e bfc1d938 3f1585c9 c00fe2bf 403b7c51 3fc85c9c 3dcccccd bfca77a7 408f906d 3f193971 3f15a6d6 be17aa10 402f102c c09b3db1 40431dec c012636e 3fe8b270 c085fdb8 bfcd85e4 c0231cd2 4021ff47 00000002 3fcb3138 be1843a9 -> bfe9adc2 401ec9b6 bfa4935c be00f741 433f622b 00000000", + "3fd7b640 4245d3d8 bf8e5180 3feca912 bf34a12a bebbd25e 4023c3a3 3f8bcf74 3f000000 3c975c69 4235cc82 bf0c41d2 3ff61c42 3fc6f307 c09322ac 40084e1c 4059edb8 bffa9c00 bfafdba4 408d0172 bedf9348 c03d3a45 bfce93a1 00000001 40105a8c 3f881791 -> bc09e4d9 3b317d0b bbfbbf34 be549567 3c49833e 00000000", + "c2477acc c214cb3a 42282bc6 3fb65244 bfc3c184 3fe971bc 4031ca9b 3e56bb17 3ca3d70a bfeef7e0 3f03e904 3e956e75 3f8f101a bfb97a3c c0396f6c c0c405bc c0f08f4c c021f8a4 40ee6c78 c0edce19 bf6dbea8 4007f5c5 be66e1fe 00000001 bf7c545a 3f656811 -> bf928252 c0b01891 3faab05d 3e71b3c6 44369c78 00000000", + "40f320d0 420f2e18 420ae070 bf0864c1 bdb98fad 3e414e53 3fc8cdd2 3fff0f0a 3f000000 3bb65731 4201591a bc3cfd70 be89faf3 3ff952ef c024e734 c0f913ba 40ffb1a4 c099b85a c022c826 409b6c42 40118cef 3fa631bf be0ae912 00000005 402295bb bdf8a462 -> 3a308d2a bc7f529d baff4006 3f152f5d 3b8130d8 00000000", + "c2478100 c1ce6ae1 c154b250 bda51001 3e47eb88 bd32e22a 3e5cdc4e 3ff9c4ef 00000000 bc3f6b96 40037df2 bd4a95c4 400b361e 3f8b5169 40995294 40baf25a bf0b5c30 3fef7270 c0af5a50 40f5366c 3ed2fc71 400bcd2c bf86b4b8 00000004 3f0b01be 3f906982 -> 3c1a2e01 bc9249f7 3d19825b 3ea4c9b4 3bcdc99c 00000000", + "c157ab94 c167ed68 c2252a4e bfaec272 bdb64b67 bd78a1fc 3faf4d86 3f84a192 00000000 3bd006f0 41d92909 3f55e7d7 3fca9bd2 3fc863d5 4050f63c c0aac42e 409bf5e0 bf78e178 c03990b0 403f6f1c bf34598c 3ee5195a 400b5918 00000004 c023ceb9 3f4451db -> bd855545 3d917e22 bd0c6035 be8ef69f 3e4fb84a 00000000", + "c1db574c 3f594b40 422ae0ec bd615e31 3fb37a70 3f8504cd 3ff02116 3e84f74e 3f000000 bda33ae9 3df9df39 3f43211d 3fdee1ff 3e385923 c0d69a4c 40ed3eaa c0b73da6 40c3b098 402c0cd8 c001fa6a 3fdd7c50 3e98b4a6 3fa2e5a5 00000006 3f7705f8 beefa8cc -> 3e58a321 3eb79ea8 3ef26f55 bef98fe9 42273317 00000000", + "c20a8978 4205685c 417bf410 bf61a0db c00ee0e8 bf0439b9 401d2067 3ffd9df8 00000000 3d947e9c 418c3582 3d933996 be45b86a bfa6d9aa 3fc75310 407fac18 bf5800d0 bfa2f5b4 40e2c4c2 c0c061d0 c0281213 be91ef15 3eb2553f 00000006 bf0d2d29 3e2a2af1 -> bde71b4e 3e21c749 bd08dc4c 3ed587b1 3dd06a41 00000000", + "41ab007c 417d22a0 4156eb5c 3e1f3265 bdff4f7d bfe62d7a 3feade1a 3fa40c5e 3ca3d70a beda0990 41b48aed bda7fbaa bf103e93 3f4fb8d0 4037ab34 bff94fe0 bdf1c8c0 bf028420 c05d9d84 c083b42c c002d3f4 c0283631 3f2450a0 00000003 c0275336 bf1c0689 -> c0690514 4028b7a7 3fcf909f 3d3a6f83 43585e74 00000000", + "c0b44d48 4204f0f0 c1b650c9 bda3325f 3f769193 ba98aa64 3f9855b5 3ee50887 3f000000 bb313e35 3f39ba1e 3f5a7ea2 bee862b9 3f358fc3 4095fde8 3ff2c8f8 c07151f0 3ef56ca0 bfb22e08 409782fa 3f941f53 3ef3c426 bee0c01b 00000001 c015fe08 bf969217 -> 3b266d90 3a03fc72 3a065e30 3d129e66 39d697d0 00000000", + "41ca8930 c21b281c 3f20e700 3d726198 3f413a12 3b921f20 3f41d2b8 3f6e9a0d 00000000 3b3f955c 41c9570b bf402abc 3ec8b462 3d859281 402a10c8 c0406bf6 bf582c48 3eaac7c0 bfd74818 408c5902 c02f9e76 3fcea2f4 402b5d4e 00000001 c0165082 bf5bfc6a -> b9f1d0e1 bae5c874 3b991f17 be1e2685 38c80158 00000000", + "c02a9df0 4214aafa 423f371a 3e5b2aea 3e9663ec bf2d6447 3f59c4b9 3f51ea58 3dcccccd 3cbab03b 40e02258 3eb314b3 3fc9e004 bf2b624b bf47e428 40d16a7a c080b778 403a6a28 4093f8de c0c0d70b c039cead 40379675 bfef73d5 00000001 40368ca2 3f4bfb22 -> 3d026a32 3c7b95f9 3d122b58 3ec27786 3c20dd80 00000000", + "c131ca20 c215af01 415938b0 3dc732ce c00bb1cc 3c5874e3 400d5e50 3f994504 3ca3d70a 409819d1 3f8e0320 3fe5fea3 bec13260 bc60b3ec 3faac290 c0fcb67d 40373774 c0e75f4b 400a3724 40da64aa bfae5775 3fa08d9a 3ff9fb95 00000004 bfc525db 3f8fc6c1 -> c11e654e c1a19a91 c22b8617 3bc62030 4711c51e 00000000", + "c1607b64 41bc6314 c1de9344 be0e3bb0 3d95ab1e 3ec44392 3f9bcc61 3fcd8ec2 3f000000 baee3526 41e5a14b bffa1e59 bf418314 be4888fb c02765e0 c0c79b58 bfbb1adc 40e52ddc bfbd0214 4073a48c 3e775b14 3fa4e1a7 bf85a3e6 00000004 c0219fc9 3f51593d -> bc0ed100 3ca53395 3b5b7ef4 3f052be0 3c5d106c 00000000", + "c19a40d8 c236a3e2 c1454224 3fcbb0e1 401ec8f2 3eabbfab 403f3acb 3f892ac3 3ca3d70a 3a8e769b 3dc88277 bf9282d9 bd44f16d 3f15dd9d c07cf312 bf6d31d8 c0615196 40f7c944 c09dea16 3da8d080 3f640724 3f9844d2 3e83dfe4 00000004 402cc07a 3f14ea8a -> 3d8f6e5c 3c3c1e1d bd016fa7 3dd2b9b5 3eb1bb90 00000000", + "42228352 413da808 c1d62cff 3ecde4dc 3f6bf3b0 3f2e5852 3fef9f72 3fa854e4 3f000000 3bb035b4 423ddf19 bf3010e8 3ffb2f5f 3da2dc21 bffe7b54 3f33db50 405dd408 3ef82860 40dc470e c09c11a7 3ffbe340 3ed392a1 bfd5f962 00000006 3f5d90e4 3f03ea63 -> 3ab0594e 3a73af29 bb7c18c2 bf023296 3ad7ca4e 00000000", + "4147a208 c1dd0c6f 42236f7a 3f316555 bf91082f 3fbce821 40002582 3f574315 3ca3d70a bbae6b27 4066c805 3fa60ac4 bf55edd0 bef93874 bf266a98 c051acda 408b887e 40e17232 400788d4 c0dddb05 beb342d8 3e0f7e44 402ae8ca 00000005 400afeab 3f42424b -> bce95519 3cf9e170 3cb78152 bf349c12 3e5e4b0a 00000000", + "bf85a280 c1efbf95 c22b1c60 3ee3a0b3 3fde78eb bfa28fb4 400eb535 3fcb38a4 3ca3d70a 3bcc1631 3f197ab1 4009c84f bf87bf43 bf3fe060 c02269f6 40a48412 c08d8ecc 3f500120 40bfe67e 40661fd0 4038be27 402f0b5b 3feabcd4 00000005 4005cd93 be141c7e -> bc1d66fa 3c3ee2d2 3c8d6522 3e91c396 3ba7f7b4 00000000", + "c0d38930 c2395499 c20ad788 3ff07976 bfdfc4db 3f2c5886 4029cc72 3eb862bd 00000000 bfad59a8 3dcc3958 bf6911f5 bf8f407e 3f983dbc 40aba542 3e786a40 c0d46209 bf782370 bf9dc934 4083a84e be37ed5c 3e9b41bd 3fe34ebd 00000008 c02a3d1b 3ecacbe2 -> c0c7ad0c 410b6b81 40e25c0c 3cc2150e 4b9df232 00000000", + "41784a20 4195f57c 421c0e08 3da92aad be88ef1e bedcb0cd 3f2da822 3fd26d98 3dcccccd c0519ff3 4038f2d8 3fd39364 3fa4b5e5 3f369a92 40f95c70 c0151496 408f4d12 c00f9f4a 409f1124 406db770 400a50d7 3efcb5f1 c012951d 00000002 4006534c becf3c53 -> 407a39fa 4048fe0a c06b2519 3e7f799d 44cfa8de 00000000", + "4196f400 c202c6e9 c23a5f66 3ea1ce65 bfbc8d35 bf5d9ca7 3fecedf9 3f91335d 3dcccccd 409977e2 409d8f26 be6abec3 bdb5ce9b 3f1994ea c0b7373a c0c22490 40169f78 3fc14030 c0d2ed7a 40205dfc bfb9b45e 3ef4436b c03fd6d5 00000001 3fea6092 bde68c9a -> c08af436 40a024db 3eb9c3d6 3d5736b5 449ff984 00000000", + "41fa9c64 c240f078 c1bb1f89 bf085976 bf0e1a65 bf8d0709 3fc1d440 3fda440d 3dcccccd bf235a34 3c4c8be3 bf93338a 3fed90bb bfe1a8d6 c0a12834 c0b7fd70 c0fc0747 c0dfe301 402ef414 4099f948 4036afef 402ae4b8 3fda4e38 00000003 4014e6cc 3f128f0c -> c2959a32 42e43ed2 c2de1df4 3f638330 49f32056 00000000", + "40f71830 c2317a68 c18ebd70 3f7014de 3ee1695e 3e1a4532 3f87715e 3f0fcd0b 3ca3d70a bd517449 3ce29702 4009f12e bf9d99c9 bf9b15ee 40d69a0e c0590b12 c09936ee 40814958 408dc3a6 c0972414 403980b3 40296153 c0193d67 00000001 bff2b67e 3f0a886f -> 40467197 bfe9555f bfd77fc6 bef7d08c 433e2daa 00000000", + "c19ba0d0 40fb5420 40ea88f8 3f4fab49 bf468452 bf250bbe 3fb112de 3f643c27 3dcccccd c002f858 3c868164 3fe3e33b bf6615e1 3f97d6bc c0bac123 40724a90 40c51e78 c0ae1058 40a3df4a 40b27612 3fea9e1f 3f5f31db bf3353d4 00000004 bf45ed53 3ef587e4 -> 435c8c2a c2e8ef1e 4310c56d 3f08b37b 4992e9a1 00000000", + "422524a6 c10b82c4 c1f24915 bf5c79a5 3ecff885 c01389b9 402be820 3ff58f3e 3dcccccd 40050a80 3d3494cd 3ebadeff 3f32680b bfddeef4 40c25922 40b18bbc c0de018b 40ba37f4 3f6661f0 c0d3fd0d c0229493 be674012 3fac3c7b 00000005 3f9d11b4 bf8a8f33 -> c18b3483 c2a487d1 4303220f bfd3f220 49fbd527 00000000", + "be2c2800 c1bab7b2 41728ed0 bf3cdfe5 3fac8a5c 3fba3f64 400890d2 3f60fd0a 3ca3d70a c113054d 3f39ee05 bf1c5867 bdf04867 3f2e9ea8 c07d18ae c09b6eb0 409dacde bcf78500 c0b95baa 40381c30 bfc27550 3e102250 3fef5569 00000003 3f80212f bf59c6a7 -> c2104dc4 c1423776 c1bb7a9a 3dbf07a8 46b37394 00000000", + "41cd97ac c0a107d0 41bf5968 3fb97991 3f2615d8 3e55555b 3fd622c8 3f378a65 3dcccccd 3f170beb 4047123a bf858f1c 3f02c935 bf67cc5c bf1f4ff0 c09b3371 40cbd39a 3f8c61c8 40e5f30a c0a5f8be 402e2812 40275880 bf5e3db9 00000008 3f062074 3e8b6b9b -> 3ccc2fb6 3f6e4085 3e3e6883 be2f956d 41a73e5b 00000000", + "40666d70 c1553f00 c1f255cd 402d4b95 3f11b02a 3f05d7f1 403611d5 3fb97824 3ca3d70a bd119ed7 3e3ea5ee be99692d 3ee1228c c02b5480 bf6b6748 bfa4d980 403fd544 40ae8c46 c087fc0f c08d6931 40302ff4 3f62754a bfb753ba 00000006 c02624ad bf4ad807 -> 40b4e106 be6e2aea 401189ee be53b0e1 462946ff 00000000", + "41720bd0 c1a857bf c20da637 c009ccea bfc2b27b bc65ca46 402af96f 3fe16954 3ca3d70a 3b5026c4 3d3d5ba9 bf9534ea bf6ad7cd 3e2b8ae2 3ffe0248 c092b425 40bd1930 be96c170 bedd2080 c015c192 c009290f 3fc0d81a 3fcb9380 00000003 c0179741 bebd6f9b -> bde701d6 bdbdfe1c 3c61af7f 3eb8e265 3f905500 00000000", + "c09932f0 c1a08c32 c24284dd 3e9888bf 3f7c44c5 bf1a1bcf 3ffb8d13 3fc5ce66 3f000000 bc10fd33 3db74b78 bf2e2f31 3f2735bf 3fbac4c5 c0e43b33 40c4cefc c0c8f02e beb2fcb0 c02b0d98 c0a1d605 3fd852f2 bfe44619 4016060b 00000008 3f09a585 bf00e469 -> bd7fc259 3d7620fd 3e1a8567 3d7576b9 3f21ad80 00000000", + "4139de08 40bb59d0 40defd98 3e9b6afc bf9da589 3ec3525d 3fa98c4d 3faba29b 00000000 40592c1c 42af514a 3f0e99f0 be07a3d4 3f24214f 404746b4 c08e17ef 401ffff0 c0e722b9 bf12f5e8 c0bfacde 3f21bca3 c025314c bfe55442 00000007 3fc93482 3e140e89 -> bf0093f5 c0caef56 bf4411e3 3f0052ee 43d189db 00000000", + "c20eae22 bfb74d40 416229b0 3ef6ffeb bf897403 3f0408db 3fa4dbb5 3cdc9725 3dcccccd 3b04ffa7 40fc2093 3f1e6cf3 3fd5f9cd 3f24d58a c0f49898 402b35c8 408ad612 3fcfe350 bc611200 c0f73c75 4005e85d c0289cb5 401e284c 00000005 3f0f75ac bf12c2ac -> bb4604a1 3aabf60a ba9b7909 3ee6bd19 390f4da7 00000000", + "c1466ef8 42373018 423e2bf2 bf436d0d bf5721ef bfbec504 3ff23a28 3e40f355 3dcccccd bcbc0e25 400ab5e5 bf9df5fe bfe198ae 3faf5210 40dab95e 40e82ee2 4014c034 c0352792 bf561370 c0daea06 bf9a32b6 3fe79dbf 3fcde64b 00000005 40135d0d bec2361a -> 3d7103a4 3bdccd56 3ca113ce bd506ff5 3f4d2f62 00000000", + "41c0c8ec c11337f0 c1b85268 c010e57c beb34448 bf19e6b6 401a0dcd 3ff71332 3ca3d70a bffb1dad 3f9992f2 3faf5b70 bdc7849e 3c1c5999 4091dbfa bf5fffa8 40b5292a 3fe5dd58 4015a614 40fdb306 3ff94e30 c0266ef6 c025e157 00000005 c0093386 be404792 -> 40a7a07d 40bb279a bfb69dc8 be5e1f0e 444bb5b3 00000000", + "c1bbfa91 c1df299a c2428555 3df7ee3a 3f573b2b bf245c83 3f899019 3efea0fa 3ca3d70a bb34a430 3eee3e4f bdb9bbb1 3d771ef4 3f46ec89 c0b7409e 3fa4dbc8 bf825e28 409bb574 3f9946f8 c0ce23a7 c03705fb 3f23b93d bf006945 00000004 c019d9a8 3f2ab435 -> bc89c025 3cc7267e 3d48f612 be400e98 3d8d9ba2 00000000", + "c18362b2 41e6d504 41ac1f90 bfa10315 3eb76aec c00ada8b 402f9488 3ed72650 3f000000 bead7b20 3c6d50f7 3f6f0b39 bf73271e bd81742f bf906ca0 40d30aa6 bf8039fc 40d7afba c0a69f68 c086e656 bf4b30b9 3fab6183 bf9effd4 00000003 c00dd84f 3e7703ee -> 419cacb6 c1adc753 bfb0275a bda96644 47d56ddb 00000000", + "c155ecf0 c24504a5 c11ae52c bc07b885 be591e90 3eb31ce3 3ed17c8c 3fd6604e 00000000 c01df9ab 3e30160a 3f2d8ed4 bfc448df bf8e9158 40ffee12 401d274c 409e5880 c0ad0144 be727aa0 40f93678 3ff21fe5 3eb37090 bfc4c97d 00000002 bf5aabac bf62c118 -> c043b429 410165ba 41972486 3eaf6939 45f1f6ef 00000000", + "c207047c c207b1bf bece8800 beab00cd 3f1db9ef bfb93065 3fcf95a6 3f354857 3ca3d70a be5de13b 3c2aacfa 3d450d92 bf0c7fa9 bf218a1b 40cfd02e c0e76b93 40a4c222 3f8bb820 3f4a01a0 bea6cf50 bf81e3c7 bff24bbe 3ee63b9b 00000003 bf4d94af bf172b4a -> c02c050e 41ee19cd 420b1614 3d14301d 470f7bb4 00000000", + "419b26e4 c10def30 42213ffc bf7b7cf2 bf054b1c bf14d5b4 3fabc8b4 3f5fbf28 3dcccccd bd5e346e 4185b599 bea4d384 3f8cda8e 3f0cb8c8 40989d30 bf2b78e8 40d8dbf2 3eb3fd00 4099e148 4047bae0 be9e4549 3f00f3b9 c00dd826 00000001 3f1bbc2e 3e8aabcf -> 3def4edf 3ce2151a 3c2874bb 3ee546ae 3d89455f 00000000", + "c104d29c c23e34bb 423b8ab6 bf8c7de0 3fc8ca6b 3e0c7d9e 3ffa324c 3fe15be5 3ca3d70a 3fcc9674 3f2b4de3 3dc2d553 3fc0ee06 3f806820 409fe11e bf557af8 c058d084 403e749c 40077eb4 bf842d68 bfa0bb3f c02cf0de be2526db 00000006 bf1129d3 bf725f59 -> 3f77ae73 407861f9 3fd3061f 3fbd388a 43d56def 00000000", + "c22ddbd9 421b8aca 41cc961c bf4ba6ff 3e7259b2 3f083bb4 3f804972 3e270ac0 3dcccccd bd043a51 3d860b5a bebd5e4c 3f72e7ae bebb8dc8 3e475bc0 40902578 4086d3ea 3fcfa398 c0137f06 3c43d800 c022efd6 be8060b4 bec6b921 00000007 bfef68ae 3f2b6304 -> c00e1deb 40aebde0 c0261164 bd7b0cb8 4572cb65 00000000", + "4232eb54 41ddd130 41eaecbc bf3257db bfc92548 bf2b9e9b 3ff3c76b 3f1841b0 3dcccccd bc7dfce9 3ec050b4 3ef997ee 3d686c4c bfa25e08 40834b72 404cc590 407b0404 40805d7c 40b4dac8 beff6830 bedd4ab3 4008fd08 bfc5a176 00000004 c007bbe6 bf8c0e38 -> 3c82ca58 bf46f5ae 3f48b354 3e34f148 41578a7c 00000000", + "c129c2e8 421fefba c1934862 3ea99261 3dc52940 3f422c19 3f5ef81d 3ff19cc7 3ca3d70a bb1ea48e 417491a1 3e097475 3fd06d3a 3fc371e5 40c0a50c c0378dba 3f88c360 40ecf160 bfc77ba0 c003278a 3fd406b3 c029f863 bebe004b 00000003 bfc2333e bf04ff14 -> 3a96dcc2 bad8a71b 3acf1f7c beb5f833 3a04aa60 00000000", + "c102191c 41f6d880 c2240e80 bf9e6fd2 bff83793 3e89bfc0 40143d53 3fc7d63c 00000000 3d94bab6 3c9d4196 bd131bf2 3fc9bfea 3e647854 c0381d68 c0dfdddc 3e1cb8c0 c0935cec 4075f394 3f8b2660 3f874554 bf468926 bfacd13c 00000005 bfb89fbd bf120307 -> bd027786 40c6716e 3f47f2d6 bdb6800a 45044bd2 00000000", + "c109e37c 423ab412 c244c0bc 3fc283cf bf3e08c5 3fc94911 40254ba7 3f0bf588 3f000000 3e2f22ea 3fe8e89e be12e0f1 3f02ff99 bfd58896 402dd1a8 c097acd3 404b65a0 bf080d18 401e22f0 c03d7f56 bfcda515 3fb22062 bf3c7d53 00000003 bfecab04 3f8a29e1 -> be766de8 3fa2405c 3efe3198 bf27bb8e 41ee615f 00000000", + "c23309a9 423d3838 40c51858 bf280455 bf01bdfd 3f57b813 3f975348 3e8734be 00000000 bedf1887 3f4574bc 3fe1de97 3eab61fc 3e21c5bc bfd3c5ec 3f922128 408f12ba c0a03550 3f150f70 402d5d44 3fd24022 3fe1c175 bfa10c53 00000007 c033ab35 bf53ea0d -> 434c6e50 c344ae24 4081c8d8 bf6c05d4 499d8208 00000000", + "c205d1cc 41fad414 422af688 bfdc0842 3fd1d9d7 3fde48bb 404419e2 3f9b9127 3dcccccd bb34bed0 400d1a9b 3f5b03da 3fcaea29 bf96d831 406345d8 c0e63a75 3f483400 c0e35a7c bf4fe960 40e35740 c02cec87 bfde617e 3f5e0205 00000004 3fdea91c bf12b896 -> ba4b7088 3b874475 3b7322d4 bd2a2d11 3ad7201c 00000000", + "c16547ac c20ac8ce 42393d0a bfdd8d29 bfa8d46b bffce35d 4043c60f 3f9947ed 3dcccccd bba456bb 40133979 3fcc3812 3fad163a be86cdd4 40d5e77a c0f7a8b0 c0ccdb8c c0f977c5 c08f5642 4076cb80 be97373f befd3541 bfd03ade 00000006 bf2e9988 3edab5ee -> 3bd95994 3ab3ebe1 b9aef29c bf12071b 3a3087a7 00000000", + "c150e8a8 41a9d334 c237aab9 3e727d92 be8a2bc1 3de911e6 3fa12a80 3fe1e9a9 3f000000 baa89509 3e12ac41 bea7e14c 3f682bc4 be0e0c5c 40b9dc7a bfa98944 c0b51aea 3f23adc0 40d495d0 40f2fd1c c017e65e c015a822 c0204897 00000002 c03c844c bf0db83d -> bb9ddb44 baf22e6c 39a40fbe bec0cd62 3c814430 00000000", + "c1779330 41d3fcf8 41a600f4 bf84b3a8 3f886b05 3f6bf58f 3fe1c18e 3f37e791 3ca3d70a 3b9885d7 3c87bbb8 bf5f9d59 bf1c4315 bfb921c3 c0ec7d8e c0de5c2a beb8d4b0 40a4f472 3f0e52e0 c0bbe219 40301a24 c03fdb6d bf85a596 00000004 c01fc2ac bf384bef -> 3f5bb52a 3f1aa69c 3fb8daee bf00e1c5 43fa9db0 00000000", + "41be9900 c20a88c7 c2161cd6 3f8502bb 3efecf2b bec8ce39 3f9bc9d0 3fc9e830 00000000 ba93f6a6 429703bc bf8107d4 be9d8665 bf870835 40637668 402544ec 3fd46c70 bf569c68 c051be06 3db81080 bec5059e 401245bf 40098772 00000001 c01e1288 3f19ec9f -> ba5506b5 bb3ab429 3ad6cdbc 3e932cf7 3826fefe 00000000", + "420a161c c1dda6b7 4237410a 3fb1e4a7 bfd8263b 3f5303f5 4048ee38 3fcd6581 3f000000 bfc29bcd 4286b4a4 3f3e19d9 bf4dd026 c0219f30 40be10fc 4025cae4 c09462a9 40d91872 c00fc7cc be488d80 3fadb356 4005ca45 40019c6e 00000003 c0355962 3f9391e9 -> c03d4ca8 41cd7902 c1197b8c be144e02 4640d16c 00000000", + "c1a8178d 422c4494 c234d038 4020866a 3e7cea86 3f9be1f1 40335976 3d02b70c 3dcccccd 3d854124 3cab2025 bf0a2708 bdcc563f be922813 c0c409e8 c0d14a2a c0ed4a0b 40cba5fe c04d0bb4 404613b0 3f9b0eac c0043bc0 4025effc 00000006 c011dd6f bf6b971b -> c326e3be c22f33f6 c2b00a79 3e7123c2 49cfff22 00000000", + "419dba70 41d42290 c0c32cc0 bec3ea43 3e71f52d 3ff70490 400169f5 3ecfa6c6 3dcccccd bb81d7e5 409c17b9 3fa5aa94 3fde6df0 3f84086d 40fb4124 40b3f9a8 3f3bacc0 c0fdc0e9 c05ca8c2 40f38292 400e859e 3f80be0b 3fff24df 00000007 3f022513 3e344908 -> 3af6d349 bb694ecb 3a0664e8 3e41f6b4 39ba538f 00000000", + "420330c6 c2147000 41f5b308 3e551f4b 3e82597d 3fdb56b0 3fff75be 3f007a1a 3f000000 3ab12b42 401c116f bf87dfda bf87cb7c 4025464b 3f53dc00 40184c8c 4084e006 40e1710e 40995c6e 40f12b96 3f0d4980 bf5a92f6 4004250c 00000008 4010abf0 3f39443f -> 3c38460e bc9a163c 3a005e80 bec76465 3fa7c88a 00000000", + "c12385f8 c1e65dc0 c13c6bb8 bf910edf 3fc77619 bfc21f0e 4057153a 3fe89b3a 3f000000 bf97cc4d 3d9e94ae 3ffc5c42 3f9f0fdc beb2e76a c0db84b4 400251b0 3f97e4e0 405a79dc 3ecd9220 c0031f4a c03ae620 bf7eab86 c0115900 00000008 3fea95ba 3f24ce60 -> 42881fbf 4254e4b2 c1d00a1b bf12c739 4aa36bf5 00000000", + "c212ea52 41aed330 4139f090 bdd3402a 3ce1cc55 be557cde 3e6fdada 3dfaa7f6 00000000 3d61738f 42b01287 3dbf3187 3f13b43a 3f2adba3 c0cb92ef c0bbdc5e 3f8a8a60 4085fc8c 3f078c90 c02aa800 c008591e 3f9ccae4 3fd57ae6 00000002 403cebc1 3ee17800 -> bdee49fe 3d8956fb bd22a158 3f30c03c 3f0770eb 00000000", + "c0b9ebe8 41244100 41da3e60 bfae2d05 3f06c2cd 400023cb 402c6947 3eddba09 3f000000 bd84edcc 3f695644 c002ab23 3fc37a7a 3f8ed19b c047faf2 3f669f80 c0fff0ed 3f1e5080 c0eae7ee 3f8641a8 c005b3a9 4000dc92 c00d2f2d 00000007 401c3e30 3f43a4a3 -> 3f9d03e0 40996bd8 c1095e7a bf2a3718 4549bfd9 00000000", + "c20f41ae 41c9c26c c1fb25cc 3f883789 3fbc2152 3db4cc01 40017e1f 3ed38c4f 3f000000 3f24eb6d 3de1a947 3e9b06a8 3ebd7b94 402e28d2 c0a300f8 3fffc270 409625de bf8ce9f4 4087074a c0a7478e bf8e2a6d bf050d7e 4017eeaf 00000008 3f907b51 3f2f6238 -> bf873e65 c012649d c08efce6 bea67ed8 461b000b 00000000", + "c1f7d32f 41777d00 403ba2b0 bf76f9a4 3e9aae5a 3ea0d637 3f8899b1 3edb422f 3ca3d70a bad28065 42c373c2 be8c1970 3e509465 3fac2dce c0ca7f6e c0a874be 3eca41e0 c0874562 c0fb60e3 c018da5a 3fb68143 c017554d 40204b2b 00000008 3fc5da88 3f503532 -> bd19fa7f bca4a5df bb8fe62e 3e9587b4 3c3b4e2c 00000000", + "c240e292 c240d529 4221edf8 bd47d0ff bf341d00 befde147 3f657041 3fda58e0 3ca3d70a 4006ebac 3faea17b bfe89837 bfd258d2 3f03767d c02dd0f2 c0d5a872 c06af5f0 40e112e2 c0a9d016 406ebcd0 bf5e1cb8 402bc514 bf3b88aa 00000003 c00746e8 bf3b91ff -> c0b2af14 bfee5cce 41588d94 3e9c6286 4561ca00 00000000", + "c1d82cee 41ed0be0 41f8e9c4 bc031b68 bf75dde5 bf1dd613 3fa9e6a7 3fee20fb 3dcccccd bd154d01 3c5310cc bea13e4b 3f8a6c77 3e5d7174 c066e396 c0f9fe52 c0155474 4067fa40 408bf19c 40253a00 3fa7cd2b 400207c5 3f62cdd6 00000005 bff8989f be7bde8c -> 3fa466ae c0871dbc bf50d1bf be08e4dc 44f1004e 00000000", + "c2276238 420f0032 414a4be4 bf8c17d8 3f891faf 40069be0 402d4c5a 3f87b0ed 3dcccccd c0dbba32 3d05af0d bfbda55d 3fbc5a8a bfac9df7 c055c364 c082c504 c08357ed bf846728 bfea11c0 bf1a3d28 bdd5924e be4b7419 403be88e 00000007 bfd89c5b 3f13f683 -> c478219e 4482bba6 c4727164 bf33b67c 4cecde19 00000000", + "409f2940 41274e90 41b331e8 3ea8a0e0 bb96152a be703e62 3f626510 3f75bcf4 3f000000 3cb8e36d 3fb6f844 bf7c6c7e 3f69b02c be8de9cc 40db8062 3effb120 c0de2b4b bf3c6aa8 c0112c8c c06c1932 3fa89715 3f8dc080 c01852fd 00000002 bf906eb4 bf54cf9a -> 3c87e0ff bcef9918 3cdb0b18 3e0ba2e3 3cd3ab47 00000000", + "c220971a 4130d710 c21e3817 3fa7f44b bf93a64b bec3dd93 3fe831bf 3fa374f9 3ca3d70a 40a81448 3d959b27 bf428a9d 3f6e1cd6 bf67b1cd 3fc19a30 40a60378 c07201da c0f08919 40b8c2e0 407a1798 3f319590 be4be73e 3f659b84 00000008 3f48df07 3e554531 -> c250a5f3 4281b5a9 c27938c7 3ddbcfeb 48a11fd0 00000000", + "4174d1f0 4212c4a2 41f4822c 3ece5bc4 3e4f88e2 3edaa021 3f1f04f3 3fd510b4 00000000 c11072f3 3d6de2e2 3d2bfbef bef5b059 3e9dcb0a be124ea0 bfb3386c 401f6f4c c0ed874a 40f473b8 3f5f2d40 3fe0d2a1 40193791 3f237e9c 00000006 c0146259 bf0d4944 -> c0b9ddfc 434b4fee 41dbe69c bf988074 4dd4d0d6 00000000", + "422a9e6c c1c2c234 42315d34 bf993595 bda1eb53 3d9ea913 3faf4b56 3fd64ff1 3dcccccd 3c3257e5 406d8b0c 3f79a39c c0083f2e bf8f92ed 402bbffc 40ac62ee c0274126 c04adfc6 c0bbedb2 c0dfbcda c003f9aa bfb8e389 bfd43de8 00000003 bfef4c66 beb0da1e -> 3c228125 3c0fa1fb bab46fa0 3e954d31 3c14ceaf 00000000", + "c1dd7640 c21f703d c1c30514 bf5e4411 3eb4b4ef 3f3d55c8 3fac637a 3fc3b419 3dcccccd be59e869 42c7252f bf999b0d 3f4d0542 3dd4eee5 4050f204 c070f898 c0a786ba 40db4fb8 3f800a40 40970a2c 40125e01 bfde13a5 3fc406f6 00000007 402131fd bf5bb42d -> 418f4ce4 42094389 c2769237 3ca10ce0 479cfc89 00000000", + "c0b239b8 c0e96df8 c24018a3 c010a41e 3f1be7c4 bf80ed98 402bd6e7 3faf2525 3dcccccd 3e722e67 3e50cc35 3ea600ef beab5394 3f542568 40ae720c 40c747bc c09b6cd4 40db5484 c09da9b7 4094236c bf0e302a 3fb65a97 3f7304df 00000004 3fe01131 3e799c0a -> beba292d 3e306ec2 bef67cb7 bf4a53aa 434bbcb3 00000000", + "4226e8b8 4213fa96 42038b2a 3f6e8ee2 3d900443 3e53dc4f 3f7e07fb 3fe10bf3 3ca3d70a bbbe215d 3c9124ba 3fe5b845 3fc11bd4 beda750d 40a45424 c097028e c0ab1726 40cd7ae8 40574748 402ec740 3e8e16e8 40324175 3fab05fd 00000003 3ff87758 bf2eb31f -> 3f916fd0 3f6ceccf be879528 bcdc056c 420f5c8e 00000000", + "c15670bc c21d8367 c11f904c 3e2c16ce 3fa9b0fd bf191e3e 3fbb66af 3f4c9e97 00000000 3b2d1b67 41af6db8 becd8112 beda948e c013b797 bf0c01c0 c0644aa8 c070affc 408fc918 400b1d30 bf210740 3fc89aa7 bfa1136d be879fcc 00000006 bfddf7df 3f987caf -> bd8d2e81 be4148be 3d220862 c0350084 3e09369e 00000000", + "c18772a2 c2371f0d c1a7afd0 bf0bc957 3db499b6 3fb597b7 3fc5192e 3f5b3bba 3ca3d70a 3caf5e8c 3cca29e8 be3ef34d bf9e2a1d bf05707f bf780f58 c0c751ec 3fbf3118 c086207c c0de9222 c0f4a4fb 3f8f8de8 beb1e874 40266574 00000003 bfd22b41 bed320ae -> 3f31e972 40844ff7 3fe0c15c 3e842b9c 42dd8374 00000000", + "41a6a53c 42449a04 41eaa73c bf51b9ae bfeb56cd c0061dea 4058c8b9 3f76902a 3f000000 bafcf6ad 406b2b66 bf23ba2c bf8f39b0 3f14f7f7 c001b5d6 408c4796 c0f8cfb5 40e6b4f4 40db76a4 c04ca2d4 40281a77 3f4d10b6 40239d1d 00000007 40150f3e 3f03a75c -> 3afb2380 3c13366f 3ae81b6c bec20b9d 3e196f89 00000000", + "420e6872 c135c9bc 41cbc884 bf39cf61 bee0666d 3eab001a 3f694a67 3f9af79c 00000000 3b8a7c93 40c6f8ca 3fe0a7fa bf09b4fc 3fc886c5 c0913e1a c0d35560 40c3b020 400335b8 40544e64 4011e92c bf94bc6b bf97ae27 bf72b05d 00000006 401b14f0 3f814e35 -> 3e33ee4c bedbe538 bf0798b8 3f3e1a95 40458310 00000000", + "41319260 420756f6 405889a0 bda5f498 bdcd517f 3f01266a 3f056da9 3cce38bd 3ca3d70a c0285fb0 3caf4e1c 3edd165a bec37323 4028aa6e c0ad441a c0bdb9fb c0594c4e 40d313d0 c0cfa1f8 40532f80 c0179ab4 3fa38168 bf813371 00000007 bf0d8e98 bf3dc8a5 -> 4180ceb8 c12a0786 427e9042 bdb94f3f 4984bca6 00000000", + "414c68c8 c219422e 4205cc84 bf1cdda4 3e929055 bf3d35d0 3f803c59 3f70a91c 00000000 3b024a22 41a16752 bec863e7 3fb7b5a2 c01f1941 40e33472 c0551434 c0e72be6 40a55ef0 c01d8554 40e27ece bfc706ab bfcb2f2a 402205f4 00000004 3fecb06e 3f1157f4 -> 3c0a6f76 bb5b635e bb796dd2 bf10ac62 3af4a9f8 00000000", + "41c4f42c c0706e70 423032fe c013394b be36d098 be633622 40145907 3fa6d69c 00000000 be59267d 401f901f be1a6a3e 3fe1b1f4 3e572fec 40dbd160 c0d2817e c065537c bf2a50e0 c0874fc1 402e3e78 c02f45c7 3fc438dc 3e5790e4 00000001 bfd79716 bf1a3ba8 -> 3ea4a5de 3d4d45c9 3e73f1a5 3cea5a01 3f86b28f 00000000", + "423fbc36 c00462c0 41b7c170 bec40393 3f411b61 befcd259 3feb9804 3fdc7022 3f000000 3e87db87 3fba3a3a bec87ea6 be819efc 3ed65350 3ed52aa0 c0bdc1e2 c0c6e24b c00825bc c0fd9355 c03b2d1c 3fc11cdf 3eed7ef7 3fc85f19 00000004 40273e2e 3f3ac5e1 -> 402d56de bed67f6c bfc5adb2 bf5f6818 441748e6 00000000", + "4225e108 423003ec c05033b0 bfde4520 3f291fbd bf0721eb 3ff73971 3fa25b97 00000000 bd315b91 41a05390 3ed35fb4 3fdb8126 bfaba2ee bfaa0080 c0b8b54e c03d0848 40fde030 4049f830 c09d762a 3f905946 3fbc7d43 bff4d609 00000006 c03edf46 bf4a2432 -> bfc37e82 c0806ea2 c0a29720 bebdf3f8 446925d6 00000000", + "4244102a 41c4a340 408c4ba0 bef118c4 3e732b13 3e229845 3f3f7324 3ffc44b6 3dcccccd 3aedfb39 41ec7b9b 3f469d87 3fcd5448 3f5f64e3 40af6194 4045914c 3f870678 40af23de c0fcfa8b c06b60fc c02a0eff 4022346b 402a07c7 00000002 401d3eb8 3f27b2f2 -> 3b8fd80d bac32a20 ba893048 3e6ba3f0 3a14a1b3 00000000", + "bf242980 c21834ae 420c5ec4 3f0b671e 40312f2b bf71886f 403e6849 3faf5abb 00000000 c0483609 3c89b385 be980867 bd550287 3f28ca36 c0d24959 3f2ce9a0 40722dac 40d90416 c0d4887d 407f362c bf8d3728 40304bff bfe2cc30 00000003 401133c2 3e9f0110 -> c2849d93 c1781302 42f29542 3f18a76c 4aca5902 00000000", + "40fc6958 c2227c8c c106e18c 3fcd5cdf 3f87a06e bf92e7e2 4014d333 3e3083bd 3f000000 3aa26845 4219ae10 3e4ce9b1 3f056793 3e63e5ff 400e4c78 bf8e820c c0e418da 407e1c1c c09d9dd6 40980024 3e338efa bf8e4d42 c02d3e0e 00000003 c03e37c2 3eb9d7cf -> 3b57b221 3ad958d6 bbdda6ce be7480a4 3a28e571 00000000", + "c0501dc0 40b15f30 4145cc04 3f8a138d bf471bca 401523bd 402f0b14 3f050f16 3dcccccd 410aea46 3ca22c46 bf1c2810 c00b988a bd7fcfa5 c0bc250c c0cdd310 40e4a7ee c08db050 c01f82e4 c0d01bbc 3fac1bfe 400d883f bffd61d0 00000002 c00377dd 3e87ec87 -> 423705bc 42966b78 41727cf4 3e83017c 4bad1ff3 00000000", + "415c30b8 c1ea9e83 c22e46c8 bf26abf6 bf3ff199 bd59f7c5 3f8cc9d6 3e5807ba 3f000000 bd29b4a7 3e424bcf bff86100 3f3ebdd0 bf614c26 3dfaaf00 c0a29ae2 bf8f86d4 c0c3cbd8 40b33634 bf587068 3f31d2cb be5d7e62 bf9b9141 00000007 3f0f95c0 bf28e87d -> 3e0c261f 3d5052ec 3df3bbdf bec983fe 4051b150 00000000", + "c23c0f63 423e359e 41cd1ac4 3e99384b 3e24d60a 3ef141b2 3f14b906 3fb99056 00000000 bf68f0c0 3e9efa50 3fa665d1 3b89dbff be34a0ed bfdd23e0 c0ab593c 40cbcf4e 40b74250 c051b9d6 40b26c0c bff8d13a c01c2b0c 4009a921 00000004 3f313bfb bee7b5a6 -> 40af743e bd366847 c0608dee be8b2e9a 4340d80b 00000000", + "c1eeb71e c23bae34 40e3bec8 3f4905bf bf06ad4f bf80bc8a 3fb0a9e8 3fb94990 00000000 ba9379e6 3d32fffd bd817e7f c016a65b 3f9cead6 c083486c 3e871660 c050ce4c 40e243d2 40a4c16e 40d3c970 4012781b bfb71cfe 400239b5 00000008 4024160a 3e35f5c4 -> ba5e69ba bc8c462c 3c457983 bea31c49 3e703bbe 00000000", + "41a25b50 c11f87c4 c1ce8fdb 3ecbfe45 be855bd6 be9534d0 3f80d119 3f658125 3f000000 3b61fb97 3fb7c212 3f71711e beb760cd c01f4138 404449f8 c0e48740 402847e0 c03ee876 c0a85cb0 c0e84b77 c00dbd5c bf88ff7d bf57b6b6 00000004 c03d7061 bed6f0bc -> bbdf02f6 bccf45e6 bc91a0be be6c75fd 3d0b2bad 00000000", + "42130ba4 42275bb6 4182df7c 3ee2e334 bffd9891 be20c947 4005ac46 3dd6a604 3f000000 bbfdeb1d 42a20864 3fa11c83 be8e3a6a 3dceb8fe 3e202f80 be87efe0 c026a466 3fea27a0 bf58d9e0 3fe94168 c027cd0a bfe5f88e bf614cc3 00000005 400b40d5 bf184a0d -> bbffcb38 bcc9e4d3 3c8ec63d bf1968e4 3ea17334 00000000", + "419d601c c2278634 422bb378 bf34ffb8 3fe14fc3 3fb3bfa7 40170cee 3f4d1ca0 00000000 3e944811 423a0aa6 3fc55e53 bef7bcd1 3f083286 c0cc3292 408915c6 40e5056a c0eba824 c06457d0 407e8918 3e8c53dd 3e074a46 4001d383 00000007 3fc9eba9 bf43eec5 -> bda7f7b0 c06d09b0 c048aefd 3e3660c9 434cf4d4 00000000", + "c21d94ab 41efe03c c1f6a08b 3e670966 be118faa bf4e0120 3f735f4d 3f83c169 3dcccccd 3cc5e35f 427f40c8 3da75526 bf981beb 3eb123f7 bf9af9f0 bfb006d8 c05d1f6a c0023aba 40410e78 3feba428 3f3a7714 bfc1292a bf43a438 00000006 4015f471 bf34d909 -> 3ea39a1f bd93aec4 be72f8f1 3de52719 4216f664 00000000", + "421fc076 41930c00 422e64d4 bd811497 be3aaf22 3dcc12b0 3e74b108 3f8bcc79 3ca3d70a bddf2326 3d26ab81 3fb11195 bfa4c8b5 3f72030a c0f1c603 beeeafa0 40ae2f0e 405e35cc c0bd58f1 c0dbaa14 c004a6ef be471be9 c007a9aa 00000006 bf903613 bf3a2460 -> c0ad1b19 407e1e2a c040ac14 bf8f4497 449cac67 00000000", + "c0abad38 c22e0eb9 c18c74fa 3dea8398 3e164646 be5ba44f 3ee91346 3fdb2af3 3dcccccd 40a0b352 419e9b9d 3ef6207d beb3d973 3f0981e9 40ab3aba c0e2671e bfc40d1c 3f09af50 3f0edac0 c09a434a 3e43b9bc bfe6e686 3ff701f1 00000001 c01ad778 be4dce25 -> c0c72681 c174b505 c09a0983 3de3e08d 44af6853 00000000", + "be12d700 c0facf18 422b2a90 bf32db7f 3e8292c7 3fdffba9 3ff6f93e 3fb3eb94 3ca3d70a 4096bba1 41a20eeb 3faf50bf 3f8baa42 3fa8d7c1 bf9980f0 c0e30c1d 40e3b386 3fa5d8d0 c0931795 c09bf954 4021821a be86302f bf2f5455 00000007 3fa109c6 bedf6f33 -> c1108cd9 bfeecac8 412e4bf4 bdbba447 4566c98e 00000000", + "c0e20860 41d66d48 42358806 bf415802 3d7d1062 3ec3d5bb 3f59e875 3df2bdd9 3ca3d70a 3e2bca92 420a97bc 3ee500c4 bed0e5bc 3e54526d 3f610c60 bf9afed8 408a9a92 c0acd086 3f3aca30 4097af1a 3f8d742d c0397f85 3f5a769e 00000004 bfb131f4 3dfbdd17 -> 3e25d9eb 3e05902d bd3e0b06 be131fc8 3f55ee0c 00000000", + "c036df60 41ba9804 c189ad98 bed53e71 3f53fe78 bff90baf 400da30d 3ded1a47 3f000000 4044c4a1 42305376 3fa47144 be6d119d bfbe386c be8ab6e0 400023c4 4080af0a c0431e86 3ff52e40 c0a82c36 403b8c08 3efbe643 3f1ac0e6 00000005 bfb4d15a 3ee6ef4e -> c0c14e9f c158e15e c057941b bf16f247 4506a35b 00000000", + "40d73338 c1948fb2 419369b4 be1e0b2b 3fb499cd 3fa2ed30 3ff626d2 3f53e472 3ca3d70a bd879871 4029cc37 beae60e1 3e416f70 bf1e10cc c0a026a4 3f9ab2b8 c016205e 40369be4 40ebd0c2 c09b810e bf0ade3b bfe59531 bfd77a77 00000004 3f085fd0 bf49cc16 -> 3d827e46 bdd58719 bdd708dc bedce6f6 3e1e779a 00000000", + "c1cffe0a 410ae74c 40bb1c30 3da5c729 3f095f9d 3f749369 3fc68abb 3f679bc3 3f000000 bc135d81 407062f6 bd862c17 3f1ae843 3f101bce 40cf859c 4095deb4 c0eaea23 4022edbc c0b2092c c027279e bee5342b 3f5daeda c030461a 00000004 c00d642e 3f273a46 -> bdf2fa3d bda120a7 3dbb3e42 3ed1fb6b 3e3948ea 00000000", + "c114e4b0 40bd4bf8 40270ed0 bca0e0de bf080b06 3e69e7c3 3f9bea63 3fa3aae0 3f000000 bcab1ec4 413aa88a be81ac5b bf35000b 3fe9296f 40b7f148 3f05c1f0 3f378730 c01cd4d6 c0237a1e c0b8bdb8 3f8f0901 beb270d0 3f92e77c 00000002 3f0b535b 3f09885a -> 3a34b770 bd44e4da bca58112 3e89fd3a 3c734d55 00000000", + "c18578da 421d54f2 c232105f 3f1c7027 4028cc85 3f93774e 403c4e5c 3e7261a2 00000000 bf84fcec 3e251741 3e80276a bfd56fcd 3e42b20a c03d5930 c0a335ef 3d7ffe00 bfc4d3a4 40da09e6 c0363c28 c0056b4c bf4576ba 40116bc7 00000008 40361db0 3f261a4f -> c18e2d54 43b4d0c5 433f820f 3db71df8 4d96086b 00000000", + "c2145cd9 422f5978 c19fa3ef 3fd40fe6 3f4cc13a 3de56f89 3fef0634 3e788850 3dcccccd bcd34211 3e190175 3feb2b6f 3e5be914 3f8bd5d3 bf9bdc24 c09b4500 bf2dc2c8 37d00000 400286f8 40d71126 bfd0942d 3f157837 bdf7a952 00000004 3f210456 3e6bc520 -> 3f36cd27 3debfc18 3eab3724 3dc2e56e 3fa37cb6 00000000", + "c229d888 41ab1364 4218ef08 bd1cff46 c00ad13e 3ef2abfc 400e65f4 3e634cf8 3ca3d70a 40ccece9 3e0002dc c002accf bd130fb4 3d8df545 40b63ec2 bffecfac 405a5d14 4034e280 bf89bb64 3fb34860 bfdd75d4 bff1ad79 3f952cfd 00000004 3f606361 be84aafd -> c2a251d1 40867d30 c1727c5f beb9a6b4 4831c1a1 00000000", + "c2321c2e c2178788 40e5ca60 beb629e7 bd933622 bf375565 3f55d6b8 3fcf873d 3ca3d70a 3f09647d 3f86cea2 3e9cb2a9 3dae5ec0 3ec779ad 40513520 c08fa698 c037da8a c0aea9e9 c0c03f43 3f775e30 c035b731 3fc15f99 bfae1aab 00000008 3f94ab86 bf400553 -> 3f047524 40238a3c 405dda22 bf1c8576 429a54c9 00000000", + "c1bf3bb0 c1ca170e bfe1b300 bfb15c0e 3ff72717 3fbd857c 4034ad4d 3f931728 3ca3d70a 40566381 3ce9a815 3fcfc014 3fcd44aa 3fd1518b 40f25ff0 40982eb8 4082005e c0a11ce9 c05a0a06 3fc7eb70 bf812599 bf7a1cc1 403ed847 00000004 c0129bf0 3e7ba45c -> c217a220 c22198c0 c23a9954 397b8670 4a8ece48 00000000", + "41a8aadc 41c666d8 c18df3a4 3f5dd420 bfc1bc12 3e9ea267 40055cac 3f1ff6aa 3f000000 3b8ec885 3d9d92f5 400ae5db 3e8fb09f 3d215081 be131ce0 c08dff9a be8d2ff0 c08d1412 c0067926 400c3ef0 bf3dba2f 3e51e234 3f4eb834 00000003 c0059863 bee46123 -> 3dbd3f74 3cd1e30b bad97876 bc24b50c 3f98d264 00000000", + "c14cd2fc c0200460 c232429d bd0e42e3 3ebc185c bdb846b5 3ec3430c 3d9e7d28 3ca3d70a 3cb371e2 41174ea6 3f1d8d61 bf024021 3e051c6e c08c0e34 40f4848e c047ba74 40587c6c 3fbb76f0 bf5fa8c0 c0388025 be52221c c01a077c 00000002 4031a30b 3f529e8d -> bcd3cd14 bc6fdfa6 bb09c7e0 be907924 3d831e76 00000000", + "4079a960 423cd28c 3ec36e80 bfa2c1e5 bf772a54 bfd8f62c 402f7f8f 3f53c742 3f000000 3bccc239 3c9f0b0d 3fe140e4 3fc006a3 3fd7fc8c 3f0d40c0 4001fc38 3f593870 bfa00a6c c09e2e02 40cdcfb0 40107533 401b9e56 bf191062 00000002 c0363889 bf86f36c -> 3cd99238 bcc32a98 3d173050 3e9d0fe2 425c1e1e 00000000", + "c1551f00 4082b550 c171221c 3f00e9f6 3ec68d36 400931c3 401b2afa 3ff1674b 3dcccccd bb335276 3c6b130f bedb5ac6 3e80c784 bef7e4fc 400e0d64 409cdb4c c00c4c6a c0a266a8 c0ef4aa3 3f4e2a30 403003a7 3ff44d2b bfe9eff1 00000006 bf4063ce 3e08f1f2 -> bdf2ca97 3d93f44a be0759c4 bd6800e1 3fe76da7 00000000", + "4240fefc c21041b3 c23abc15 bf85ab3e 3edffd61 401a331f 40479663 3f69af06 3f000000 bd429213 3da733ab bf9d4b04 bdb71732 3f8ceef5 c0a8b9dd 40cb2b9a 3f161110 c0579f54 40ba0cb6 c0392384 bfbaa1c8 c00f3486 400809a5 00000006 c00b76fc 3f6a5cd4 -> 4147188f bf41e4c2 c1a1709e bd96b257 47145ce3 00000000", + "41bde074 c1c7c246 4131dbbc bfc1098a beef27bb 3eaba05c 40220c6c 3feb0410 3f000000 bff794bd 3cade4c1 3f66f99e c00bbfb8 3e15ae1d c05cde54 bdebf9c0 40cdb8ca c00cc8de 3f0d1840 c02f215c bfe57800 3f842cfc 3fb0471f 00000006 40350f98 3f3ddfea -> 451b9ec3 c5bb28dd 437f4bfe becb536c 4f741e02 00000000", + "41457c78 c1bcd52e 41e4f868 bd418989 3e9a0ab9 be2f31cc 3eb74959 3db172a3 3dcccccd c0079d97 3eb40f82 3f078adc bf2a492d 401578d5 3fee3eb0 bfe182d8 c0b72bae 40ccb500 c0046aba bff226b0 bf1e9bc4 3e2c0b75 40196c71 00000006 bff7ecfc bf97cba9 -> 4319e8a7 4367cfbc c42fb02a 3e4b15f0 4a8a9b98 00000000", + "c1757f00 41a17a04 c23fdfdc c01c6053 3f8fe40c bf0c315d 40353347 3e312466 3f000000 3ca66f17 3e430e2a 3f4d712d bf0084bf 3f068bac 40423344 c0bb3c56 403eed60 c0eb1b2f c070ea68 c030823c bf5c4f47 bf89243f 401d107b 00000008 bfffb15e 3f8629a8 -> 4272e22a c2107abc 41ca1786 3e19fcb8 469184ac 00000000", + "c234e03f c1632bf0 c07400a0 bef2b93a be883733 3eb041ac 3fb92b5d 3fcd9d6c 3f000000 402a23c0 3cc7a3ef 3fd0ea41 bfc92990 3f9bf2f7 3fa93500 40ff9ad8 bf29a310 408fe600 bf502600 3f3667d0 3f7fab39 3fa939c3 bfcc2df5 00000004 3f085b28 3f1e65ce -> c2db71e2 42db5c4a c2ac5759 bedf4cce 4943e203 00000000", + "41ec6ab8 c1b12374 c1f5283c be2f4023 be15d2d9 3e1b3af0 3e908790 3f0adc83 3ca3d70a bda3da6b 3cb6587c 3e411c8d 3f51caea 3f24005d 3fb01ed0 40114594 beed7520 c0c22d94 c09b9ffe c0eb9f73 bf860b6f 3f9c2431 4008d9a6 00000002 3f6dfdaa 3f1c8e29 -> 3ebff82e 3fc47529 3fa63dd4 3d6b6cc9 44251ef5 00000000", + "41fd1eac bf2abb80 c2424913 c01388a0 bf88c21b 3fb3f38e 4039d7bd 3e81b4f5 00000000 c1167494 3f16186b 3ece1776 3f2c5953 bf89601c c085b0f2 c00cd874 c0d21cba 3fadbd78 bfed8718 40a2ef02 4037c102 40181743 c028c1e8 00000006 c0366ceb 3f081ca4 -> 422c8510 4375acba c36f7a90 3eb22029 4bcf8834 00000000", + "41f33204 41f5cfb0 41ce0340 3faca5c5 bfaf6f83 bf9f0246 4036bf67 3f90ec73 3f000000 4093c969 40b21a0b 40075a0b bd9b554d 3facd20f bfae19e4 c0b068ac c0c286c5 4070d164 c0edae18 bfbff6f4 3e7807f2 3e9d2ec8 c0187e56 00000003 3f47d1b8 bf5e3e11 -> c0351956 4099c66f 40b86bbc 3e33179a 450627fd 00000000", + "41a12aac 41ceb578 41f12678 bfc8610b 40169af5 be086fa0 403526be 3d4cdee0 3ca3d70a 3ff210a4 41182aae be4ab6d7 3f8251f3 40339edc 402140d0 3fb6f9b8 40dc6d6c c021562c c0cb6fea c0f45ac8 3e246849 3f9c7e45 3fb662c7 00000001 bfc02c44 3f11db69 -> 4080745e bf128dda 3e745bb8 3f447271 4346ff65 00000000", + "4140a864 c2268b16 41f98564 be0445f6 bf3937d0 3f3492e8 3f875a5a 3ec68526 3dcccccd 3be9cd13 3f9ae9d9 bf5296fb 3eace979 3fd85113 c0d6314b 40f1b9f2 3f8d41f0 be061920 bf70bb90 be847ee0 3f8c58d0 c020e01c c00a473d 00000004 3f8d574f bf855472 -> 3b95e588 3d0fc0b3 bd39b1d4 be01353c 3c008e74 00000000", + "41de9a84 c2175ce9 c1aac4bc bf82d997 be2aaa3a 3f278b33 3fa02ca5 3fa7658a 3ca3d70a 3b11205e 408d65d6 3f07e431 3eba2c9c 3f5e6d6b 3f07f5c0 40b3515c c02e6eb6 c0b903b2 40ca5ad6 bffc9110 402e3d95 4026b800 bfdd2c14 00000002 c0390587 3e0f6d5c -> b92cb670 3a7987b6 b9fc0278 be97d6e9 3973fc8b 00000000", + "419728cc c1f1daa3 c243c99b 40346323 3e84f2ff 3b18f53f 403526b6 3cc57fe0 00000000 3e9fcadf 4164cd34 bf8d0806 bd88d2cb bf922384 c0577f2c c090470e c0225090 3f2316b0 406c9480 40d688fa bf0799e4 3f9b94e9 3fb58630 00000003 3f052057 3ebe1d92 -> bef78a52 3e8446b6 3f05b866 beea114d 40423349 00000000", + "c1b20019 c1d738b1 41ed480c 3fd77f47 bf194879 3ed981ea 3fec1ab2 3da16f9d 3dcccccd c08f60b4 40fe3a9b bfba7d15 4015d297 bda1f07c 40e3dd92 c0c597c2 bf222ad0 3efb5620 3f4a3a90 40b67674 bfc93990 c026d353 3f6369a6 00000005 c01d849c be2b3791 -> 40cbb598 401302dd c167af02 bd482e71 45b31524 00000000", + "423e51c2 c1b68684 c239aeae c00a8cf1 3f62ffe9 3fc28210 403315f2 3edbd3d8 3ca3d70a c082cf6d 3f98e9fd 3ec3440b bf3ba579 3ea2e028 3d91a200 c0a52088 4038dbe4 40e55b72 402ba504 c0d7ca18 3e8f1a6e bf83b55b 40336a9a 00000004 bf839596 3e86ce53 -> c0e1f751 417b2fd3 bef5f23a 3e971dec 44a010ae 00000000", + "41cd58b8 417c9890 41f81400 bec03ea0 3f38cf7d c035acc9 403cfded 3f491405 00000000 bd0bce23 3e6fe1ff bf568c16 3d5bf929 be84646e 40a35ab8 40983a0a 3fd08030 c0c9e3a4 c0447f3e 3f0fab40 3f0b4d95 3e010a27 c00337db 00000004 c03a0cbc bedb41b8 -> beb579e5 3dc51014 3dccd7f7 3e13c7be 42116d32 00000000", + "c217afc7 c23e3f2a c065d0b0 3e242a0b bf324568 bf1852cb 3f918e43 3ed43fca 3f000000 c0035bdb 3ec3c74c 3f1a6d5d 3fb2a504 bf6134bc c000b23e 3fe2a220 402dfdac bfe4ed30 c0484a12 c0d192e6 bfe08796 3f3778a0 c01286e8 00000003 3fc86c30 bf4eb72a -> 40aaeab0 416dc1d2 bfcc8894 3f1603c8 462cd0ce 00000000", + "41d6c248 c0872e58 c23a6dfe 3fca60ee bf3c6dd6 bfb8449a 4015f3c2 3f50d991 3dcccccd 3e04d448 3d6c0c47 bea8996e 3e9945bc 3ed228d3 3f2fd7c0 40f04880 40acfbc2 bfc36400 403cab7c 407cb814 4024e660 bfbe5443 3fa28977 00000003 c01335fd beff35e3 -> 403e5e0c c0631cef c081432d bde0ae49 453aa0e7 00000000", + "c233efef c046c240 c243c531 3ed05c8d 3f95a261 40109848 4024dfe4 3f6ea295 00000000 bb5cc36c 3f7e45c8 bf59ca9b 40114463 3f00543b c0bc2130 c0c2f814 40a88cde c0c89c5a c05d96fe 40a78796 3fa7a152 3de9112c 3ffd3df3 00000001 c0368227 bf28ef89 -> 3b3e1e5e b89d93b5 bb7fe645 3de6a534 3960ee2a 00000000", + "c221ea7a 41bc0248 c2285699 bef83fad bf219847 bef8e5a7 3f852e40 3e5ccbe8 3f000000 3cb92fc3 3e21e2fc 3e0146ce 3df22b98 3fa5be4b bfef52bc 3f71e610 c0a08598 4080d320 3ef105a0 3f6ff950 bfab8c85 3e52c34e 401a1dbe 00000007 c00aa2d3 3e9b7c36 -> bdf2a570 bdc92d6d be96adb2 3ec8a143 41ea18eb 00000000", + "c077cf80 c1ef37d6 410e0090 bf68c42d becf8b46 bd6e6419 3f7f4923 3ec299d8 00000000 3f10a861 42b83b56 bfd43cb9 3f29b551 3f544695 c048b734 bf47b1c8 c0879f11 407cd748 c022a7a4 c07c1c14 3f2092a3 c0163523 3f643ea9 00000001 bffabb1a 3f667eda -> bf43df1f bfb8e2a6 bebccba5 be69518e 40f37198 00000000", + "c243e1c9 c1e7a97f 42275eba 3fac0669 bd52275e be247c31 3fb0fc56 3fb497e6 3ca3d70a 40edc9f2 4150e55f 3ea87f86 c012bfba bfe8ccc0 c0b3442c 40598668 40d615f2 c0c57be8 409879fa 400297e4 c020b287 3f9c09eb bf7257bd 00000008 bf80521e 3f996ddb -> c157ff5a c2ad34c6 42c8d0f4 c0931926 47497752 00000000", + "421bd65a c2429dd3 c1897688 c0132ae6 bd958d3f be57c37e 40164d7b 3d9c7532 3f000000 3ef6737a 408fd924 3f75b47d bf1627b2 3f9d065e c0cbedd0 4028f6c8 3f4b19a0 c0ec712b c06bfc1a 408906ac 3f456cd3 c03b4f59 3f8f0489 00000003 c03bff73 be899542 -> 3cd39338 3f32dac0 3e5c8a08 3e0b1c6b 42e0317f 00000000", + "c225bc62 c0ba7df0 4183dc18 3fa90bed bdcc56ea 3f8638eb 3fe06ac7 3f23bd15 3dcccccd 3f1b847a 3e35e7db bf77e96c 3e11c4e1 bf83c422 c0bcb07d c096880e 40f6683e 3faa1570 408a43bc bf436928 c015f798 3f3eccc4 bf13bec1 00000003 3ffba4c2 3f734a07 -> c1ad8b84 4085d535 c1c915f0 be7fd650 4638b08d 00000000", + "c23abe2f c1e9b97c 41b39f68 3f4d3247 3eb56b83 bee35c8a 3f80d6d9 3f9a37c4 3ca3d70a 3e817bc7 41184c4c 3e1cb070 bf93c1d5 3d1d2bc5 c09ee33e 3fb69160 4017b64c 40a288be 40ca5514 402fea08 c0030753 4010c7ac becdbaa8 00000004 402dca65 be652192 -> 3e1dc914 3e8fd065 3e8f316c bdae3dc8 41e6ebf1 00000000", + "c21250e7 42470272 c22cdc30 be95249c bfb7d0ac 3e6052f0 3fd0ff8e 3fc18d45 3dcccccd bff94f5f 40617026 3fcedaa0 bf23d16f 3fa99e28 40a51d76 40b7870e 40923b40 c0305c24 c0f4a133 4005a944 403b46e7 3e899939 c03d898b 00000002 3fff5617 3f73f0a7 -> c018dab7 40ec622f 408296dc 3dbbc03b 447cdfc8 00000000", + "413426a8 4239a924 c1cb2840 bfeab6ad bcc2d4cc bf9f50da 400dd89a 3f406a40 00000000 babdc858 3c6a3005 bf948733 400e75ad bf7ebd08 3f43d140 40867dae 402c29e0 c0a9e77c c0c0f0ee 40f153ea c001cacb 401c5b42 bf00b7de 00000006 3f3a49fc 3ebff777 -> bd078e8b 3d734f29 bccec968 3e9798cf 3e9ab56d 00000000", + "420ef392 c11b236c 417460a8 4024ef63 be43d82d bfafe9b0 403cbaa6 3f8ca75b 3ca3d70a bd28c79e 3d8306c8 3f28ae75 bfd1e4a7 400a182e 3fc644b0 be938ad0 bfd9acbc 406f2d58 409c2792 beadcbd0 3ef20ce5 be07ff64 c018991d 00000001 3f73f96a 3f4c261c -> 3f2f2721 bfe058c1 4020b985 beb89921 4187c214 00000000", + "41160078 4223edd6 c083a360 3fcc892b 3ff59c7d bfb48051 403788fb 3fc5cd09 00000000 3a9a2334 40bcd416 bf841040 3f047dfb 3fa06793 406a68c8 c0f59b92 40346208 40a7240e 40e3eee2 c0fae6b7 bf2e5027 bfcb1260 40337bb9 00000003 bfcc294d bf53bc8c -> 3aa60884 3be9948a baa3bf06 3f4bc15c 3990593c 00000000", + "c1155f94 41eff288 4146e130 bf338a5d be60f18f beaafd03 3f578f74 3fde9ed7 3ca3d70a c0951dbb 3f13b8e8 3f3fa5b7 bebfd31c c034be56 bec2eb40 3eae0a00 40dd3a16 c06ba51e c0fe81f1 40a602a8 be299fa9 c0251069 3e6b3f32 00000008 3f1d8c65 bf7b8563 -> c050c90c 40f5d9e5 4037235e beae8d28 45b1a952 00000000", + "4013a220 c23c0512 415a482c 3fdd8cf2 beda3546 3f1ad94f 3ff38a37 3f81ae32 3ca3d70a 3ae87cca 3ec3ac9f 3d862d4f bff5d114 bdda11fc bff4bbe4 40cffed6 c0a4b628 40b42d14 401b45e4 3fe4d7d8 3f5b7fc5 3fe88b7d bdce8319 00000004 40161cb2 3f695a5c -> bbb02bf8 bdd6cfaf 3c98e563 3f5ed210 3d6f8747 00000000", + "40fa00f8 41f9bbb8 421b2aba 3e82de96 bdfc0034 400a9ca1 400bcbb9 3fc4e1fa 00000000 c03abd53 40aa50cd bf834c77 3f4e98b8 3f4fba72 c0547508 c0728404 3f0dcf50 40fb1128 40e66790 3f233c90 40108013 3e8c8d54 c01e846a 00000007 c00c1e68 3f8f3871 -> 42c7b89c c47dd036 44c2c3af be615cc8 4b2e29ab 00000000", + "c211c674 423d90a8 40f67168 c021c6f3 3f45dc59 bedcfaab 402b6927 3f9fa334 00000000 3d646bab 3d4e49c7 3f8c8158 bfc31555 c002f1a8 40d127fa 3f144970 403b0ba0 bea7bdd0 40e5b0d4 40e65b92 c028d326 3ff3faaf bea794d2 00000005 bfe780f5 3e2447af -> 3f6b5ff2 bfa6f088 bfe12b41 3f4d2091 42a1964c 00000000", + "4150283c c1dea5fc 410a93b4 3f4465e5 3f879ff9 bf92c866 3fdeabc3 3f83e698 00000000 3b3079e6 3e3a673f 3eb9b071 bd824f8b 3f0d06b0 40a0ab0a c0cc914e c0ce42c6 bf9f4e40 bf3d0fa8 40aa3a0a bff1b9b7 401e4adb 3ef08ce3 00000003 bf9b2cfa 3e11df04 -> bcc3ea83 bb952143 bd002c7c be12d7c7 3ba9440b 00000000", + "4220b2a0 c0b83d60 c22e8c8a be2a39cb bf930b1a 3f7f5613 3fc8bef7 3ff2b316 3ca3d70a 3f85bc03 40a9c97a be7be55f bd397fcc bfd43697 c057e4f4 c0eb3984 c09ffe5a bf9f6a18 4012734c 3f9ae9a8 bfc5e2a9 bf82ddd0 c01f62ae 00000004 3f9f8aa9 3eef32b5 -> 3e4e7e65 4024dd0e 3e985b6c 3eaee0e0 42dfd94f 00000000", + "c1db91cb c222fcc1 41c0f0b8 3ea44155 bca849dc be78d9f4 3ece5488 3f1603e9 00000000 3ead15f9 3d6103ba bfc09b86 bf89603b 3f168c38 4095edda 40d04e02 c0d354dc c091395d bc07fa00 bf81a7f8 c0180a70 3fa356a1 bfca8fcc 00000002 c01e6b00 3ee628e7 -> c1955ce7 c158e7ea 40e18e48 3f3af70b 45494fc2 00000000", + "4226de18 4188d520 c0c41c40 be9196a7 bf49b202 3e43b6a4 3f5bf0df 3fc24762 00000000 411bd50d 40b07a5a 3f31463e 3fdda684 400ef37a c0e10f3a c062c30a c068ee9c c0ca4204 40a6661c c0e66900 3eb5683f bfdbc4a1 3fc6566a 00000008 3f9ef6be be9949b1 -> 40ddcdf4 c1535b41 4137d7f7 bea4ebbb 4542b7f7 00000000", + "c2470b25 c11ce8c8 414d66b8 beba1a2d bfbe79ed bfe0e24e 401debd0 3faed2c6 3dcccccd 40a6b04e 3ff2836d 3f119caa bdc94849 beb7f3b5 c09883d8 40d8bd06 40d4414e 40d29a40 c0cf0d96 bfe4198c 400a379d 3fb1390d 401c13a9 00000005 bfa07d41 bf8b120c -> 421303e3 c20ebab4 428468ee 3f8f10e2 46e18511 00000000", + "c1cbab11 4234693c c05b7910 3f590f0c bff18a92 bf4da1be 401ddc00 3efd3e6a 3f000000 bcf01b0d 3fab9a58 bd881f5d 3e503383 bf1dca94 bfffd600 c0b1b336 be617340 40736cc4 40869f2c 40d495fc bfefbb29 3f13f286 3f9b8964 00000007 bf972bd0 3f3b2a88 -> bd38880c bdec4062 bddbf7ae bcb419a2 3ea361fa 00000000", + "4223bfc4 42223f9e 41f378a4 bf01c3b7 c0194e74 bebce385 401e785a 3fa47e75 00000000 bd6270b3 3f2132b6 3df2eabb 3ec4304d bf416570 40e3ef28 c0aa3b32 c0b95978 c085b6bb 40c289d6 c0d72f98 3ed9e67f be804f01 beeb1008 00000008 3f9d40ba bf6fce9b -> 3f718bc1 be013637 bed0895b 3fcff256 40c3418f 00000000", + "c19de57f 41b9cec0 42319390 be8cdabd c039af92 3f2a4e79 40496074 3fc934b7 3dcccccd 3fed9767 40738aed bf5f8243 bf4191a8 c010e358 40616e68 c0fca245 c0db9cbc bfe97cdc c0badc6a c0474aa6 be66c8ec 3fea8746 4013c5d9 00000008 bf7d01aa bf128958 -> c022ce99 3e52e8be 3eafec17 bf5e1c67 43d754fa 00000000", + "c1a17c07 c20a154e 41860d70 bfb796fd 3eaf0589 3fcba996 400ad530 3f47facc 00000000 be798fcc 3c834210 3f5dc42e 3ee415fa bf1e992b 40bd004a 40dab4c4 c0d83e8c c0cff820 40fc3614 c0d4d234 401676c5 3de4934c 40035909 00000007 3fae52df 3e9bfb8f -> 3f82ad5c 3f75cf97 bef8c87c beb54db8 46472c02 00000000", + "4243337e 41ca1f5c c247d7d7 4019614a be2bfea3 3fac8f01 40398d35 3fb8d67d 3dcccccd ba877902 3de9aeba bf4848fe be35c7fc 401e6ef8 40bcf81c bfd9b3c8 c0d7da63 3fc0ee10 beafa810 406e2f5c 3fc83362 3f705ede 3fac0a0e 00000008 c0275ec0 bf4e8df1 -> 3e65dbdb 3e50ec54 bffee14c bf333500 44592514 00000000", + "40b8ac60 c1450a60 40007f50 bea0f4e0 3fc50ea0 bf14e0a8 3fd952dc 3f8f54a8 3ca3d70a 3eb3cc4f 42be0a70 3fc3a9c3 3f6d056c bfb66703 bec704b0 40abb18e c0846528 c0e80ff9 bd0ef680 c09c98d5 bfc60597 bfd2371f 401e0d14 00000007 400b77e6 3f40d39f -> 4139e5d8 41be69da 41df3f23 3f26b6bf 469bba13 00000000", + "c2198e4a 4185c2e0 bf393040 3f35d92c bfc642cc 3dd1b88d 3fdd102e 3f7f5ecc 3ca3d70a 3c647684 3c881d65 3f6169aa bf264d9e bf0016b6 c09a7ae7 40e9ff5e c0e729f3 40ea7f8a c0a79a7c c0d399b6 bfe9e95a bfb3424c c0282ba6 00000006 400730b2 3f935a39 -> c172a35d 4169b16e 41177399 3e8b1b42 4829ebc0 00000000", + "c0859470 c129a44c 41ebf95c bf08df57 401a8974 bf9f81c3 405adc83 3fa67ec7 3f000000 c00be392 3fa74bb8 3fb401f8 bf4bf01e bee8a384 3fd0a718 c07245be 4067dcf4 40927400 40a509b4 3f6cab20 400310b2 c01cecb8 c02e614b 00000003 c01cbf82 3e3b46d4 -> 40620bb7 400a045e 3fb059a8 bd54b809 445b5a16 00000000", + "413f49ec c23b9605 41f54df4 befc8a34 3ec2d2cc 4011456f 401d6efd 3e594c9c 3f000000 3ddacbee 41bfa9a4 bf33ea94 3fedd5ea 3f21ee53 c0bb0c62 c0166284 40c33c0c 4074428c c054a9a8 c0bb5a40 bfff8873 bea66a0a 3ff05bb2 00000007 3fedca4f bf64cf9d -> 3fca1c6a 3f3e73be bfa8e472 3f7e6a7e 444a4df2 00000000", + "c1225df8 c1b04148 41749fb0 bdb110a0 bfa70fc1 bf408adc 3fd13ab4 3fa0f7c3 3dcccccd bd722fb4 42374e33 bfb6316b 3ef3adfc bfd3794e c0ca021d 404db38c 40e5e046 c0a5fc49 c0f90548 3fd2bcb0 3f869193 3fbb2602 3f9bf36f 00000008 3fa9b9d0 bf0017c8 -> 3bd3e9ec 3d3d2ecf 3c4873ea bf2c6f42 3f2648e0 00000000", + "c2399250 421ef7a8 41f48b60 bf079a2c 3f6ce8bd bf12e744 4000ff2e 3fcdfefd 3f000000 bfa3001c 40e8c470 3eb544f2 bfa0aac9 3f5276a7 407e2ac8 4000ad14 c0da1d75 3d307200 c0d996a6 c0ddefaf bf12d2b9 3fa628ad be9e40ee 00000002 bf106dfa 3f8f71c8 -> 3f737900 4040f1dc 405ddf6e 3ee19a13 42f4d621 00000000", + "42006530 41e49e2c 415450a8 3ff57128 3eda626e bfd57cd4 4058689e 3fcdf078 3f000000 3ad735f3 3fd92494 3dae8e24 bfa4ec8a be8076a2 40726f2c bfbd0f5c c0a7b760 c0a09efe 3f837ab8 402882ac be9fd356 4020e40e 4028fb2d 00000008 c0114192 beada74e -> bb87ddf1 bb3555e9 38c33f68 3f30365b 3c873869 00000000", + "41252d10 c0539d90 4160ec50 3fee0079 3f83859e bf34f436 400fe9a6 3ef8c519 3ca3d70a bf4acbec 3ca8f4d5 3f6d5e06 c005b776 3f3913ca 40b316e8 c02ac87a c0b7bf2c 406d11f0 c03c56fc c0ecbe74 4032da72 bf95dd6a 40255872 00000003 c02a26cb beda9840 -> 41ed0ec8 c279a35a 419a9502 3f490653 49838348 00000000", + "c127fdb8 c1f062ea c115d160 3eca2e1b bbadc78a 3d497ed2 3ed4598e 3f56b87f 3ca3d70a 3fcd8492 3f41a800 3e3fe71d 3f3d58d2 bf0d0dad 400300f0 40fc2b52 40e9f630 c02e79b0 c084bbec 40682fc0 3fc2349f 3e8e36a5 403b8e87 00000002 3f39c783 be26b1ca -> 3fb4d9ca 3f4f54c3 c0b0222c bf597d6f 435b765a 00000000", + "c1ded520 4225ed86 424527e2 3c6441be bf75e8db 4013d319 402bc462 3fe951ce 3dcccccd bd9328ec 419ce808 3f97d38d c01cf12f bd8c6440 3fcf9318 4060bc34 c08c758d c06b49c8 c0e2f632 c0aacb7c 3ff87de3 3fbd789e 3fd2e1cd 00000005 c0241186 beaffce8 -> bd9dc4bc bd1cf4d0 bc380b00 3eb12af7 4150f647 00000000", + "421bf0f0 4140c508 423d72a6 3de02520 bcdc2b8d be883d7e 3e93f497 3fb15ae1 00000000 3bad140d 3e835589 3e89c9d2 be6783e5 3ed85e8b 3dd3ce00 3f17dec0 c08dab00 c0ab73ae bfa1a3fc be5235a0 3f9ed7fc bf17726b bf5f0ec0 00000005 40214a47 bf460df7 -> 3e43ce14 bd8f2753 be2994a8 be930cad 40be929e 00000000", + "c23e11dd c1311ff0 42441c0a be99289f 3f94d118 3f11ccc2 3fd73523 3f348373 3f000000 bbc6b346 41b274c0 be670569 bea2bb70 c00188a1 c077e324 c0d61966 c07d9d1c c0a8ea32 c00e827c 3f657650 3fb2dd89 c0031edb bfd409bc 00000005 3fde3d1e be1a7d97 -> baee079f bc55996e 3aae5136 beb0e367 3b11ccdd 00000000", + "41b31508 c21522e2 4188b5d8 bfaac1d0 bff82d23 bf5090b9 4020ccdb 3f8cbc13 3ca3d70a bdc62875 3fb6b81e 3ffebd46 3e511819 bff5112f 40459044 40b17470 40a65aa8 c0cf3bf1 40c645ce 401baa38 3f968ce9 becc1378 bfe84788 00000006 3f253762 bec9f094 -> 3d00910f 3e0e3707 bdc032bd be14a513 3e5d3c31 00000000", + "c19fc1c6 4238f19c 4201281a 3f2e2fdf bfb3fa7d bdcb13ef 3fdaf040 3fb9ec0d 3dcccccd 4023ba54 3fbdaf43 3ee023ce 3f06c306 3f496837 3d45f000 402b7fc8 c074792e c00af9c0 409e65f0 c051f52e bf85f9c3 40174246 bf75fa9c 00000008 bfe9173b 3ef8bbf0 -> c0a5c6ad c1480551 40959cd6 3f09dd63 46ce98ac 00000000", + "4241b6e6 418a11a0 4192bb70 bf02e578 bea73eca bd30286e 3f1bb795 3f977536 00000000 baacc669 3c6a2262 3f56bfce bfaac53e bf302076 40d48e36 3fd97ec8 c0bea9d8 c0c94c92 3e3d3780 c07b113c c004a1c3 3fdff019 bf8a0cc7 00000004 bf4362c4 3e023402 -> bd860aa2 3dd94cad 3d59a42d bf4602c2 3e5d60de 00000000", + "41d738c4 c1f6b6a4 41b820f8 3eb0f29a bf8a2f9a bff45ca0 400e4364 3e05e27f 3ca3d70a bd607f71 410a63c0 bf631210 bf71f0c7 bf4e0491 c08e0700 3e4d52c0 40ddefea c02c1da4 c05bfdfc 40948772 3fca372a c00f53f4 bf84e38c 00000003 3feb33c5 beedbe16 -> bdfa56f0 bdeae2d8 3e833b11 3f25bde1 3edd7a21 00000000", + "bf417d80 c0a917b8 c14e6520 bfab4cdc bf20cced bf2d42a1 3fd01dfe 3d5d9f2d 00000000 3da14cc5 411978ac bf643651 bf9996ac 4024a4bf bfa4e8f8 c0bccd30 4020427c 40f72334 409b9ffe c02461d2 3fe4e98d c01cc9a8 bf2d83d8 00000003 bf8f070b 3e331d70 -> bd4cb137 be1e1e0e bdca36f2 bd67e1ca 3e066bd8 00000000", + "411e6650 c2429db4 c10485b4 3fc0a78d bfa96ae9 3f2600f2 4008161f 3d21e78b 3f000000 3d118969 3f5b3fac bfba9353 bd4e17e3 3f884464 bfedcecc 40dcdab2 c0800905 40943f8e c02ac822 c0631b40 bf175153 bf986799 3f9e77c3 00000005 c0042875 3e5577d4 -> 3cbfb0e4 3d561b59 bd5cb122 be861679 3ebb44cc 00000000", + "41cc071c c10ca168 41120cf0 bd76260b bff05e86 3f20a3f8 4009c860 3fdc261f 3dcccccd be1cdb33 3cf3863a 3f613d9c 40180f9c bfbf2bff 40634b8c c0c42eaf bfdc0f58 40c16b14 c0ade6bd 40088ae4 3f7271d3 be6f1ffa be14932d 00000008 c031dad2 bf260033 -> c24f91de c2d6ce35 42d7791a bf3cf130 4d0b09aa 00000000", + "c1c28014 4222e312 4206754e bf6be48a bfc27d60 bfe291e0 4021d277 3f81d6a0 3ca3d70a bcac126e 40aefdec bf2b7ba8 c0124ab9 3faee6d3 4066085c 3ea88c80 c09acd74 409e8c78 406e4018 40f6ae9a c03a48b3 3e55b500 3f330d0f 00000001 40294e21 bf13baaa -> bd360946 3d0a21b9 3d43fa28 3e9575c2 3c3cd727 00000000", + "420ccbb2 422ab13a c1a59913 4027eab9 3e140650 bf69a2b2 403a126d 3fa0eb34 3dcccccd 3e80ce2d 3ca60ef8 3ed4852d 3f0b2074 3f90318c bfda9ad4 c0c76b65 40c0ba3a c0aa709c c00b43b0 40fb7e66 402b4e07 bf2e5385 bf374b00 00000006 3fa3b17c bf20cf17 -> c1905afd c1ae254c c239914d be1b285e 475719c6 00000000", + "424726d4 c22c5421 c231672d 3e3733e7 bf14e384 be406c19 3f2ae157 3e9cf2c6 3dcccccd be2aa840 3f64fdc4 3ff04c2d bf2b558d 3eafcd74 c0c3c06b c0df93fc 40e17742 c0383262 bf070bb0 40ee5f16 bf0c1aee 40193634 4021a414 00000002 403fae4e 3e57544c -> 3f327d88 3de687da 3e675446 3ece0026 4098bff1 00000000", + "c21d96a9 41f7a458 c18d61de bff10754 bfbd6de7 3fdc2057 403d1ae9 3ea139d9 3ca3d70a 40067508 3c8e5e5f bf8b9a6c bfb572ed bf1ba4bd c09383f3 c080d659 c0eb031e 4045d950 406d1f70 3e62fb40 3ff4a77e bfc8500a bf45c78a 00000007 bfe96e66 be2b5038 -> 430dded5 433754f1 42a76b85 3f09e4ff 49dc1118 00000000", + "c1db0a28 423b436e 4232fbd0 be62f212 3eedd217 be9f7e12 3f1a9b11 3def7fd9 3ca3d70a 3f7a716b 3ffc5074 3e7fdc14 3f8727cb 3f3df688 40a46b6c c06e37b8 c0a7a1d0 c01e47a4 3efa2160 c053ab92 c003558a 3e6ae3e8 bf808caf 00000008 bf3a2fe9 3efc34e1 -> bf6bfe26 3f405ace c0263fd4 3dda6cbd 420abee5 00000000", + "4230e04a 41244120 421c75e8 3e65d8cc be9cc663 3e48e111 3f140941 3fc12d9d 3dcccccd bb2545ef 400a46a3 3f0e92d9 bf8716cf bf2740f9 40cb75d8 406b69f4 c06f60e6 c0cb2efa 40b80658 c0f3e00c 3fc871cc bf918f98 3f34aed5 00000006 c01dedd0 bf4a4ab7 -> be6e997d 3c38a5c4 be29e9d3 3f001728 3f0fc813 00000000", + "c206db58 4230fe9c c147ea7c 3d9e83ec 3ea1cc56 be9fb19b 3fa0a366 3fcde53c 3f000000 bc1baf93 419bf09b 3ff1a0c1 bf846ebf 3ea56a9e 40ba6ede 405061b8 400f2994 bf190478 40a18472 c0d95ed8 c00a26e4 bfd01807 3eaa6867 00000007 bfa64b26 bddad6e5 -> 3aa210ec bbcd1db8 bca451a2 3dd3fe7d 3b5e92aa 00000000", + "c20188ee 4021e890 41d891d4 3f5b3ab9 be80c4ff c00a3080 401df5eb 3e86e353 3f000000 3baa1c80 3f8357dc c004c770 bf000bdb bf694cab be3300a0 40cc6666 402aabb4 40aab6e2 3f4bb2d0 409e89e8 bfb09b0a 403506e4 c031d9f4 00000001 bf8d5036 bf6e2d27 -> bb5e01ca 3a91196e 3c7e4345 3e15a6e3 3a4b8f3c 00000000", + "c01c24a0 c23557c3 41ee01f4 bec67a37 3c7d2033 bf60e04b 3f8ad670 3f9f37c2 3dcccccd be8cdd22 40d295ce 3f22e6b9 be9f8d69 bf86f190 3f39fe40 4046cd94 402762c0 3f6b61b0 bff4cfb8 c09834bb 3ef8d0b0 40179f12 bf877696 00000006 3f82d165 bf63e410 -> bcd5ca00 3ff2e6d7 bf0441f8 be722a68 414d28f4 00000000", + "c2017612 422c7a44 c2383cf6 bf0896b6 bf8bc3d0 3e80853d 3fa1f528 3f9bcbe3 3ca3d70a bdb0fb30 41a8ee37 bf78d1c2 3e3c2f4d bf06074c 40e1cf82 3cecee00 40b900d2 c0f7be60 c0849b62 c0fe2459 3ea298ff c013cf1a bf462bce 00000001 402d754d 3f6c3558 -> be1a2d66 3c5692fa 3e947620 bef7d182 3e44b3c1 00000000", + "422715aa c12fc940 c1c063b1 3ec1290d 3f0afcf0 bd6fd728 3f33f3b5 3da0a38c 3f000000 bb63fdc5 3fc1712f 3b9474c3 bf888fec 3f4b1057 409060e6 3fe13868 3faa9c28 c0b38f18 bfe549a0 c08df9e0 c02a4209 c01d550a 3ed9a376 00000008 c02eea9d 3e5893e6 -> 39d41ca3 3a7d29cd bb68df88 3e470eb1 3d295fe6 00000000", + "c1a5f096 416c2840 423c863c 3ef24e9f 3fb32858 bfd51180 400e9d0a 3e04ca36 3ca3d70a bcad76c6 42735fa0 400df247 3f7d2a96 3f1250ea 402f57e4 3fc71fe8 40a34470 4008200c 401b3efc c0f863f6 bfc80d9f 4020d3be c025a79f 00000005 bff6a40a 3ea8ff42 -> 3caef1c8 bd9b9a3c 3d4a2acd bf5b91a0 3dd0b656 00000000", + "c1d1c8dd 4158dca0 c2064484 bf2ee5de be676350 bf5da67f 3f901ac2 3df94c1f 00000000 bb483a17 3c951328 3f974a92 3fe6389d bf8c34de c0489638 4084c2be 4000c054 407d2074 40d0ab64 bf8bea14 3ff473f3 bf092f3f bfd26c55 00000004 3ffb30c0 bf68728b -> be71d9ad bee96ccd 3e63c06b bfb931ee 4244497a 00000000", + "417ee070 41091450 42443cd8 3f9704ab 3eababf0 3ea94b08 3fa464d6 3f331852 3ca3d70a 40365692 41338f8d 3fdb9f7f bfeb687b beb47642 3f38b190 3fc9a038 bf2fc920 c04137f4 402c1870 407deab8 3fb8d31f 3fbefd0a 3f4268c6 00000005 bffe61d0 3f430551 -> c12c8067 c1ab3cc9 428fe6f9 be0084e0 4757b032 00000000", + "4220e808 414798b4 421dc16c 3e6fc9cc c029f82a be94058d 402ce499 3f7cc5e2 3ca3d70a bf032be1 3c7624f9 bf648a30 3ea9fa8b 3ea38972 40a71f1a 3e9de060 c065f4d0 40e33ade 408b8654 c08a1d16 3fdaa11b bfba5588 3f3d86a5 00000006 bf956162 bf75b4a0 -> c24aaea2 41af7bbd 4195e2c2 3f1ed6fd 494d3542 00000000", + "41be24a0 41970304 401fbad0 3ee33723 c013de2c beeac1bb 4023a459 3fccc439 3dcccccd 3e6fa682 3e271c00 beedf1d6 3ebdbc31 3e8e1665 c0b94058 4093ad88 c0853f2b c0c5e48f c03e90ec c08a64f4 c037af3c 3f7ae526 3fb9df26 00000003 c0159625 3f900748 -> bf16d254 c056be46 bf6a28c9 3bf7f880 45a0aede 00000000", + "c01af050 41db21d8 c22bf580 3f94768a bf894411 3f3344d7 3ff606c0 3ff8885c 3dcccccd 3ed4b948 40d931b4 3f97fec0 bf6565d2 bed6f2ad bea7bb10 40db38ba c0f833dd c07d7e6e c063bcb8 c0dff399 bf412c4b bf977ddd c03de0a4 00000001 402fa481 bf8874fa -> 3eb4aec5 3ee58436 3dcc1059 3e6c08e9 4119adf2 00000000", + "c1e4c6c2 c0a34c88 41fb7804 bc1faf5a bef3e264 3fe491a7 3fef379d 3f84a204 3ca3d70a beddaa08 41d540c4 bf6e6e9d 402265d6 bd5c8b78 c0b2c23e 40b20c56 40fdfb06 c01fa06e 409ecd14 40b245ea 3f2f1c5c c0169d16 c035a8b2 00000004 40392c81 bf9616c7 -> bd01f7d0 4012a540 c1da2b06 3eb198c8 462b40b6 00000000", + "420b99ba c1c059ee c1383740 3f80fe1a be40fb79 bf2a0774 3fb676c8 3ed0d4d8 3f000000 bbba4c30 421555ba bf318b30 bf215d9c 3f3f3a2e bf3188c8 40b60ffa c0f4ccea 40970dc8 c090fdb1 c0f87562 bfd07dde c018845b bfb1d3d7 00000006 3f99b680 3ef0bb37 -> bbfc933e 3b882317 bb41aca6 be6582d9 3b1c5f28 00000000", + "c22b488c 4172a558 c2192820 3e9e5b46 bde3ddc5 becd08c9 3f04a0d8 3f3dcf44 00000000 3f187832 4155a11e 3ff0ebdb 3fc28533 3f0d263a 40646c80 c060986e c00d0cb2 bf873a70 40420ff8 c0f10580 3e04fbb2 3f945e7d c03fd90c 00000004 bff44771 beef6109 -> 3e8937ea bf6c17b0 3fbe2d2d 3ece5189 42ab09cf 00000000", + "420bae06 c194bead c1bf7341 be4163c3 beaa5f1f 3fb752e1 3fc2c95c 3ec959c7 3dcccccd bbf3d19a 3eea1865 3eaf88b4 bf34eabc bfbfee95 c0e1bf67 c09f58e0 c0098f76 bf117520 401d9e68 40a2d844 c02f7a5f c02c7c77 c005a4d6 00000006 3fb0c05e 3f62341f -> 3da72af0 3d207109 bb83130c bf14f05d 3e8950a9 00000000", + "c1b9eb26 c0f53968 c1715270 3cce1b86 bf4af9b3 c017737b 402c2472 3ff82a98 3dcccccd bc608c0d 3cdd2581 3e459e7a bf0ca21b bf57b208 c0e6f949 40acbf84 40cb7046 40bcc794 c07ba9de 40d77566 402c0c61 bffb36de 3f8ca7d9 00000007 c03ebe33 bf33b8d8 -> c125c082 4204e4c4 424d3128 3e6d1a34 49753577 00000000", + "4209c1ac c1c77876 402fce90 bf3cdacd 3edfaa1a be190e47 3fc1d006 3fa4d513 3f000000 bfd95c8a 3ce92ee2 bde6b284 3d84a928 3fbd9834 c0524566 bfda917c 4067f400 40ced79e 40b2c300 40929648 c00843ef bfa65712 3f381e62 00000004 bf014172 bf5dc0a7 -> 40700268 40249778 c1c78617 bf6cb143 48779bca 00000000", + "c102bb74 40962118 c208ca3b bed8d9ac bee9ee73 bfe9b091 3ffb70ac 3fe1f113 3ca3d70a bab24464 3f024a0e bf35c605 3f39c770 bf1092cc bf54cd28 408aa6ac 3fd982f0 c0868062 be33b1e0 c08c8bd2 3f9708a7 403d3eca bf957b9d 00000007 403952d1 bf71a3d5 -> 3f671a3a bfc0892f 3dcdacc2 bf8b0fe0 42d8bc7c 00000000", + "c1604278 bf85cfa0 4122674c 3fdcf3b1 3f93d886 3ee6edc7 4013d6be 3ebd05c8 3f000000 409af42d 4098b639 3f8fbf49 bd01a031 40000a5c c057049e 40c6c486 3f806cc0 40ecb1e4 409b2128 3dd77380 40352156 bfe79e53 bf82d2fe 00000003 bff3d111 3f83e7f9 -> c1863514 c219bc11 c0cfaafe 3f0ad677 46becea5 00000000" + ] + }, + { + "glsl": "SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float)", + "tolerance": "ulp-bounded", + "cases": [ + "3fea7280 c08e1168 c22af615 bf891134 3edbd972 3f0f5500 3fa425ad 3f798864 00000000 3cdea50a 41ea595f bf4784ae bd468e15 bf388132 bffa4d00 c02988a0 40afb644 40892b4c 3f8f75e8 c08b2272 c006306e bfce3d85 c0297638 00000004 bfbabfb6 bec22876 -> 3d1fb19c bb9dc733 bd188555 3ed7d430 3d522e6b 00000000", + "c242f160 c18a36f8 41483378 bf1f4212 bf92778b 3f0b16f4 3fbb8e48 3ddd6425 3f000000 3fc7741b 3edea0d0 be5916fe 3f0db1ee 3f073e38 3f15fb30 bfa85654 40c6a9ea 40acc428 4056f278 40f9ebde bfb08250 3fc77334 bfd6cd94 00000008 c0399e95 be8910a7 -> 402d3939 c108162f c024bb57 3ec2649c 48b80388 47b20476", + "c233f27e 40b1bf30 41a825e8 3e48405f 3f3d0efc c030a387 405fdd04 3fa25487 3f000000 bb141adc 3c6811d7 bed1f1c4 3e47984b becf3eb0 c03e3816 3ed168c0 c0cca5ac 3f1dc9a0 40af4cb6 bf860984 3ef05342 3ea195f4 bfcd8373 00000008 c01c3e0b 3f8d234f -> c30deb7d 42928537 c306a1a6 410499af 4957e1d1 49b773f6", + "c1b10d86 41a0a6ac c1ddcf0c 3dfc30bb 3cee6825 3e48f7d0 3e7250a0 3e1ff8aa 3ca3d70a be00b574 3f4abc2e 3f24a953 3fdc6da6 be3e064c bf252158 c09c0ccb c04eae1c c0dc0d98 40323924 3fe8b228 403b8b83 bfc1daeb c02bdae4 00000008 4017da02 beca84f1 -> bf5ba05d bee48bdf 3edd154d 3f53b614 442578d6 3c5aec66", + "411ec370 c145d27c 4208ced2 bf88be4e be2d0d9e bf1a28b2 3fa045c6 3ce8abc1 3f000000 3c5960f1 422ef127 bf58352e bf48e90f be4e879a 40641170 c0a5cdac 40ad2e06 c086af04 c0c40af6 c0431922 c0054415 3fccd579 bff33b7e 00000005 3f70de56 3f85e0ff -> 3c664f30 bc2ffbcb bc8995ee 4093e0c1 3ca733bc 00000000", + "c1a41334 c212a9ba c1d73489 3e48fbb2 bf8bf89b bf054c8f 3f9d0d3b 3f001d97 00000000 bfb18964 421c0d09 bf7292a0 bebab488 3efb3913 c08cf84c c0a0b146 40ac8146 c0887542 be450cc0 bfc92d30 bfd6ba01 bfb600f0 bfef49d8 00000005 c0266889 3f88cd91 -> c2fc3a2c 4320325a c2fba4cf 4082beec 4831ac90 3de57502", + "423dc6e8 41445c24 4198394c bdd17054 3f18f21f 3f8e7bd4 3fa6dac4 3fe6d836 3ca3d70a be80453b 40f00224 bf6dba76 bdc61d84 be90c7e1 c0fd6411 408fbaa0 c0cab49a 3f24f2b0 bf76ab28 c01f1976 c03bd27d 40138e3b 400806fe 00000008 bf477940 3f978968 -> 3e799f63 bfab746f bd978761 413adb2a 4091a11e 00000000", + "411fe358 421199ca c1f64dfc bf648f7c 4006e3f1 bf70484e 402d70aa 3ef1bc73 3f000000 4060b792 3c51ef4e bed16128 bfa055cf 3f1abf58 40be0552 c0dafba5 40ba4e0c 40127fb4 3fef9bf0 3fa8b2b8 3faf05d7 3fdf73a9 c0298a19 00000007 c009b3e4 3e6f1e20 -> c2957009 c36e9035 42e64af0 3f8f43f2 4c0eaa00 4c3d89c8", + "42067dca 3f6da340 41efd84c 4001f727 bfbd2a4b 3fbed4a7 404c3232 3f0a2b14 3f000000 bc86ebc1 3f25d1c1 bf9f1d1e 402d1d75 be183aee bf3ab740 c0670742 40b24b16 4025587c c0de86f7 c0bb56a2 bfdcf734 bf0fc968 3e482a9a 00000002 bffc1ff4 3f0a8f95 -> bb97cdd0 3d88b2b3 bc21748c 3f82ab2e 3dd769c8 00000000", + "c1afc628 41db8b98 c23f1c7e 3f86e2b4 3f163eda bfd27f9f 400a3f3e 3f9a707e 3dcccccd bb574b28 40193250 3ea7e9b9 bdd9892b 3efeb299 3f4e3930 c0fbf211 40930e4c c01a2824 bfecb0dc 4097dd90 bf43998d bfc012b0 3de7aeff 00000003 3fb56df1 3f34857f -> 3bad6df9 3a41f180 bbe11a87 3fe7d6f4 3a8fb5fa 00000000", + "c2213bb6 c1e0e03d c1f111ad 3fabe695 be6e72d3 3ff01418 40146351 3f274817 00000000 3ec4c9f0 4012f0e2 4026ad78 3f255d47 bf0cb7d9 c0bfeeab beb06ca0 c0e3e2bb c0dca982 bc8e7000 c06deac2 3fd0d71d c026f32c 3f568448 00000005 bfd9ba46 3f315845 -> bf3d91a4 be3b910c c0a405d1 400611a8 42e2cd50 00000000", + "4053bfd0 c2023130 41a3dacc 3fb615c5 be5301d1 3f0c6d0a 3fc4ed7c 3fcb331f 00000000 bcae401a 3fca3645 3eb54a82 bf591cda 40246076 40233f8c 40d4d24e c09bf06c 40a3ee04 40950fba bfadb15c 400f2a3f bf378c46 c01a95c0 00000005 bfcba818 3e9269ce -> bc83eae4 bc81cfa6 bb840e0f 3eed1a0e 3d14abb5 00000000", + "423f979e c06b2850 4220875a 3ec7acb4 3f1ad3be 3ef165c9 3f5ef74c 3f08226d 3ca3d70a be7ec92d 429565c1 bf841a9d bf4e4d2f bd29afda 407fdb8c 40a626c2 bf03b020 bf93f368 40a68ebe 408236aa bf23ee28 be48ab48 3f19c6ea 00000001 3f5af316 3f467110 -> 3ca382d7 bd1cd072 bc3feff7 3e90bea8 401cf7f5 00000000", + "408bc148 42000906 c18c6134 bf94df64 c0125e33 bdcb9a95 402454da 3fe8f76c 00000000 3bdce0d4 42b4064f 3f17d9b1 bf2b7c2c be6669c1 40d51372 4097d964 403eac74 bf27ab40 40f1342c bfbdccc4 bea650dd c02628c6 bfdc298c 00000007 bf82df8c 3f33d9c0 -> bc3da1ec bbbcb9ea bc5a8d6b 3fd3fe31 3b941b61 00000000", + "c2458b67 bd7ce400 4107f9e4 beae2f90 3f7b7195 bf9fd3b3 40267817 3ff9f55d 3f000000 3cd8fc0a 3ce92964 bfa7835d bdb91727 3f69ab59 40e79b9a c073e1c4 40c4172e 40998622 c0665d56 beb36e70 bf443343 400c11dc bfad9f12 00000004 c02953e7 bf4ab08b -> 4079da04 3dd951c8 c01e1500 bf409ee7 461c3cbf 454c2269", + "c2120b1e c0ef4580 c194c533 bfac1582 bfa8fe84 3f908f26 400ff61b 3f0699d9 3dcccccd c01b8f0d 3e238855 bf674428 c0068408 bf2a5bcf 40d3231c 405bc520 bfdb0f84 bf111298 3da13300 40197c1c bff8c25c 4034d6e4 bf6d8190 00000001 bfe10307 3f65b2f1 -> c12e7358 c1ca4449 c09dc8b2 3f73424a 464964a1 43cd9f3f", + "c031e8d0 4130b434 c202229c bdfd5cb5 40037d57 c005c1a8 403c2d6a 3d8eca00 3dcccccd be1203b7 3c70fb97 bf8dfa3e 3eb0385a bf2054e6 3fc96690 bf45ba48 c03f3a60 c0bc8ea2 400f74d0 4006fb2c 3ff97831 400ffcc1 3fd25adf 00000001 4006fc96 bf462aad -> c048df51 3f6cbe9d bfd69394 3f73a9c2 45a36c5c 437c2bd4", + "c10503ac 4204640a c182165c 3eda129f bfe7aa4b bed27596 3ffefdd4 3f610663 3dcccccd 3b21c203 3f0d93ba 3f661ab8 bfc50e75 3fdfc371 c0aeb42c c04cc09a bffc162c c0f324bc bf5a7028 bfd0c3cc 3e93287f bfc43ddd bfa09eff 00000003 bf459bf2 be085bc9 -> 3a288521 3ab7dbc7 3ba7bcbf 3e78b5f8 3a15d0b5 00000000", + "c0a32680 41ec9d28 4247ba34 bf2cf583 3eaa0ec8 c0086aaa 40126026 3fa9fd8d 3ca3d70a bfe6f845 3c62123e bfc887f8 be9508ca bfecc666 40f0c276 bfaa07f0 c04ca5fe 40184624 3f9fa628 4050c994 3e3906a0 3f86443b bff6d0b2 00000007 bfb1644c bf8073ac -> c47aa3a8 c330d2be c49371c9 3e866ddf 4cbf6f30 48351954", + "42231636 40abfba0 41e35188 3ea51426 3f426e09 bd3f7306 3f5446f4 3e0ed7fd 3ca3d70a 3ec66c23 3ee7e223 3ed7bb1c 3ebf41b2 c0237d69 40f0f330 c0a6b382 bdc5bc40 c007d0fa 405be880 40f62f6e c03a9a13 bfa6b44e 3fdd80ed 00000004 c0122838 3ed4342e -> 3f5a2d78 3f4efd42 3f4e2afb 3f98b965 44307775 3ce2a6ac", + "c21e226d 420c26d4 403a7600 3f987e2f 3f78b8f3 bf032c5f 3fd09f45 3ef13924 3ca3d70a 3e04c347 3dcc98dd 3f76adf7 bf504475 3ea68622 40fb4a4a 40ad7af2 402a3d50 bec6ec90 4024e9c4 3ffb2560 bf3dbb71 403c32e4 4025bfe8 00000001 4010f424 3efaee93 -> 3da33db1 3e560006 bcb15928 3f772bc8 4290d45b 00000000", + "c1c33363 41b1ca50 41885658 be72c714 3e55f01a bf810f9e 3f8869ba 3ee86415 3ca3d70a bb910d39 3d055447 bf0377bb bfcaa426 be4d9ec3 bf1d0370 3fd37980 40d17abc 40883636 c069d9ce c0c4f35f 3f1ee87c c039f4ea 3fc32681 00000001 bfc7d71c bf81bba9 -> bc951803 bd9951f0 bbb7bcc4 3ddff510 3f546da3 00000000", + "c1d718e1 41f78200 bfcd5b40 3f470ce0 3f7a628f 3fecee7c 401013b5 3f658fde 3ca3d70a bdc2701c 424b20e8 3e99efd6 3ede2322 3ea20be3 402d28cc 405128f0 40c8550a c014598e 40bf7124 40feb9b6 bf77bf01 40071465 bedb6157 00000002 bf8044b6 3f786a24 -> bdb50b4e 3db9dfa8 bd27362e 3fb71f5a 3ece3e43 00000000", + "c2099819 41364e10 41b84738 bddac114 c014084f be45a3fa 4014b444 3cc633ba 00000000 be7bd38e 3fa75586 bea779b7 c00f9d56 bf2bf5be c0f16228 3f4d4340 c0cdea35 bd9f7880 bfe16824 c086c1cc 3f230d1d 403a7e7d 3fe7e947 00000006 bf6e2c2c be35532c -> 3ec63516 3e863340 bf9dde43 3ecce24c 40081933 00000000", + "41a2c848 be7feb00 42410d0a befca6d5 3f19e543 be244aff 3f9143e4 3f2e783e 3f000000 3ba5d4da 40ba37e3 bf71100a 40033d31 bf552a28 409e1cc4 407890f4 40db0428 c0b62ef2 40fea7d6 c0127bf6 be4ec094 3df24f08 c030906c 00000001 3f0a8748 be558e01 -> 3c50d233 3b0d3201 bbcdf889 3f6a78ff 3a4a945f 00000000", + "41fa8888 c1ace9da c23d13d5 be83d76f bfa412e7 3f7d0264 3fd44f65 3f7c7144 3ca3d70a 409df318 3d12aa82 be6a40c6 3f472266 3e21fd04 c0f6486e 4089d62c be374f80 409224da 404b08d0 c07110b0 401fbd77 bfd8519e be9064f3 00000002 c00acc84 bf0b9cac -> 430d1d06 c401f65c c2dc1a74 3f0e413b 4a661018 4515e520", + "41bb4034 c19cb20a c22b51e4 c027ce69 bf602661 beb76cb8 403d958d 3fdfc78c 3dcccccd bdfd5c62 3f05ff58 bf44d503 bf2d73a7 bd874210 c0afd3b4 40951636 40bc3c2c 407c1680 407c75a0 40b17308 403e7453 beabc6c7 bfb09e9a 00000007 4003a0e7 bf98d243 -> c294bf6e 429b6251 422dc110 3eb81a74 477b2d3c 459eec96", + "c23a3442 4247fc6e c204a66a 3e6fa6a6 bf10abd2 4015bfc6 401ac804 3ffe7cc9 00000000 3a83877b 3e13a65e 3f013601 3db4d3a1 3ecd8a49 3f936fa0 3e024bc0 bf7f3930 3ff3e788 40729f4c 40d033a8 beb13dbf 40217a64 bf800c4a 00000004 3fafdcac 3f88e973 -> 3bdd28a6 bbfa59fa 3c99ef52 40321f86 3c9412ae 00000000", + "c20bbb12 c0724e60 41e8da1c 3dd462a0 be0831b4 3f91261e 3ffce425 3fd44a9e 3f000000 3d9b447a 3f0d2b9d 3fa55223 3ff9b414 3dfb7b2a 40c8b3bc 3f9f8958 40f647ac c0fe9997 40b316a8 40e3a67a 40198d0c c0310424 40256b5b 00000006 bf1bed42 3f4bf5aa -> bd33ad72 3e4fd0b8 bd860c99 4036e175 3fba30ac 3f1858e6", + "423cd182 412a9448 3ffd54e0 bf3aa1f1 beb5f0a1 bc5ec628 3f6e1171 3f981494 3dcccccd bd810982 414c9cac bdcfe1fe bf2355f5 3f48c676 c0d16b6e c068c848 bf2bdc70 c058480c c0ba91b0 3f57e1c0 3f0d74b0 c02360ea bfd7aa04 00000008 3fb56f6a be5816ca -> 3cea206e 3c591645 3b17e982 3f409037 3e14f81c 00000000", + "403668c0 c1529be0 c1c4cf08 3f17edd5 bfacaec4 be515fd5 3fc65385 3f1d5503 3dcccccd c082f3a9 424ce35b bec23fcb bfe5c607 be68ef17 40b3bfea 40ffa144 c0a679e0 bf491798 c0f1a11e c0ea43de c02a5889 bfeb7521 c0045995 00000006 3f1c1e50 bf87598d -> 411158a2 c0465f35 412fb56f 3ebcf11c 44bf1e57 41a50482", + "41ff05f8 40eef330 423e9f06 3f94e61c bfce203d bf1c1e1b 4005e728 3f355ab8 3ca3d70a bfbb5702 3e86ac2d bf1af3ba 3e36fbfb 3eb4edd7 403b639c 40a3cb0a 40fd65ca c0857cfe c00c19f4 40d756c6 3fbe79c7 beac7a96 401b7fcd 00000007 402c7941 3eca9741 -> c12b2e60 40de3953 41a02488 3fa92ddd 49b5851f 4504a2a8", + "c2081236 c22f0ce2 41be72d4 be766201 be92a88c beec84b7 3f8ea784 3f8522e4 3f000000 c06bca79 3e993d9e 3fe4ed16 c0134dbd 3ee1a380 c0d107ae c0bd2894 c00ce5d4 3e84bde0 3f0b8410 3f61c690 bfa2829b bfcbd6a5 3fb9ab08 00000001 403a4227 be9bdf1a -> 4185a550 c17ce0b5 416e4db4 3f610379 4603bcd3 43e10866", + "c22e48ed c1e79f94 c0c16370 bfd29ce9 3f9c7fd2 3f431d56 40105811 3f2eee17 3dcccccd 40a88d6c 42c46d09 bf70f7a9 bfbf6845 bcc53b6b 401b1658 40cf2624 409c0214 40c77838 bf9984c8 40c79260 c01f55ec 3f1074f8 bf9e40a0 00000003 4001ad83 3efad906 -> c0602a2e 4016639a c1ad28f0 3f8620b1 462a3b57 440889ac", + "41d69a58 c1ffe54d 40db5d60 3e9ecf98 bf91770c bda28a1d 3f985270 3eeedd0c 3ca3d70a bd1cc74b 3fec4948 bebdd541 3e907e0d 3f485cda 40f846b6 3ff76f50 c0c95424 400118a0 407eff8c c02667ee c03765cf bff88611 403905c1 00000008 3fca16e0 beaa88cc -> bd9cc623 bda91322 3ccc5d66 3dbbda63 3e38231a 00000000", + "4063df20 bfe8bd20 41a4f3c0 bd604ccf 3e7bb9b0 3d13a5da 3f1b4866 3f344dc8 3f000000 bd3989ee 4152d630 3f497416 3f8c25dd 3f1519ee c0f15b4e c0badda2 40dea2ac 40207c80 3fcbd368 405af600 3fbb8bc6 403d1213 3f9ca8d9 00000005 bf989372 3f81d4bd -> be737f54 3da92dbc 3e3de229 404335c8 3f6427cb 00000000", + "4200bc86 c007cca0 41f7484c 403b58f3 3c1166d6 3efe6880 4048b15e 3fd54b2c 3dcccccd 3bb87d52 3fae160d bfdaefbf 3f6e6a31 bfd9d9a9 c09dd1f4 c0ba85b6 bf95fa04 40be700e 3fbcae78 c0c36d88 402a3077 402e7f15 4017b376 00000007 4018598e bf6bdf0e -> 3eb34ee2 3f7a783c 3fc41b28 3fa4eda7 423070c8 405973d3", + "bf870120 c20c2b89 c13e4bf4 bf61632e becbe2e4 400ab23b 4019127f 3f72aae4 3ca3d70a bac58759 3cc47cbb bf8b40f3 bfd2c732 3fd39cc4 403b9f9c 40ee8be0 409e3a22 c0c1f092 bf8a8ebc 40eb463a bf7de81e 401af927 bfcf70d0 00000002 3fb9aed7 3f055b03 -> 3c222d72 3c3fd32d bc0e801a 3f81a04a 3e54a178 00000000", + "40ee2d70 4193ad48 c10c3fd0 3f034d3e 3f338bfd 3dedb647 3f60691f 3fc28a75 00000000 ba8326c1 3e69e935 3f62f1d0 3fb5fbf1 bfc0a9d9 c004e3c0 bf10d450 40c72b68 bdb87940 c0c0533e 40414570 bf64385b bffb50b2 bf830fa7 00000003 bfdd6c9e 3ede9ff9 -> 3a30a30a bb881efa 3b10da06 3f996a62 3aed1f25 00000000", + "422f33f0 42191696 420a544e bfd24655 bf735d08 bee08c31 3ffde779 3fe3ed21 3ca3d70a bc95231e 40bf94f0 be5dffe0 3ff79e7c 3ee60514 c0e94a88 40a5b96a 3fc3fa70 409d1d06 409efc3c bf2e75f0 c0383c3a 3e2485d9 3fa418f7 00000004 bf86a17c bed5b99d -> bc029dd3 bbba19a4 bc2b1b98 3f57551f 3c4c5d81 00000000", + "422d5658 c129f3a0 c00419b0 bf8bc7a0 3fb68e9e 3f5fc09d 4000d913 3f47bafa 3ca3d70a 3d6c5360 4110ef14 3f6614bf 3e4a2da5 4017a113 40e7f7e2 40e0888c bfef4658 3fc91c80 bfc22b54 40a4dd00 3f65a8e5 3f81596f 3fbc97c6 00000004 bfdde638 bf889eb1 -> bcba6190 bf12f92e 3da6a680 bee619a2 40847d0a 00000000", + "c18ec486 c1d0e741 c2021e2f bf155271 be2305f4 c0016d23 4007e6a5 3f22c000 3ca3d70a 3eea9899 3f502447 3f9be9b8 bc8bea82 c0217138 c02c7ee0 40daeacc bf9df588 bed76b40 3f466c80 3f4d0120 3f91c6f5 bf2621e7 3f0a6238 00000003 c01d450e bf8e495f -> 40d8bc4e 3fdaa46a 408a9d48 3f753c55 4466e8d4 3f8c0100", + "42143d40 417443e0 41b8ab5c be8d4a6b 3c396f92 3bd55cc5 3e8d72e2 3e4a9a44 00000000 3acded64 3da82838 be90f6b4 bf6c6564 c009e0eb c05e34a6 4083435e 403a0030 c0afb266 3fe0ab30 c0e7673f 3dfb8665 40276f46 3f3318f8 00000006 c002e1dd 3f08beb3 -> bc997612 bb9c2e1e bd62f21a 3fc148ef 3f74011a 00000000", + "41fd555c c1a019df 4080c540 be610fa0 3e8511e3 3fd467bd 400ca14c 3f80dd84 3f000000 3f0589d1 3dce81c2 be05ca38 bf065133 bf096667 c0ff7b8a c0a60f24 407c18a0 c0813e9e 3fd56dd8 c0d707d1 3f9b074a 3f52786b bffbe45d 00000003 3fccdce4 3deb5089 -> 3f3f02d4 40a08142 40961e1b 3f3efd62 45008631 44bcb8c4", + "c0854798 41b1e17c c17dbaf0 bdf70a3a be393fd2 beacf065 3f040d46 3f92136c 3dcccccd 3d6d5bbe 3d9da3f4 bf9c0c7a bee538ae bf17353f c099d39c c088b3ca c0680334 c096af48 c0ddc8ff c0fa4f43 3fbebfac 401bc897 40032d1c 00000006 c01a84dd bf45b19f -> 4185d489 412d96a0 41569aec 3f8558c4 47925405 42f007c0", + "40260090 c09e1f38 c200467e bf08c9ac 3cbeabcc bf35a424 3f80969e 3f94901c 3dcccccd 3dca2f4f 3e22b911 be30bbec 3f019ac7 3f5303d1 bfbabdc4 409c0384 4046476c c0afb7ce 40f0a542 c03291d0 3f8d3194 bf98b27f c0320a25 00000007 4009fbae be96c5c7 -> 3d1fe5bc bec294fd 3e2d365b 3f12ac94 43e9040a 4010ca32", + "42094590 c2281d8c c1d54f09 3fc76dfd 3e44f52d bfdd007c 4018ff98 3f11fcf6 3dcccccd bbfebca5 420c6f94 bf37a4bf bfb4a3ed 3f6ad10d 3e617a00 c01b488e 40cf251a c02b291c c0b49af4 3f0b24f0 c0174fc1 3f0c0025 3fdef72c 00000001 bfa33801 3eb47537 -> bc5c4702 3c827fc1 3c472633 3f24decf 3ac71fd4 00000000", + "c1a1b19c 41605d20 4235e8e8 bf44583e 3fa82ca5 bcba5344 401fe642 3ffa1744 3f000000 c014b381 4040f656 bfae3065 3f1b01c5 3e8d2f74 40dd5474 40ccb418 40b8a220 3f007020 40bc23d0 bf440380 3f69c475 bed58421 c03b4245 00000001 bf580972 3ea4255b -> 3f9fd909 c08ded13 40827bf2 3f3730b8 439630cc 434c1837", + "42292858 c0876980 421206f0 bfc07646 bf8395c4 be8b225d 400d9067 3f3d9d20 3f000000 bd37c2f5 3d628972 c0070111 3f3c1611 bff7185a c0f242aa 408135a4 400d3948 406d5734 c0ec072a c0ab4ef8 3fb43cec 3f32bffc 3fbb5536 00000004 bf04c4e3 3f5727be -> be95df69 3db8729a beb91f6d 3fe3375f 423cbc37 41d2cc18", + "41038620 42065dfa c22cf126 3ec66ac4 3cd0c853 3fa5bb5b 3ffd6bec 3fa0ca21 3f000000 bbf7b4a1 4101741d bff2239a 3f5f0e6b 3faac588 3ed3b9a0 c047014a bf23d848 c08e4cad c0ff1651 c07fb278 bef19e58 c015199d c02eca9e 00000005 bfbf3b86 3e8e4eb1 -> bbe6b16b bbea74a6 bb52c4f8 3f5d4f42 3b9d1174 39ef6fab", + "41c4db64 420cb244 3fc61b40 bf30f98c 3fb05fa9 bfb6eee2 40130440 3ec7a71a 3f000000 3bb95f78 3d26224a 3fd4b898 bf8eebfe 3ebe7ca7 400c5714 40374cc0 c08ac116 40f4f4f0 c09190ea 404918c0 401e7a4a c008162d be1edf8a 00000004 c00c6978 3f62b67d -> 3fb7997f bf712f64 3ea29730 403115e7 42da6921 42cd3420", + "c200abde 416544c8 c199fa7a bec20642 3f2efe07 be9c4c53 3f7268c0 3f8a0611 3dcccccd be97adf1 3d1c5fc1 3eef2aa5 bef23d1b 402d73a1 bf287c58 409f424e 4086916c c0c1c112 be82d2c0 40b5ea18 3fe286d5 3f8580ce 3ed85727 00000007 c01a75c1 bdd8581c -> 409889d5 c0792288 41b52781 3f6bad2a 46038a3f 4228f243", + "c196c191 c1e1e8bd 41faf76c bf659b4a bf11065f 3f78669b 401a20ec 3ff87922 3f000000 3d4aa591 42a725c6 bf974ef6 bfccada1 be33dc54 c0fe0380 bff59a88 3f75fd10 40de1112 409368b4 402a1184 c03fc74a 3f64382d 3f6374f5 00000002 bf6768a7 3f0c711b -> 3dbe19a9 bd81cd00 bd38c684 3f8ab59b 3e385db2 3d2134e0", + "4242d8f4 42131010 41303d38 bfac0995 be631508 bf6315d8 3fd012ca 3e257f3c 00000000 3e8a4fec 3cdf42ac 3e00a39a 3fbd69cf bffa3963 4012e154 c09bba6a c09601a7 4014f070 c0518414 4097d396 c034aa0c 401c7976 3f087d4d 00000002 3fc30870 bf47de0f -> bfd88ce0 c184ccc4 41b8a7e4 bc8fade0 4573b484 00000000", + "420ff00a 41d3a02c 4199c188 be9f46e3 3f9e521a bff68fe3 40380a1f 3f90bd6b 3f000000 bc9a7ac3 40f3d525 3e657fee bec1c4d7 bf0d4ecf c021b548 3e340c80 40992fc4 c0d85aa8 408b3f4a 40f3336e c01623ad c0129176 bdf5485a 00000003 bfee7464 3e996eda -> bcaad2cc bd13433a 3c5155d4 3f9d1a80 3d60747e 3d1a9cb1", + "415400e0 3fe8a640 420912ee 3e4d3a11 3e1f115c bd46aada 3f8a012a 3fd1eb24 3f000000 be380a78 40e4fee2 3f6d58e5 3f311b3e 3ebb4f85 c04519f4 40c467ee 40e22898 bfea2708 c04e350c c03a967e bffe09bc 3f8dfe7c 3fe20a19 00000006 bf1d6bea 3f949d89 -> be400e0f 3d74b99d 3ed08af8 40c97b64 4038e8b6 3d79fcc8", + "4102c70c 41f45a78 c18d9c62 beabb108 bf2207c7 401a3bb3 4020e6d2 3e8104b8 00000000 3b14a7cc 409b0463 bf0de2e2 3f30662a 3f5f73f9 3eaedce0 40dad914 3fe27028 bf95ff40 c0e426b7 c0e07f5b c03e7319 bf53a5ae 3eb5a9cc 00000008 3fb64f80 bf84bf18 -> 3d9e3262 bc5d9b0e 3d9efeea 3f08b31e 3d338f7f 00000000", + "c069edb0 c1d9a784 42113700 3ed6a6a5 bfa22e93 bfb797e0 3ffad416 3ca45b53 3ca3d70a c00c2e03 4113dd5f bfb68878 bee94476 bea751ab 40f43b32 4029532c c0c3fec2 4080fede 4053ece8 c06a3152 c0311c0e 3fe867c3 3ff052db 00000001 3f7589c4 3f95f158 -> 3efaf314 3e48a7a9 c01661a8 3f6ccf51 43857860 3eca859a", + "4155e1e0 4193bef0 41913b68 3e89e493 c01c9020 3f8c34ea 404f43e6 3f8b70ac 3f000000 bfa10ff4 3d14d576 bf12fee9 be7bb656 3ee466eb c0e9b53f c0d80358 3fdae500 40386eec bfe62e6c c0acd630 400902d7 c01bb1b8 bf8d444e 00000002 40381a36 be66c8a0 -> c269443f c1afdb21 423afed2 3f502f17 483aaeb3 4838dd14", + "c18c2c30 4213f24c c23ee667 bedae7b9 3f8f6c93 bf2643d3 3ff28230 3f87ddf2 3f000000 40145aef 3df4002f bf227d52 bf1ce43f c005bd3f c0348b4c 3fea53d0 3fb83b10 be17a2e0 be79ae80 402caeac 3ff51a38 3fa9926c bfd2cd69 00000006 bf8ed37a bf049bd5 -> c0b3ee77 c126d354 c20535fd 3f224e46 47570f87 4636585f", + "c20841de c18cddde 41af1688 400ef975 be2acfbc beb03f7f 401bb070 3fd4add8 3dcccccd bc29747b 4226bb4b 3eb9ae5c bf238da5 be157fd7 bfd60b54 402c7b34 404fc260 c0e2598a 3fee6e28 3fe699a0 bfef869a bf2cc9b2 bfd1aa4d 00000004 c0392fb7 3edcb01e -> 3d23e030 3cd18c8c bc24400b 3f5a0ca2 3eb59d18 00000000", + "c1f6bdae 41704400 c20ddf2c bcb8f611 bdb629da 3e5cd2aa 3f4070d8 3f8471c1 3f000000 c07e1d61 3d82ee23 bf80a85a be1b4c87 bee82870 405bc35c bfcfa370 4055b430 c066d91e 40c54884 40690bcc c00313d9 bfa74b59 401ed56d 00000004 bf8ee446 bf582351 -> c38fb142 c222f40d c30a199e 3e089150 49629359 46239dda", + "42188056 c1c939e7 422c7f70 3f8cb81a bfeec60c 3f7fa9f8 4018ad49 3c32a067 3dcccccd c10668ca 3d0f6bd3 be084942 bd861d20 3f0108c0 3f664200 c0c1de1a bfe7ffb0 40819570 c085c65b c0e6c649 c02faf3b 3fba4069 3f5b976b 00000003 bfb467bf 3f2ea054 -> 42451398 c1c3a214 c329963e 3f67e1d7 4b4cd140 488f4957", + "41a50924 c0f20390 c214f079 be21929c 3d7626b5 be951253 3edb60b3 3f6b4506 3dcccccd 3e2cec28 3ee27d8a c025b4b5 3f2cbace 3e8ec015 403d151c 40807ef8 bfb208ac 408d0722 c069340e c011fc4a beac70f4 c0198398 be693576 00000005 bf4a2d3e bf47c07e -> 3e52d4ea bf06bb65 bee07cb7 3f9c8e89 410687d3 00000000", + "c1f89cc2 c1f80bf0 c1991238 c00c5ca0 3f212e08 bf109034 40166fdd 3e5fe880 00000000 3ad9ecc6 42900389 3ee1c2c1 3f560816 3c80f6b6 c0be1092 c0bb6440 c0667a1e 40da4930 409cd790 40cf3d34 40003de4 c02abec7 c0278f4c 00000002 4025f3df 3f367373 -> 3b7e64f8 bb04cccf ba508f2c 3fc21e6c 39fa955e 00000000", + "4184b7dc 401a6950 c238b14d 3f644082 3f904daa 3f404fa7 3fcf9758 3fd918d3 00000000 3f8de33a 4002b066 bf597c26 3fb6f8c6 3e09a83a 4039cd28 bf97de40 3fb7ee30 c0d3892f 40eff156 c03d2324 3ffcb335 c0277509 c0142959 00000005 4014f23e 3f8738e4 -> 422bf85f 422d3be1 c2a1bea6 4081414e 4702fc16 00000000", + "40ac6518 c20bc007 4246c90e bf3ff4a3 3fc95cc2 bf891061 4002e7d3 3fdcb91f 00000000 3f43ad75 42928f93 3fdc6638 be7f60b2 bf405c2b c064c5ee c0a057ef c0fb5e82 bf91501c 40aab4ee c024b7ea c0324165 3e6d5a9e 3fe448ea 00000005 3fcd82a6 bf8d935b -> 413b086e 420a41e4 4173437e 3dae1768 449919f4 00000000", + "c21b5bd7 c202438a c1e10d2b 400c423d bf05eb5e 3fe6136c 4039372c 3f1635ee 3ca3d70a bf23b53b 420a551b 3dd9e3f2 3fc29635 bfc8989d bf99b1c4 4095f7c8 c0c59a01 c0c9a881 c08d6d63 c0a62f1a c0090daa bfcf51a2 3ee4bfea 00000006 402e4b65 3f379b33 -> c0062328 c1c5a799 c1caa532 3ff0f4ae 47e90f4a 43a97449", + "bd423800 c170345c 41875bc0 c016098f bf6cc6a5 3fb9451b 403b4c95 3f81d0d5 3ca3d70a 3e16e24a 4094fb74 3ea8c370 c02093a2 bf7646ad 40831abe c0b40608 3fbe0078 3fe86e78 c0d7f85c 3fc57cc0 40010b21 bf30f575 c00e7c45 00000002 bfd68132 3f28c37b -> 3de91632 3e364c98 bd2b16ff 3f9d3a0a 4001f925 00000000", + "c22bd2d9 c05808c0 40cd45f0 3e27b1a3 bcf975cd 3e811da7 3eaea98f 3ff90945 3ca3d70a 3ec83f40 3cc69e2d 3ee20625 3ef51edb 3f5d6a0d c0ef705f c08cf02a 4095a0a4 408827f4 c0d35d93 40adb96e c0005524 be770fba c025ae3b 00000001 40069b98 3eb1e550 -> 3fcdfe15 40423f18 409fab73 3ee75c9c 46623c1f 00000000", + "c1c67439 c22425ee c19d1da3 bfc688db 3fd98179 bd80b667 4016d28f 3f0ce3c5 3dcccccd be5dff47 4030d576 be82d672 bdcef25e 3ffc5fe9 40d127b4 40c239b8 3fb918d8 c04f9c18 c098870a c0db67ba c021e3dc bf055efd 3e308efd 00000003 c00ec91a bf0aa7d6 -> bf192eac 3ecbe6d8 beec51ae 3f2cb1a7 41e0cc06 3f9ef8a2", + "c1f243ec c1ce9137 41d098a8 3ffd7b9a bffd2be1 3e923531 403f86da 3fe559b6 3dcccccd 3dbf6fb8 41123ef9 3f256383 bf840ef4 3fc7bf6b bf0d7540 40e1ed04 402d6294 c0fca598 407696e8 3dda5f80 bfed56d4 c018934f 3f783f7f 00000004 403b60fe bf8e7067 -> 3eb411a8 c0bd1fe7 c0867f0c 3e4bcde8 43eb8fc3 41f94e83", + "41abdc14 415d07d0 3f969660 bf2891b9 bf564dd1 40272e23 40696cf1 3fd38845 3f000000 bccd8751 3c78cef3 c031ac6e 3f75a257 3f10327e c0557d9a 40840bf4 bddc7280 40811a1e bf5fee00 c0849450 c034bd1f bf520f63 3f7d6393 00000008 c003bb73 3f8cdb2b -> c3f03aa8 43374404 42ad3926 40c9f680 4b0d3eda 4ad75d5c", + "40b8dc10 c23a05fd 3fed78c0 3f78d649 bf92dba3 bf8eca4f 3fefa8a0 3dcff8a7 00000000 40263715 3f28aab6 be0b52fd bf50fd2e bf4745bf 40982036 bf9fac30 40035058 c0c1c61c c0b6d607 3f9db768 c00d45e5 bf91c195 40334e56 00000003 4003e1b3 be72b84a -> bfb95b45 befa1af8 c096ac1d 3f191e4e 44b23a23 00000000", + "c1e61367 41207fa4 41e8c5a4 3e887d71 bf1eddb3 bfb59849 402098f4 3ff02499 3f000000 3f460e9b 3d82ca92 c00ec4b0 bc6a5b93 3e918486 3f472fd0 c079f2a4 40c54066 c09041d4 400ab0f0 c0a7a9f8 c00efc60 40172009 c0180f77 00000003 c00e2aa8 be959c06 -> 421a1d41 bf7c7857 c08be5ef 3f372041 471034f2 4624a779", + "41ce21b4 c191b7f0 c18d649a bd5cf80b 3e91dbe1 3f2f5a19 3f3e6a27 3fdcd182 00000000 bade42d2 3c9a0a99 3e2aa7b8 bcfb6332 bf9df02f 40b075b4 40fecbc0 c0ec0022 bfbf0720 c063bc48 406fe194 402ba660 3f870a17 40256552 00000003 bfdb06ee 3f4cae62 -> bb2d0790 bbc7d950 bd5cebb8 3fe625a1 3fbaa7cc 00000000", + "c051c350 c226a55b c1274a58 bf7dbf36 3fc780bc bf5e812e 400afcc5 3fa6cae0 3dcccccd bd6ff458 41f68d4b bf089cf5 3f5df8b7 3f51b5f0 c0e2f76c c0e7936a c0247dbc c0f28000 c0cb5363 40084704 3fe988b5 4037ecdf bf2fbc48 00000004 c015189e bf9611fd -> c02d4392 c0933b3e 4057c87c be232118 42671d20 4029bd9e", + "41810294 c12620a8 c22ea73d 3f8ab95c bf19dffd beba2fc3 3fa551a2 3eb24fe7 00000000 bf00b878 3f8d253c 3ffe500c 3fb8d332 bfd36600 40ac2304 c00ccb3e c0e68cea c01edf8c 40ee9770 bff1016c bf0eadfc 400cec9c bfba371f 00000008 c02e6ba9 3f1d2faa -> c164c98c c1079c6e c1f998a8 3ff09ed6 4911265e 3d615673", + "41cd1820 c1d3ed1f 41436130 bddc2031 3f34fa60 bfe3551b 401a7872 3f7f7f79 3f000000 bb9ffc7c 3c6c2895 3e10bf90 bf73c204 3cb7a53e c0ec8071 4080fe18 c014ec94 be89ff70 3fff5558 bf827e64 bf275975 c01088ba 3f10ea25 00000001 c007b3d6 3f2f5926 -> 3d6e2a29 beb4941e bb0d4d2f 3f32e426 40cc4ad4 404f483a", + "418a7b1c c207d1ce c1686580 3f3bd880 3f8798c8 c0112e99 402800b3 3f4e0bcc 3ca3d70a bf8504ab 405e4808 403377c8 bf056635 bf2ab192 bfe3d63c 3fec44d8 4085f044 c057cbb4 4081df28 40e66e02 3fa7354b 3f7f9f04 3fbf382f 00000005 4026692c bf9883e9 -> c208e9e5 42333b5c c345a6a0 3fa00f3a 488d1215 443bb654", + "c22f47fa c0a19c40 c19fce87 3f499935 bd69f365 bfc0e8e1 401d02bc 3fc07ba0 3f000000 bec2535a 41ee2309 4029708c 3ea60eba bd7809e8 c089ed2c 40c8e444 c0437e0a c03f71bc 40971910 405ea168 3f81f385 3f8f972a c00bbebf 00000001 3f842362 bec4c173 -> 3de61d37 bf25844c 3f3be08d 3f6d682e 41003efc 4115593e", + "41271358 c2101e25 409498f8 3fbe7320 3e3c79fa bf5b6ffc 3fdd4367 3da86392 3ca3d70a c0a31b24 40275442 3fbae5fe 3c93a8ad 40087169 40174dbc 401f3bf8 3e439900 403d08f8 40f2a0ac 40073330 401887d3 bfa7d8fc 3f01e478 00000004 4005e36a 3e91f6c1 -> c0243383 408fe051 c09ab747 3f85147c 4601ccd5 41265d38", + "4230fa3c c1cf482c 40e82408 3ef91178 ba4762c4 be872f30 3f0db1ef 3f484bb5 00000000 3eb230a3 41426b0e 3e240509 bebc44df bf0ae1e4 c084609a bfd5884c c077b8f4 c0a7b7f4 c0a9fc66 3fcfbff8 4009aaea 3f0bc13e bf6711f8 00000001 c0024d7b be867db6 -> 3e7a69f0 3e726ff5 bdb75f63 3f351620 40409d9c 00000000", + "be88ec80 420c1962 3ec8af00 be861079 bde0456a be1632d1 3ea3932c 3fc239e9 00000000 400d8373 3e0432d2 3f991eee bd913160 3f68171a c062ae48 c0afd3b1 bffb2ee0 c083598e beabe7c0 3e3b5a40 bf9bdca1 4005ffa8 3fa4b50c 00000008 3f61d4f4 3f233fc2 -> 41f33b8e bfdc94c3 41c355b7 4010cf06 4639f5e8 3995dc80", + "4129e73c c1ad1da0 415cce44 3fbb7e0d c002cb36 bf011a6c 40261ee6 3fc78bf9 3ca3d70a 3f8ecdaa 3c2cd424 400f1f50 beaf7b79 3f948245 4056e80c 40e84dbc 40c976d8 40c8b314 40f55f1e 40e6195a c02ce8f0 3f63298c 3f450278 00000002 bf2b6348 bf5b7862 -> c1f8b3dc 3c1ee000 c19cf842 3e7f6708 49525756 44968ea8", + "400a1980 40ea07b8 c1b6b0b2 3f5b4a22 3f8fdbbf 3eeee1df 3fbe7ba4 3cb2239a 00000000 be1d9830 3cb9de6b 3f71505f be5b7c8c bec7105c c0014dd6 bf02a908 bf2f0468 40d3b6e6 40e04242 bfbd4f8c 3eea79ec 40337a94 bdf2d2fd 00000001 3f6f833d bf50c55c -> c11151f1 3fe21b98 40827c8d 3f2c3bc0 44a0fd75 00000000", + "c15b6f18 41e26ff4 41ab9414 be145621 3fe7e38b 3e8852b9 400991d1 3f201ba4 3f000000 3b6bb177 3d6a187f 3f8073b4 be3a6760 be5f0286 409189e2 3eba0ea0 40d0195a 40ef9e0e 40a5904c 405f77bc 3f9e61fe 3fdaf7e8 bfd3b779 00000008 3f91f116 bf6fb75d -> 3b807220 ba766b40 bbf99788 3e7b4134 4075c939 3f32918b", + "c1adb9c9 c21289da 411dcccc 3ea145ea bde8200c 3f025fbe 3f22dee1 3fab526d 3ca3d70a be7f2aa9 413db298 bfb7f07e 3e82bffd 3f986383 3ff30d58 c04e4548 c0a00eba 3fb2fe38 40d92de4 405e54ec 4005a51e be52ba5c 3f73cb97 00000004 3f1b749c 3e3500a6 -> 3c7f31ae 3ec8dc60 bd9bb537 3f897837 3fb49a23 00000000", + "c226432e c1a10579 4233da2c 3fb94e5e bf0b9ab8 bf848c8b 3fee4911 3f8aee2c 00000000 be0369f8 3fd20aea be8eb572 bfc4e70c 3f887d18 4097c562 409ee11e 408a4142 c00e078e bf0779a0 bfa45868 be21f2e6 3fb38383 be5a0e16 00000007 bf20b94f be6d7098 -> be557f7e 3ddf56be be73ad34 3f06e4f2 3e58ed59 00000000", + "c21b0e3a 40eeb868 c22234b9 3f898ea3 beb23ee6 3eb9adf1 3fa3c2b6 3f6df166 3dcccccd bb1991c8 4237f111 3e796cf8 bf272cc0 bfb3aa81 3fdbcac8 c07c2dec c0329026 40ca666e c08b7432 c0ec05c1 3fac4cbb 403f316b 3f2e024e 00000002 3fc99b20 3f59c664 -> 3b67d0da 3bc18ca6 bb157065 3fdcec88 39f18920 00000000", + "c2101754 c1b1e708 4110481c 3e7e996c bed26ca4 3e6658fa 3f233a66 3f893922 3dcccccd c11717c2 3e69ef93 3f540592 3fdafd61 3fec1e47 40b00fe8 c01745da c0a07c64 c0266eb0 401515a0 4009fd58 bee5e486 bfeb51fd c03be3f0 00000005 401e3d63 bf306021 -> c2904658 c20d4d78 430a1fac bd20ed88 4c158cee 46f4ed53", + "41fcfee0 c00829f0 c1ff32b0 3d6f0eaa becea094 bf99647a 3fa6ee98 3ff522aa 3ca3d70a bad30873 3c55fea2 bf51964c 3ebf2bb3 3e7dd637 c0441c16 4053ab0c c072d636 40d63858 c0bd3518 c0e93d31 c0326598 3f02a9b9 bfd93854 00000003 bf6d8d58 3de3d2a0 -> be379d91 3dacc438 3d6a8e83 3f57a028 3eac69d9 00000000", + "c1d428ee c1916639 42348622 be788dbf 3f45ea9c bf8b7392 3fafc0f9 3f43df42 3ca3d70a 3c53bc46 418ebf7f 3d5f52ba 3e057e61 bf04b9a7 bf2870c8 3f89d528 c0a24555 c0dbe576 c0e79dbc c090680e bfdc9d56 3fff3d01 c014328f 00000002 c017dd10 bf300b53 -> 3d1727ca bd39bf77 bc13d8ca 3e606dd4 3ca32e4f 00000000", + "c1c7a10e 41ed517c c185786a be86d413 3eda143a be3446fd 3fb5bbfe 3fe394ad 3f000000 bea17efe 3c807ef7 3f2308b5 bfac73a9 bf82ffb9 403f9ddc c0a7934e 40c38c14 c00717f8 40a92e96 40496af4 3fc93577 3fb12b11 3f08b0c6 00000007 3fcc62b4 bf533a73 -> c2d31870 435e53a0 43295374 be28ef82 4a61a9e0 48c7c6d4", + "c1ad06c5 40a3c080 4116ec74 3f068a05 c0089a73 3f30ab98 401921a1 3f631d05 3dcccccd bcad8ce5 404675b4 bffaf472 bc97d3f4 bfd3b4ee 40ad7de2 3faa19e0 befb05d0 40fb739c 408788aa 40843dc0 3e0fcecf 3ecc8f71 4013b732 00000002 3fdd608a be9afba8 -> 3cedc305 bda30e69 bcaf190a 3e5331a4 3cf496fc 00000000", + "c210359e 421c52b8 41f81bec bf10f75c be96efb6 3fa805d3 3fbbaae5 3ea571c0 3ca3d70a be81c379 4285dfe6 3fa5e027 3f594ea7 3f453c80 c0766db2 c0efa670 3f9959e0 40c005b2 c0b48dda 402e8cac 3f91990e 4031343c bf2dc575 00000005 4006cb90 be83f21a -> 3f1eb020 bf1fecb9 beacfa4b 3f1c6785 419d721c 3c333302", + "c15de874 c199303b 422bd976 bd93b918 bf6603f2 be38986d 3f836709 3f896696 3dcccccd bf1654a7 40c000ea 4022693d bf54962c bf5fe40d c0d40e5d c08e7010 c0750b22 c0ac5ef8 3f544c40 4096f8ae 4020536e bed5e297 c03bf118 00000004 bff2bf97 bf181908 -> bf6b2a5c bcf18a40 bff4c320 3f111f14 43a762bf 4026bb58", + "3efcf200 422ce730 421fe8e2 c03a2f33 3f35f611 3dc0a27c 4041fd02 3fdf446c 3ca3d70a 3c9d0a35 4097c439 3ebe601c be982a51 bf0cc9b4 c069a160 40a5fa0a c093ede8 409d774e c0ea3c2a 3fec3ed8 bf0d90ea 401914ce beee0810 00000008 3f7192f6 3f13bc28 -> bcb8c777 bcba9d32 bcb16782 3f948adc 3cc39039 00000000", + "c1b20093 c23fbcef c231f54d bf4c237e 3e10cdb5 3fc83f0e 3fe3727f 3e1cd38c 3dcccccd be42f56b 3d44bd3b beb30d97 bfa65144 3de000e8 c060c236 4015fb84 40b7a360 bf560da8 3fdc26f0 40e58ba0 bf02040c 4017ddc4 bfa1cf8f 00000005 c03438f3 bee61b65 -> c0e56e26 c1e7e643 4023a1f4 3f3c4e08 483f1a9a 443ae949", + "40d62540 c11c9ef4 422fbe16 3d85a235 bfad46d1 bf44f358 3fd750ac 3f9e5c1a 3dcccccd bbd77675 3c401297 bf9548cc 3ce63fd5 bfd3e4c4 40e13fde c083136e 40b66864 c02a4cc6 c0f58842 40d11aca 3fd62a63 3f83587b bfe05808 00000003 3f91a990 be94bdaf -> bf76ccdb 3a89abbd bfacb60c 3ef79553 41d48b26 00000000", + "c1ec8af3 c17888d0 bfc49420 3f44ea9c 3f8424cb 3f9dd470 3fe949fa 3fff6f37 3ca3d70a bb367dbd 3e306652 3f0a1704 bf6bb839 bf5a63e4 c0d5d09c 40b198f6 3f154dc0 bfe7edf8 40862d96 c0d94fe4 3e89aa85 c026079e 4002ab7c 00000001 4004105e beaf6439 -> b97329fb 3c939ff7 3c4f1fd7 3edf634c 3c0a2dd6 00000000", + "c1288558 c21aba46 c113a77c 3f5f759f 3f41bac3 3fe9aeea 400ffa85 3e36ae34 3f000000 be4f98ab 40f9bf11 3f80e8d9 3f13e8d6 bf11e049 bfb4ae58 40e367ea 408c1168 40856198 3f8cd798 c09a5452 3f1465f5 bdd79f97 c023fd38 00000005 c03313c4 bf9911dc -> c1834f1c 423b74a1 420cc609 3f199b66 4699b3fc 46bad0d8", + "c2194ee2 41b1860c be22e900 3e8edef1 bedab753 bfbda154 3fc88fe6 3f4213a0 00000000 3d9aeb74 404336e2 bfa7b8d6 400aa9d6 3ef26a85 c0c7f2c7 40fbcab4 3fba7620 40e07c76 40a5385a 40821f96 bf73a45e c0207068 4003d555 00000008 402e637f bdd5f2b7 -> bd4f2416 bcd69a55 be07eb64 3f13e7bf 3f13668f 00000000", + "c1da52a9 c204814c 41235b18 bf9486c2 bf86815e 3f4a2bb8 3fe298c1 3f58667a 3ca3d70a 3e131c3e 3f02af52 3fbc728c bfe217ff 3fa50efb c02c131a 406bf330 c0ca1b71 bfb08998 40e3a990 c07ceb76 3f92bd69 bf81faeb bf6c4b0a 00000001 c02fe12f 3e43a01a -> 3e9e6cfd be914f8f 3e8f9077 3f37c117 4024cb97 00000000", + "c1d28c78 c23404ee c22d59c5 bc8befd3 3fc95e1b 3fe4e908 404aa697 3fc8d64c 3f000000 3e573197 4268798e bf76cff6 c028cfee becc156e c0a26c7a c085525c c0e04b0c 40d50ee6 3dc4b880 403e43f8 400ddcb9 3fb17879 3f9d3047 00000005 3f9fbd9c bf7fd59d -> bfd34bae 3f2d1f98 beedbe69 3fa9fef3 420faeac 4252696c", + "c20db53a 4183c854 c20dcc80 3fca4c16 bcfc2ca9 3fb244eb 401a4de8 3f1bc58d 3f000000 c09848f8 419aa961 be6a2666 becb49c2 bed486a0 402b2580 c0802890 40c4f14a c0c488ef bf37f798 3f82b4d8 bf4d08e6 c02ea82e c02d9689 00000007 3f8e1464 bf3f081e -> c106728f c2040e96 4213f8c7 3f587768 45d9463f 45c4b932", + "42322dfa c2275029 414344e4 bff6ca86 bf957c1c 3fd1c46b 403b2bdf 3fb02961 3dcccccd 3f1aba86 3fb2a0c6 3e0e4f4d be63dfdc bf065f2c 3f864f30 c008b7a2 bd2caf80 c005dcd8 40e52e62 404418e4 3f40075a 40240fcb 3fe18acd 00000004 bfd74e6f 3f15fae0 -> c02cb6a6 bf338d28 40023f57 3ffcedae 434a746a 418b172a", + "41f5926c c1db949d 4218b210 3f0a0e27 bf6a03e4 3f62007c 3fe6962f 3f578660 3f000000 3d861259 3f2bd5d5 3e31ed58 3db4fcb1 bef134f3 c0842c3c c09158f0 409ab06c 4088cb84 bd217180 c0bccacb c037df72 c0374729 c0044d84 00000003 40229960 beaa1959 -> bb8e1048 3c9d7720 be7a9fdf 3f01cc80 40832344 40021e8c", + "c195ebad c0c43870 40741270 bdd0abd2 bf43244f 3decab29 3f4ef040 3fc4453e 3ca3d70a 3ec1c89c 422083ea 3ff3fb44 bf752d2f 40015547 3fad1de8 c0878106 bf0c12b0 40c6f410 400c3160 c0301ee2 c039b669 4022390d 4032662f 00000001 c014b9d2 bf5fd889 -> 3e6d1730 be84e20f beb2489f 3f6be030 406dfe86 00000000", + "c04e93a0 41e85d04 40ff7048 bffb9d57 bf8090c6 be949c1d 4016aa45 3e82b8c1 3f000000 bf368399 415c429e 3e3983bb c00f6248 be1e7e20 409a2422 400fd89c 3db22f00 40f10470 c0b878d7 c009bb24 402bb0c9 bf5f2557 bf9d5044 00000008 3f306bd6 3de3ec75 -> bdc79810 bd5dc635 bee705fe 3f7d3a50 41eee2af 4212b9b7", + "c1816d5a c23e9afc c22d8f92 be2401e0 3f9bbd37 beae8507 3fa3077c 3f7e8ae2 00000000 3d6a8368 4001e0b6 3ffcfb29 bfabb179 3d7c9ae3 40f7dcfe 404ef864 c09c000f 3f720fe0 40e45ac6 3f2d7360 3e94c3ed bfd30121 c037b69e 00000002 c03fd270 3ed8a1bc -> 3d4b5bde bcb662bd be0ce6c4 3f855bba 3eb64c9a 00000000", + "41e341cc 423875ee c236e2f2 3f5148fa c007c3ee 3eac3d7c 40141e06 3f4ef035 3ca3d70a 3c3423eb 3d5ccdd3 3deaa56f bf2d1ddc bec5c390 3f4b09a0 40c135b8 c0e32f7c c0834b20 c0887b58 409f7e04 40363816 bf358a7a bff9face 00000004 3fed529c 3f56042b -> 3ec8f4fc bfe3fc08 bf6b587e 40093477 428f664e 3d5ea319", + "c1a82414 41d9989c 415c7128 3fa49cd8 3ff979bb bef14008 4018bca7 3e5fab27 3ca3d70a bbf0e3cc 40133824 bf1940d7 3c403486 bd3ddd68 c0cc0a1c 40d33b6c c0c72010 c08cbd26 c0e1e443 3f32e340 3fcbc861 3f489190 3efca998 00000004 bfd2ebe0 bf418e7f -> 3c6e126c bd19e9ea 3c6b0d00 3d82bd74 3cae9cc1 00000000", + "405d5e00 42448d66 419434e4 3eb63650 bf2c29d4 3f2c299c 3f880940 3ef2320e 3dcccccd bba650fb 40a4b240 bf4a628a 3e817dc1 bf53a97d c096138c 409e88dc 3f4f1430 c0bc8a37 bf93a48c c0eeaa8a bfaeb077 3f123123 3f21ba7e 00000004 40208933 bef1d82a -> bbc8007a 3c8c01a6 3bba9809 bd985561 3cf4a0a2 00000000", + "41a33b10 41c2171c c1d7ad11 bf85d3d9 bf7cc078 bf485706 401f03fc 3fd8f20f 3f000000 bcf5771c 42720f57 be99d4cc bed356d0 be8b6925 c0cce9e9 beb35ea0 c0af92e5 40507ef0 be89d240 40d0f872 bf6332d6 3e4d8e9a 3f59bb19 00000002 40063cbe bf2c7d42 -> 3e0b7aac bd5c9d80 bd833aca 3d9b0f38 3e39a484 3d335012", + "421884f6 c243bedd 41ba50c8 3f5a56c0 bd116843 bf36a695 3f93a184 3ed11283 3dcccccd 3cf2d74a 3eedbda3 bfcc1333 be9becda 3fc75b0a 3f210f90 40eb3bcc c0d0f1de bff44fc8 c002b782 bfc4a960 bfa862f9 bf700761 3ff6e6bb 00000006 403e5d9e be96e194 -> 3e38b126 3ddc22cc bec9d8cb 3f4a689f 422f2810 3f19ddb2", + "41265a20 415df998 42188a8c c022d417 3f885d2e bf7ff981 403bc4ca 3f8a45af 00000000 be1d84c8 42666978 3f10cd54 3f6a8dcb 3f436504 bfe6e4a0 c02be40c 3fdb8100 40dced5e bfd5a978 40ed04d6 401cba05 401d7c1e be5076b2 00000002 bfd1067b bf88cd17 -> bdb17a3c 3ebd1028 bec0ab12 bc27e6c0 401554ad 00000000", + "c1a22c2e 41d675ac bf2ad580 3f89ee40 3e4f3fc4 ba8b1369 3f8f4a61 3f93671d 3ca3d70a bc9d1dc8 3d1f8fbc bf232a94 bfd15b16 3fa0245b 400ea160 4011e914 c0c7c979 3fd03240 c0cc4a90 40fadfd4 4038b4cf 3e196092 3f2fe774 00000004 40278a96 bf9363c6 -> bfecdb4c c0d0eeac 406dfd9a bc188e00 45ebb776 3f8ae834", + "c1f9ad60 c21791c0 40dc15f0 3f6306e1 bec5153c 3fb68a32 3fe071d4 3fc3b536 3ca3d70a be1610ed 41fde6fe 3f985603 3e21406f bec1f6f5 c0858bd8 40e3ef4a 40ad57a6 c08b31d4 c05b3828 3e4e6440 3e4f8209 4036d2da bfb6e2a4 00000002 3f2aafe4 be06ce86 -> bce63ec3 be05f470 be10c223 3f0ca5af 3f0f4a87 00000000", + "4159f3b4 c11554f4 421f5096 bfa1b748 bf964c99 bf78c154 4007d335 3fb67f81 3dcccccd 3bbc300a 426c6244 bf7997af 40269fb7 bed7a8ff c0175ad6 bf99837c c033440a 40827f54 bf9059c0 c0388bd4 400d7042 3f3f11fe 3f247e2a 00000006 bf73a615 3dce4932 -> bc17fbb0 bb501816 3a7ff013 3f85afd0 3a4fe0b2 00000000", + "c20cb7b5 40989250 4028d370 3e22ab45 bf84f571 3eba64f5 3f912591 3f8bf190 3ca3d70a 3ec0fa14 4188cbbb bfbaf502 bf67035b 3f442b43 c088b2e0 409cee68 3e93de60 bf3ccd78 40594040 40a5cf96 c01aa39f 3e6002d9 c035a12e 00000007 c02882ad be040f6a -> be2af283 be897b31 bf1fe82a 3ee4da6a 4203c2ff 3c23fcba", + "41cc6488 423fc49a c02ea340 3fd0638b be4cde75 be8f5a69 3fd98d17 3fe3e5df 3ca3d70a bdd2f002 42bcdc8c be6d007f bee6e80e 3d82f38b 40539724 c075e974 bff33074 3ea5b2a0 c06821b8 3eeb9d80 c02421b7 3fcf6cd2 c02aaa85 00000005 c0185e68 bf8b90c7 -> c0b0eca8 3fbe19b9 c11a4249 3f8ebf06 447b5469 3e114223", + "c1e6ab06 c2476b3e c0d1eb08 be5f69a1 bd57dd96 bdff7178 3ee9bc88 3fffa9f6 3dcccccd 3f73ec46 419c6314 3e55056b 3e36e4c1 3fe6bd3f bfe3fe14 bf72ebf0 406c94f4 40d69a06 3fcdd4c8 40bdeb40 400faa72 3ff70fc1 40193188 00000002 bfb647b5 3f948371 -> 3ff280e8 4008953c be7f76fe 3fdbc7cc 431497bc 3e25c6c3", + "41f982d0 41ca5848 41930524 bf09a164 bf25bc99 be8c8f1c 3f801b5f 3f940ad0 3dcccccd 400011a9 3e6e741f 3e965c98 3f398321 bf21e66d 406eafd4 3fd45428 c0bc7b18 c0ade870 c083e87a c0a6a1f2 3fbcdac9 4039373d c00c3749 00000008 3fcd711d 3f208551 -> c11aeefb c171bf61 41454168 3fdf3fcb 4893dafd 44b9298d", + "4213e936 c14921d4 423de094 bf162f1f 3eaf7c7c be99155c 3f3e0896 3f2be470 00000000 3c0afd6c 407a3138 bf9ff9d5 bffc0fca 3fe7bcb1 40cb8fa6 bfc0dac8 3dc65400 c0da22af c03268a2 bfad2390 bfd7f2e5 bf8d3a2e bec6acb5 00000001 3fe8b5ac 3e1df387 -> 3c6a0106 bb75969c 3bd629c7 3e4514ec 3b2aa01b 00000000", + "c16a350c c2322a93 417218b8 3c73a53b 3a6b69b8 3eb2ed0f 3ee604d6 3f7ea6db 3dcccccd bed06a39 3e448f52 becd8520 3f03945a 3f10df87 c0ca0539 bf95986c c0bad06c 4090ccc0 c0914424 3ffb2948 3f0c45e6 3fa29f93 3fdd5c21 00000007 401a5cfe bf347413 -> c0f30bc6 c1753bdb c23e40e8 3f2bf6f7 49c00f71 44cbb6b2", + "c2408499 423eb648 c1e4d635 3f5d7fd1 bf1344c2 bef74a5c 3f95889e 3f8f9eb2 3ca3d70a 3e493f54 413aa7a7 3eec6917 3e871a79 bece3dfd bfe07040 407550b0 3ea68540 c033007a bd8e4fc0 3ea04860 3f67ec7d 3eba04b5 c032ee80 00000006 3fae4c5a 3f5189e9 -> bf8e28a5 3ed7b853 bf660a78 40528333 41c3cf4a 00000000", + "418304dc 41e9d4b0 c2183157 3dea1c4c 3ee6cf13 3f03cbdd 3fb93faa 3fc0e385 3f000000 3bdefbf1 40395934 bf970daf bed22f92 beb31613 c0537200 c02a3d66 c0d1e0da c0a5a773 c0a4950c c0b0a7d7 bfc88552 c00d7901 bef650da 00000004 3f127ac2 bf4f4abc -> 3c487a0e bb08787f bca8ae6d 3eec0df8 3b0221d6 00000000", + "c208f3d6 41ef0588 41f700c8 bfaf68f7 3fd49e23 3efa01f8 40284dae 3f57e52c 3f000000 40c1549f 40252a24 beb2fb43 bfd18905 3fce5410 c063a24c 409dd52a 40e493e2 3fe88120 40810026 4092eff6 c006a9ba 3fd050e6 bfe4330a 00000004 bfb57eb2 bf8abcae -> 418b4778 41e7dcf0 41a65edd bf03adb1 4710fee4 474c1028", + "4209eb72 c1e02fa8 4191f07c c0205386 3d8613f0 bebfbf3f 40222980 3f82a55c 00000000 3ee726b9 3f110a3b be563342 bf0416ae bf216b49 bf88c854 c01d42a6 40fe7d0c 40b82620 c0874a1d c099ffa3 bf618d6e c02e441b bfd7712e 00000004 bfbbb600 3f8d6f6a -> 406e099b 3e356044 c089766c 4033f9dd 43b8c901 00000000", + "c1f1cd3b 4243a39c 41f2b388 c0044e62 3f4281f9 bf53a49a 40202d4a 3fc04a0a 3dcccccd c05212b6 4265eef6 bee86415 be888687 bdd6b352 bf3c3c80 40d88332 c08c20e0 be530720 bfab1818 bfc82754 3f72b0f0 c03f43a2 bf0fc33a 00000003 401f7743 3f48f6f0 -> 40944c42 c1ab12f0 420c0b1e 3fd28b9f 468ac177 444a1d5c", + "4213d654 c23643c1 41559bac 3f13fac6 bf9dfc51 402a0789 403f191f 3fb80e40 00000000 40751f00 3f0f4644 3ed3401a bee4e6b6 beaf08f7 c014db00 c09e3cbe 40c41a2a 40f6aa6e 408fbade c08ea5d4 40092deb 4033f9d3 c02b70aa 00000007 3f8cdaca 3f50d3f2 -> 4168e436 c102ed21 4153ee07 404bbf32 46047958 3b71bfc9", + "421b1c4a 4197b814 41244164 3f8cad88 3d92be01 bf36671e 3ff0e6f6 3f91fe99 3f000000 bfc772a5 413f3ba9 bf4bb6bc bf528b8c 3fa9c2a3 400d8da8 407b54f8 40f5c6ba c0aec372 c07049ba c09b3b87 bfcf4efa c02c4bca 3f8d4bd7 00000004 4005b4e0 3f6e8e24 -> c03b1484 c18740bd c153fbe4 402fadf8 463ba213 458858c9", + "42151534 42109578 c195be17 c002920b 3f44a94f bf5ca5c3 4017731e 3f8e102a 3ca3d70a 3ec581a2 3ea4b07b beeba22c c02b0593 3f8f0a80 c0df69f5 c095c94a beef4ff0 c0abf6f6 402d4a3c 40849186 bfa4894b 401cee21 3f1ba1aa 00000006 bf0d66ea beda3763 -> 3e4e3d9b c0281e09 3f22e524 3eeca174 42b1600f 00000000", + "41c8a888 417405f0 c1bec1f1 3f682542 3e965542 3dd9bfcb 3f758657 3f8dfd43 00000000 3b945321 3dc4671d 3e8f9abb bfd9f96b bfd3eea5 3f8784d8 406ec0b4 40c67aba c0dee19a 3fc6ef18 c036839a be6c7e8e 3e8dbb81 4024e023 00000008 bf6f1645 be3a5599 -> 3c612472 bd0d973c bc4e541e 3f2fecff 3d74703f 00000000", + "421d00ca 423b3ac2 420aef90 3f04c9f1 3f43b6d4 3f6ec805 3fabeec7 3fc27ff0 3ca3d70a c08e3152 401eb95c 3ce02357 bfbdb781 bf46be1c bf720148 bf8f7e00 3f17eed0 bda03b40 c0fa1950 bf714c80 c02c6c3c 3fa5dcb5 3f1a6bf4 00000002 3f062979 3f4a405d -> 410cc2f0 bfb29506 c0a420aa 3f838236 44a72135 00000000", + "42100c56 420277a0 40c806d8 3f96809c 3fb17fb7 bf03ce96 401a1f29 3f84c044 3f000000 3b00708d 3ddfae58 bf9665e2 bfaed904 bff5374a 409a631a c027ac04 c0dafae8 c030456c c063d114 40cbe896 3eb1f629 3fec8b22 bfb736c5 00000005 400b3a32 3f265888 -> bd91e7a6 bda7cd4a bde73073 3fced169 4016b51a 3fdbe910", + "41b870e4 4237f190 c209a0a6 3fa7a5dc 3f0c085d 3f971685 40011478 3fdaa457 3dcccccd c0447376 41399842 befcf508 3e801af8 bdb2c713 40025100 40e06f96 40bd4642 c0e8816b 4075d67c 4036fc94 3fd71798 4029eb70 3f8bb113 00000002 4022d4f5 bf20409c -> bfa4990a c0143ae6 c078ab3c 3ec50704 45258aa4 42a54e9f", + "4104ef28 c0db4188 4178e2d8 3e3ed993 bf608a18 3f15b4f7 3f9f24a3 3fdd26e8 3dcccccd 3eaa1e5a 3eacfd7b 3f5d938c bfe1b6fc 3ff0df2e c0bdb876 405a8d24 c0df0772 beeadb50 c0d4d918 c0bb2262 3fb2f9a1 beba0c5e 40327670 00000007 bf5b106f bf5ef043 -> be5e2086 be842633 3faf8891 3f056d66 430c6c70 3f91b869", + "41f00938 c1ad30d7 c21f61d7 3f69d1fd 3f783f44 3fa7e67c 3fefabc7 3e13d626 3ca3d70a c01a4000 41af5f39 3fbce4b8 40042f81 be4b809d c05a1140 c05f4028 c03240f0 c0f69156 c0e0102f c08a77ea 4008ae8a 3fb64d0f 402413c6 00000003 bfb389d0 3f801d81 -> 4015fce6 c0313cd4 c020be30 3ff7db23 450a5049 3fd0e246", + "40430660 4210255c 4124efe8 3fe5e82a bf233806 befcb121 40004482 3fe25abb 3ca3d70a bc8528cd 3fb38406 3f6d42d8 beb1bc5e 3f340563 3fc50680 40a36ece 40f6119e c0c5b813 bfb28dbc c0f4c7fa be88bb78 bf7323b2 c00131db 00000002 3f83227d bf787cbf -> bcf2aa7e 3c853da8 3c654ab6 3e87672c 3c285208 00000000", + "40845238 41bf0310 c1819460 bf81c686 3ff5442d bf1ea54b 40104caa 3f857fa0 00000000 3c1e5fca 3dabda47 3f368439 3f4fd836 3fceedf3 c0ca0aae bf557d40 409f9cfe c07f4d64 40a3647a 40e7436c bf21533f 3edce022 3ee755e5 00000008 c03599eb 3e8a29b2 -> bd8ee903 be05902d be54064e 3fa466f8 431fd509 00000000", + "c15dddc8 423b2112 c2098551 bff4134b bfffee42 bf80ff18 403c3964 3fa93416 00000000 3cd71ff4 42066b23 3dc97936 bf84670e be25fe4c 401910dc c0860ade 4078d9b0 bfc5a358 c0c8f2cd c0e2790f bf4b9a04 bf114f44 4029ee91 00000007 c006056d bf6f760a -> bfea207e bed82401 3f9b629f 3f342d4f 42df3d4a 00000000", + "c20cf166 4230d7d4 c05be2b0 bfea4e16 3f29801e bfcf0ecc 4038b542 3f35cc40 3f000000 bfbe5ea6 4188d738 beba2873 bef6561f bd4a4b0b c06ad304 c0f0fd0a 3e766e80 c0deffd6 40fa2042 c0897d34 4032df97 3ef552f0 4028fa0e 00000007 bfe5017d bf824735 -> 42012e15 c1b4e9d2 c2e1ffe0 3ebd1dfd 484f4e35 48b60f82", + "c05f4b80 c228765d 4200ec32 3d1926a5 bf5cc310 bee3bae1 3f7e2c0c 3f8bc45d 3ca3d70a bebfb26a 422dedac bb57fb84 bf179e79 beeb6c87 409c2dbe 40326d6c c02b4418 c0cb2ef3 c0cbaa4f bf13cb10 c02f0a9e c038fde8 40028c58 00000006 bfbde930 be554c11 -> be34c3ec 3ee37ad8 bf0f69ad 3f20e76b 4102ec28 00000000", + "41b509c8 422f82d6 bfd9c3c0 3fc83436 c0106a7f bf8e1bf3 403e8433 3e1c5647 3dcccccd bc07f681 3ca04f08 3f36d446 bf8759d8 bf82e73c c0691a60 c02e02aa 40e071f0 40ea33a4 c0b06364 4027e1ac be94e57f be24ff2a 3ef1abc0 00000001 3f4ab252 3f74f0f2 -> 3e884bfb bec286da bec7be35 3f5d524d 41201984 3f6c3158", + "c09c3aa0 c11a4968 41a4c5f4 3fe82933 bf952f70 bf3bda0a 4011c10c 3f966a6f 00000000 3b4e10d8 3d1ebe66 bf8ec049 3fe9e252 3fab4e77 40fd6fc6 3fb465d0 40b8a29a 408d3a86 3e6b9100 3e7b1f40 4018e85d 3f5b7f19 bebae0a6 00000001 3f66eaaa bf834931 -> 3da836da be01d65b bdb5ff3f 3f098c44 3e46dcb1 00000000", + "41ddaa04 4183e8c8 c1d012ca 3d7601bb 3f96891d 40058b76 401a723d 3f5c8d91 3ca3d70a 3cbe52db 40c2fc3a bf1eeb24 bfd4437f 3e941278 c0d5311a 40d8f84c bf22fac8 40dc3fba c0d3f707 40ca2f1a c01f2ae3 3fc758dc 3fc3c260 00000003 3fa1de7a 3e5b67c0 -> 3d130eec bbfe40cb 3b33a6ec 3f85bbb4 3c9c7e19 00000000", + "bf332780 42335d28 c1f0b69a bed9427d bfe1fc01 3ede081d 4002be98 3eb442b0 3f000000 be7ace3d 3e0201bc 3daf7b97 bf8c980c be2b8c63 40212a48 c0e60fb3 40e0eebe c0a61065 c0dc5f7a 408bb54c 3f52b7db 3f2c0167 3dd43f83 00000001 bf9c68f4 bec25c5b -> 3f2828b3 c063b65f bf497c34 3f28edaa 434f157d 433476c0", + "c20818a4 c1c20596 c14557bc 3f2774b5 3f150cd9 bf319ae2 3f909984 3f1f8819 3ca3d70a be0bdfe1 4158e341 3f99825c bfe2ce69 3e0a51c1 3ec38500 40f93dcc c04f8454 bf2ae330 40bfcef6 40a84d28 3ffc4c42 c00ecca4 3f828059 00000002 3f284ef5 bea22f6d -> bd38dd8f bd849180 bcd63ebe 3f1550c8 3efd091b 00000000", + "40e4e7a0 3de85a00 c16f228c 3e9d2769 bfa67276 3eee593c 3fd76c7e 3f0946eb 3f000000 3b7e603b 4195c3c6 3ea330f3 bf8bcb23 3e0c5585 40e99652 c0076416 406db390 bf652068 40e42f12 bfd9d25c bffaa28f c03cffe0 3ff9485a 00000002 3fb19775 be861b4b -> bb9e52f2 bb1fdb27 bc0d9678 3f1d341e 39f2d25c 00000000", + "c12fd5a4 c23dc797 c1d74b84 3db68ec3 3cbc0525 4028e855 402965de 3e9b12f0 3ca3d70a bc47ea8c 402c4f38 bf9890cd bfb66cc2 bfddba01 be6bd160 c0a01cc0 407d1920 c06e6378 c0b6da56 40e5edd2 bfc5ee15 c012f14e bec72e02 00000005 bf9138c0 3f36190a -> bd36f7b6 3d94f02b bbbb8fed 3ff80a47 3c730758 00000000", + "c21ddcbc c1e6aaf7 4098efa8 be97b15f bf3a05de 3efabbeb 3f6f3c3e 3dc2d971 3dcccccd bf0f9332 3cb32aa9 bfecdc9d 3f050341 3fc51fe2 c061a7dc be62aaa0 bec53120 c0c967f0 3f13b620 4085919c bf4042d9 40191edb 3e50d584 00000007 3fab4191 3f9667c0 -> c45582bd 4389a300 442c59ec 40f682a6 4ab755a6 46a63c7e", + "c0c47f30 c223a06f 421b7a10 3c2d6bc5 3ffbfd9a bfd567ff 405b402c 3fd89187 3f000000 bb0822f2 40f38feb 3f0a24e8 3d8e8e61 be91630e 409ae534 c011e10c c06277ec 408a2708 40a01eae c080373d 4037009e bec93741 c037ce04 00000007 3f194c50 be0a4fca -> 3ac60383 bba723f6 3ac43e0f 3f35c31f 38edc599 00000000", + "413f6a34 c120926c 41dcef90 3f23170d 3f069923 c0293e46 406a6eec 3fe480c2 3f000000 baca1979 40bb4442 bf6c30fd bf36810f bee0483c c0ced2a7 c0facd1d c0c4255b c0da80c2 c049a476 4081288e c0280bb6 c0194d4d 3e505c72 00000007 c0378306 bf8d77e8 -> 3ea31cb7 bf946d8a bef6020f 400f9850 42a75bd5 435ce415", + "c20a41cf 4205b35c c23b455c 3e9eae4a 3f972942 bf4df0d1 3fbdd3a6 3e55e2a0 3dcccccd 3d5e433a 3cac6360 3edcb820 3edd7c83 bef48cfd c0361ab8 40e5d3a2 bfedddd4 40ebcc7a be9de2d0 40ce56d6 be06c646 403bc73b c03efbbc 00000004 c01cb000 3ee75b27 -> bfbd0898 bfd4dfaa 3fea6822 3fc21a1b 4632b421 43645608", + "415b0504 c1de0a10 4087ce80 3f1709c5 4026c2b5 3f26763d 4031eee0 3fc3f2a7 3ca3d70a bc0e100b 3ec325e5 bf3283a0 bf26fddd 3f892c67 c0a20084 4096a308 40ddfcc6 4017df28 c0b81396 c0cb05d7 3f247207 c0336b22 3fbfa767 00000008 3f76bb68 3f82966d -> 3d0e5fbe bbfb8c9e bdffffde 40ca42ac 3db51604 00000000", + "c1ac6922 41e4f3f8 c122175c 3e516572 3f78e05a bf2b4fe8 3f9951d8 3e5fe343 00000000 3df982ea 402a64b4 3f73f622 bf4f3807 3dee7f30 c056b30c 40d3af5e c0d3547e c0cf4ebf 408e2e38 409f4d06 3fad72a4 3ff0ff42 3f33b0b1 00000006 3f126d28 bf805d91 -> beacba9b be8a490d 3d413b69 be554f1c 3f2f7c31 00000000", + "c227cdee c22b288e c22ffc98 3e57b3f7 be0c7768 bf8c2390 3fa8800e 3ec5b832 3f000000 bee0f9b0 3f625952 bf9b73e0 bf32fd3b 3f6f6e3c 3f861d00 40fdf8f0 407b45ec 40cea606 c0a35256 bf7ae098 3f500c6d bf8b3e35 bf871af1 00000008 c01168a4 3f1b5d8a -> 41392480 401584db 410b56ef 3fec19c9 476981bf 46a1105c", + "c22e3468 c1c04c8d c014f7d0 3e3f1313 be239c61 3e4f3dca 3edef6d8 3f96012c 3dcccccd 3fbcde70 40d7cf38 3ef39094 bf71843c 40040b09 40336014 3fb4ba10 bfc915b0 3db2a480 409b1cd8 40fde7c4 be0577a3 c0244f38 3f039ec2 00000007 c000022d bf81e8fc -> c22c5549 c31c8114 c2bbb167 3f5c9958 492fcc95 43f8c68b", + "c2447810 4235772e c2173d03 be3cef78 3fa2ba2b 3c8e4325 3fa802e5 3fb235e1 3ca3d70a c073b0a2 40478c4a 4018cb3a bfc0b161 3f0aba7e c0321c46 c03cb964 c003ee88 40441abc c0af0b9c 40900ec4 3fd6d952 3f932572 3f81d04a 00000008 4006b040 3ea2f733 -> 3fd099b9 412f7b82 3fb0d05f 3faa0bfe 46e08848 417f0220", + "c21cdce3 c240f969 41c8fa7c bf92c581 3e685eeb 3bc5fea6 3f998197 3fc24eb2 3ca3d70a bb07b0c0 4258d170 3e83898f 3f239a4c bed59347 c0cb4615 3f46d0b0 40fde91c c097785c 3ff4de30 40dc575e c017d84f bf9f0930 bfdd2fb5 00000001 401ee780 beea04cb -> 3876ba0a 3b249fe4 3b883197 3f31790b 38e34cdd 00000000", + "4214903c 4219c4fe c232eaa1 3ef824f3 3f04b49b be8857ee 3f90e649 3f3f83fc 3f000000 be42cc13 3e4b1e83 bfdfcf25 be5dafec bf0abf0a c0a1cea2 c04b2510 405c75e8 bf9f2aa0 c0d7eb07 c0c2de22 4013fff5 3ece6993 c018ef9a 00000005 bf4ae5ce bf62bc63 -> 3eefc2a4 be9b5204 bf20162e 3f00631e 42da693a 418c5907", + "41a75624 42280c8e 420a1a26 bf850577 3ee2f51b bd3b9ee2 3fa59516 3ea6c614 3f000000 3adf7ccf 3c76aa1a bfc5c2ef bfddbdea bfe9dc41 40daa5e4 40e3d2ac 40b46bc0 c0dd8598 3fc37868 3fbd6f48 40322650 3fe823be 3e962ea2 00000007 bfe0ba46 be7a40d2 -> 3ca3f8d1 3cf4085e 3ccc9335 3e4ceccb 401fab4d 3e24e374", + "40b951a8 41aac368 422c6000 40129fd4 3d12247f bfcdc10f 4034b830 3f9eeff4 3ca3d70a bce1f9d7 408ca779 3e665833 3e7a4d30 bfbce18e c0a0199a c0c42b0d bffbe200 402cf560 3ff24160 c0e0857d beaca4e4 401bc9a2 3ede87be 00000003 3fc2d6f3 be0f5e3c -> 3b9c6e4a 3d9e7c8a 3981f5e9 3f36764e 3cbd6544 00000000", + "becc4780 40889f28 c206854c bfe71bcd 3e9f9f09 bfb0a158 4013b43f 3f34a9b7 3ca3d70a bb6ac332 3d6b33a3 c030d9c8 3f5e14b4 bf19c4c6 40c6f0c6 c0ea426e 401fee44 40b9a2ca c0af6f1f bfd626bc 40337375 402dd93c 3fe53c10 00000005 3fd9f9b4 bf2de2eb -> 3f0559b2 be47a596 3ddffa08 3f36254f 40936aea 3bc094d8", + "422f0bb8 416c4240 419d471c 3f546161 3d7b094c 40017f58 400e2586 3fd53572 3ca3d70a bad5f983 3ecdd2a3 bf748d09 3fc14b04 3ede0a9e 4040fc30 409d271c 3fd2f460 c09b96e8 c01e9a6e c083bdf4 c03fabc7 3e384c5e 3fed644f 00000003 403452f2 bf65b849 -> bd1f21f0 3c6e65e6 3cd35c15 3f1b3baa 3cf105ad 00000000", + "41ac55dc c191dbdb c1ccb4ce bf63dce9 3f287ec2 bfef5f4c 40143e7f 3fb73551 3dcccccd 3e331722 41637057 bec9000a bf1c0806 bf37dea6 4099bb30 bf0e3be8 408c5b76 bf69c470 404dc2dc 3fe6e950 c02ca89b 4024d173 bff261a4 00000008 c03bdad2 befb4cb8 -> c00f0df8 bf46397e 3fd738a3 3ea946e2 4752893d 450a57ec", + "41c7b1b0 c08f3428 41f7339c be855c02 3c28b277 3fa7483d 3fb97674 3f94daae 3dcccccd c10368a3 3f4ceac8 bc8275b5 bf2414ba be3b4de1 c0215c1a 40e4cd76 40cbdd02 40edf1d6 40ff34fe c050454a bfc3e950 c0210242 c013c3c0 00000007 bf891858 3f685a3d -> 4247b4b8 c244b350 c20d7350 40762b20 473562a1 443b162d", + "c2439162 c228e543 c20df8df 3f8b8281 3fd487bf 3ed54bc9 400ae9de 3fb4aa5b 3dcccccd be68ba18 3e41bb60 bf0a0ec0 3f7ec08c 3cdd03ee 40f6bae6 c0f60db9 3fc866b0 bec0e900 3fef9920 3fe43a70 3fd2ce3a 403e2afe 401e90f8 00000001 4002562d bee308aa -> 3db703fa 3eb4ab4c beb80019 3f79bc4f 42a097a0 3ffa1a79", + "421320a4 c2241b1c 42373c68 bf2075cd 3ea44dad 40019258 40092f77 3f867b89 00000000 bb953739 3ebe5e1b 3fe46f17 be1d7578 3fa1ebe7 403c55fc c05ea4d8 3fcac480 3e8ff640 bc26c200 408a1c24 3f06622b bf99d0af bee9a878 00000002 3fde7643 3f829ffe -> bc39690a bce067f0 bc46b270 3f9a5679 3ca2a31e 00000000", + "41080400 41d637ec c12ccea4 bee4709f be25423a be8ffc3f 3f20cc4f 3f4407b8 3dcccccd bfc211c8 3d67b858 beb3e659 3fa9dd1d c00db66c c007ae2e c08af21e c0c3e66c 40fa9d1a c0d52a08 c0d79d37 c0298ece c007e8b9 3f0f5819 00000005 4006a3b3 3ea668be -> bfe2fa2a c1a19813 41feaf68 3f8ab30d 48e5975c 44235e69", + "41ea520c 423554bc 4229cb46 bf4b0a7b bf4f6913 c02e6470 4044290f 3f917c13 3dcccccd bbb0d1e0 3d1a61da beaeeb1b 4030aabc 3f391028 3ee13e60 408408c0 40ccac4a c0c44b0e c0a9b29d 3fcd8ae0 c03ad682 c035d735 bf0592c2 00000003 40102bd4 bf9626a7 -> bdbea3ef 3f867c78 3e4d8dc6 3f4435c6 424052e5 3da4ba21", + "410221b8 c10a060c 42390e2e 3f11f4ca bfdd6b31 bd882795 3fe94a5f 3fe0a818 00000000 3d25f222 3c3378ad 3edab8f6 be8df112 3df6e85a bfd7d834 3f823148 3e9cf860 3ff68138 c0cc7e21 c0c85af9 3fd2ad43 3fc5d382 c01e232e 00000005 bf7954c2 bf8a0d73 -> c14a78b9 4103845f c057fe12 3fb442ca 44c6d540 00000000", + "419649fc c241a51e bfb62c00 3ffc3400 bfb21c98 3f3fa4e0 4023f7a8 3eba1013 3dcccccd 3c727512 40208866 c01eb644 bf09b7a5 be84d9ce 3fa67058 bffc8fd0 c0d0b0c6 3f942e18 4072f008 c0ab6ff4 be711294 400e3230 4010c475 00000001 bf1fe14e 3f3414b6 -> bc66e773 3d038a61 3d2eafe0 3f109296 3b9ac6da 00000000", + "c1b2edd6 c1b99b2b 41f0154c 3db93da1 3f8a4853 bfc74a2c 3ff6a073 3e975e66 3dcccccd bbe12db3 408770e8 3f82236a bf94b110 4005000f c0862cf3 3f4cefb0 40ee55e0 4056c1a8 3e9c8a60 40634758 c017e33f 401bb399 bf2a59d1 00000006 3fcc2ff1 bf71f954 -> 3d3faba0 be0e32c3 bdb84b04 3eb769c3 3e274b4a 00000000", + "c1a45ef8 c219ad6b c14dd6f8 be8e40a4 bf7f46bd bf86deae 3fc1ea90 3ff27ddc 3ca3d70a bfbcd6f8 3e9b1dd7 bef6e0c6 3f870a9a bef8a6d7 403dbeb4 bf9ee7b0 bf76f7f0 409cec54 c0fbfcaa c0f01dcd 3feb7f80 4025a4ee c0133ee6 00000007 c0031539 3f14e4e7 -> c1d09605 42132dc6 c0a6f3de 3ff8a2b6 48e5de65 431c197c", + "41ac900c 40efc048 c23f71f8 3f020faf 3e8af1a7 3f021472 3f73d7a3 3fec10c0 3dcccccd bd87feb0 40121016 bfb5aaed bfce7b72 3da9f597 3fff3ef8 c0457a84 3f5f5b30 4060984c 3fcd8f18 40ffde76 be39a45f bfcab762 c0265233 00000008 3f0ad820 3ee403ea -> bd86c735 3db694fa be04a3c2 3fa54d87 3e0fac30 00000000", + "41b2480c 41e7c140 40930ac8 3f9739f8 3db9cdcd bedb4306 3fa55f7f 3fccfebf 3ca3d70a 3fecdbd7 41bf960a beb5d4ca beda53c7 3ebdc061 4081c802 c08c6792 3fbcde80 c0ce2bd6 3f94c750 40d3e0ce 3f675068 bf949da7 40197aff 00000005 bfbb34c7 be3aa7ce -> bf42a57e 4083b8fe 407b8299 3ed8a6be 439abdb1 3ca80fb6", + "c1db83dd 4061bdf0 41dff8f4 bf24f048 3ea0ea7c beb8f821 3f53cec4 3e7c2c40 3dcccccd 3bce21e1 41f1601a c018a39a 3f6c577f 3f319f0e 4080949e c01808ee 4058a120 c08df236 c0b3c680 c0681634 3e962d1e 3e480314 be132509 00000001 403d25ac 3ea8147c -> 3ba1555e 3c37d8c3 3b3016db 3e67cfac 3aaca449 00000000", + "c1d94843 beed3d00 4106e780 bfb468a2 bf6ce940 40035ec4 405db8b8 3fceda33 3f000000 3aa13dca 3d3e0809 bf0f9485 402031a3 bea7fdc6 3f9fbed0 408bd5c2 c076bb0e c060e40e 40e8ea9c bf1266e8 3ebf0759 40070519 c02b29b3 00000008 3f4cabc4 3e607af2 -> bb3c3d20 3c80e377 bbc34a63 3f93c09e 3d0cc930 00000000", + "c22dd7a5 41f46eb0 41140788 bea13bdd c0148929 3f87873a 40256f85 3e154f86 3dcccccd bdb250a0 3f302598 3f65f673 3f125287 bf8d727a 400e3268 40d15c7a 406c154c bf5bde00 40e76fd4 4032928c bfc2c2df bfe36239 be03ce1d 00000001 c0288828 3e5c8ed0 -> 3da46b6d 3e5637e6 be99d29c 3f55104e 3edd2481 00000000", + "c1ac2e5e 42032226 c184335e bf8cd11e bfaf3d4b 3f4ec37c 3ffa54ea 3f90a2f0 3ca3d70a 3ca3e626 3c343112 be92bac3 bf4fdbea 403746f6 beda2e70 bf0d6c30 408cb95a c060ffae 40bc18e2 c0950fea 3fda67ed bfda0de7 bf98a046 00000001 bf55c9f7 be1a9a94 -> bd22f3be be19afeb 3ee7451e 3f6fedb2 433823bb 3db09d16", + "41a81e18 4215ced2 c15e044c 3f275b3f bebd379d bf8b46d0 3fac0080 3f8ac1ab 3ca3d70a bfeeb6a7 40094ee5 3eb6b6fa 3e2ae8d6 3fbdcb51 c0292786 40c0a83c c03b710e c0441efa c08f43b9 c09ea83b becc82ce 3f168d1c 402d87c5 00000001 bf80d21e bf16eada -> 40432b85 3f69be54 bf4eabca 3f7ba26b 4302dc39 3e17bd9c", + "c18074ea 41d60cd8 c115cf98 3e5037ad 3db79ea7 3e0a7d7f 3f9260fd 3fe22915 3f000000 3fbc534b 40862ffc bf89538c 3f9c325e 3f018d4a bf3ed4e8 bf777a30 c03a0970 406e9454 bff51dd0 c02954ee c002717f 4032267d bf39c3c9 00000008 3faaf710 3e40486e -> 4037f41e 3fd0fe22 4052229a 3f76086c 43351852 41a80a1e", + "bffc46e0 3f467080 bfa0e1a0 3d9a5b52 be9270ab 3ff52774 3ff80f6e 3cf9bc60 00000000 4054ea47 41a5090d 3f08aab7 be7c9a1b be73a7d3 3d49e100 40ca0b46 409d1e14 3f0411d0 c0bb73b4 c06dcaca 3f911b80 4010555a bfbe8a51 00000007 bf778ff0 be1b635d -> 4041b15f bf9d6ab8 41048ac0 3e66f4a8 43c1a39d 00000000", + "c20ae504 beeccb80 4244a5ee bd8d3ac0 bee26391 be0299ae 3eee3fca 3dc74147 00000000 3b90f8d4 3c70428b bf9cf86c 3fa6ca69 3fe3bcfd 3f87d8a0 be474400 c0c2dac7 40ce524e 402a3444 c0deb7b6 3ea55deb bf126f52 c03502b3 00000007 3f77e5ba 3f831a89 -> befd0292 3f067cac 3f308159 40b41102 413be292 00000000", + "40f148d8 3fa7b500 40f36488 3f97e39b 3dc14a0e bfb45c45 4010477a 3f51cf1b 3f000000 bf0ca859 422b75a3 bfc59f20 bfb307b6 bf34daa7 40dc4ac4 40c83bc6 c0c71f41 409f3a3e c031099e 3fc08e50 be9779c0 bf890c2a 3fccf6f4 00000008 3fed5a52 3ed91803 -> be18834a bf8edfd2 4020ddf0 3fbf0103 44320a80 4435c9c6", + "41f73a54 c1cfac7a 4209339a 3ed8e695 3e913522 bc887e2d 3f029454 3f8c2d8b 00000000 3da20ab4 3e2e6b4b 3f7445df bfd7cb0c 3f17299d 40c29e12 40ad69c0 404e74dc 3f46c910 3f9dda00 40ee972e 4018c2e8 bf3b32d3 c0261f0a 00000003 4021d5c4 3efaaf25 -> bf2b0460 40111965 bf057306 3f71b0f9 429f1e06 00000000", + "c230ba00 c23f913f c23b614d bec94484 40152eb4 3d6f3f0e 401755cb 3eb45334 00000000 3f6762f1 40c1555a 3f7e2408 c0028c96 3ef1284d 40a5c072 bf373618 40d06054 c044da0a c0d8af2e 40a72e60 3ffaaed2 bf91c44b 3f883d5d 00000008 bfd64a97 3edd4040 -> 3f168e85 3e6f6218 bf954401 3fa3fb7c 43dd8231 00000000", + "4001cee0 c22517e4 41305d84 bf030fd2 3d9b14b7 3da4c4c0 3f35e1ed 3fef0483 3dcccccd 3ab33176 42bddaff bf2f2f28 bf81dce1 3f31abc1 c08df16a 40d723a2 bfde057c 403be448 406fa384 40fff258 3fd9c322 bfcf8538 3ffd33c4 00000007 c02274ea 3ea77d3f -> 3b7b136a bb8c52aa bb22bb35 3f8822c7 3c1e32f6 00000000", + "420cf0f8 c22048be c2390d07 3e00e24c bd0c4d74 3ed7bc10 3eed030e 3f8bb3ed 3ca3d70a 3f841c85 3fdd8aae bfd5db97 bfcbea89 3f7b098b 40eebae8 c007f0d8 bf813054 c0bf3dcb 40b68838 40910a26 40397c29 bf661dd7 403d800c 00000002 bf791000 bf323e3e -> 40078a08 be148fc8 3ed38f14 3edacbb6 4239099a 00000000", + "40746710 c1fb3287 bf5ce3c0 bfbc2d12 bf5b1eb5 bf138c28 3fe71c67 3ef13e39 3ca3d70a bf6739cd 411da0c0 3f08e726 3f38b569 c01a38f7 40a92822 c03232c4 beb49d60 4020df10 3f9590e8 c0e5b090 c00448ce 3e897f6e bfbfd22f 00000002 c018d2e6 bf8f96b5 -> 40fae6a5 bfd602c8 3fa4d747 bcaa4da0 43a4eb4f 3d8a2fca", + "c1f4273f bfeed700 c1b33f21 be858480 bfe697cf 3e87f297 3ff196c1 3ef511b2 3dcccccd c0f03c1f 3e5149d9 3ed9151a 4031776c be81e7fb 400f6cdc 407d5244 bfad3ffc 40313ec0 3fd4a270 bed1d170 bf94c083 be4e3a32 40326c3f 00000004 c0052040 3ef221cd -> bf6e0230 42242f20 41d51e46 3f8bab8b 4990cc6c 4720ee63", + "c1fdbb19 41d28878 42236faa 3f8bf76e bf125524 c005da28 401ef97e 3f0e915d 3dcccccd be8f9d15 40ac8504 bf8fe3b1 3e7e7e2a bf90a6b5 c00b23a0 3e040900 bddf8800 be2dc0a0 c0eaa6c0 40f74b56 3f996baa 3ece0c6f 403edf91 00000002 40156d15 bec7590c -> bebc7521 bd4fe418 3e838c32 3f2997d9 4154305c 3f20e6e1", + "4233c5d0 c1fa4197 c246a55b 3fa7d7c8 bf95e510 3ed24919 3fe77140 3e0c3942 3ca3d70a bfa36b9e 4253025f 3f00b7b4 c025eeff 3e885fe4 4008572c 3fcbbd28 c0729fdc 4038fd48 c091dc28 3f670670 bf4cd0f2 3fa43abf bf0cb1e3 00000001 bf378ccc 3f19e0ae -> 3f5d9067 3e101a13 bfc94457 3f751eb8 42799f1d 00000000", + "40467c20 c1158f5c bdbf8200 bf82d1e1 3f2b89b1 3f0f5481 3fbb831c 3f9a7b45 3dcccccd 3f06abd8 3d316d3a 3f431ea2 3e938cdb bff9a866 3fc84ae0 bcfde100 40aa10c8 40b3a468 40ab1682 40ef5ba0 3ff6c5d0 3fd1d5ad 3fe3913d 00000001 c0102718 bf95b213 -> c1853667 c0c40233 42315bd7 3f32d792 460039d8 42cf6c9f", + "c22b099c 420535cc 421c43f4 3e824856 bf4695db bfb8c57f 3fd648a7 3f48f2fc 3ca3d70a bfde71c7 3e096abc be8fb255 bef95d2d be369972 bf7d5200 408712dc 401a6730 c05a3938 40fd5436 c011dfc2 bee78393 4029421c bfde6e7a 00000008 40053a37 be559d3c -> c01eb491 c00ba064 c014ba43 3f42c1b5 4752548d 42105219", + "3eacb900 c1656ac8 41df234c 3f90934f bfb9e5a2 3eaa3271 3ff82106 3e0d12d7 3f000000 3e731932 406e3dba bf89e6dd 400e1969 3f7af565 c090867a 40b0165c befc3710 3feff8d0 40c58432 3f916828 3fd2ad37 3fa88a40 3f69ee30 00000005 bfb6d3ec bf06e7c3 -> 3fa86cca 3f8db435 bf2940ce bbffac98 4191c038 411a1690", + "c17675f8 423de1ce c1ade794 3ee7205d bf35bc83 402b8642 4035d91e 3fcf2ae4 3ca3d70a 3ea8bc56 3df8ace5 bf3aff6d bf48dc05 3ffc4a10 3f13c300 c0bba0c0 c05d2534 bf2934a8 c0eb094a bfdf37ec 3fd73da3 bf837353 be4e2e5a 00000007 bf257d20 bf152aa3 -> 3facf8d8 3f95249a c04f20b8 3f432b62 4406f9f2 3f87f013", + "c1f60fb0 c0828048 c23d74c6 3e09e1bd 3e9a4f96 bf6f33c4 3f7e74d3 3e192b89 3ca3d70a c0da9fd4 3cbe2437 bdccd08e 4009e073 3fec0ead 40fd27bc c03347a4 40dc938e 401943e4 40a6dd4c 40f554e0 3ffc2259 bfcee7e5 bfc79531 00000002 bfae25e3 3ec1f232 -> c20daff4 43960a11 4382c584 3f9e106f 4afa47c7 449dfdd9", + "c21782da c1cfa990 41e9a100 bf7f1cdd 3f18ec94 c00bb281 401f1efb 3f2d3b48 3ca3d70a 3cd139a7 41088dd9 bf11bf1b c01700de 3fdf8f61 bf214370 407d2a0c c00c3be6 bfd555dc 407d2470 bf89bf9c c00ae3ac c0153a80 3fa1fcec 00000008 bf900ad0 3f591078 -> 3c0da248 bcd9fa20 bd02d9aa 4076220d 3e284629 00000000", + "41598db4 407d6c70 c22ab723 3f2f5771 3ffd68b5 3fc4da70 4033c154 3ed6e9cc 3f000000 bba34626 41b36d0b 3fccc02c befb159b bed8270a bea6a1b0 bfb28b10 c0fbc5f8 c09e4a30 3fc8d718 4082e77c 40027da8 bdf6d107 40127020 00000007 c015736e 3f90afdc -> 3f907e32 3d7ba320 407d65fa 40d73ae2 42b4d8f5 43286cc8", + "c1eda4fa c120827c c144c7f0 bd68b357 3f8ff218 bfe638fc 4012be40 3fdab1fb 3dcccccd c077a003 409dee32 bf51b051 3fdb117f 3f3cbcdc 40987bd6 c0b75db4 c066d11a 40e42180 40d38276 3fcc0ac8 3fc3347a c0222e3a 3ffdf249 00000006 3fc7d313 bf1715d3 -> 3f946570 c0339098 40fd040c 3e4695fc 46693784 44363433", + "41d7ac00 4224c4a0 c20ad69d 3f349a29 c0069414 bf89bfbd 401dc6f2 3ea86d39 00000000 bee63c74 404d300b beafbcb1 4035abce 3f2af444 c0a4954c c095fa72 c08f32f0 c0df5f7c c0cff5c8 40815338 bf13170d 3fb7dabf 3f98a87e 00000004 3f9c06f6 3f8ac415 -> 405dc1b2 3e8a037a 3f45ace0 4031fe46 425f75d6 00000000", + "41adc838 c1ac282e c1ee25cc 3ee586b2 3f3c86c3 bfdfa635 3ffb587c 3f4333f2 3ca3d70a 401e3cee 40a17629 3f375e56 3f9df7c3 3ed6f8c1 40f03cee 40f69590 408db662 40e14096 408a7234 40a41b64 3ed36d57 3f7d1d54 401a17bf 00000003 c037d2b0 3f807ad2 -> c1df82f0 413bb820 c0986653 3fe03fba 46cee0a6 4219b155", + "41b960e0 41d4221c 416b0170 3fafdd80 beb766c4 3ff36ff0 4019ccaa 3e98154b 3dcccccd 3d070e73 40379194 c0090a6d bd3219af bfee05b6 3f8f9ad0 3f11aef0 c095179e 3fa5be40 40258a58 c0d44136 c01ccd47 bed6c52e bfeba6c6 00000007 bfc7e41f 3f6a724d -> beb7136e 3ebd6c4c 3f10366c 408eb558 414bc86e 3e25cce1", + "42294be0 c134b8f0 bd151400 bf1064eb bed09957 40057fbf 400cbaee 3e8ab696 00000000 c0da59bc 3fdc1971 bf1e563d 3f1285ed be984f84 bf804950 40e67fe8 3fc95678 c07c3b2e 406aac8c 3fd87bd0 3e49cf20 3fae8939 3fa358a3 00000004 bfcc2f12 bf8a3f1f -> 400bbd40 c23d165c 421087d3 be1dcb38 471350a6 00000000", + "c1216d1c 41aeb718 c1526560 be95e283 3f461880 3f80e248 3fa9b84f 3f917d41 3ca3d70a be45c4c6 41108b3d bf4f9f11 bfb904db bf984429 c0e4461c 40a3edce 408357e8 40c27dae 40d3560e 404be218 c01bc57c 3e97cced 402a1c99 00000001 bf5073b4 bf90f350 -> 3db2d706 3e600727 be80b885 3f19376a 3f2915a2 00000000", + "c203f577 42364fcc c18c7174 bf31a700 3f66eeb6 3fc343ae 40026539 3fabb527 3dcccccd be0b0d7e 3ce1ca82 3f80790f 3e427746 bfd26ea5 c019114e c0cadc62 40c15c36 40eac0b6 40a6e55c 40cb9ae2 4005a81b 40388d92 403d7aaa 00000002 bf6c257e 3f0d0aa6 -> 408f9d3f 3f59a114 c0f6b52e 3f7a1baa 44f7f8da 4283a852", + "c0d639d0 3f958360 c1ac930c bebc9f73 bed4b607 3e148cd4 3f133d90 3d80657e 3ca3d70a 408444d0 3de262e5 c00ed154 bf3bca69 bf92ea94 40d55dc8 c0df3bc0 40bb20f8 c06b9162 c050a92e 401df2c0 3e43b721 3f62ede7 40351177 00000004 403a2827 3e98a699 -> 42244f14 4185494c 422bb1e4 3f8bb854 49f64fc0 432e4f5a", + "bfbf7aa0 c1b7f8f7 c1007e98 3e8ee766 bf63b00e be61d038 3f7c4a59 3e8d5bdf 3dcccccd bdbde0d0 408b1b5d 3f54d221 3f2e31ee bf8761e9 c02f8390 402b3a44 c0b25b03 c0a41f82 40b9c89c 3fb51840 3f349ef1 3fbca3eb 40257d24 00000006 bf14e574 bdddb3eb -> 3d04c3d8 bd482408 3d0dd9d5 3f2bf7d4 3e89e5d9 00000000", + "41c8f5ac c18813ca 42359962 be3c07bf be771792 3eb7c397 3ef090ba 3fe58e47 00000000 3e47e434 3fa33370 c00aadf7 beae0e57 bfdc87ba 40c908fc 40e47d4a c0b375fa 40f6bc68 4098ca7e c0bbe7ac c02f17ed 3f5d52ed c03e0c40 00000005 bf5f43e3 3f975806 -> be4bcc74 bf089912 bf85faa0 4073e0d0 40808138 00000000", + "424660b6 415a6198 421fc8b4 3f878036 3f12a37c c012ed0e 402cf115 3e617b2e 3f000000 bce50c74 3eeb9752 3e80a002 3ea547b4 beadc5ea c09654e0 409fa95a bf8fc4ec 407e35a4 409671fa c0d0667c 4023a237 3f6a5c44 c02789b8 00000001 3fd95a5e be725210 -> bda85bea 3d37cb87 3ceeed3c 3f3a93de 3e5260ec 00000000", + "41a1647c 41b63494 c2106df9 3f6804c5 bf4d99e6 bfd44635 40054ba3 3fbb6428 3ca3d70a c0513461 425cf399 3c56395a 3fd0e63c bfbaf301 c0fe7d97 3f13b8f0 40b8d8e4 c0e6a2f0 c0adda50 40f00afe 40040e75 c0324bbc bf2ad2cb 00000005 c0117b96 3f7dfd3a -> 42174b6f c33e57a9 c34dc06e 405e9e5a 48f91204 44591766", + "c21ce6e8 c1bca152 c1d508ae bfa161ee 3fd0b89b bfe33641 40346a96 3f7ceaa4 3dcccccd bd7e12d5 3ef3c2ad 400b6112 bead4de9 be075537 c068e484 402285d8 c0fe4b5a 40f9dac0 40ba9ec0 c0c19a76 c03441b8 3eee70a2 bfd8e614 00000007 c020a6a0 be5bf82e -> be42993d 3da93c9a 3e1faebb 3f420456 4199690a 3f6ae662", + "c2224066 c1af1196 419b89d8 bf784bda 3f25228e bf4742cd 3fb3526f 3f510742 00000000 3d8843d5 4005a01b bec472d9 bfbb5175 3f60f79f 40add0f4 3fec3f68 beb24c20 40af1e90 40cfdabe 402d4a70 3e9e6fd8 3fa85078 3f13e66a 00000007 c02965aa 3eb2a887 -> bd9bfb97 bd96161f bd82e061 3fa29121 42437d84 00000000", + "c1d676b8 c13450c0 c2261d0a bef31efd 3fc87eec 3ef0b11b 3fd9f9e3 3fce80fd 00000000 410a58d7 42871523 bc32e252 bf6ad891 bf8fe30c 409c5f42 bf8cd934 c06ac8c4 bf83891c 4037cab8 40e062f6 bf53c6d8 3fb0e675 bf47c704 00000003 bf8d7fd8 be2cecee -> 4079b264 4127fe0a c103f9a5 3ee02aca 44d78dec 00000000", + "c1755a34 41f6cd7c c20ee96b 3f25cd57 bf37e706 bd4de917 3f77f195 3fb52e6c 00000000 c0eb5e51 4021f711 3fbb3978 be9b6155 3fec1955 c0e36714 bf3307b8 c0564386 bf5e7740 40e06d90 c0a0c9f8 3fcb6603 3fc48c30 bf8da881 00000004 c03b7132 bed9b901 -> 42819343 c060c740 c2447eee 3ee4d0de 476de4ba 3c6c7f20", + "c1730440 415f9b7c c20f427b 3f985376 3ec1efab 3fdd63df 4014f45d 3ff862b8 3dcccccd bd4c8e33 4032861d bfdaef8a bfa75c39 bfea9eab 409681b6 c00e5e3c bf88fe00 4073d1b0 c0c5bb67 c098ef3a 400ee8ed 3fa4dc03 bf8d31b4 00000004 bfafe4e2 3f57c168 -> be4b0186 3d095dac 3da105db 400ea3ad 3f6c5a95 00000000", + "c146f5dc 3f8a6800 4184164c 3f6acaaa bfb0fe52 bff546b1 40244b6d 3fcee1ee 3ca3d70a bff011ca 4063b010 3fb474e2 3f9aa3f1 400aaf07 40f1c8a2 40323af4 407501bc bee50350 40db6f92 c0fdb14a 3fa09ba0 3fedac24 3fc8ecbc 00000006 402478e5 3f513bc7 -> 41ea80d4 c2581071 c10fcb00 402586ab 49865fb0 4510428b", + "c1d4a3c2 41e980e8 c203bcaf bd301c4b bd7ba3ba be5ed535 3e6bb111 3fd6df9f 00000000 3acd1104 3e1872f7 bf370aec bc570408 3fdd0b41 c023e018 bfd6ac74 bf110158 c0e544c0 c08ac5e4 3f6441a0 bfd7d4f0 bfd06e83 bf5107ca 00000004 3fa6187d bee02c39 -> bc7d3db4 3a437ef8 3d36c523 3edca53e 3c0eb56e 00000000", + "415e9658 40a013e0 3fc57180 3f2699ee bf7211e6 40069a7d 401958c1 3ece4e1b 00000000 bab33a04 3c639795 bf32ad7f 4013b460 3f91931f c0efb5fc 40ff278a 40de2e44 c09e1e9c c0da67df 40f2c4be 3f47d353 3ec75bc9 bf5d9a2c 00000004 bfe03272 be134208 -> 3d125aad bde91169 bd53cc84 3f3224de 3ec05419 00000000", + "c16e83d0 42042352 c122c750 bf95d12d bfeed5e9 3fd81749 40319c75 3f94518e 00000000 bb347d4e 3fcecbd3 bf836979 bf2cac70 3f87ad7b be164880 c0ea4e73 40dfec10 40e2891c 408646a0 bfeef8ac bf413682 3ff61851 bf5b9887 00000002 bf66d6dc 3eb2e62e -> 3b31deaa 3bb9909e 3bed17a5 3f1e6015 39918001 00000000", + "42269fc2 c1711870 c1f91a4d 3f9c026b 3f4e398c c01c803c 40364e39 3fd78c82 00000000 3fa31418 40c3cc5a 3f67b4d7 3f8b7f43 3f8faedb 407e4158 c0b4a6be 40e450ac c0a78f4e bfd9a92c 3e14d080 bf122cd0 403bc438 c02f910d 00000006 bf9087f6 3f1033d0 -> 403a37fc c02ff588 bd9022da 3fca2654 42fb2282 00000000", + "42242ee2 41bba57c c216fb90 bf0bc2b9 bf9d73b9 be8b34f1 3fb17c26 3f2e34b0 3ca3d70a c027ca51 41cf1d93 becbe7fc bf2b2fe1 3e35b269 405d1274 bf54a9d0 400cd6f0 c0cbf4ae 4007cc20 bf614268 c035d4ee 400fb104 401ca604 00000003 40310264 3eef716a -> bd642200 40708dae 4155d8a3 3fb3fdf5 45cfa8e8 4003618a", + "4234fbfc c2122ff2 c1beddcf bf846681 3fa0c25c 3f820b44 40027e10 3f9a8d3a 3dcccccd 3b3ce2bf 3c7525e2 bfac5028 3f987c0c 3fffcf05 c0eb7841 402c6edc bfec0648 3fbf2ce8 c0c8c9ea 3fa28838 be8f0921 befce5e2 3f7cc1fa 00000005 bf3d522c bec87b61 -> 3dc3bcdc bdbabd2f be12751d 3f0419d6 4024b9a0 00000000", + "4104c9f8 41a5b7fc 4143b1e0 3ccd6849 3e3b9ce8 bf132247 3f1ee2b6 3f582a4b 3ca3d70a bd1b6c92 3fe42ad2 3fd088bc bed718e4 bfea25ae 3fca08b8 40e55822 400d8458 c0aaa5dc c079e134 c0ed0298 bf8448e8 c00fde83 3e933a6c 00000005 c03dbdf8 3ecd25ba -> be227229 3d45882e bd31c577 3fa67293 41506870 00000000", + "c2415e3f 420a58f4 c15e6b38 3fbabbe5 bee0d915 3db67f44 3fc44699 3eba4314 3ca3d70a bc2e36cf 3c6737a0 3ffac37f 3faa6173 bfb48235 c02c0b1c 3ff38700 c0d3760e 40f3e2dc 3ed2dd80 c0bee1e9 3fe665af 402c1ed1 402d0934 00000003 402ecc9f 3f026d94 -> bffca0ee bfa8a864 3fb24e56 3fa6ee44 4411ca7b 3de29426", + "41311d5c 42476422 4175b460 3e06c431 3e912ee1 3e18eedf 3eb162cc 3f883ca7 00000000 be71d6b9 3ed724da bf0cdc75 3d8dab3d 3d75dd7d 404a2910 c0fba821 40b0fbe6 c0e93675 40b9daf0 c0d310d3 3ff00ac3 bf9ebf7b 4001bfa3 00000005 3f8eb6b0 3f958453 -> 40d5d4da bcbc7b40 c0148ef2 409cc645 42c9293e 00000000", + "41849800 c188d01c 41c72b04 bdd28ae4 beedfee5 bf837164 3f90e18e 3f3f55f0 00000000 3b0d2fc8 4236f87d bf39e059 3f2e8b81 3f74e94f 409bab14 4004ea5c c0dfa028 3f15b8f0 40bd0658 40f00d4c c03d5e5d 3fd6171b 3f14477a 00000008 4023bcd8 beb3a626 -> 3c1191c1 bb1feb74 3c0b933a 3f428fb6 3da36fdc 00000000", + "422b6520 41258304 c0839c80 bf7be9f9 bf5e6f5d be832d17 3fb0ec81 3db71d93 3f000000 bab888e5 3d2777da bffc2caa 3fcdc7ff bfbf07b1 bfad3168 406be260 c0b132a1 3f177070 3f3a7250 c03e6f00 402b13e6 403e9444 403f1752 00000005 3fc9b537 3e80067b -> ba77e11a 3bb8a89f bae78866 3f9248c4 3e00dfe5 3aeaa69b", + "41d93ec4 422da77c c2100d66 3f52fd44 3febeba3 3fa7c7f0 401c2757 3fd15b5a 3ca3d70a c08b04fe 3c9ab5f4 3f64b957 3f00166b 3e8499d1 c0318716 c0d2f8c4 c03076c4 40aaf1a8 bee4d250 c01196bc bf93badc bfc7a3c4 401329e9 00000003 4038f474 bf67d4d5 -> c56ce0f4 c5053dc2 c4891c8e 3f0ccf21 4d38aa8b 48af23ac", + "c1929f8e 41790590 bd960400 401c4ee9 bef7d899 bf905fa8 40316b2a 3ec68420 3dcccccd 3e2351d4 3d8250a0 bf11a5ca be6aff5d 3e360e5b 3fb5bef8 406b31b0 bf6c5108 be5bcca0 3f9523a8 be3fd840 bfbe3742 3f6faee8 bf6227f9 00000004 3fd683e2 3f187ba5 -> 40594256 3f654700 bed6cbe8 3f9fc85b 455dd8d8 43679f29", + "40653cc0 41fa519c c2431b4d 3eec3129 3fc4bc98 bf3584e5 3fe13e56 3e87c466 3ca3d70a 4047c965 3d3a6a63 bf948db3 bf1fca65 bf84aa3d 40f42854 c0860ed1 3fb75a08 40ed70ea 40a2e562 3fa437d0 bfe25dd3 c0025cd0 3e6a13b4 00000007 bf12ee58 3f7ada76 -> c363268b c2e25afd c34d34a0 40995dd9 48bcadde 42ffb11b", + "42060ba2 c2181a78 41e2e484 3f9298b5 bfd5ea1c bf6b356e 4048300d 3fe741dc 3f000000 3b27c028 423a8cc6 bec5937d 3f22c857 3f807614 3fe5bac8 40d1014c 40e06ba6 c034bf4c 40a3aa92 40217fb8 40396fc4 3df08d77 3fb28b80 00000003 40099c68 bf32bfca -> bb131d2c 3b94b2a7 bb43e70f 3f9c4e3e 3b4883f5 3c04918b", + "41d5de38 c1ada306 42039892 bfdd6bd1 bfaa73dd bee8d71e 403bc376 3fb43217 3f000000 3a946acd 3f963133 be85f94a becba2e1 be643b88 c0aa4450 bfdf4a84 40c9c04e c0131930 bf47dfe0 3f835170 bf538f19 be8d9115 40279934 00000007 3f031cfa be896c82 -> 3a026a39 39981093 ba5dbea0 3e3ebb35 3836a673 00000000", + "4195aa60 41b06974 c21ceea8 3f58b126 bf4c74e6 bf2c6c1f 3fad0ed5 3d97abcd 3dcccccd be1939cb 41426600 3f2ab0e4 3f134efd bf4d326f 3f79a300 c07f9092 4011104c c0d31088 40b2fae8 c0513bd8 c03c3727 40211be6 bfcb09c4 00000005 3f7ce6ae bf1e3a1b -> 3d4b7df4 be7b82fa bdfd8add 3ed9abbe 40062b0e 3c839832", + "41afca98 c0f0ba90 c15c98ac 3ed180bb 3f8070f1 3fdea70b 4012bfa5 3ef96064 3f000000 bb40db89 3f0a9a78 3e08c4f6 3f662756 bf9ec6fa 40fe6132 c093367a 3fbd2668 40305d14 c0dd1a7e c0a512be 3efa9ec6 3e6f1752 bfc093cd 00000004 bf844966 3eada52d -> 3a2173c0 ba484745 bbc58f13 3f243750 3ae80b11 00000000", + "41d094bc c202b819 c201aa2a bf7db73a 3f9b96e6 3f7d3b07 3fed5833 3ff07d68 00000000 bfd4b274 3fa8aeee bf09f985 3f958589 bcbeb2bc bf1afd58 c0976a28 c0edfad8 40ed87b2 beef1f80 40fb0eac bff86753 be018fb7 bf38c8ab 00000007 bf9c384c bf4064ae -> 40a21cf9 c07ba750 c0dcf284 3d2b8aaa 441ce254 00000000", + "c1ae9742 411b1700 c20d8743 bfda1baa bf8340a1 3fd19fa0 404a5212 3f95c42b 3f000000 4035279f 4291afee bff4e053 3f60a32b bed1ffd7 c06cb2a4 40f5c9ec 3e754840 3ec956a0 c01b3976 40dc8798 3f58755c 3f2d2388 3ef062db 00000002 bfe74772 3ec94537 -> 408605d2 41007456 c03fc0f4 3f34cb03 447e7cb4 44fa7761", + "c216ee85 c127f310 c1bb144f 3eb77b33 bfb32866 3ea24baf 3fbd55d5 3eb3f30c 00000000 40eb80bf 413c5bcc 3da00a8c 3f3c7390 bf5c0da6 c09c9d27 c083e545 c0664168 c0ceeafa c0d07cc7 40282d48 beb600df c02401e0 40308e26 00000005 40189521 3f8e03e2 -> c410da48 41c1ab28 4245c80d 406bcc9f 4a74ac64 00000000", + "3df8a600 41598410 42434180 3e827a58 be43e9b8 be55a288 3ed2928c 3fc27f6f 3ca3d70a be245184 41cd0868 3f34c621 3d445a60 bfce4ebc be93ea70 bd493980 bfb703bc bbadf400 40aea938 40dd1e1e 3fc4f850 c0152028 bf022156 00000004 bfef5367 3f97db24 -> 3ecd1490 bffaec2f 3e0b1778 405fa670 4244231a 00000000", + "41bdbcf4 41622840 c1ee87b1 3fa4e545 3fcf23cd be2dcbe8 400c034d 3f8fc47b 3dcccccd 3e161f66 42619d37 3eb3b58e 3f5f2465 401dcfcd 40344a08 4088201a c0b31977 c0cd9128 c0b95c18 40d06f64 bfd5894c 402ece16 bf7b814f 00000001 3f14f412 bee5526e -> beb98b0a 3d0c16dd 3d0af233 3ebf0216 3f6e13dd 00000000", + "c0ae7b30 c21d3785 c215d66e bf0fa51c 3f8fb861 3f431a42 3fea596c 3f398b34 3f000000 3f114470 3d345583 bf566182 bd9aa56d bf5dcbdf c0b532e2 c042b046 40ae6634 c0522c8e c09bd0e2 c098fdba 3f5cb1a1 c009ff86 40068ad1 00000003 3f9a3b1e bf0dd5a1 -> 4022dc59 bf1f6e19 40620d1c 3f296649 46b1063a 451be93a", + "415b0230 4210e88c 423e04be bd46ba60 3ca434f5 bf353c05 3f3e2716 3fd28434 3ca3d70a bc15ff6f 40747f9a bf010946 3f12bbf8 be6344b0 3f94efe0 40f56e6e bfbf48b8 40a32f82 40f13d52 c03a1644 c00ea8e5 bfa67b49 40133be3 00000001 403798f4 bf3f30f0 -> bc8654aa bc81c89b bc97d886 3f25dee3 3a89058f 00000000", + "4232537e c2118822 40c5caa0 bf8e31ff bfd0dd19 3f06d5c6 4002e188 3dc7c676 3ca3d70a be9d9aac 41171417 bfb892ab 40147a0d be061071 c0f2a4e8 c09788e7 3d808680 c096ec7d 40d0184a 3fabfe40 3f48c058 401a3a79 3eb9126d 00000007 40143588 3f41e7e3 -> 410c3cfe 409a2cba c0ccd5fa 400d21fe 46f550ea 4249a858", + "407f85f0 c1430d68 c146ae30 bf5a17da 3f809e1d beaf8ee2 3faf0aaf 3ea02383 3ca3d70a c0a71a2a 4269582d 3f429f25 bf6e1ac3 bf394a53 3e859220 3f139b00 4083f2a6 bf125398 c0b081b6 c082cbf4 bf13ad7e 3f0a4877 beae2505 00000001 3fb20b39 3f4f7f91 -> c00fa6f3 c0ef51fa 40e1db5d 3f1d8b80 44bda93e 3ebc96a6", + "c21935c3 c2468d13 c2177725 3ee5a1ca 3f464808 3f35170b 3fa3ce23 3e8e405a 3f000000 3c040ee5 419a36df 3f9c325c 3e962edd 3fc9c396 c094bc2c 405512d4 c0fcda26 c0b686c6 40cdcc9c c0b0fe28 be945c82 403cd347 bf99b9f9 00000003 3fb49022 3edec033 -> baa5bcfa 3c2a21bd 3a09f296 3f2cdd9e 3bb96438 00000000", + "c214748d c1dc96cf 420d9cd4 3f094d59 402de007 bd1b4e2c 40340513 3eddc881 3dcccccd 402650d2 3f8ffe67 becf807a bee210ec bd54483c 3fd82798 40778220 3fd19578 c0d2dbfc 3f8da4f8 3f7c65d0 c02ccefd 3e8c0c23 3ff7d0ee 00000005 401e66e2 bf5e4016 -> c2c2db8f 40d3d520 c1c7bd53 3f9c6fe2 48b4b6f6 46d05d5e", + "41f4bbc8 41545bc0 41b08a4c 3f1dd455 bfb98915 3f9eab0d 4012ce84 3f142fba 3f000000 bacab197 3ee544ae 3f0118c2 3f78e0c4 3fd9a379 c0a907da c0137ff8 3fe9d9d0 4049e2d4 c02e415c 40a1faa0 c011e17a bffc1409 3fc915dc 00000004 40252540 bf163326 -> 3c81b888 3cc9ca80 3d01ac32 3e87f86e 3d43603a 3cabb410", + "c1c6c2f9 423ffc58 422dc620 3fad3ff7 3c6cc92a bf0fa80f 3fbb8f01 3faf0e7a 00000000 bede19bb 3fadeba7 c0046d06 3f82d312 bf8e137f 40b3eab0 407d9c04 40e2f1b0 c054887a 406016a0 c09eb08a bfd8e72f 403b9891 be95fc09 00000005 3fec2f24 3f91d46e -> 4088b79c c0a2d686 40bb3c64 409215f7 44db3283 00000000", + "c10ba524 c22f602c c160b040 bea8bd09 3ddeb0d7 be8018a2 3edb0b8a 3fbc02a8 00000000 3e37dd5b 40d74659 3f986756 3fb2228f bd9fbced c0b87494 40509e2c c08a1e22 3f8dc790 c0595346 c0f2d506 3f46e0b2 bfac97dc bf1603eb 00000004 40396822 3f4b22e1 -> 40043b50 bfec2a47 c09b9a66 3ffb9912 439bc3b3 00000000", + "41bb4080 c1987c4b c0e9b150 3fc933c7 3de43987 bf971892 40040060 3f6f971e 3dcccccd bc635862 3d298882 bf67aaf7 400e205b becf2411 c0e63b1d bf9aa8d4 c08a05a1 3f726780 40ad77de 402f57d0 3fea7d3f 3fa8c63d 3fa3413b 00000006 3f9e1aa2 be93c726 -> 3c5b8c21 bd92a1b7 3c1df783 3f173ea7 41317dc2 3abf439e", + "41a12408 40734380 3fa48300 bccd64bd 3e781408 3e9e81a1 3ec9ae0b 3ec2bc4e 00000000 bea0258f 426dad90 c02d4d77 3f70e878 bf60d080 40983a14 4093c36a c080e544 4049929c bff50968 c0d777e7 3f94ca0e 401ff7c0 400a45af 00000005 40390b96 be13e8e9 -> 3e1bfc00 3ece6bd7 bd8e17d5 3f56a3ca 41842974 00000000", + "412b6f40 c1953156 3f41ba80 bfb9da99 3e6951a1 401cc5b6 404235a9 3fe3a835 3dcccccd 3e86da80 3d4e1eb5 4027ba2f bf053e7a 3f9d34e4 c06afaf6 3fd57cd0 c07e8ec0 40a21dd4 3e0bed00 405a1a80 bf0d7fc5 3e770e98 bfec1039 00000002 400bd496 bed78fd7 -> c0666ba0 3f77e0c6 c023292a 3ed58d64 458f5e1a 42496d16" + ] } ] } From 4250dd28d57096fce4e5ea0203d6938ad0c20687 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 03:02:13 +0200 Subject: [PATCH 23/24] cultmath: compile-hlsl.ps1 fails on warnings and on a missing shader without its path; compile-glsl.ps1 passes struct arguments dxc runs with -WX; a ShaderPath that is not a file throws naming the parameter and entry index, never the path; the GLSL wrapper builds a struct parameter from its declaration's field zeros. design.md and README state the struct-argument twin rule, the span twins and the missing shader identity. No C# change; the compiles run on Starfire after the last commit. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/README.md | 9 +++++---- packages/cultmath/docs/design.md | 25 +++++++++++++++++++----- packages/cultmath/tools/compile-glsl.ps1 | 17 ++++++++++++---- packages/cultmath/tools/compile-hlsl.ps1 | 7 +++++-- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md index 0111949c..8648263b 100644 --- a/packages/cultmath/README.md +++ b/packages/cultmath/README.md @@ -82,9 +82,10 @@ shader semantics. the analytic Hessian of `snoise(float3)` on the same lattice, hash and tie order as `snoise_grad` (a symmetric `float3x3`; GLSL `mat3`), and `integral_clamp_linear(a, b, lo, hi, t0, t1)`, the closed-form integral over `[t0, t1]` of `clamp(a + b t, - lo, hi)`; and, in C# for now (shaders follow), the octave stages `span_fbm(path, stage, - octaves, lacunarity, gain)` and `span_ridged`, over a `SpanPath` and a `SpanStage` (`SpanStage.Identity` - is the plain fbm), which return a `SpanNoise` (`field` as `float4(gradient, value)`, + lo, hi)`; and the octave stages `span_fbm(path, stage, + octaves, lacunarity, gain)` and `span_ridged` (shader twins `cultmath_span_fbm` and `cultmath_span_ridged`, + whose callers build the `SpanStage` themselves), over a `SpanPath` and a `SpanStage` (`SpanStage.Identity` + is the plain fbm in C#), which return a `SpanNoise` (`field` as `float4(gradient, value)`, `curvature`, `fold`) for a stretched, offset, shifted octave sum over a span. Three measured bounds describe `snoise`: `SNOISE_LIPSCHITZ` (gradient length, `NoiseBoundTests.MeasureLipschitz`), `SNOISE_HESSIAN` (Hessian spectral norm, `MeasureHessian`) and `SNOISE_THIRD` (third-derivative operator norm, @@ -178,7 +179,7 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`: -OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.metal ``` -`compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC. +`compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC; a warning fails it too (`-WX`). DXC emits Vulkan SPIR-V with `-spirv`. `-TargetEnv vulkan1.2` is exposed by the script, but the current Windows DXC release compiled this shader successfully diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index d11e1518..3511d755 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -113,8 +113,9 @@ modifying a temporary). The HLSL side is checked too: `tools/get-dxc.ps1` fetches Microsoft's DirectXShaderCompiler (x64), and `tools/compile-hlsl.ps1` compiles both copies of -`CultMath.hlsl` with it (`dxc -T lib_6_3 -HV 2021`); the script throws when dxc cannot -launch or exits non-zero, so a stale exit code never reads as a pass. +`CultMath.hlsl` with it (`dxc -T lib_6_3 -HV 2021 -WX`, so a warning fails like an error and the +text must compile cleanly); the script throws when a shader file is missing, when dxc cannot +launch or when it exits non-zero, so a stale exit code never reads as a pass. The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from 0). It proves the text of `CultMath.hlsl` computes the same float32 results as @@ -124,13 +125,20 @@ compiled under `using static CultMath.math;`, so its intrinsics are C# `math` itself: the test proves the composition in the text, not the intrinsic rules, which only `HlslSemanticsTests` pins. +Every mirror function is compared, internals included: its counterpart is the `math` +function of the same name, public or private (`snoise_mod289` and `snoise_permute` are +private), and a function with no counterpart fails by name. A struct parameter maps to the +CultMath type of the same name, and only if both have the same instance fields by name, type +and order (`SpanPath`, `SpanStage`); the argument is built from the case's scalars field by +field and copied to its C# twin by field name. + The comparison also allows exactly one struct return shape: a mirror function whose HLSL return type is a plain struct of `float`/`floatN`/`int`/`intN` fields is compared to its C# counterpart field by field, recursing into each field's own components, down to bit-for-bit scalars, matching each leaf's declaration path (field names, not just position) and declared type as well -as its value. `CultCellular` (`math.cs`, `cellular`) and `CultPhasor` -(`math.Phacelle.cs`, `phacelle`) are its consumers. A struct return with a mismatched, reordered-by-name, or +as its value. `CultCellular` (`math.cs`, `cellular`), `CultPhasor` +(`math.Phacelle.cs`, `phacelle`) and `SpanNoise` (`math.Span.cs`, `span_fbm`) are its consumers. A struct return with a mismatched, reordered-by-name, or retyped field does not get a second comparison path or a shape of its own; it fails the same walk that already handles a bare vector return. @@ -809,7 +817,8 @@ configuration bound. A change to the kernels must re-run the measurements. ### Span stages -`span_fbm` and `span_ridged` (`math.Span.cs`, C# only so far) are the octave stages +`span_fbm` and `span_ridged` (`math.Span.cs`; `cultmath_span_fbm` and `cultmath_span_ridged` in HLSL over +the same struct types, `cultmath_span_stage` behind both, and GLSL derived) are the octave stages of a warp chain, `span_fbm(path, stage, octaves, lacunarity, gain)`, each returning one `SpanNoise { float4 field; float curvature; float fold; }`. The inputs are two structs with named public fields and no positional constructor, so the three floats @@ -831,6 +840,12 @@ Lab's stage is `1 - 2|n|` per octave: with `G` the sum of `gain^o` over the octaves (signed, so the alternating sum for a negative gain), its value is `2 value - G`, and its gradient, curvature and fold are twice `span_ridged`'s. +The shaders have no `SpanStage.Identity`: a shader caller builds every stage field itself (in GLSL, +`SpanStage(1.0, 1.0, vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0))` is the identity). A constant would +need a second transformation rule (C# has no const structs, GLSL wants a constructor) and a mirror +exemption, for one value; the mirror test proves instead that the shader text at a C#-built identity +returns `cultmath_fbm_grad` and `cultmath_ridged_grad`'s field bit for bit. + Bounds come from the three pins only. With `|M| = |frequency| max(1, 1/stretch)` and `nu = f_o |M| speed` the noise-space speed, the Hessian of the noise on the span has operator norm at most `Hl = min(SNOISE_HESSIAN, ||snoise_hessian(x_mid)||_F + e_h + diff --git a/packages/cultmath/tools/compile-glsl.ps1 b/packages/cultmath/tools/compile-glsl.ps1 index 45929cac..8ec2b82c 100644 --- a/packages/cultmath/tools/compile-glsl.ps1 +++ b/packages/cultmath/tools/compile-glsl.ps1 @@ -29,12 +29,21 @@ if ($Phacelle) { if ($Cycles) { $library += Get-Content -LiteralPath (Join-Path $repoRoot "shaders\CultMath.Cycles.glsl") -Raw } +# A parameter of a struct type the library declares is passed as that struct's constructor with each +# field's zero, read from its declaration (a field may be a struct itself); every other type is its zero. +$structs = @{} +foreach ($declaration in [regex]::Matches($library, '(?s)struct (\w+)\s*\{(.*?)\};')) { + $structs[$declaration.Groups[1].Value] = @([regex]::Matches($declaration.Groups[2].Value, '(?m)^\s*(\w+)\s+\w+\s*;') | ForEach-Object { $_.Groups[1].Value }) +} +function Get-Zero([string]$type) { + if ($structs.ContainsKey($type)) { return "$type($((@($structs[$type] | ForEach-Object { Get-Zero $_ })) -join ', '))" } + if ($type -eq 'int' -or $type -like 'ivec*') { return "$type(0)" } + if ($type -eq 'uint' -or $type -like 'uvec*') { return "$type(0u)" } + return "$type(0.0)" +} $calls = foreach ($match in [regex]::Matches($library, '(?m)^\w+\s+(cultmath_\w+)\s*\(([^)]*)\)')) { $arguments = foreach ($parameter in ($match.Groups[2].Value -split ',')) { - $type = ($parameter.Trim() -split '\s+')[0] - if ($type -eq 'int' -or $type -like 'ivec*') { "$type(0)" } - elseif ($type -eq 'uint' -or $type -like 'uvec*') { "$type(0u)" } - else { "$type(0.0)" } + Get-Zero (($parameter.Trim() -split '\s+')[0]) } " $($match.Groups[1].Value)($($arguments -join ', '));" } diff --git a/packages/cultmath/tools/compile-hlsl.ps1 b/packages/cultmath/tools/compile-hlsl.ps1 index 6742503c..6e19ec82 100644 --- a/packages/cultmath/tools/compile-hlsl.ps1 +++ b/packages/cultmath/tools/compile-hlsl.ps1 @@ -29,11 +29,14 @@ New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null $index = 0 foreach ($shader in $ShaderPath) { $index++ + if (-not (Test-Path -LiteralPath $shader -PathType Leaf)) { + throw "shader not found (parameter ShaderPath, entry $index)." + } $shaderFile = Get-Item -LiteralPath $shader $output = Join-Path $outputDirectory "$index-$($shaderFile.BaseName).dxil" - & $DxcPath -T lib_6_3 -HV 2021 -I $shaderFile.DirectoryName $shaderFile.FullName -Fo $output + & $DxcPath -T lib_6_3 -HV 2021 -WX -I $shaderFile.DirectoryName $shaderFile.FullName -Fo $output if ($LASTEXITCODE -ne 0) { throw "dxc failed with exit code $LASTEXITCODE (parameter ShaderPath, entry $index)." } - Write-Host "compiled shader $index of $(@($ShaderPath).Count) (lib_6_3, -HV 2021)" + Write-Host "compiled shader $index of $(@($ShaderPath).Count) (lib_6_3, -HV 2021, -WX)" } From 338a952be059fd7547e5ec541788143ec2bb7403 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 03:11:04 +0200 Subject: [PATCH 24/24] cultmath: near-origin fold test checks the exact departure to 1e-13 The additive 1e-9 absorbed the whole exact departure at scales 1e-4 and 1e-2 (1e-11 to 1e-9), so dropping the value allowance's absolute term 2^-14 survived the test. The tolerance is 1e-13. Test only; builds. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs index 1d1b4b87..8324b5a1 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanStageTests.cs @@ -688,6 +688,10 @@ public sealed class SpanStageTests /// opposite to the float one in about a fifth of the draws, so fold must still cover the exact /// departure there: a fold forced to 0 at halfLength 0, or an S_o whose reach term drives e_n below /// zero under the loose bound, leaves it 0 and fails. span_fbm's fold is 0 throughout. + /// Near the origin S_o is tiny (scales 1e-4 and 1e-2 of the coordinate), and only the value allowance's + /// absolute term 2^-14 carries the kernel's own rounding: the exact departure there is the coordinate's + /// rounding times the gradient (1e-11 to 1e-9), so the exact check's additive tolerance is 1e-13, under it. + /// A larger tolerance absorbs a dropped absolute term whole. /// [Theory] [InlineData(false)] @@ -706,7 +710,7 @@ public sealed class SpanStageTests var fold = stage.Ridged(span.Path).fold; var (departureFloat, departureExact, rounding) = BranchDeparture(stage, span, 2); Assert.Equal(0.0f, stage.Fbm(span.Path).fold); - Assert.True(departureExact <= fold * (1.0 + 1.0e-3) + 1.0e-9, $"midpoint {q}, speed {speed:R}: the exact branch departure is {departureExact:R}, above fold {fold:R}"); + Assert.True(departureExact <= fold * (1.0 + 1.0e-3) + 1.0e-13, $"midpoint {q}, speed {speed:R}: the exact branch departure is {departureExact:R}, above fold {fold:R}"); Assert.True(departureFloat <= fold * (1.0 + 1.0e-3) + 1.0e-9 + rounding, $"midpoint {q}, speed {speed:R}: the float branch departure is {departureFloat:R}, above fold {fold:R}"); if (departureExact > 0.0) crossing++;