-
Notifications
You must be signed in to change notification settings - Fork 0
NonEscaping DE DevPlan
Companion pages: Technical Index · User Bulb Sandbox Dev Plan · User Bulb 3D Dev Plan · Fractal Equation Design Guide · User-facing User Bulb Guide
Tracking issues: parent #279, slices #280 (NonEscaping runner), #281 (dr/de DSL slots + DE body), #282 (Amoser complex-sine preset), #283 (Expr-compile CPU kernel). File ↔ issue links are two-way per project convention.
Status — SHIPPED (PR #284, merged). All four slices landed on
main. User-facing usage lives in the User Bulb Guide §6.1–6.2 and the in-app help (Math → User Bulb → Non-Escaping DE). Follow-on docs pass: #285. The sections below are the design record.
Origin: fractalforums.org — Amoser's complex sine formula (login-walled; formula + frag quoted below from user paste).
The map:
z = vec3(sin(z.x)*cosh(z.y), cos(z.x)*cos(z.z)*sinh(z.y), sin(z.z)*cosh(z.y)) * Scale + C
Two coupled complex sines (real parts on x/z, shared imaginary on y) → quasi-conformal Kleinian-like dynamics with double-periodic (lattice) symmetry. Renders as pseudo-Kleinian spiky halls / sphere-grid hybrids.
The step map is already fully expressible in the Sandbox-Bulb DSL (vec(...)*Scale + c, with Scale a UserBulbParam; folds via absx/absy/absz; rotation via rot; Regulator/domain-inversion via let). The DE is not. That gap is this plan.
FF UserBulbCalculator DE is escape-based, every path:
| Path | Form | Requires |
|---|---|---|
| Numerical Jacobian (default) | `0.5·r/ | J |
| Analytic running-derivative | 0.5·log(r)·r/dr |
power-law growth + escape |
| Quat-exact |
2·q·dq recurrence |
detected q²+c
|
The Amoser DE is non-escaping and structurally different:
float dr = 1.0;
float de = 1e20;
for (int i = 0; i < Iterations; i++) {
if (abs(z.y) > 8.) break; // stability clamp, NOT escape
// ... compute sx,cx,sz,shy,chy ...
z = vec3(sx*chy, cx*cz*shy, sz*chy) * Scale + juliaC;
float stretch = max(0.9, 0.75 * sqrt(chy*chy + shy*shy)); // analytic |derivative|
dr = abs(Scale) * signFactor * stretch * dr + offset; // running derivative bound
de = min(de, 1.0 / dr); // running MIN, INSIDE the loop
}
return DEMultiplier * de;Differences that make FF unable to match by tuning alone:
-
No escape. Stability clamp (
|z.y|>8), not a bailout. FF treats Bailout as escape → wrong semantic. -
No
rmagnitude, nolog. Distance is purely1/dr. -
Running
min(1/dr)accumulated inside the loop. FF returns one value after the loop; there is no running-min accumulator. -
Map-specific analytic
stretch.0.75·sqrt(chy²+shy²)is the derivative magnitude of the hyperbolic part — hand-authored, not FF's numerical Jacobian. -
Seeding. Mandelbrot:
z=pos, c=pos, offset=1. Julia:z=pos, c=JuliaC, offset=0. Theoffsetfeeds thedrrecurrence, not the map.
Conclusion: not a settings problem — an engine extension. (Verified: the user swept Iterations/Bailout/MaxSteps/Epsilon with no convergence to the forum look.)
The Amoser DE is single-trajectory (1 orbit + scalar dr). FF forces a 4-trajectory numerical Jacobian on this map. Implementing the non-escaping DE is therefore both the correctness fix and a ~4× per-step speedup — a large part of the user's "slow near the surface" complaint on low-end hardware. (Their reference preset runs MaxRaySteps = 1397 precisely because each step is cheap.)
Extend UserBulbDEModeKind (Abstractions/Models/FractalParameters.cs, currently Auto | Analytic | Numerical) with NonEscaping. A dedicated runner in the UserBulbCalculator DE dispatch:
- seed
z = pos; Mandelbrotc = pos,offset = 1; Juliac = JuliaC,offset = 0; -
stability clamp (configurable component + threshold, default
|z.y|>8) rather than escape bailout; - single trajectory; track scalar
dr; -
de = min(de, 1/dr)inside the loop; - return
DEMultiplier * de.
DEMultiplier (their FudgeFactor), stability axis/threshold → new FractalParameters.UserBulb* fields + FractalParamsView controls (per the tunable-params convention).
Tier 1 (ship first): user-supplied dr DSL body. Add read/write state slots dr and de to SandboxBulbExpression, and a second per-iteration DSL body (the "DE body") authored + persisted alongside the step. Their recurrence is then trivially DSL:
let ez = exp(z.z) in
let stretch = max(StretchMax, StretchScale * sqrt(0.5*(ez*ez + 1/(ez*ez)))) in
dr = drScale * stretch * dr + drOffset
All their sliders (Scale, StretchScale/Max, drScale/Offset, DEMultiplier, InvertC, OffsetVec) are already supported as UserBulbParams — no new param plumbing.
Persistence: extend the userbulbs.json bulb record with an optional DeBody string. Absent → falls back to numerical/analytic as today.
Security: interpreter path only, no BCL surface, no source compile. The #27 raw-C#-removal invariant holds unchanged.
Tier 2 (follow-up): auto-analytic dr. Differentiate the step AST for a running scalar-derivative bound, so users need not hand-author dr. Precedent: FF already carries analytic-DE AST work (2D inverse-trig, #215/#220). 3D trig/hyperbolic chain rules are the hard part; the scalar-|dz| approximation is the tractable target. Not required for parity — Tier 1 authors it by hand exactly as the forum does.
Ship a curated UserBulb preset (step body + DE body + default params from the frag #preset Default: Scale=1, folds on, StretchScale=0.81, StretchMax=1.04, Regulator=true, camera Eye=(0,-12,0) Up=(0,0,1)). Do not overwrite user copies (per the no-save-over-examples rule).
Sbx3Node → System.Linq.Expressions → .Compile() JIT delegate. No Roslyn, no source, whitelisted node set → #27-safe. ~5–20× over the interpreter, stacks on the 4× from single-trajectory DE. Benefits all bulbs, not just non-escaping.
| # | Slice | Depends | Effort |
|---|---|---|---|
| #280 |
NonEscaping DE runner: seed-at-point, stability clamp, running min(1/dr), DEMultiplier, mode enum + UI |
— | M |
| #281 |
dr/de DSL state slots + persisted DE body; wire into #280 runner |
#280 | M |
| #282 | Amoser complex-sine curated preset (step + DE body + params + camera) | #280, #281 | S |
| #283 | Expr-compile CPU kernel (Sbx3Node → compiled lambda) |
— | M |
Later / not in parent scope: auto-analytic dr (AST diff); difference-DE (ε·r/|z−z'|) for other non-escaping maps; domain-inversion (c/dot(c,c)+InvertC) as a first-class pre-step.
-
DE body vs chain reuse. Can the existing
SandboxBulbChaincarry a step tagged as the dr-update, or is a separate DE-body field cleaner? Lean separate field — the dr-update readsdr/destate the chain steps don't. -
Stability clamp axis. Frag hardcodes
z.y(orz.zin the elevation-swapped .frag). Expose axis + threshold as params. -
GPU parity.
UserBulbSandboxGpuCompilerwould need the same non-escaping runner +dr/deslots to keep the GPU path usable for these maps. Defer until CPU lands; note as a fast-follow.