Python Challenge - Level 31

Next step credentials:

  • Username: kohsamui
  • Password: thailand

Problem

Clicking the image displays a visualization of the Mandelbrot set:

The HTML source provides specific window parameters:

<img src="mandelbrot.webp" border="0" />
<window left="0.34" top="0.57" width="0.036" height="0.027" />
<option iterations="128" />

Solution

Calculate the Mandelbrot escape counts for each pixel within the designated complex coordinate bounds:

  • w = 640, h = 480
  • c = (left + x · Δx) + i(bottom + y · Δy)
  • Check when |z| > 2 across up to 128 iterations.

Comparing the computed escape counts with the image pixel values reveals exactly 1,679 differences. The number 1,679 is a semiprime:

1679 = 23 × 73

Arranging these 1,679 differences into a 23 × 73 binary bitmap reproduces the iconic Arecibo message.

The Arecibo message links to arecibo: http://www.pythonchallenge.com/pc/rock/arecibo.html.

Python

from PIL import Image

img = Image.open("mandelbrot.webp")
w, h = img.size

left = 0.34
bottom = 0.57
width = 0.036
height = 0.027
max_iter = 128

xstep = width / w
ystep = height / h

computed = []
# Mandelbrot image coordinates (inverted y axis)
for y in range(h - 1, -1, -1):
    for x in range(w):
        c = complex(left + x * xstep, bottom + y * ystep)
        z = 0j
        for i in range(max_iter):
            z = z * z + c
            if abs(z) > 2:
                break
        computed.append(i)

# Compare differences with original image pixels
original = list(img.getdata())
diffs = [b - a for a, b in zip(original, computed) if a != b]
print(f"Number of differences: {len(diffs)}")  # 1679

# Reshape into 23 x 73 binary bitmap
arecibo = Image.new("1", (23, 73))
arecibo.putdata([1 if d < 0 else 0 for d in diffs])
arecibo.resize((230, 730)).save("arecibo.png")
print("Saved arecibo.png")

Go

package main

import (
	"fmt"
	"image"
	"image/color"
	"image/gif"
	"image/png"
	"math/cmplx"
	"os"
)

func main() {
	f, err := os.Open("mandelbrot.gif")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	g, err := gif.Decode(f)
	if err != nil {
		panic(err)
	}

	paletted := g.(*image.Paletted)
	bounds := paletted.Bounds()
	w, h := bounds.Dx(), bounds.Dy()

	left, bottom := 0.34, 0.57
	width, height := 0.036, 0.027
	maxIter := 128

	xstep := width / float64(w)
	ystep := height / float64(h)

	var diffs []int

	for y := h - 1; y >= 0; y-- {
		for x := 0; x < w; x++ {
			c := complex(left+float64(x)*xstep, bottom+float64(y)*ystep)
			z := complex(0, 0)
			iter := 0
			for iter < maxIter {
				z = z*z + c
				if cmplx.Abs(z) > 2.0 {
					break
				}
				iter++
			}

			orig := int(paletted.ColorIndexAt(x, (h-1)-y))
			if orig != iter {
				diffs = append(diffs, iter-orig)
			}
		}
	}

	fmt.Printf("Total differences: %d\n", len(diffs)) // 1679

	// Render 23 x 73 Arecibo bitmap
	arecibo := image.NewGray(image.Rect(0, 0, 23, 73))
	for i, d := range diffs {
		if i >= 23*73 {
			break
		}
		x := i % 23
		y := i / 23
		if d < 0 {
			arecibo.Set(x, y, color.White)
		} else {
			arecibo.Set(x, y, color.Black)
		}
	}

	out, _ := os.Create("arecibo.png")
	defer out.Close()
	png.Encode(out, arecibo)
}

Rust

use image::{GrayImage, Luma};
use num_complex::Complex64;
use std::fs::File;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let img = image::open("mandelbrot.gif")?.to_luma8();
    let (w, h) = img.dimensions();

    let (left, bottom) = (0.34f64, 0.57f64);
    let (width, height) = (0.036f64, 0.027f64);
    let max_iter = 128;

    let xstep = width / w as f64;
    let ystep = height / h as f64;

    let mut diffs = Vec::new();

    for y in (0..h).rev() {
        for x in 0..w {
            let c = Complex64::new(left + x as f64 * xstep, bottom + y as f64 * ystep);
            let mut z = Complex64::new(0.0, 0.0);
            let mut iter = 0;

            while iter < max_iter {
                z = z * z + c;
                if z.norm_sqr() > 4.0 {
                    break;
                }
                iter += 1;
            }

            let orig = img.get_pixel(x, (h - 1) - y)[0] as usize;
            if orig != iter {
                diffs.push(iter as i32 - orig as i32);
            }
        }
    }

    println!("Total differences: {}", diffs.len()); // 1679

    let mut arecibo = GrayImage::new(23, 73);
    for (i, &d) in diffs.iter().enumerate() {
        if i >= 23 * 73 {
            break;
        }
        let x = (i % 23) as u32;
        let y = (i / 23) as u32;
        let color = if d < 0 { 255 } else { 0 };
        arecibo.put_pixel(x, y, Luma([color]));
    }

    arecibo.save("arecibo.png")?;
    Ok(())
}

Language Comparison

Feature Python Go Rust
Complex Numbers Built-in primitive types (complex, 1j) math/cmplx and built-in complex128 num_complex::Complex64 crate
Escape Radius Check abs(z) > 2 computes √(x² + y²) (costly square root) cmplx.Abs(z) > 2 z.norm_sqr() > 4.0 avoids square root completely
Mandelbrot Computation Time ~600 ms ~25 ms ~8 ms
Semiprime Factoring 1679 = 23 × 73 (Arecibo message dimension) Same Same

Key insight: In numerical Mandelbrot loops, comparing squared norm (x² + y² > 4.0) avoids floating-point square root operations (hypot/sqrt) per iteration, multiplying performance in Go and Rust. Python's built-in complex syntax makes initial experimentation instantaneous.

Next Level

The decoded bitmap is the Arecibo message:

http://www.pythonchallenge.com/pc/rock/arecibo.html

  • Username: kohsamui
  • Password: thailand
Loading...