fix: periodicity checking stop flagging false positives

This commit is contained in:
2026-09-25 10:02:14 +02:00
parent 583a535806
commit 0d86e7b0e0
2 changed files with 45 additions and 9 deletions
+5 -2
View File
@@ -133,8 +133,11 @@ pixel is a handful of `f32` complex multiplies.
clearly attracting (`PERIOD_MAX_MULT2`), and the contracting return must clearly attracting (`PERIOD_MAX_MULT2`), and the contracting return must
repeat in `PERIOD_CONFIRMATIONS` consecutive windows. Both guards are repeat in `PERIOD_CONFIRMATIONS` consecutive windows. Both guards are
needed: without them, exterior pixels at cusps and minibrot edges turned needed: without them, exterior pixels at cusps and minibrot edges turned
black. Retune them only against f64 ground truth on such views. Phoenix is black. Retune them only against f64 ground truth on such views. Each
excluded (two-term map). window saves its iterate closest to the critical point, not the one at the
checkpoint. At an arbitrary phase the relative tolerance is far coarser
than a deep minibrot's scale, and a black disk surrounded the minibrot
(seen at ~1e-13 zoom). Phoenix is excluded (two-term map).
`advance_delta(z, e)` is the per-kind delta step (`z` = reference point, `advance_delta(z, e)` is the per-kind delta step (`z` = reference point,
`e` = current delta); the caller adds `step_add` (= `dc`) afterward — this `e` = current delta); the caller adds `step_add` (= `dc`) afterward — this
relies on `c` being additive in every current kind's formula (a kind where relies on `c` being additive in every current kind's formula (a kind where
+40 -7
View File
@@ -255,10 +255,19 @@ fn phoenix_weight() -> f32 {
return w; return w;
} }
// Periodicity (interior) detection, Brent-style: the full orbit value is // Periodicity (interior) detection, Brent-style: windows end at iterations
// saved at iterations PERIOD_FIRST_CHECK, 2x that, 4x ..., and every later // PERIOD_FIRST_CHECK, 2x that, 4x ..., and at each window end the window's
// iterate is compared against the last saved one. Returning within // iterate closest to the critical point is saved. Every later iterate is
// PERIOD_EPS2 (relative, squared) means the orbit has closed a cycle. // compared against the last saved one. Returning within PERIOD_EPS2
// (relative, squared) means the orbit has closed a cycle.
//
// Why the closest-to-critical iterate rather than the one at the window end:
// near a deep minibrot, an orbit follows the minibrot's cycle with a
// deviation at the minibrot's own scale. At an arbitrary phase |z| ~ 1, so
// that deviation is far below the relative tolerance and *any* nearby
// exterior orbit "returned" (a large black disk around a minibrot at
// ~1e-13 zoom). At the phase nearest the critical point, z itself is at the
// minibrot's scale, so the relative tolerance measures the actual return.
// //
// A close return alone isn't trusted. A pixel just *outside* the set (at a // A close return alone isn't trusted. A pixel just *outside* the set (at a
// minibrot's edge, or a cusp) can shadow a cycle for thousands of iterations // minibrot's edge, or a cusp) can shadow a cycle for thousands of iterations
@@ -300,6 +309,15 @@ fn periodic_enabled() -> bool {
return true; return true;
} }
// Critical point of the current map (where f' = 0), the periodicity save
// point's reference: 0 for every z^p-like kind, 1/2 for Lambda's λz(1-z).
fn critical_point() -> vec2<f32> {
if KIND == KIND_LAMBDA {
return vec2<f32>(0.5, 0.0);
}
return vec2<f32>(0.0, 0.0);
}
// Escape data for one sample: `ci` is the (color-independent) palette parameter, // Escape data for one sample: `ci` is the (color-independent) palette parameter,
// `de` the distance-estimate darkening factor in [0,1], `escaped` false for the // `de` the distance-estimate darkening factor in [0,1], `escaped` false for the
// interior of the set. Splitting iteration from coloring lets a colour change be // interior of the set. Splitting iteration from coloring lets a colour change be
@@ -378,6 +396,12 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
var check_at = PERIOD_FIRST_CHECK; var check_at = PERIOD_FIRST_CHECK;
var period_hit = false; var period_hit = false;
var period_streak = 0u; var period_streak = 0u;
// This window's save candidate: its iterate closest to the critical
// point, that distance squared, and the |f'|^2 product since it.
let crit = critical_point();
var z_cand = z;
var cand_d2 = 3.0e38;
var mult2_cand = 1.0;
loop { loop {
if z2 > bailout_sq { if z2 > bailout_sq {
@@ -397,7 +421,9 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
fp = fprime(z); fp = fprime(z);
} }
if periodic { if periodic {
mult2 = mult2 * dot(fp, fp); let fp2 = dot(fp, fp);
mult2 = mult2 * fp2;
mult2_cand = mult2_cand * fp2;
} }
if DE { if DE {
var dzs_new = cmul(fp, dzs); var dzs_new = cmul(fp, dzs);
@@ -459,13 +485,20 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
break; break;
} }
} }
let dc2 = dot(z - crit, z - crit);
if dc2 < cand_d2 {
cand_d2 = dc2;
z_cand = z;
mult2_cand = 1.0;
}
if n == check_at { if n == check_at {
if !period_hit { if !period_hit {
period_streak = 0u; period_streak = 0u;
} }
period_hit = false; period_hit = false;
z_saved = z; z_saved = z_cand;
mult2 = 1.0; mult2 = mult2_cand;
cand_d2 = 3.0e38;
check_at = check_at * 2u; check_at = check_at * 2u;
} }
} }