CultLib/packages/cultmath/tools/phacelle-lipschitz/Program.cs
Libby b57501b33e
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
cultmath: phacelle-lipschitz prints usage and exits 2 on missing or malformed arguments
Without arguments the tool threw IndexOutOfRange; a malformed number threw FormatException.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 18:33:18 +00:00

92 lines
5.6 KiB
C#

// Measures the Lipschitz constant of math.phacelle's (cosine, sine) field, the number docs/design.md quotes and
// PhacelleTests.GradientNormStaysUnderTheDocumentedLipschitzEnvelope pins.
// dotnet run -c Release --project packages/cultmath/tools/phacelle-lipschitz -- <samples per cell> <normalizations> <side lengths>
// e.g. ... -- 4000000 0,0.5,0.75,0.9 0.25,0.5,1,1.5,2,4,8,12.5,25
// Columns: sampledMax (random p), climbedMax (hill-climbed from the 256 best samples; the number to quote), secantMax
// (value differences around the climbed argmax, the check that the analytic gradient is the field's slope),
// scalarMax (the larger of |grad cosine|, |grad sine| over the samples), p99.99 (the typical tail), and climbed*(1-n)/|side|.
using System;
using System.Linq;
using System.Threading.Tasks;
using CultMath;
// Lipschitz bound of math.phacelle's (cosine, sine) field: sup over p of the operator norm of its 2x3 Jacobian,
// found by (1) random sampling of the exact analytic gradient, (2) hill-climbing from the top samples over
// (p, off, side direction), (3) a secant check on the VALUES (|f(p+d) - f(p)| / |d|) so the gradient itself is not trusted.
static double Op(float3 p, float3 side, float off, float n, out double comp)
{
var r = math.phacelle(p, side, off, n);
double a = r.cosine.x*r.cosine.x + r.cosine.y*r.cosine.y + r.cosine.z*r.cosine.z;
double d = r.sine.x*r.sine.x + r.sine.y*r.sine.y + r.sine.z*r.sine.z;
double b = r.cosine.x*r.sine.x + r.cosine.y*r.sine.y + r.cosine.z*r.sine.z;
double tr = a + d, det = a*d - b*b;
comp = Math.Sqrt(Math.Max(a, d));
var op = Math.Sqrt(tr/2 + Math.Sqrt(Math.Max(0, tr*tr/4 - det)));
return double.IsFinite(op) ? op : 0;
}
static float3 Dir(double u, double th, double len) { var s = Math.Sqrt(Math.Max(0, 1-u*u)); return new float3((float)(s*Math.Cos(th)*len), (float)(s*Math.Sin(th)*len), (float)(u*len)); }
float[]? norms = null, sides = null;
try { norms = args.Length == 3 ? args[1].Split(',').Select(float.Parse).ToArray() : null; sides = args.Length == 3 ? args[2].Split(',').Select(float.Parse).ToArray() : null; }
catch (FormatException) { }
if (args.Length != 3 || !long.TryParse(args[0], out var N) || N <= 0 || norms is null || sides is null)
{
Console.Error.WriteLine("usage: phacelle-lipschitz <samples per cell> <normalizations> <side lengths>");
Console.Error.WriteLine(" e.g. phacelle-lipschitz 4000000 0,0.5,0.75,0.9 0.25,0.5,1,1.5,2,4,8,12.5,25");
return 2;
}
Console.WriteLine("n\t|side|\tN\tsampledMax\tclimbedMax\tsecantMax\tscalarMax(cos|sin)\tp99.99\tclimbed*(1-n)/|side|");
foreach (var n in norms) foreach (var sl in sides)
{
int T = Environment.ProcessorCount;
var tops = new (double v, float3 p, double u, double th, float off)[T][];
var allv = new double[T][]; var scal = new double[T];
Parallel.For(0, T, t =>
{
var rng = new System.Random(unchecked(99991 * t + (int)(n * 1000) * 131 + (int)(sl * 100)));
var top = new (double v, float3 p, double u, double th, float off)[16];
var list = new double[N / T];
double sc = 0;
for (long i = 0; i < N / T; i++)
{
var p = new float3((float)(rng.NextDouble()*64-32), (float)(rng.NextDouble()*64-32), (float)(rng.NextDouble()*64-32));
double u = rng.NextDouble()*2-1, th = rng.NextDouble()*Math.PI*2; float off = (float)rng.NextDouble();
var v = Op(p, Dir(u, th, sl), off, n, out var c);
list[i] = v; if (c > sc) sc = c;
int m = 0; for (int k = 1; k < 16; k++) if (top[k].v < top[m].v) m = k;
if (v > top[m].v) top[m] = (v, p, u, th, off);
}
tops[t] = top; allv[t] = list; scal[t] = sc;
});
var all = allv.SelectMany(x => x).ToArray(); Array.Sort(all);
double sampled = all[^1], p9999 = all[(int)(all.Length * 0.9999)];
// hill-climb the best 16*T by random local search
var seeds = tops.SelectMany(x => x).OrderByDescending(x => x.v).Take(256).ToArray();
var climbed = new (double v, float3 p, double u, double th, float off)[seeds.Length];
Parallel.For(0, seeds.Length, s =>
{
var rng = new System.Random(s + 17);
var cur = seeds[s]; double step = 0.05;
for (int it = 0; it < 10000; it++)
{
var q = new float3(cur.p.x + (float)((rng.NextDouble()-.5)*step), cur.p.y + (float)((rng.NextDouble()-.5)*step), cur.p.z + (float)((rng.NextDouble()-.5)*step));
var u = Math.Clamp(cur.u + (rng.NextDouble()-.5)*step, -1, 1); var th = cur.th + (rng.NextDouble()-.5)*step; var off = cur.off + (float)((rng.NextDouble()-.5)*step);
var v = Op(q, Dir(u, th, sl), off, n, out _);
if (v > cur.v) cur = (v, q, u, th, off); else if (it % 1000 == 999) step *= 0.5;
}
climbed[s] = cur;
});
var best = climbed.OrderByDescending(x => x.v).First();
// secant check around the climbed argmax: values only, tiny displacements in random directions
double secant = 0; var sr = new System.Random(5);
var side0 = Dir(best.u, best.th, sl);
for (int i = 0; i < 20000; i++)
{
double e = Math.Pow(10, -2 - 2*sr.NextDouble()); var d = Dir(sr.NextDouble()*2-1, sr.NextDouble()*6.2832, e);
var a = math.phacelle(best.p, side0, best.off, n); var b = math.phacelle(best.p + d, side0, best.off, n);
double dc = b.cosine.w - a.cosine.w, ds = b.sine.w - a.sine.w; var dl = Math.Sqrt((double)d.x*d.x + (double)d.y*d.y + (double)d.z*d.z);
secant = Math.Max(secant, Math.Sqrt(dc*dc + ds*ds) / dl);
}
Console.WriteLine($"{n}\t{sl}\t{N}\t{sampled:F3}\t{best.v:F3}\t{secant:F3}\t{scal.Max():F3}\t{p9999:F3}\t{best.v*(1-n)/sl:F3}");
}
return 0;