From fbf0ef64af065feee554e02ddc13b14de728c1d7 Mon Sep 17 00:00:00 2001 From: supersurviveur Date: Fri, 25 Sep 2026 10:06:01 +0200 Subject: [PATCH] fix: add c parameter again for lambda fractal in julia mode --- src/app.rs | 2 +- src/fractal/kind.rs | 4 +- src/fractal/reference.rs | 93 ++++++++++++++++------------------------ 3 files changed, 40 insertions(+), 59 deletions(-) diff --git a/src/app.rs b/src/app.rs index c81d824..c22103f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2142,7 +2142,7 @@ impl FractalApp { return; } - if self.mode == FractalMode::Julia && self.kind != FractalKind::Lambda { + if self.mode == FractalMode::Julia { ui.horizontal(|ui| { ui.label("c ="); ui.add( diff --git a/src/fractal/kind.rs b/src/fractal/kind.rs index 60b1e45..bdf8e17 100644 --- a/src/fractal/kind.rs +++ b/src/fractal/kind.rs @@ -26,7 +26,7 @@ pub enum FractalKind { Buffalo = 6, /// `z -> z^2 + c + p·z_{n-1}` (two-term recurrence; `p` is `phoenix_p`). Phoenix = 7, - /// `z -> lambda·z(1 - z)` (logistic map). + /// `z -> lambda·z(1 - z) + c` (logistic map). Lambda = 8, /// `z -> z^power + c`, where `power` is a complex constant (the /// `complex_power` argument), via the principal branch `z^p = exp(p·ln z)`. @@ -104,7 +104,7 @@ impl FractalKind { FractalKind::Perpendicular => "z = (x² − y²) − 2x|y|i + c".to_string(), FractalKind::Buffalo => "z = |Re(z²)| − i|Im(z²)| + c".to_string(), FractalKind::Phoenix => "z = z² + c + p·z_prev".to_string(), - FractalKind::Lambda => "z = λ·z(1 − z)".to_string(), + FractalKind::Lambda => "z = λ·z(1 − z) + c".to_string(), FractalKind::ComplexMultibrot => { format!("z = z^({:.3}{:+.3}i) + c", complex_power.0, complex_power.1) } diff --git a/src/fractal/reference.rs b/src/fractal/reference.rs index 1b66c44..457e984 100644 --- a/src/fractal/reference.rs +++ b/src/fractal/reference.rs @@ -59,41 +59,6 @@ pub fn compute_reference( lambda_l: (f64, f64), complex_power: (f64, f64), morph: Option<(FractalKind, f64)>, -) -> Vec<[f32; 2]> { - compute_reference_inner( - z0_re, - z0_im, - c_re, - c_im, - max_iter, - precision, - kind, - power, - phoenix_p, - lambda_l, - complex_power, - morph, - false, - ) -} - -/// [`compute_reference`] plus `set_plane` (see [`StepConsts::set_plane`]): -/// picks the `f64` fast path or the `FBig` path by precision. -#[allow(clippy::too_many_arguments)] -fn compute_reference_inner( - z0_re: &Big, - z0_im: &Big, - c_re: &Big, - c_im: &Big, - max_iter: u32, - precision: usize, - kind: FractalKind, - power: u32, - phoenix_p: (f64, f64), - lambda_l: (f64, f64), - complex_power: (f64, f64), - morph: Option<(FractalKind, f64)>, - set_plane: bool, ) -> Vec<[f32; 2]> { // A zero-weight morph is just the plain kind; skip the second formula. let morph = morph.filter(|&(_, w)| w != 0.0); @@ -104,7 +69,6 @@ fn compute_reference_inner( l: lambda_l, cpow: complex_power, power, - set_plane, }; return compute_reference_f64( (z0_re.to_f64().value(), z0_im.to_f64().value()), @@ -125,7 +89,6 @@ fn compute_reference_inner( cpow_im: big_from_f64(complex_power.1, precision), power, precision, - set_plane, }; compute_reference_big(z0_re, z0_im, max_iter, kind, &k, morph) } @@ -137,7 +100,6 @@ struct StepConstsF64 { l: (f64, f64), cpow: (f64, f64), power: u32, - set_plane: bool, } /// [`compute_reference`]'s fast path for shallow views (see @@ -204,16 +166,11 @@ fn step_f64( ) } FractalKind::Lambda => { - // λ·z(1 - z) (+ c on the parameter plane). + // λ·z(1 - z) + c. let (lr, li) = k.l; let (re2, im2) = (1.0 - zr, -zi); let (lzr, lzi) = (lr * zr - li * zi, lr * zi + li * zr); - let (re, im) = (lzr * re2 - lzi * im2, re2 * lzi + lzr * im2); - if k.set_plane { - (re + cr, im + ci) - } else { - (re, im) - } + (lzr * re2 - lzi * im2 + cr, re2 * lzi + lzr * im2 + ci) } FractalKind::ComplexMultibrot => { let (pr, pi) = complex_pow_complex_f64(zr, zi, k.cpow.0, k.cpow.1); @@ -250,9 +207,6 @@ struct StepConsts { cpow_im: Big, power: u32, precision: usize, - /// Parameter plane: the GPU adds `dc` every step for every kind, so the - /// Lambda map (which has no `c` of its own) is `λ·z(1 - z) + c` there. - set_plane: bool, } /// [`compute_reference`] at arbitrary precision (`FBig`), for deep views. @@ -369,18 +323,14 @@ fn step( (re2 + cr + pzr, im2 + ci + pzi) } FractalKind::Lambda => { - // λ·z(1 - z): logistic map (+ c on the parameter plane). + // λ·z(1 - z) + c: logistic map plus the usual additive `c`. let re2 = 1 - zr; let im2 = -zi; let lzr = &k.lr * zr - &k.li * zi; let lzi = &k.lr * zi + &k.li * zr; let re = &lzr * &re2 - &lzi * &im2; let im = re2 * lzi + lzr * im2; - if k.set_plane { - (re + cr, im + ci) - } else { - (re, im) - } + (re + cr, im + ci) } FractalKind::ComplexMultibrot => { let (pr, pi) = complex_pow_complex(zr, zi, &k.cpow_re, &k.cpow_im, k.precision); @@ -455,7 +405,7 @@ pub fn compute_set_reference( morph: Option<(FractalKind, f64)>, ) -> Vec<[f32; 2]> { let zero = big_zero(precision); - compute_reference_inner( + compute_reference( &zero, &zero, center_re, @@ -468,7 +418,6 @@ pub fn compute_set_reference( lambda_l, complex_power, morph, - true, ) } @@ -680,6 +629,38 @@ mod tests { } } + /// Lambda Julia orbit adds the Julia `c`: `z -> λ·z(1 - z) + c`. + #[test] + fn lambda_julia_reference_matches_naive_f64() { + let (lr, li) = (-0.5_f64, 0.2_f64); + let (cr, ci) = (0.1_f64, -0.3_f64); + let points = compute_reference( + &Big::try_from(0.2_f64).unwrap(), + &Big::try_from(0.1_f64).unwrap(), + &Big::try_from(cr).unwrap(), + &Big::try_from(ci).unwrap(), + 60, + 200, + FractalKind::Lambda, + 2, + (0.0, 0.0), + (lr, li), + (0.0, 0.0), + None, + ); + + let (mut zr, mut zi) = (0.2_f64, 0.1_f64); + for point in &points { + let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs())); + assert!((point[0] as f64 - zr).abs() < tol, "{point:?} vs {zr}"); + assert!((point[1] as f64 - zi).abs() < tol, "{point:?} vs {zi}"); + let (lzr, lzi) = (lr * zr - li * zi, lr * zi + li * zr); + let (ar, ai) = (1.0 - zr, -zi); + zr = lzr * ar - lzi * ai + cr; + zi = lzr * ai + lzi * ar + ci; + } + } + /// Celtic reference matches a naive f64 iteration: real = |x^2 - y^2| + cr. #[test] fn celtic_reference_matches_naive_f64() {