spacr.qt.widgets.fractal_mandelbrot

A continuous deep zoom into one point on the Mandelbrot boundary.

The fourth spaceout pattern. The viewport is recomputed every frame – no previous frame is reused as image data – and the only camera motion is zoom.

WHY IT NEEDS MORE THAN A SHADER. Past about fifteen decades a double has no bits left to tell neighbouring pixels apart, and the picture dissolves into blocks. This uses PERTURBATION around one high-precision reference orbit:

dz[n+1] = 2*Z[n]*dz[n] + dz[n]^2 + dc

Z is iterated once in arbitrary precision and handed to the renderer; every pixel then iterates only its small OFFSET from it, in the precision the hardware actually has. That is what buys hundreds of decades from float32.

THE TARGET IS A MISIUREWICZ POINT, preperiod 4 period 1, refined at startup by solving f_c^5(0) = f_c^4(0). It has to be ON the boundary: an interior point fades to flat colour as you descend into it and an exterior one escapes, and either way the zoom stops finding anything. A boundary point keeps revealing structure at any magnification.

Classes

ReferenceOrbit

Z[n] for one centre, in a form both renderers can use.

SteeringCamera

Where the dive is pointed, and how it gets there.

Functions

a_more_interesting_anchor(orbit[, budget, candidates])

Pick a point the dive will still find structure at, once, up front.

best_reference_in_view(orbit, offset_re, offset_im, ...)

The point in the current view that makes the best reference.

boundary_mask(→ numpy.ndarray)

Bounded points that touch an escaping one.

candidate_score(→ float)

How interesting the neighbourhood of one point is.

depth_after_restart(→ float)

Where the dive is, having started again if it reached the end.

depth_decades(→ float)

How many decades of magnification seconds of flight is worth.

eased(→ float)

Smoothstep, for a camera move that starts and stops gently.

exact_misiurewicz_center([digits])

Refine the boundary target to digits decimal places.

iteration_budget(→ int)

How many iterations a given depth needs.

perturbation_escape_map(orbit, width, height, scale, ...)

A low-resolution map of what escapes and how fast.

plan_guided_step(orbit, scale, max_iter[, strength, ...])

Choose where the dive should head next.

rebased_orbit(orbit, dx, dy[, digits, max_iter])

A new reference at (dx, dy) from the current one.

scale_at(→ float)

The viewport's half-height at depth decades.

steering_from_one_number(→ dict)

Turn one "how much does it wander" control into three numbers.

structure_mask(→ numpy.ndarray)

Where the picture has detail worth steering toward.

Module Contents

class spacr.qt.widgets.fractal_mandelbrot.ReferenceOrbit(max_iter: int = 2200, digits: int = 320, center=None)[source]

Z[n] for one centre, in a form both renderers can use.

Parameters:
  • max_iter – how many points to iterate.

  • digits – working precision.

  • center – the centre; refined from the Misiurewicz guess when omitted.

BUILD IT OFF THE GUI THREAD. Iterating a few thousand points at 320 decimal digits takes seconds, and the backdrop has to keep drawing while it happens.

Iterate the reference orbit at high precision and pack it for the shader.

Build it off the GUI thread: iterating a few thousand points at 320 decimal digits takes seconds, and the backdrop has to keep drawing while it happens.

Parameters:
  • max_iter – how many points to iterate; clamped to the hard maximum.

  • digits – working precision, in decimal digits.

  • center – the centre; refined from the Misiurewicz guess when omitted.

property is_bounded: bool[source]

Whether the orbit stayed bounded for its whole length.

class spacr.qt.widgets.fractal_mandelbrot.SteeringCamera(strength: float = 0.09, interval: float = 0.4, duration: float = 3.8, seconds_per_decade: float = 24.0)[source]

Where the dive is pointed, and how it gets there.

A PLAIN OBJECT WITH NO QT IN IT, on purpose. This logic used to live inside the GPU canvas’s __init__, which meant it could only be exercised by building a GL context – so every claim about how smooth it was came from a SIMULATION written beside it rather than from the code that runs. Three “fixed” reports in a row were wrong that way. Here the real thing can be driven frame by frame and measured.

THE CAMERA FOLLOWS; IT DOES NOT MAKE MOVES. Easing from A to B over a few seconds and then holding until the next target is chosen makes the picture slide, stop, slide, stop – and that alternation is what reads as jerking. Instead it eases toward wherever the target currently is at a constant rate: choosing a new target moves the target, and nothing starts or stops.

Parameters:
  • strength – how far off centre to look; 0 means do not steer.

  • interval – decades of descent between one target and the next.

  • duration – the follow’s time constant, in seconds.

  • seconds_per_decade – how fast the descent runs.

Create the camera at the origin with no target yet.

Parameters:
  • strength – how far off centre to look; 0 does not steer.

  • interval – decades of descent between one target and the next.

  • duration – the follow’s time constant, in seconds.

  • seconds_per_decade – how fast the descent runs.

advance(now: float) → tuple[source]

Move toward the target, and answer where the camera is.

Parameters:

now – a monotonic clock in seconds.

Returns:

the centre, as (re, im).

An exponential approach: it covers the same FRACTION of whatever distance remains every tick, so it is quickest when furthest away and gentle as it arrives. There is no moment it starts and none it stops.

A frame that arrives very late – the machine slept, or a run took the CPU – is treated as a short one, because a single huge step would put the camera at the target instantly and look like a cut.

aim_at(offset, depth: float, scale: float) → None[source]

Point at offset, given in screen units at scale.

Parameters:
  • offset – (dx, dy) from plan_guided_step, or None when nothing was found – which asks again sooner rather than giving up on steering for the rest of the dive.

  • depth – current zoom depth in decades; the next steering deadline is scheduled relative to it.

  • scale – current viewport half-height, which converts the screen-unit offset into a complex-plane displacement.

configure(strength: float, interval: float, duration: float, seconds_per_decade: float) → None[source]

Reconcile the three numbers, wherever they came from.

Deriving them from one control stops the panel producing a contradiction; it does not stop an older install or a settings file carrying one. A move longer than the gap before the next re-targets the camera before it has settled, which is the reported jerking, so the duration is bounded here as well as there.

Parameters:
  • strength – how far off centre to look; 0 means do not steer. Negative values become 0.

  • interval – decades of descent between one target and the next; floored at 0.01.

  • duration – the follow’s time constant, in seconds; capped at 0.45 x interval x seconds_per_decade and then floored at 0.5.

  • seconds_per_decade – seconds of flight per decade of magnification; floored at 0.1.

drag(dx: float, dy: float, span: float, depth: float) → tuple[source]

Move the view by hand, and stop chasing the current target.

A drag is a decision; leaving the target in place would have the camera pull back toward it and fight the hand.

Parameters:
  • dx – horizontal pointer movement since the last frame, in the pointer’s -1..1 widget space.

  • dy – vertical pointer movement since the last frame, in the same space.

  • span – the viewport half-height at the current depth, as scale_at() gives it; the centre moves by the movement times span.

  • depth – the current depth in decades; the next re-target waits until depth plus the camera’s interval.

restart() → None[source]

Back to the anchor, as a restart of the dive requires.

A restart that kept the course would begin at the surface already pointed a whole descent of steering away from the centre.

wants_a_target(depth: float) → bool[source]

Whether it is time to choose somewhere new to head.

Parameters:

depth – the current depth in decades; True once steering is on and it has reached the scheduled next re-target.

property steering: bool[source]

Whether it will steer at all.

A strength of zero means DO NOT STEER. With no reach there is no direction to look in, so every choice is arbitrary – which is what “moves every second in a random direction” was.

spacr.qt.widgets.fractal_mandelbrot.a_more_interesting_anchor(orbit, budget: int = 600, candidates: int = 64)[source]

Pick a point the dive will still find structure at, once, up front.

Parameters:
  • orbit – the reference orbit to look around.

  • budget – iterations for the survey.

  • candidates – how many points to score at the surface.

Returns:

(dx, dy) in screen units, or None if nothing is found.

SURFACE STRUCTURE IS NOT ENOUGH, and scoring only that is how the first attempt “focuses into a monocolor area”: a point can sit on a busy edge at the starting scale and be flat a few decades in, because the edge was a boundary of something the dive immediately passes through.

So every candidate is CHECKED AT DEPTH. The best few by surface score are surveyed again at a hundredth of the scale, and the one that still varies there is chosen. That is a direct test of the thing that matters: will there be anything to look at after the descent.

The choice happens once, before anything moves, so the dive is exactly as steady as a fixed path – because it is one.

spacr.qt.widgets.fractal_mandelbrot.best_reference_in_view(orbit, offset_re: float, offset_im: float, scale: float, max_iter: int)[source]

The point in the current view that makes the best reference.

Parameters:
  • orbit – the reference the view is currently drawn against.

  • offset_re – where the camera sits, relative to that reference.

  • offset_im – imaginary component of that same camera displacement.

  • scale – the viewport’s half-height.

  • max_iter – escape-iteration budget used to distinguish bounded, boundary, and longest-surviving survey points.

Returns:

(dx, dy) relative to the CURRENT reference, or None.

A REFERENCE HAS TO BE IN THE SET. Perturbation measures every pixel as a small offset from one orbit, and that orbit has to stay bounded for as many iterations as the frame runs – so it must be a point of the Mandelbrot set, not merely somewhere near it.

That is what breaks when the view is dragged. A reference placed 0.3 from the anchor escapes at iteration SIX, and the detail in the dragged view falls to nothing – the picture pixelates within a minute where a fixed camera stayed sharp for many. The camera had walked away from the only point the maths was anchored to.

So after a drag the reference moves too, onto the longest-surviving point in the new view: bounded if there is one, and otherwise whatever escapes last, which is the nearest thing to the set the view contains.

spacr.qt.widgets.fractal_mandelbrot.boundary_mask(escaped: numpy.ndarray) → numpy.ndarray[source]

Bounded points that touch an escaping one.

THE BOUNDARY IS WHERE THE STRUCTURE IS. An interior point fades to flat colour as you descend into it; an exterior one escapes and the frame empties. Only the edge keeps producing detail at every magnification.

The frame’s own edge is excluded: a point there may look like a boundary only because the map stopped.

Parameters:

escaped – 2-D boolean map, True where the point escaped.

spacr.qt.widgets.fractal_mandelbrot.candidate_score(escaped, iterations, row, col, max_iter) → float[source]

How interesting the neighbourhood of one point is.

Three things, because none alone is enough: how often the escape answer CHANGES across the patch (detail), how much the escape TIME varies (depth of structure), and how BALANCED bounded and escaping are (an edge, rather than a speck in a field of one or the other).

Parameters:
  • escaped – 2-D boolean map, True where the point escaped.

  • iterations – escape-time map of the same shape as escaped.

  • row – row index of the point; the 7 x 7 patch around it, clipped to the map, is scored.

  • col – column index of the point.

  • max_iter – the iteration budget the escape times are divided by.

spacr.qt.widgets.fractal_mandelbrot.depth_after_restart(depth: float, max_depth: float = MAX_USEFUL_DEPTH) → float[source]

Where the dive is, having started again if it reached the end.

Parameters:
  • depth – decades descended so far.

  • max_depth – how deep it may go; see MAX_USEFUL_DEPTH.

Returns:

a depth within range.

A RESTART, NOT A STOP. The alternative is a backdrop that spends fourteen minutes getting somewhere and then holds a black frame for the rest of the session, which reads as the application having died. Going back to the surface and descending again is what the pattern is for.

Wrapped with a modulo rather than reset to zero on a comparison, so a frame that arrives late – the machine was asleep, or a run took the CPU – lands where it should instead of skipping a whole descent.

spacr.qt.widgets.fractal_mandelbrot.depth_decades(seconds: float, zoom_rate: float = 1.0, seconds_per_decade: float = 24.0) → float[source]

How many decades of magnification seconds of flight is worth.

Parameters:
  • seconds – seconds of flight; the result is never negative.

  • zoom_rate – multiplier on the descent speed.

  • seconds_per_decade – seconds of flight per decade at a zoom_rate of 1.0.

spacr.qt.widgets.fractal_mandelbrot.eased(fraction: float) → float[source]

Smoothstep, for a camera move that starts and stops gently.

A linear move between two points is a lurch at both ends; this is the difference between the camera being steered and being teleported.

Parameters:

fraction – progress through the move; values outside 0..1 are clamped.

spacr.qt.widgets.fractal_mandelbrot.exact_misiurewicz_center(digits: int = 320)[source]

Refine the boundary target to digits decimal places.

Parameters:

digits – working precision for the solve.

Returns:

an mpmath.mpc on the Mandelbrot boundary.

Raises:

RuntimeError – when mpmath is not installed.

Solves f_c^5(0) = f_c^4(0), which is the defining equation of a Misiurewicz point of preperiod 4 and period 1. Newton is given two nearby starting points rather than one, because the derivative of a fifth iterate is stiff enough that a single-point secant wanders.

spacr.qt.widgets.fractal_mandelbrot.iteration_budget(depth: float, base: int = 300, per_decade: float = 55.0, ceiling: int = 2200) → int[source]

How many iterations a given depth needs.

Parameters:
  • depth – the depth in decades.

  • base – iterations at depth 0, and the minimum.

  • per_decade – iterations added per decade of depth.

  • ceiling – the maximum.

Returns:

at least base and at most ceiling.

DEEPER NEEDS MORE. Near the boundary the escape time grows with magnification, so a fixed budget draws the deep frames as solid interior – the picture stops changing and looks broken rather than deep.

spacr.qt.widgets.fractal_mandelbrot.perturbation_escape_map(orbit, width, height, scale, max_iter, offset_re=0.0, offset_im=0.0)[source]

A low-resolution map of what escapes and how fast.

Parameters:
  • orbit – the reference ReferenceOrbit.

  • width – number of horizontal samples in the survey grid.

  • height – number of vertical samples in the survey grid.

  • scale – the viewport’s half-height.

  • max_iter – requested escape-iteration ceiling; evaluation is also capped by the number of points available in orbit.

Returns:

(escaped, iterations), both (height, width).

Vectorised over the whole grid rather than looped per pixel: this runs while the backdrop is drawing, and a Python loop over 5,184 points times 900 iterations is a second of held GIL.

spacr.qt.widgets.fractal_mandelbrot.plan_guided_step(orbit, scale, max_iter, strength=0.09, candidates=24, step_index=0, offset_re=0.0, offset_im=0.0)[source]

Choose where the dive should head next.

Parameters:
  • orbit – reference orbit against which the current neighbourhood is surveyed.

  • scale – current viewport half-height, used to translate the survey into perturbations around orbit.

  • max_iter – escape-iteration budget for deciding which structures survive at this depth.

  • strength – how far off centre to look, in screen units.

  • candidates – how many directions to try.

  • step_index – which step this is; rotates the search.

Returns:

(dx, dy, score) in screen units, or None when the view holds no boundary at all.

THE DIRECTIONS ARE SPREAD BY THE GOLDEN ANGLE and rotated per step, so consecutive choices do not favour one side of the frame – which is what makes a “guided” path that always drifts the same way.

spacr.qt.widgets.fractal_mandelbrot.rebased_orbit(orbit, dx: float, dy: float, digits: int = 320, max_iter: int = 2200)[source]

A new reference at (dx, dy) from the current one.

Parameters:
  • orbit – the current ReferenceOrbit; its centre is the origin of the offset.

  • dx – real-axis offset of the new centre from the current one.

  • dy – imaginary-axis offset of the new centre from the current one.

  • digits – working precision in decimal digits; it is also set as mpmath’s global precision.

  • max_iter – iteration budget of the new orbit; a candidate that escapes before 90 % of it is refused.

Returns:

(centre, orbit), or (None, None) when the result escapes too early to be usable.

A REFERENCE THAT ESCAPES IS NOT ONE, so a candidate that does not survive most of the iteration budget is refused and the caller keeps what it has: a poor reference draws noise, where an old one merely draws a view that is off centre.

spacr.qt.widgets.fractal_mandelbrot.scale_at(depth: float, initial_scale: float = 1.25) → float[source]

The viewport’s half-height at depth decades.

Clamped at 307 decades, which is where a float64 underflows – past it the scale would silently become zero and every pixel would sample the same point.

Parameters:
  • depth – the depth in decades; values past 307 are treated as 307.

  • initial_scale – the half-height at depth 0.

spacr.qt.widgets.fractal_mandelbrot.steering_from_one_number(steering: float, seconds_per_decade: float) → dict[source]

Turn one “how much does it wander” control into three numbers.

Parameters:
  • steering – 0 (straight down) to 1 (restless).

  • seconds_per_decade – how fast the dive descends, which is what turns an interval in decades into an interval in seconds.

Returns:

{"steering_strength", "steering_interval_decades", "steering_duration"}.

THREE NUMBERS THAT MUST AGREE. Set independently they can contradict each other: a short interval can re-target faster than a long move can finish, leaving the camera permanently mid-course-correction and making even minimal steering look jerky.

Derived together they cannot disagree. The move always takes a fixed FRACTION of the interval – never more than half – so there is always as much settled time as moving time whatever the control says. That is what makes the low end calm rather than twitchy: at zero it simply stops steering, and the way down to it is gentler moves further apart, not the same moves crammed together.

spacr.qt.widgets.fractal_mandelbrot.structure_mask(escaped: numpy.ndarray, iterations: numpy.ndarray, max_iter: int) → numpy.ndarray[source]

Where the picture has detail worth steering toward.

Parameters:
  • escaped – 2-D boolean map, True where the point escaped.

  • iterations – escape-time map of the same shape as escaped.

  • max_iter – the iteration budget the escape times are divided by.

Returns:

a boolean map the same shape as escaped.

SET MEMBERSHIP IS NOT ENOUGH ONCE THE ZOOM IS DEEP. Around a Misiurewicz point the set is measure-zero: measured on a 96x54 map at a scale of 1.25e-3, every pixel escaped and boundary_mask() found NOTHING, so the guided path stopped steering after two decades and the dive went straight down again.

What is still there is the escape TIME, and its level sets are the filaments the picture is made of. A pixel whose escape time differs sharply from its neighbours sits on one of those edges – which is where detail survives at any magnification, and is what the eye reads as structure.

The true boundary is preferred where it exists, because a bounded point beside an escaping one is the strongest evidence of an edge there is.

Nested helpers

_GlideCamera.consider._score_at(point) → float

The survey’s score at point, or 0 outside it.

Parameters:

point – (re, im) relative to the reference.

spacr/qt/widgets/fractal_mandelbrot.py:1116

exact_misiurewicz_center._iterate(c, n)

Iterate the map n times at arbitrary precision.

mpmath rather than float: a Misiurewicz point is found by Newton’s method on an orbit that is chaotic by construction, and double precision loses the point long before the iteration converges.

spacr/qt/widgets/fractal_mandelbrot.py:288