Zalgorithm

Creating Mandelbrot fractals with Processing

The Mandelbrot set is a two-dimensional set that is defined in the complex plane as the complex numbers cc for which the function fc(z)=z2+cf_c(z) = z^2 + c does not diverge to infinity when iterated starting at z=0z = 01

I understand that to mean:

The formula that’s used for determining if a complex number is in the Mandelbrot set #

The function is simpler than its description. Testing it in the Python REPL (IPython):

start with z=0z = 0:

In [9]: z = complex(0, 0)

In [10]: z
Out[10]: 0j

start with some value for cc (this would be a point from the complex plane in a real implementation):

In [11]: c = complex(2, 3)

In [12]: c
Out[12]: (2+3j)

update the value of zz using the current values of zz and cc:

In [13]: z = z * z + c

In [14]: z
Out[14]: (2+3j)

zz now equals the initial value of cc: znew=00+c=c=2+3jz_{\text{new}} = 0 * 0 + c = c = 2+3j

determine if cc (the initial value from the complex plane) is indicating it will diverge to infinity:

The test is to check if the magnitude (or absolute value) of cc exceeds some threshold. My (tentative) understanding is that if the magnitude of cc is greater than 22, it will eventually (possibly quickly (?)) approach infinity. (I’ll look into this more later.)

In [15]: abs(z)
Out[15]: 3.605551275463989

Since 3.6>23.6 > 2, cc (2+3j) is not in the Mandelbrot set.

I’ll find a value of cc that doesn’t exceed a magnitude of 2 on the first iteration:

In [20]: c = complex(1.3, 0.7)

In [21]: z = complex(0, 0)

In [22]: z = z * z + c

In [23]: abs(z)
Out[23]: 1.47648230602334

Since zz when starting with c=1.3+0.7jc = 1.3+0.7j doesn’t exceed 2 after the first iteration, it’s possible it’s in the Mandelbrot set. To find out, call the function again:

In [24]: c
Out[24]: (1.3+0.7j)  # the initial value of c (it doesn't get updated)

In [25]: z
Out[25]: (1.3+0.7j)  # the value of z that was set in the first iteration

In [26]: z = z * z + c  # update z

In [27]: abs(z)
Out[27]: 3.5497042130295866  # the magnitude of z exceeds 2.0

So 1.3+0.7j1.3+0.7j is also not in the Mandelbrot set.

c=0+0jc = 0+0j (the real/imaginary intersection of the complex plane) is guaranteed to be in the Mandelbrot set. Zero times zero plus zero will always equal zero:

In [28]: z = complex(0, 0)

In [29]: c = complex(0, 0)

In [30]: z = z * z + c

In [31]: abs(z)
Out[31]: 0.0

Testing some more interesting complex numbers #

c=0.5+0.5jc = -0.5+0.5j is a good case. The (Python) code below assigns the starting value of 0+0j0+0j to zz as usual, and assigns 0.5+0.5-0.5+0.5 to cc. It then tries running the z=zz+cz = z*z + c function 50 times, to see if the absolute value of zz exceeds 2.02.0:

In [42]: z = complex(0, 0)

In [43]: c = complex(-0.5, 0.5)

In [44]: for i in range(50):
    ...:     z = z*z+c
    ...:     print("iteration:", i, "z:", z)
    ...:     if abs(z) > 2.0:
    ...:         print(c, "is not in the Mandelbrot set")
    ...:         break
    ...:
iteration: 0 z: (-0.5+0.5j)
iteration: 1 z: (-0.5+0j)
iteration: 2 z: (-0.25+0.5j)
iteration: 3 z: (-0.6875+0.25j)
# ...
iteration: 45 z: (-0.5500488745365836+0.23115432457577173j)
iteration: 46 z: (-0.250878557391119+0.24570764784566523j)
iteration: 47 z: (-0.49743219765120045+0.3767144395370289j)
iteration: 48 z: (-0.3944749776955948+0.12522021690831092j)
iteration: 49 z: (-0.3600695946946244+0.401207515456113j)

Based on a test of 50 iterations, it can tentatively (in a real sense this time) be said that 0.5+0.5j-0.5+0.5j is in the Mandelbrot set. The values of zz on each iteration are bouncing around the origin 0+0j0+0j. The values are showing bounded chaotic behavior. EDIT: it seems that most of the numbers that fall withing the Mandelbrot set either resolve to a single number or enter into a non-chaotic period. See notes / Tracing how numbers change during the Mandelbrot iterations

Note that the magnitude of a complex number is the distance of the number from the origin of the complex plane. In Python I’m calculating it with abs(z). When a complex number is given as the argument to the Python abs function, the magnitude of the number is returned:

iteration: 49 z: (-0.3600695946946244+0.401207515456113j)

In [52]: abs(z)
Out[52]: 0.5390895876215921

The magnitude is essentially the hypotenuse of the number on the complex plane:

iteration: 49 z: (-0.3600695946946244+0.401207515456113j)

In [52]: abs(z)
Out[52]: 0.5390895876215921

In [53]: z.imag
Out[53]: 0.401207515456113

In [54]: z.real
Out[54]: -0.3600695946946244

In [55]: np.sqrt(z.real*z.real + z.imag*z.imag)  # the square root of a^2 + b^2 = c^2
Out[55]: np.float64(0.5390895876215921)

Returning the magnitudes from the Python script #

Here’s a similar script that returns the current magnitude of zz, starting from c=0.12+0.75jc = -0.12+0.75j:

In [78]: z = complex(0, 0)

In [79]: c = complex(-0.12, 0.75)

In [80]: for i in range(50):
    ...:     z = z * z + c
    ...:     print(f"iteration: {i}, abs(z): {abs(z)}")
    ...:     if abs(z) > 2.0:
    ...:         print(f"iteration: {i}; {c} is not in the Mandelbrot set")
    ...:         break
    ...:
    ...:
iteration: 0, abs(z): 0.7595393340703298
iteration: 1, abs(z): 0.8782127361863982
iteration: 2, abs(z): 0.011724955561199504
iteration: 3, abs(z): 0.7595269050363009
iteration: 4, abs(z): 0.8780252871118147
iteration: 5, abs(z): 0.011391498992753827
iteration: 6, abs(z): 0.7595241904450459
# ...
iteration: 44, abs(z): 0.011405972274973704
iteration: 45, abs(z): 0.7595245098608843
iteration: 46, abs(z): 0.8780333368437104
iteration: 47, abs(z): 0.011405972274973704
iteration: 48, abs(z): 0.7595245098608843
iteration: 49, abs(z): 0.8780333368437104

Based on 50 iterations, 0.12+0.75j-0.12+0.75j is in the Mandelbrot set.

A complex number that’s not in the Mandelbrot set #

c=0.8+0.2jc = -0.8+0.2j escapes the bounds of 2.02.0 after 15 iterations:

In [81]: z = complex(0, 0)

In [82]: c = complex(-0.8, 0.2)

In [83]: for i in range(50):
    ...:     z = z * z + c
    ...:     print(f"iteration: {i}, abs(z): {abs(z)}")
    ...:     if abs(z) > 2.0:
    ...:         print(f"iteration: {i}; {c} is not in the Mandelbrot set")
    ...:         break
    ...:
iteration: 0, abs(z): 0.8246211251235321
iteration: 1, abs(z): 0.233238075793812
iteration: 2, abs(z): 0.8131416604749754
iteration: 3, abs(z): 0.32005852224930614
iteration: 4, abs(z): 0.820739300530877
iteration: 5, abs(z): 0.3944899747490221
iteration: 6, abs(z): 0.8499974263391781
iteration: 7, abs(z): 0.47633146090875766
iteration: 8, abs(z): 0.9179848147566967
iteration: 9, abs(z): 0.588735685816267
iteration: 10, abs(z): 1.0730524976637672
iteration: 11, abs(z): 0.793560584649992
iteration: 12, abs(z): 1.4456959535695484
iteration: 13, abs(z): 1.386852094167046
iteration: 14, abs(z): 2.1324535049646074
iteration: 14; (-0.8+0.2j) is not in the Mandelbrot set

How is the Mandelbrot set used to generate fractal images? #

Instead of sampling numbers randomly as I’ve been doing, the numbers used to generate Mandelbrot fractal images are taken from the complex plane, close to the plane’s origin (0+0j0+0j).

The complex plane in the image below covers the range ±15\pm 15 on both its real and imaginary axis. The Mandelbrot set exists in the range 2.5,1-2.5, 1 on the real axis, and somewhere in the range ±1.75\pm 1.75 on the imaginary axis (to be confirmed later).

Complex plane
Complex plane

Mandelbrot fractals are generated by populating a plane with complex numbers in the appropriate range and then keeping track of the number of iterations it takes each number on the plane to show that it’s going to diverge to infinity. In practice this seems to mean, the number of iterations it takes for the magnitude of zz to exceed the threshold of 22.

The iteration counts are recorded in a 2D object (for example an array or a Python list). Numbers (points on the plane) that don’t meet the threshold of 2 within some number of iterations (for example, 50 iterations) are assigned a value of 0 in the iteration counts array.

Iteration counts are then arbitrarily associated with colors. For example, 0 (numbers in the Mandelbrot set) are commonly set to black. 5 iterations could be set to red, 10 iterations set to green… In pratice the color assignment is probably done programatically.

The iterations array is then mapped to pixels on the screen. For example iterations[0][0] will be the top right corner of the screen. If iterations[0][0] = 15, and 15 iterations is associated with the color red, the pixel at [0][0] will be colored red.

It’s easier to demonstrate this than explain it.

A basic Processing implementation #

This started by looking at the (Matplotlib) “Code #3” example at https://www.geeksforgeeks.org/python/mandelbrot-fractal-set-visualization-in-python/ .

It’s been revised a few times, with more revisions to come. Today I realized that I’d been rendering the imaginary plane upside down!

linspace is a simplification of the NumPy linspace method. It creates an even number of divisions between a start point and an end point. Importantly (for fixing the upside down imaginary range issue), the start point can be greater than the end point:

float[] linspace(float start, float end, int num) {
  float[] result = new float[num];
  if (num == 1) {
    result[0] = start;
    return result;
  }
  float step = (end - start) / (num - 1);
  for (int i = 0; i < num; i++) {
    result[i] = start + i * step;
  }
  return result;
}

The Complex class has methods for add, mult, magnitude.

class Complex {
  float re, im;

  Complex(float re, float im) {
    this.re = re;
    this.im = im;
  }

  Complex add(Complex other) {
    return new Complex(re + other.re, im + other.im);
  }

  Complex mult(Complex other) {
    return new Complex(
      re * other.re - im * other.im,
      re * other.im + im * other.re
      );
  }

  float magnitude() {
    return sqrt(re*re + im*im);
  }
}

Most of the actual computation happens in the setup function:

void setup() {
  size(1000, 1000); // cols, rows
  colorMode(HSB, 360, 100, 100);
  iterations = fillArray(imaginaryComponents, realComponents);
}

mandelbrot.pde (WIP):

int rows = 1000;
int cols = 1000;

int maxIters = 2000; // adjust, especially for zoomed in areas
int maxIterColorCutoff = 80;

int[][] iterations = new int[rows][cols];
// note that the imaginaryComponents range is from high to low;
// this means that in the array that stores the iteration counts, position[0][0] represents both
// the top right corner of the Processing window and the index of the smallest real component and
// _largest_ imaginary component:
float[] imaginaryComponents = linspace(1.25, -1.25, rows);
float[] realComponents = linspace(-2.0, 0.5, cols);

void setup() {
  size(1000, 1000); // cols, rows
  colorMode(HSB, 360, 100, 100);
  iterations = fillArray(imaginaryComponents, realComponents);
}

void draw() {
  for (int i = 0; i < iterations.length; i++) {  // i indexes the imaginary axis
    for (int j = 0; j < iterations[i].length; j++) {  // j indexes the real axis
      if (iterations[i][j] == 0) {
        // 0 means "(probably) in the Mandelbrot set";
        // these points iterated maxIters times without diverging
        stroke(0, 0, 0);
      } else { // not in set
        // float hue = map(iterations[i][j], 1, maxIterColorCutoff, 167, 360);
        // better approach for setting hue; compresses high values and spreads out low values:
        float hue = map(log(iterations[i][j]), log(1), log(maxIterColorCutoff), 167, 360);
        stroke(hue, 100, 100);
      }
      point(j, i);  // having to flip i and j here messes with my head
    }
  }
  // optionally save an image
  // save("mandelbrot_imp_full.png");
  noLoop();
}

// the logic to determine if a point on the complex plane is in the Mandelbrot set
int[][] fillArray(float[] imagVals, float[] realVals) {
  int[][] result = new int[rows][cols];

  for (int i = 0; i < imagVals.length; i++) {
    for (int j = 0; j < realVals.length; j++) {
      Complex c = new Complex(realVals[j], imagVals[i]);
      Complex z = new Complex(0, 0);
      boolean diverged = false;
      for (int iter = 0; iter < maxIters; iter++) {
        if (z.magnitude() >= 2) {
          result[i][j] = iter;
          diverged = true;
          break;
        } else {
          z = z.mult(z).add(c);
        }
      }
      if (!diverged) {
        result[i][j] = 0;
      }
    }
  }

  return result;
}

float[] linspace(float start, float end, int num) {
  float[] result = new float[num];
  if (num == 1) {
    result[0] = start;
    return result;
  }
  float step = (end - start) / (num - 1);
  for (int i = 0; i < num; i++) {
    result[i] = start + i * step;
  }
  return result;
}


class Complex {
  float re, im;

  Complex(float re, float im) {
    this.re = re;
    this.im = im;
  }

  Complex add(Complex other) {
    return new Complex(re + other.re, im + other.im);
  }

  Complex mult(Complex other) {
    return new Complex(
      re * other.re - im * other.im,
      re * other.im + im * other.re
      );
  }

  float magnitude() {
    return sqrt(re*re + im*im);
  }
}

Images #

The full Mandelbrot set
The full Mandelbrot set

start (imaginary): 0.625
end: (imaginary): -0.625
start (real): -2.0
end: (real): -0.75

Mandelbrot 2xzoom: centered at -1.375 on the real axis
Mandelbrot 2xzoom: centered at -1.375 on the real axis

start (imaginary): 0.10625
end: (imaginary): 0.09375
start (real): -0.75375
end: (real): -0.74125

Mandelbrot: seahorse valley
Mandelbrot: seahorse valley

start (imaginary): 0.103125
end: (imaginary): 0.096875004
start (real): -0.750625
end: (real): -0.744375

Mandelbrot: seahorse valley (more zoon)
Mandelbrot: seahorse valley (more zoon)

start (imaginary): 0.1015625
end: (imaginary): 0.0984375
start (real): -0.7490625
end: (real): -0.7459375

Mandelbrot: seahorse valley (more zoon)
Mandelbrot: seahorse valley (more zoon)

start (imaginary): 0.10075
end: (imaginary): 0.09925
start (real): -0.74825
end: (real): -0.74675

Mandelbrot: seahorse valley (more zoon)
Mandelbrot: seahorse valley (more zoon)

start (imaginary): 0.090040006
end: (imaginary): 0.08998
start (real): -0.74762696
end: (real): -0.747567

Mandelbrot: seahorse valley (more zoon)
Mandelbrot: seahorse valley (more zoon)

Floating point precision issues for tiny ranges #

When the difference between the imaginary and real start and end positions is tiny, images start to become pixilated. The problem is caused by floating point spacing, mostly here:

float[] linspace(float start, float end, int num) {
  float[] result = new float[num];
  if (num == 1) {
    result[0] = start;
    return result;
  }
  float step = (end - start) / (num - 1);
  for (int i = 0; i < num; i++) {
    result[i] = start + i * step;
  }
  return result;
}

I need to learn more about this, but….Floats don’t have uniform precision. The gap between numbers that can be represented consecutively grows with the magnitude (size) of the number. At around -1.79 (the range I’ve been testing in) consecutive floats are spaced 1.2×107\approx 1.2\times 10^{-7} apart. I’ve been trying step sizes in the range 3.2×1093.2\times 10^{-9}. This results in the same value repeating for 38 steps or so:

In [4]: 1.2e-7 / 3.2e-9
Out[4]: 37.49999999999999

I’ll deal with this in notes / Processing Mandelbrot improvements.

References #

Geeks For Geeks. “Mandelbrot Fractal Set visualization in Python.” Last Updated: September 4, 2023. https://www.geeksforgeeks.org/python/mandelbrot-fractal-set-visualization-in-python/ .

Wikipedia contributors. “Mandelbrot set.” Accessed on: January 21, 2026. https://en.wikipedia.org/wiki/Mandelbrot_set .


  1. Wikipedia contributors, “Mandelbrot set,” Accessed on: January 21, 2026, https://en.wikipedia.org/wiki/Mandelbrot_set↩︎