557 lines
19 KiB
Rust
557 lines
19 KiB
Rust
//! High-precision reference-orbit computation for perturbation rendering.
|
||
//!
|
||
//! We iterate the fractal's formula `Z_{n+1} = f(Z_n, C)` at high precision
|
||
//! (`dashu-float`), storing each `Z_n` as an `f32` pair. Every pixel is then
|
||
//! rendered on the GPU as a small `f32` delta from this orbit — that is what
|
||
//! makes deep zoom cheap. See `shaders/mandelbrot.wgsl` for the delta side; the
|
||
//! delta formula there must match the orbit formula here.
|
||
//!
|
||
//! The `(z0, c)` form serves both set types:
|
||
//! * Mandelbrot-set: `z0 = 0`, `c = view center` (the c-plane point per pixel).
|
||
//! * Julia-set: `z0 = view center`, `c = fractal constant` (fixed per view).
|
||
|
||
use super::kind::FractalKind;
|
||
use crate::view::{Big, big_from_f64};
|
||
|
||
/// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel
|
||
/// bailout so pixels escaping alongside the reference can still reach their
|
||
/// bailout before the stored orbit runs out.
|
||
const REFERENCE_ESCAPE_SQ: f64 = 1.0e10;
|
||
|
||
/// Compute the reference orbit `Z_0..Z_{len-1}` where `Z_0 = z0` and
|
||
/// `Z_{n+1} = f(Z_n, c)` for the given `kind` (and `power`, for Multibrot), up
|
||
/// to `max_iter` steps at `precision` bits. Each entry is `[re, im]` in f32.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn compute_reference(
|
||
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),
|
||
) -> Vec<[f32; 2]> {
|
||
let cr = c_re.clone().with_precision(precision).value();
|
||
let ci = c_im.clone().with_precision(precision).value();
|
||
|
||
let mut zr = z0_re.clone().with_precision(precision).value();
|
||
let mut zi = z0_im.clone().with_precision(precision).value();
|
||
// Previous iterate, for the Phoenix two-term recurrence (Y_{-1} = 0).
|
||
let mut zr_prev = big_zero(precision);
|
||
let mut zi_prev = big_zero(precision);
|
||
// Phoenix distortion constant `p` (a small fixed complex number).
|
||
let pr = big_from_f64(phoenix_p.0, precision);
|
||
let pi = big_from_f64(phoenix_p.1, precision);
|
||
// Lambda distortion constant `l` (a small fixed complex number).
|
||
let lr = big_from_f64(lambda_l.0, precision);
|
||
let li = big_from_f64(lambda_l.1, precision);
|
||
// Complex Multibrot exponent (a fixed complex number).
|
||
let cpow_re = big_from_f64(complex_power.0, precision);
|
||
let cpow_im = big_from_f64(complex_power.1, precision);
|
||
|
||
let mut points: Vec<[f32; 2]> = Vec::with_capacity(max_iter as usize + 1);
|
||
|
||
for _ in 0..=max_iter {
|
||
let fr = zr.to_f64().value() as f32;
|
||
let fi = zi.to_f64().value() as f32;
|
||
points.push([fr, fi]);
|
||
|
||
let mag = (fr as f64) * (fr as f64) + (fi as f64) * (fi as f64);
|
||
if mag > REFERENCE_ESCAPE_SQ {
|
||
break;
|
||
}
|
||
|
||
let (new_zr, new_zi) = match kind {
|
||
FractalKind::Mandelbrot => {
|
||
// Z^2 = (zr^2 - zi^2) + (2 zr zi) i.
|
||
let re = &zr.sqr() - &zi.sqr() + &cr;
|
||
let im = ((&zr * &zi) << 1) + &ci; // << 1 is exact ×2 in base 2
|
||
(re, im)
|
||
}
|
||
FractalKind::BurningShip => {
|
||
// (|zr| + i|zi|)^2 = (zr^2 - zi^2) + 2|zr zi| i.
|
||
let re = &zr.sqr() - &zi.sqr() + &cr;
|
||
let im = big_abs((&zr * &zi) << 1) + &ci;
|
||
(re, im)
|
||
}
|
||
FractalKind::Tricorn => {
|
||
// conj(z)^2 = (zr^2 - zi^2) - 2 zr zi i.
|
||
let re = &zr.sqr() - &zi.sqr() + &cr;
|
||
let im = &ci - ((&zr * &zi) << 1);
|
||
(re, im)
|
||
}
|
||
FractalKind::Multibrot => {
|
||
let (pr, pi) = complex_pow(&zr, &zi, power.max(2), precision);
|
||
(pr + &cr, pi + &ci)
|
||
}
|
||
FractalKind::Celtic => {
|
||
// |Re(z^2)| + i·Im(z^2): abs the real output of the square.
|
||
let re = big_abs(&zr.sqr() - &zi.sqr()) + &cr;
|
||
let im = ((&zr * &zi) << 1) + &ci;
|
||
(re, im)
|
||
}
|
||
FractalKind::Perpendicular => {
|
||
// (x^2 - y^2) - 2·x·|y| i: abs the imaginary input.
|
||
let re = &zr.sqr() - &zi.sqr() + &cr;
|
||
let im = &ci - ((&zr * &big_abs(zi.clone())) << 1);
|
||
(re, im)
|
||
}
|
||
FractalKind::Buffalo => {
|
||
// |Re(z^2)| - |Im(z^2)| i: abs both outputs.
|
||
let re = big_abs(&zr.sqr() - &zi.sqr()) + &cr;
|
||
let im = &ci - big_abs((&zr * &zi) << 1);
|
||
(re, im)
|
||
}
|
||
FractalKind::Phoenix => {
|
||
// z^2 + c + p·z_{n-1}.
|
||
let re2 = &zr.sqr() - &zi.sqr();
|
||
let im2 = (&zr * &zi) << 1;
|
||
let pzr = &pr * &zr_prev - &pi * &zi_prev;
|
||
let pzi = &pr * &zi_prev + &pi * &zr_prev;
|
||
(re2 + &cr + pzr, im2 + &ci + pzi)
|
||
}
|
||
FractalKind::Lambda => {
|
||
// λ·z(1 - z): logistic map.
|
||
let re2 = 1 - &zr;
|
||
let im2 = -&zi;
|
||
let lzr = &lr * &zr - &li * &zi;
|
||
let lzi = &lr * &zi + &li * &zr;
|
||
(&lzr * &re2 - &lzi * &im2, re2 * lzi + lzr * im2)
|
||
}
|
||
FractalKind::ComplexMultibrot => {
|
||
let (pr, pi) = complex_pow_complex(&zr, &zi, &cpow_re, &cpow_im, precision);
|
||
(pr + &cr, pi + &ci)
|
||
}
|
||
};
|
||
|
||
// Shift the previous iterate (only the Phoenix arm reads it).
|
||
zr_prev = zr;
|
||
zi_prev = zi;
|
||
zr = new_zr.with_precision(precision).value();
|
||
zi = new_zi.with_precision(precision).value();
|
||
}
|
||
|
||
points
|
||
}
|
||
|
||
fn big_zero(precision: usize) -> Big {
|
||
Big::from(0i32).with_precision(precision).value()
|
||
}
|
||
|
||
/// Absolute value of a `Big`. The sign check via f64 is exact except for values
|
||
/// so tiny that |x| ≈ x either way — negligible against the f32 orbit storage.
|
||
fn big_abs(x: Big) -> Big {
|
||
if x.to_f64().value() < 0.0 { -x } else { x }
|
||
}
|
||
|
||
/// `(zr + i zi)^power` by repeated complex multiply at `precision` bits.
|
||
fn complex_pow(zr: &Big, zi: &Big, power: u32, precision: usize) -> (Big, Big) {
|
||
let mut rr = Big::from(1i32).with_precision(precision).value();
|
||
let mut ri = big_zero(precision);
|
||
for _ in 0..power {
|
||
// (rr + i ri)(zr + i zi) = (rr zr - ri zi) + (rr zi + ri zr) i.
|
||
let nr = (&rr * zr - &ri * zi).with_precision(precision).value();
|
||
let ni = (&rr * zi + &ri * zr).with_precision(precision).value();
|
||
rr = nr;
|
||
ri = ni;
|
||
}
|
||
(rr, ri)
|
||
}
|
||
|
||
/// `true` if `x` is (numerically) zero. The f64 check is exact for a true
|
||
/// zero; only matters here to special-case `ln(0)`.
|
||
fn is_big_zero(x: &Big) -> bool {
|
||
x.to_f64().value() == 0.0
|
||
}
|
||
|
||
/// `(zr + i zi)^(pr + i pi)` for a complex exponent, via the principal branch
|
||
/// `z^p = exp(p·ln z)` where `ln z = ln|z| + i·arg(z)`. Used by
|
||
/// `ComplexMultibrot`; must be kept in sync with the shader's `cpow`.
|
||
/// `z = 0` is special-cased to `0` (the formula's `ln(0)` would otherwise
|
||
/// panic; this is the correct limit for the `Re(p) > 0` region the UI
|
||
/// exposes).
|
||
fn complex_pow_complex(zr: &Big, zi: &Big, pr: &Big, pi: &Big, precision: usize) -> (Big, Big) {
|
||
if is_big_zero(zr) && is_big_zero(zi) {
|
||
return (big_zero(precision), big_zero(precision));
|
||
}
|
||
let r2 = &zr.sqr() + &zi.sqr();
|
||
let ln_r = r2.ln() >> 1; // 0.5 * ln(r2) = ln(sqrt(r2)); exact halving.
|
||
let theta = zi.atan2(zr);
|
||
let exp_re = (pr * &ln_r - pi * &theta).with_precision(precision).value();
|
||
let exp_im = (pr * &theta + pi * &ln_r).with_precision(precision).value();
|
||
let mag = exp_re.exp();
|
||
let (sin_a, cos_a) = exp_im.sin_cos();
|
||
(&mag * &cos_a, &mag * &sin_a)
|
||
}
|
||
|
||
/// Convenience: parameter-plane ("Mandelbrot-set") reference (`z0 = 0`,
|
||
/// `c = center`) for any `kind`.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn compute_set_reference(
|
||
center_re: &Big,
|
||
center_im: &Big,
|
||
max_iter: u32,
|
||
precision: usize,
|
||
kind: FractalKind,
|
||
power: u32,
|
||
phoenix_p: (f64, f64),
|
||
lambda_l: (f64, f64),
|
||
complex_power: (f64, f64),
|
||
) -> Vec<[f32; 2]> {
|
||
let zero = big_zero(precision);
|
||
compute_reference(
|
||
&zero,
|
||
&zero,
|
||
center_re,
|
||
center_im,
|
||
max_iter,
|
||
precision,
|
||
kind,
|
||
power,
|
||
phoenix_p,
|
||
lambda_l,
|
||
complex_power,
|
||
)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// The high-precision reference must agree with a plain f64 iteration for a
|
||
/// shallow point (where f64 is accurate).
|
||
#[test]
|
||
fn reference_matches_naive_f64() {
|
||
let cr = Big::try_from(-0.75_f64).unwrap();
|
||
let ci = Big::try_from(0.1_f64).unwrap();
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::Mandelbrot,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
// Independent naive f64 orbit.
|
||
let (c_re, c_im) = (-0.75_f64, 0.1_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
for point in &points {
|
||
// Tolerance is relative to magnitude: f32 storage only keeps ~7
|
||
// significant figures.
|
||
let tol_re = 1e-4 * (1.0 + zr.abs());
|
||
let tol_im = 1e-4 * (1.0 + zi.abs());
|
||
assert!(
|
||
(point[0] as f64 - zr).abs() < tol_re,
|
||
"re mismatch: {point:?} vs {zr}"
|
||
);
|
||
assert!(
|
||
(point[1] as f64 - zi).abs() < tol_im,
|
||
"im mismatch: {point:?} vs {zi}"
|
||
);
|
||
let nzr = zr * zr - zi * zi + c_re;
|
||
let nzi = 2.0 * zr * zi + c_im;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// A point inside the main cardioid never escapes: full-length orbit.
|
||
#[test]
|
||
fn interior_orbit_runs_full_length() {
|
||
let cr = Big::try_from(-0.2_f64).unwrap();
|
||
let ci = Big::try_from(0.0_f64).unwrap();
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
500,
|
||
120,
|
||
FractalKind::Mandelbrot,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
assert_eq!(points.len(), 501, "interior orbit should not escape");
|
||
}
|
||
|
||
/// Burning Ship reference matches a naive f64 iteration of the same formula.
|
||
#[test]
|
||
fn burning_ship_reference_matches_naive_f64() {
|
||
let cr = Big::try_from(-1.75_f64).unwrap();
|
||
let ci = Big::try_from(-0.03_f64).unwrap();
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::BurningShip,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
let (c_re, c_im) = (-1.75_f64, -0.03_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||
let nzr = zr * zr - zi * zi + c_re;
|
||
let nzi = 2.0 * (zr * zi).abs() + c_im;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// Multibrot (power 3) reference matches a naive f64 cube iteration.
|
||
#[test]
|
||
fn multibrot3_reference_matches_naive_f64() {
|
||
let cr = Big::try_from(0.3_f64).unwrap();
|
||
let ci = Big::try_from(0.2_f64).unwrap();
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::Multibrot,
|
||
3,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
let (c_re, c_im) = (0.3_f64, 0.2_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||
// z^3 = z * z^2.
|
||
let (r2, i2) = (zr * zr - zi * zi, 2.0 * zr * zi);
|
||
let nzr = zr * r2 - zi * i2 + c_re;
|
||
let nzi = zr * i2 + zi * r2 + c_im;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// Julia orbit (fixed c, z0 = center) matches a naive f64 iteration.
|
||
#[test]
|
||
fn julia_reference_matches_naive_f64() {
|
||
let z0_re = Big::try_from(0.15_f64).unwrap();
|
||
let z0_im = Big::try_from(-0.1_f64).unwrap();
|
||
let c_re = Big::try_from(-0.8_f64).unwrap();
|
||
let c_im = Big::try_from(0.156_f64).unwrap();
|
||
let points = compute_reference(
|
||
&z0_re,
|
||
&z0_im,
|
||
&c_re,
|
||
&c_im,
|
||
60,
|
||
200,
|
||
FractalKind::Mandelbrot,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
let (mut zr, mut zi) = (0.15_f64, -0.1_f64);
|
||
let (cr, ci) = (-0.8_f64, 0.156_f64);
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol);
|
||
assert!((point[1] as f64 - zi).abs() < tol);
|
||
let nzr = zr * zr - zi * zi + cr;
|
||
let nzi = 2.0 * zr * zi + ci;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// Celtic reference matches a naive f64 iteration: real = |x^2 - y^2| + cr.
|
||
#[test]
|
||
fn celtic_reference_matches_naive_f64() {
|
||
let cr = Big::try_from(-0.6_f64).unwrap();
|
||
let ci = Big::try_from(0.4_f64).unwrap();
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::Celtic,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
let (c_re, c_im) = (-0.6_f64, 0.4_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||
let nzr = (zr * zr - zi * zi).abs() + c_re;
|
||
let nzi = 2.0 * zr * zi + c_im;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// Perpendicular reference matches a naive f64 iteration:
|
||
/// real = x^2 - y^2 + cr, imag = -2·x·|y| + ci.
|
||
#[test]
|
||
fn perpendicular_reference_matches_naive_f64() {
|
||
let cr = Big::try_from(-0.7_f64).unwrap();
|
||
let ci = Big::try_from(-0.2_f64).unwrap();
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::Perpendicular,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
let (c_re, c_im) = (-0.7_f64, -0.2_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||
let nzr = zr * zr - zi * zi + c_re;
|
||
let nzi = -2.0 * zr * zi.abs() + c_im;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// Buffalo reference matches a naive f64 iteration:
|
||
/// real = |x^2 - y^2| + cr, imag = -|2·x·y| + ci.
|
||
#[test]
|
||
fn buffalo_reference_matches_naive_f64() {
|
||
let cr = Big::try_from(-1.2_f64).unwrap();
|
||
let ci = Big::try_from(-0.35_f64).unwrap();
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::Buffalo,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
let (c_re, c_im) = (-1.2_f64, -0.35_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||
let nzr = (zr * zr - zi * zi).abs() + c_re;
|
||
let nzi = -(2.0 * zr * zi).abs() + c_im;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// Phoenix reference matches a naive f64 two-term iteration
|
||
/// `z_{n+1} = z_n^2 + c + p·z_{n-1}` (z_0 = 0, z_{-1} = 0).
|
||
#[test]
|
||
fn phoenix_reference_matches_naive_f64() {
|
||
let cr = Big::try_from(0.5667_f64).unwrap();
|
||
let ci = Big::try_from(0.0_f64).unwrap();
|
||
let p = (-0.5_f64, 0.0_f64);
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::Phoenix,
|
||
2,
|
||
p,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
);
|
||
|
||
let (c_re, c_im) = (0.5667_f64, 0.0_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
let (mut pr, mut pi) = (0.0_f64, 0.0_f64); // previous iterate
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||
// p·z_{n-1} = (p.0 + i p.1)(pr + i pi).
|
||
let pzr = p.0 * pr - p.1 * pi;
|
||
let pzi = p.0 * pi + p.1 * pr;
|
||
let nzr = zr * zr - zi * zi + c_re + pzr;
|
||
let nzi = 2.0 * zr * zi + c_im + pzi;
|
||
pr = zr;
|
||
pi = zi;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
|
||
/// Complex Multibrot (power 2.5 + 0.3i) reference matches a naive f64
|
||
/// iteration of `z^p = exp(p·ln z)`.
|
||
#[test]
|
||
fn complex_multibrot_reference_matches_naive_f64() {
|
||
let cr = Big::try_from(0.1_f64).unwrap();
|
||
let ci = Big::try_from(-0.2_f64).unwrap();
|
||
let power = (2.5_f64, 0.3_f64);
|
||
let points = compute_set_reference(
|
||
&cr,
|
||
&ci,
|
||
60,
|
||
200,
|
||
FractalKind::ComplexMultibrot,
|
||
2,
|
||
(0.0, 0.0),
|
||
(0.0, 0.0),
|
||
power,
|
||
);
|
||
|
||
// Naive f64 complex power via z^p = exp(p * ln z), ln z = ln|z| + i*arg(z).
|
||
fn naive_cpow(zr: f64, zi: f64, pr: f64, pi: f64) -> (f64, f64) {
|
||
if zr == 0.0 && zi == 0.0 {
|
||
return (0.0, 0.0);
|
||
}
|
||
let ln_r = 0.5 * (zr * zr + zi * zi).ln();
|
||
let theta = zi.atan2(zr);
|
||
let exp_re = pr * ln_r - pi * theta;
|
||
let exp_im = pr * theta + pi * ln_r;
|
||
let mag = exp_re.exp();
|
||
(mag * exp_im.cos(), mag * exp_im.sin())
|
||
}
|
||
|
||
let (c_re, c_im) = (0.1_f64, -0.2_f64);
|
||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||
for point in &points {
|
||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||
let (pr, pi) = naive_cpow(zr, zi, power.0, power.1);
|
||
let nzr = pr + c_re;
|
||
let nzi = pi + c_im;
|
||
zr = nzr;
|
||
zi = nzi;
|
||
}
|
||
}
|
||
}
|