-
Notifications
You must be signed in to change notification settings - Fork 0
CalculatorGen Authoring
Companion pages: Technical Index · CalculatorGen Architecture · CalculatorGen Roadmap · User-facing CalcGen Guide · Resources & Bibliography
Walk through the full pipeline from prose math to a checked-in generated calculator. Tricorn is the right tutorial because the math is small (one operator change) but it exercises every emitter path (the conjugate operator changes the Jacobian sign and breaks the perturbation derivation if you do it naively).
The Tricorn (Mandelbar) fixed-point iteration is
In component form (z = a + bi):
Note the sign flip on b_{n+1} versus standard Mandelbrot. This is what makes the perturbation
derivation non-trivial — the partial derivatives change sign:
Substituting into the perturbation template
— note the conjugation propagates through the δ-update. The Differentiator pass picks this up because it walks the AST symbolically.
dotnet run --project CalculatorGen -- `
--name Tricorn `
--equation "Complex.Conjugate(z) * Complex.Conjugate(z) + c" `
--bailout 4.0 `
--out Calculators/Generated/TricornCalculator.cs `
--selftest| Flag | Meaning |
|---|---|
--name |
Class name. Generator writes TricornCalculator : IFractalCalculator. |
--equation |
Raw expression in CalcGen DSL. The parser auto-converts Complex.Conjugate(z). |
--bailout |
Squared bailout radius. 4.0 ⇒ ` |
--out |
Target file path. Overwritten on each run. |
--selftest |
Emit TricornCalculatorSelfTest.cs (validator: scalar vs SIMD vs perturbation). |
The emitter produces ~1 200 lines split across five #region blocks:
// Auto-generated by CalculatorGen 0.3. Do not edit by hand.
// Regenerate with the command at the file footer.
public sealed class TricornCalculator : IFractalCalculator
{
#region Scalar (reference)
private static int CalcScalar(double cr, double ci, int maxIter, double bailoutSq)
{
double zr = 0, zi = 0;
for (int n = 0; n < maxIter; n++)
{
double zrSq = zr * zr;
double ziSq = zi * zi;
if (zrSq + ziSq > bailoutSq) return n;
double nzr = zrSq - ziSq + cr;
double nzi = -2.0 * zr * zi + ci; // ← sign flip from Mandelbrot
zr = nzr; zi = nzi;
}
return maxIter;
}
#endregion
#region AVX2 (Vector256<double> × 4 pixels)
private static unsafe void CalcAvx2(Span<double> cr, Span<double> ci, /* … */)
{
// … 4-pixel vectorised lane with the same sign flip … //
}
#endregion
#region Perturbation (reference orbit + δ-update)
private static int CalcPerturbation(double dr, double di, /* … */)
{
// δ-update: δ_{n+1} = 2·conj(Z_n)·conj(δ_n) + conj(δ_n)² + d
// Differentiator-emitted; conjugation propagates through the δ-step.
}
#endregion
#region BLA (auto-fall-back to perturbation)
private static int CalcBla(/* … */) { /* … */ }
#endregion
#region ILGPU GPU kernel
private static void GpuKernel(Index1D idx, /* … */)
{
// Device kernel — same recurrence, packed as (zr, zi) pairs.
}
#endregion
}dotnet run --project CalculatorGen -- --selftest-run Calculators/Generated/TricornCalculatorSelfTest.csExpected output:
Tricorn self-test
Grid: 64 × 64 iter cap: 1024 bailout²: 4
scalar vs AVX2 : 0 mismatches
scalar vs perturbation : 0 mismatches
scalar vs BLA : 0 mismatches
scalar vs ILGPU (CUDA) : 0 mismatches
Total drift : 0
Verdict : pass
Open Abstractions/Models/FractalType.cs and add an entry above the // — Registered — divider:
public enum FractalType
{
// …
Tricorn,
// …
}…then wire the enum case in Hosting/CalculatorRegistry.cs:
case FractalType.Tricorn:
return new TricornCalculator();Rebuild. The new entry appears in the toolbar Type dropdown.
Pick Tricorn in the dropdown, hit R to reset. The view should jump to the three-cornered
"Mandelbar" with cusps along the three lobes meeting at the origin. Compare side-by-side with the
canonical image in Wikipedia → Tricorn (mathematics).
![]() |
![]() |
|---|---|
Hand-tuned TricornCalculator — conj(z)² + c. |
CalcGen-emitted GeneratedTricorn — same iteration AST, scalar path. |
Both renders use HSV at the home view (1600 × 1000). The two outputs are visually indistinguishable — the equivalence is exactly what the test suite asserts.
Tip
The same pipeline produces correct Multibrot (z^d + c), Burning Ship ((|a| + i|b|)² + c),
Phoenix (with the second-order term left out, since the AST currently lacks two-step memory),
and arbitrary polynomial Julia (with z₀ = c instead of 0). The δ-update derivation falls out
of the same ∂f/∂z and ∂f/∂c walks — the writer never has to hand-derive it.
CalculatorGen is a small console tool that turns a user-supplied
fractal equation into a drop-in IFractalCalculator class file. The
goal: stop hand-writing one C# file per fractal type. Once the
generator ships, "promote this sandbox equation to a real calculator"
is a CLI invocation, not an LLM session.
The current generator (v0.3) emits a single C# file containing five fully-validated execution paths for every input equation:
| Tier | Path | Bound by |
|---|---|---|
| 1 | Scalar | Reference path; double
|
| 2 | AVX2 + FMA | Four pixels per lane; Vector256<double>
|
| 4 | Perturbation | Reference orbit + symbolic δ-update; deep-zoom safe |
| 5 | BLA | Per-iter linear approx; auto-falls-back to Tier 4 |
| 6 | ILGPU GPU | Auto-dispatched device kernel; CPU colour post-pass |
A --selftest flag emits a sibling validator that compares all five
paths on a fixed 64×64 grid and reports per-path drift. Verified
0 mismatches for z² + c and z³ + c.
A sixth "Tier 0" optimisation (imag-zero ComplexExpr flag) cuts roughly
30 % of the AVX2 prelude by skipping dead Vector256<double>.Zero adds
when subexpressions are provably real-valued (constants, real-only
arithmetic chains).
This document covers all the above plus the new FractalType .GeneratedMandelbrotZ2 dropdown entry that surfaces the generated
calculator in the Avalonia toolbar alongside the hand-tuned
MandelbrotCalculator.
Developers extending the generator itself — adding new operators, new emitters, new precision tiers, new GPU backends — should read CalculatorGen-Architecture.md for the internals: the AST pipeline, emitter contracts, template substitution, the deep-zoom path order (perturbation / glitch detection / HP-direct fallback), status labels, and debugging tips. This file is for authors invoking the existing CLI; the architecture file is for modifiers of CalculatorGen itself.
From the repo root:
dotnet build CalculatorGen\CalculatorGen.csproj -c Release
dotnet run --project CalculatorGen -c Release -- `
--equation "z*z + c" `
--name MandelbrotZ2 `
--out Calculators\Generated `
--selftestThis writes:
Calculators\Generated\MandelbrotZ2Calculator.cs
Calculators\Generated\MandelbrotZ2CalculatorSelfTest.cs
Rebuild the main project; the generated class is picked up by the
existing Calculators\Generated\** glob.
Verify the five paths agree:
.\bin\x64\Release\net10.0-windows\FracturingFog.exe --gentest MandelbrotZ2
Get-Content .\bin\x64\Release\net10.0-windows\gentest.outExpected:
MandelbrotZ2CalculatorSelfTest — scalar ↔ AVX2 ↔ GPU agreement
grid: 64×64 = 4096 pixels
max iterations: 256
mismatches: 0 (0.00%)
mean |Δit|: 0.0000
max |Δit|: 0 (tolerance: 1)
gpu in-set: cpu=523 gpu=523 diff=0 → PASS
perturbation: cpu=523 pt=523 diff=0 → PASS
bla: cpu=523 bla=523 diff=0 → PASS
qd ref orbit: in-set=523 out=3573 → PASS
result: PASS
The self-test runs at Zoom = 1, so it does NOT exercise the deep-zoom
paths (DD/QD per-pixel HP-direct, per-pixel glitch detection). To
validate those, run the app and zoom past 1e12 — the status bar shows
the active path (PT, QD-PT, DD-HP, QD-HP). See
CalculatorGen-Architecture.md §6 for
the full label scheme.
Select the generated calc in the Avalonia toolbar: Type → "Mandelbrot Z² (Generated)".
| Construct | Example | Notes |
|---|---|---|
| Variable z | z |
The iterating complex value |
| Variable c | c |
Pixel coordinate (complex) |
| Real literal |
2, 0.5, 1e-3
|
Treated as (n, 0) complex |
| Addition | z + c |
Complex |
| Subtraction | z - c |
Complex |
| Multiplication |
z * z, 2 * c
|
Complex |
| Integer power |
z^2, z^3, c^2
|
Exponent must be 0..16 |
| Parentheses | (z + c) * (z - c) |
Standard precedence |
| Unary minus |
-z, -(z*z + c)
|
Complex negation |
Not yet supported (use Sandbox or hand-write):
- Division (
/) - Transcendentals (
sin,cos,exp,log, …) - Absolute-value folds (
|z.real|→ Burning Ship) - Conditional branches
- Magnitude / argument decomposition
The polynomial restriction lets the symbolic differentiator and perturbation expansion stay closed-form one-liners with no CAS dependency.
One .cs file per equation containing:
-
Class declaration — sealed, implements
IFractalCalculatorandIDisposable. Holds GPU accelerator handles released on dispose. -
Header comment — original equation, auto-derived
∂p/∂z,∂p/∂c, the dz/dc per-iteration update, and the symbolic δ-expansionp(Z+δ, C+ε) − p(Z, C)used by the perturbation path. -
Calculatedriver — picks the active path in this order:- GPU if
UseGpu = trueand ILGPU init succeeds. - Perturbation if
UsePerturbation = trueandZoom ≥ PerturbZoomThreshold. BLA layers on top viaUseBla = true. - AVX2 + scalar tail (default).
- GPU if
-
IteratePixelScalar— reference implementation, one pixel,doublearithmetic. Emits both thez_{n+1}body and thedz_{n+1}/dcbody each iteration. -
IteratePixelScalarRaw— iteration-count-only variant (no colour writeback). Used by the self-test. -
IterateLaneAvx2—Vector256<double>over four pixels per lane. Complex multiply viaFma.MultiplyAdd/MultiplyAddNegated. Per-lane bailout viaBlendVariableso escaped lanes freeze. -
IterateLaneAvx2Raw— iteration-count-only AVX2 variant for the self-test. -
Kernel— ILGPU device kernel; one work item per pixel. Uses the same scalar arithmetic body as path 4 (ILGPU accepts thedoubleops directly). -
TryRenderGpu— lazyContext+Accelerator+ kernel load, allocates anArrayView<RawPixel>, dispatches the kernel, reads back, runs the CPU colour post-pass inParallel.Forso the fullIColorMap(incl. distance-estimate / normal themes) is honoured. -
TryRenderPerturbation— reference orbit at view centre (plain double precision); per-pixel δ-iteration loop using the symbolic Taylor expansion. Auto-aborts and returns false when the reference orbit itself escapes (which means the centre is outside the set). -
BLA inner branch — when
UseBla = true, computes per-iterA_n = ∂p/∂z(Z_n, C)andB_n = ∂p/∂c(Z_n, C)arrays during reference-orbit construction. In the per-pixel loop, attempts the linear stepδ_{n+1} = A·δ + B·εfirst; falls back to the full polynomial expansion when|δ| ≥ BlaRelative · |Z_n|. -
ColorFor— surface-normal + distance-estimate computation feeding the 9-argumentIColorMap.Mapoverload. Themes that ignore normals/DE inherit the 3-arg fallback through the default interface implementations.
Take z*z + c. The scalar inner update (post imag-zero opt):
double dr_new = (((zr + zr) * dr - (zi + zi) * di) + 1.0);
double di_new = ((zr + zr) * di + (zi + zi) * dr);
double zr_new = ((zr * zr - zi * zi) + cr);
double zi_new = ((zr * zi + zi * zr) + ci);dr_new/di_new are the symbolic dz/dc update (z + z)·D + 1 (which
equals 2z·D + 1 = ∂p/∂z·D + ∂p/∂c for p = z² + c).
The AVX2 path is the same identity in Vector256<double> lanes,
emitted as SSA-style temps (the JIT folds redundant copies):
Vector256<double> d_re1 = Avx.Add(zr, zr);
Vector256<double> d_im2 = Avx.Add(zi, zi);
Vector256<double> d_re3 = Fma.MultiplyAddNegated(d_im2, di, Avx.Multiply(d_re1, dr));
Vector256<double> d_im4 = Fma.MultiplyAdd(d_re1, di, Avx.Multiply(d_im2, dr));
Vector256<double> d_re5 = Vector256.Create(1.0);
Vector256<double> d_re6 = Avx.Add(d_re3, d_re5);
Vector256<double> dr_new = d_re6;
Vector256<double> di_new = d_im4;Fma.MultiplyAddNegated(b, d, a*c) = −(b·d) + a·c = real part of
(a+bi)(c+di). Note d_re5 = Create(1.0) binds only Re (no Im
temp); the imag-zero optimisation recognises the + 1.0 literal as
provably real-valued and elides the dead zero-vector add that earlier
generator revisions emitted.
The perturbation comment line shows the symbolic δ-update produced
from p(Z+δ, C+ε) − p(Z, C):
// δ_{n+1} = ε + (z + z)*δ + δ*δ
i.e. ε + 2Zδ + δ² (exact, not a Taylor truncation — for polynomials
the expansion terminates at the polynomial's total degree).
For z³ + c:
// δ_{n+1} = ε + ((z + z)*z + z*z)*δ + 0.5*(2*z + z + z + z + z)*δ*δ + δ*δ*δ
i.e. ε + 3Z²δ + 3Zδ² + δ³. (The unsimplified coefficients are
artefacts of how the multinomial-from-Taylor expansion is built;
emitting collected-form polynomials is on the cleanup list but the
JIT folds them either way.)
CalculatorGen/
├── Parser/
│ ├── AstNodes.cs — ZRef, CRef, DRef, DeltaRef, EpsRef,
│ │ RealConst, Neg, Add/Sub/Mul, Pow
│ ├── EquationLexer.cs — string → List<Token>
│ ├── EquationParser.cs — List<Token> → AstNode (recursive descent)
│ ├── AstSimplifier.cs — peephole: 0+x, 1*x, x^0/^1, const folding
│ ├── AstDifferentiator.cs — symbolic ∂/∂z, ∂/∂c, dz/dc update builder
│ ├── AstSubstitute.cs — z → expr, c → expr (used by perturbation)
│ ├── AstExpander.cs — distributive expansion (reserved)
│ ├── AstPerturbation.cs — Taylor-series builder for p(Z+δ, C+ε) − p(Z, C)
│ └── AstPrinter.cs — AST → source string (for header comment)
├── Emitters/
│ ├── EmitterCommon.cs — abstract EmitterBase; ComplexExpr w/ ImZero
│ ├── ScalarEmitter.cs — emits double expressions
│ ├── Avx2Emitter.cs — emits Vector256<double> w/ FMA, SSA temps
│ └── PerturbationEmitter.cs — emits scalar code w/ Z/C/δ/ε bindings
├── Templates/
│ ├── Calculator.template.cs — IFractalCalculator skeleton. Placeholders:
│ │ {{CLASS_NAME}}, {{EQUATION_SOURCE}},
│ │ {{DPDZ_TEXT}}, {{DPDC_TEXT}},
│ │ {{DERIV_TEXT}}, {{TIMESTAMP}},
│ │ {{SCALAR_Z_BODY}}, {{SCALAR_D_BODY}},
│ │ {{AVX2_Z_BODY}}, {{AVX2_D_BODY}},
│ │ {{PERTURB_DELTA_BODY}}, {{PERTURB_DELTA_TEXT}},
│ │ {{BLA_A_BODY}}, {{BLA_B_BODY}}
│ └── SelfTest.template.cs — scalar↔AVX2↔GPU↔perturb↔BLA grid validator
└── Program.cs — CLI; parses, diffs, simplifies, emits,
writes file(s)
Both arithmetic emitters subclass EmitterBase, which provides the AST
walker and per-primitive virtual hooks (Const, OpAdd, OpSub,
OpMul, OpNeg). The PerturbationEmitter is a third subclass with
different variable bindings (Zr/Zi for reference orbit, dr/di for
δ, er/ei for ε). To add a new target (e.g. AVX-512), add an emitter
subclass and override those five primitives + provide an EmitBody
entry point.
ComplexExpr carries an ImZero boolean. Const(double) sets it
true; ZRef/CRef/DRef/DeltaRef/EpsRef set it false (those values are
arbitrary complex at runtime). Add / Sub / Mul / Neg propagate:
| Op | a.ImZero | b.ImZero | Result |
|---|---|---|---|
| Add | true | true | (a.Re + b.Re, 0, ImZero=true) |
| Add | true | false | (a.Re + b.Re, b.Im, ImZero=false) |
| Add | false | true | (a.Re + b.Re, a.Im, ImZero=false) |
| Add | false | false | (a.Re + b.Re, a.Im + b.Im, false) |
| Mul | true | true | (a.Re · b.Re, 0, ImZero=true) |
| Mul | true | false | (a.Re · b.Re, a.Re · b.Im, false) |
| Mul | false | true | (a.Re · b.Re, a.Im · b.Re, false) |
| Mul | false | false | full complex multiply (4 muls / 2 adds) |
This collapses (2 · zr - 0 · zi, 2 · zi + 0 · zr) → (2 · zr, 2 · zi)
and saves the dead Vector256<double>.Zero SSA binds in the AVX2
prelude. Empirically: ~30 % shorter prelude on a derivative-tracking
calculator (cuts ~5 SSA temps per iteration on z² + c).
AstDifferentiator.Diff(node, Var) implements:
-
ZRef→ 1 if Var = Z, else 0 -
CRef→ 1 if Var = C, else 0 -
DRef/DeltaRef/EpsRef→ 0 (opaque) - Constants → 0
- Sum rule, product rule, chain rule on Pow:
(u^n)' = n · u^(n-1) · u'
Plus BuildDerivativeUpdate(stepFn) which builds
dz_{n+1}/dc = (∂p/∂z) · D + (∂p/∂c) where D = DRef is the
symbolic placeholder for the current dz/dc value. The emitter binds
DRef to (dr, di) registers at runtime; the result feeds the
9-arg IColorMap.Map for Inigo Quilez normals (z · conj(dz/dc),
normalised) and Milnor/Hubbard distance estimate
(½ · |z| · log|z| / |dz/dc|).
AstPerturbation.BuildDeltaUpdate(stepFn) produces the symbolic
δ-update via Taylor series:
δ_{n+1} = p(Z + δ, C + ε) − p(Z, C)
= Σ_{k+m≥1} (1 / k! m!) · (∂^{k+m} p / ∂z^k ∂c^m)|_{Z,C} · δ^k · ε^m
The expansion terminates at total polynomial degree (≤ 32 in the
current grammar), so this is exact — no Taylor truncation error.
Implementation iterates (k, m) pairs, differentiating the step
function k times w.r.t. z and m times w.r.t. c, simplifying after
each derivative (otherwise the intermediate tree grows exponentially
in the unsimplified form and the run hangs). Constant-zero partials
short-circuit the term.
The emitter (PerturbationEmitter) walks the resulting AST with
bindings:
| AST node | Variable |
|---|---|
ZRef |
Zr, Zi — reference orbit iterate at step n |
CRef |
Cr, Ci — view centre coordinate |
DeltaRef |
dr, di — per-pixel δ |
EpsRef |
er, ei — per-pixel ε = c - C |
The runtime TryRenderPerturbation:
- Computes the reference orbit at the view centre using plain double-precision iteration. Bails to AVX2 path (returns false) if the reference itself escapes.
- For each pixel: walks the same number of iterations, but updates
δ via the symbolic expansion. Stores final
(Zr+δr, Zi+δi)for colour mapping. - Falls back to the AVX2 path if the reference orbit can't be
built or
Zoom < PerturbZoomThreshold.
Bilinear approximation: at each reference iteration n,
δ_{n+1} ≈ A_n · δ_n + B_n · ε
A_n = ∂p/∂z(Z_n, C)
B_n = ∂p/∂c(Z_n, C)
Both A_n and B_n are emitted by the scalar emitter from the
already-derived ∂p/∂z and ∂p/∂c ASTs (no new emitter needed).
They're computed alongside the reference orbit and cached into
blaAr[], blaAi[], blaBr[], blaBi[], plus a validity radius
blaR[n] = BlaRelative · |Z_n| (BlaRelative defaults to 1e-3).
The pixel loop attempts the linear step first when UseBla = true;
when |δ|² ≥ blaR[n]² it falls back to the full polynomial step.
For z² + c the linear step is (A·δ + B·ε) = (2Z·δ + ε); the
quadratic δ² term is dropped under the validity threshold.
The current implementation is iteration-filter BLA: it speeds up
individual iterations when the linearisation holds, but does not
yet skip ranges of iterations via composed multi-level BLA tables.
A future enhancement (sketched in Future Work) builds
power-of-two compositions
A^{(K)}_n = ∏ A_{n+k}, B^{(K)}_n = Σ … · B_{n+k} for skip-K steps.
The generated calculator emits a Kernel method:
private static void Kernel(Index1D idx, ArrayView<RawPixel> output, GpuParams p)
{
int x = idx % p.Width;
int y = idx / p.Width;
if (y >= p.Height) return;
double cr = p.CenterX + (x - p.Width * 0.5) * p.Scale;
double ci = p.CenterY + (y - p.Height * 0.5) * p.Scale;
double zr = 0.0, zi = 0.0;
double dr = 0.0, di = 0.0;
int it = 0;
int maxIt = p.MaxIter;
for (; it < maxIt; it++)
{
double r2 = zr * zr + zi * zi;
if (r2 >= p.Bailout2) break;
// {{SCALAR_D_BODY}} and {{SCALAR_Z_BODY}} substituted here
...
}
output[idx] = new RawPixel { Iter = it, Zr = zr, Zi = zi, Dr = dr, Di = di };
}RawPixel is a value type with int Iter; double Zr, Zi, Dr, Di;
captured per pixel. The CPU post-pass reads the buffer back and runs
ColorFor in Parallel.For — so the full IColorMap is active even
in GPU mode (the kernel itself contains no colour code, which is what
lets it stay device-compatible).
Lifecycle:
-
TryInitGpulazily createsContext.Create(b => b.Default())and picks the preferred device viaGetPreferredDevice(preferCPU: false). Stores the kernel delegate. - On init failure, sets
_gpuInitFailed = trueand stashes the exception message inLastGpuError. Subsequent calls short-circuit to the AVX2 fallback. -
Dispose()releases the accelerator and context.
The kernel is double-precision. On consumer NVIDIA cards (RTX series) that's ~1/32 the FP32 throughput, so the GPU is most useful for very high iteration counts or large output buffers where the iteration loop dominates.
--selftest emits <Name>SelfTest.cs alongside the calculator. The
test runs four cross-checks on a fixed 64×64 grid at the standard
Mandelbrot view (centre −0.75, span 3.5, 256 max iterations):
-
Scalar ↔ AVX2 per-pixel iteration-count drift.
Tolerance:
maxAbsDiff ≤ 1(one ULP shift in the bailout-radius compare). -
GPU smoke: render full grid with
UseGpu = true, count in-set pixels (those that hitMaxIterations), compare to the scalar count. Tolerance ≤ 4 boundary pixels. Init failure → SKIP (not a hard failure). -
Perturbation smoke: same comparison with
UsePerturbation = true, PerturbZoomThreshold = 0. Tolerance ≤ 4. -
BLA smoke:
UseBla = trueon top of perturbation. Tolerance ≤ 8 (BLA may admit a few extra boundary pixels under linear approximation).
Hook into the main Program.cs so FracturingFog.exe --gentest MandelbrotZ2 runs it. Because the binary is a WinExe, output is
also mirrored to gentest.out next to the exe so a redirected shell
can read it.
The generated calculator is exposed via FractalType .GeneratedMandelbrotZ2. Wiring touchpoints (all already in place
for the MandelbrotZ2 demo, repeat the pattern for further calcs):
| File | Hook |
|---|---|
Abstractions/Models/Enums.cs |
New FractalType enum value |
Abstractions/ViewState/FractalViewState.cs |
Default centre/zoom in SnapToFractalDefault
|
Rendering/FractalRenderHost.cs |
Field + ctor + colour map setter + Resize + SelectAltCalculator dispatch |
UI.Avalonia/ViewModels/MainViewModel.cs |
Entry in BuiltInFractalLabels
|
The Avalonia dropdown shows the entry as "Mandelbrot Z² (Generated)".
The Server allowlist (Server/Guard/FractalTypeAllowlist.cs) blocks
only user-authored types (UserEquation / Sandbox / UserBulb); generated
calcs are compiled into the binary and therefore safe to expose over
the network — no allowlist change needed.
- Add
Emitters/Avx512Emitter.csmirroringAvx2Emitter.csbut withVector512<double>,Avx512F.Add,Avx512F.Multiply,Avx512F.FusedMultiplyAdd. Eight pixels per lane. - Add
{{AVX512_BODY}}placeholder inCalculator.template.csand update the template to dispatch whenAvx512F.IsSupported. - Wire it through
Program.cs(one extra.Replacecall). - Add an AVX-512 leg to the self-test template.
- Add
Abs(AstNode Operand)toAstNodes.cs. - Teach
EquationLexer.csto emit|tokens (or aabs(...)call form). - Add
OpAbsvirtual toEmitterBase; override in scalar (Math.Abs) and AVX2 (Avx.AndNot(signMask, vec)withsignMask = Vector256.Create(-0.0)). - Update
AstDifferentiator(Abs is not analytically differentiable along the axis — return Sign(x) by convention, document the cusp). - Update
AstPerturbation— Abs makes the iteration anti-holomorphic, so the Taylor expansion picks up axis-discontinuity terms. Burning Ship perturbation typically uses signed components rather than straight Abs to preserve the polynomial chain.
For multiple generated calcs, replace the per-calc enum value pattern with a registry:
- Add a single
FractalType.Generatedenum value. - Add a
GeneratedCalculatorRegistry.Register(name, factory)static class. The generator emits a[ModuleInitializer]-decorated method into each calc file that self-registers on assembly load. -
FractalTypeEntrycarries the registered name when the entry isGenerated.FractalRenderHost.SelectAltCalculatorresolves via the registry.
This is sketched but not implemented — the current demo wires
GeneratedMandelbrotZ2 directly because it's the only generated
calc shipped.
-
Polynomial subset is small but powerful. Covers Mandelbrot
z², all Multibrot powers (z^n), Mandelbar via aConjnode (not yet wired), and any polynomial perturbation thereof. -
No
^on subexpressions.(z+c)^2requires expanding manually to(z+c) * (z+c)in your equation, by design. Keeps the differentiator's life easy. -
FMA rounding ≠ separate-mul-add rounding. Scalar and AVX2
outputs may differ by 1 ULP at the per-iteration level, producing
≤1 iteration count divergence near boundary pixels. This is the
same trade-off
MandelbrotCalculator.csalready makes; documented in the self-test tolerance. -
Perturbation expansion is unsimplified. The Taylor builder
produces e.g.
0.5*(2*z + z + z + z + z)*δ*δfor the½·∂²p/∂z²·δ²term ofz³+cinstead of3z·δ². The JIT folds these but the source is noisier than necessary. Adding term- collection to the simplifier is a future cleanup. - BLA is iteration-filter only. No multi-level skip table yet — the linear step doesn't yet skip iterations, only avoids the polynomial expansion within a single iter. The composition algebra is sketched in Future Work below.
- GPU is double-precision. Best for high-iter / large-buffer renders where the iteration loop dominates. Consumer NVIDIA cards (RTX) run double at ~1/32 of single-precision throughput; data- centre cards (A100/H100) run at full speed.
- CPU colour pass on GPU path. Trade-off: keeps the GPU kernel device-portable (no IColorMap virtual call on device) at the cost of a 36 MB readback for a 1080p render. Acceptable on PCIe Gen3+.
-
One accelerator per generated calc. If you ship several
generated calcs each instantiates its own
Context/Accelerator. A shared static pool is a future refactor.
-
Multi-level BLA composition. Build
A^{(K)}_n,B^{(K)}_nfor K = 2, 4, 8, … log2(maxIt). Validity radius shrinks with K. Per-pixel: walk the largest level whose radius covers the current δ. Real Yang-style skip BLA — expected 10-100× speedup for deep zooms. - Series Approximation (SA). Symbolic Taylor expansion of δ as a power series in ε, truncated to order 8-16. The AST already lets us extract the per-order coefficients; the runtime caches them per skip block and applies the series for any pixel whose ε falls in the validity disk.
-
Double-double SIMD path.
Vector256<double>lanes that hold hi+lo of a DD number; complex multiply becomes DD-multiply + DD- add chains (~40 flops per lane per iter). Unlocks zoom past 1e15 without perturbation. -
Burning Ship / Tricorn via
AbsandConj. Add the AST nodes- emitter hooks; the perturbation expansion needs an extension rule for the anti-holomorphic case.
-
Registry-driven UI. Drop the per-calc enum values in favour of
a single
FractalType.Generated+ name-based registry as sketched above.
- Inigo Quilez — Rendering the Mandelbrot Set
https://iquilezles.org/articles/mandelbrot/
(Normal-vector + distance-estimator formulas used by the
template's
ColorForhelper.) - Claude Heiland-Allen — Perturbation and BLA for the Mandelbrot set. The polynomial-AST approach for Tier 4/5 follows his derivation.
- Zhuoran Yang — Multi-level BLA tables. The composition algebra that the iteration-skip future-work entry references.
- ILGPU documentation —
Context.Create,Accelerator,AutoGroupedStreamKernel, the JIT path used by the GPU emitter.

