KeiruaProd

Mandelbrot fractal in Rust

Mandelbrot fractal is a classic. Here is a WIP implementation in rust ; I’ve first ported mandelb0t’s version to python3 in order to run it, but it was terribly slow.

We’ll need to save images (image crate) and to use complex numbers (num-complex). Also we’ll put a sprinkle of parallelism for speed using rayon.

$ cargo new mandelbrot --lib
$ cd mandelbrot
$ cargo add image num-complex rayon
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding image v0.23.14 to dependencies
      Adding num-complex v0.4.0 to dependencies
      Adding rayon v1.5.0 to dependencies

We will use the classic architecture:

├── Cargo.lock
├── Cargo.toml
└── src
    ├── bin
    │   └── mandelbrot.rs
    └── lib.rs

One of the examples of the image crate is an implementation of the Julia fractal and we will use this as a base to save images.

In order to implement this fractal, we need to do 4 things:

That’s what our library will do:

# lib.rs
extern crate image;
extern crate num_complex;
use rayon::prelude::*;

fn compute_iterations_mandelbrot(complex_x: f32, complex_y: f32, max_iterations: usize) -> usize {
    // Counts if the complex point c(cx, cy) diverges (it’s norm is > 2.0) in a finite
    // amount of time (the max amount of iterations)
    let c = num_complex::Complex::new(complex_x, complex_y);
    let mut z = num_complex::Complex::new(0f32, 0f32);

    let mut nb_iterations = 0;
    while nb_iterations < max_iterations && z.norm() < 2.0 {
        z = z * z + c;
        nb_iterations += 1;
    }

    nb_iterations
}

pub fn compute_iterations(
    width: u32,
    height: u32,
    xa: f32,
    xb: f32,
    ya: f32,
    yb: f32,
    max_iterations: usize,
) -> Vec<usize> {
    (0..width * height)
        .into_par_iter()
        .map(|offset| {
            // extract the x, y coordinates out of the linear offset
            let image_x = offset % width;
            let image_y = offset / width;
            // convert the x, y pixel coordinates into values of the complex plane in the area [xa, xa], [ya, yb]
            let complex_x = (image_x as f32) * (xb - xa) / (width as f32 - 1.0f32) + xa;
            let complex_y = (image_y as f32) * (yb - ya) / (height as f32 - 1.0f32) + ya;

            compute_iterations_mandelbrot(complex_x, complex_y, max_iterations)
        })
        .collect()
}

pub fn save_image(
    nb_iterations: &[usize],
    width: u32,
    height: u32,
    max_iterations: usize,
    path: &str,
) {
    let mut imgbuf = image::ImageBuffer::new(width, height);

    for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
        let i = nb_iterations[(y * width + x) as usize];
        // Shade pixel based on the number of iterations somehow
        let red: u8 =
            (((max_iterations as f32 - i as f32) / (max_iterations as f32)) * 255f32) as u8;
        let (green, blue) = (red, red);

        *pixel = image::Rgb([red as u8, green as u8, blue as u8]);
    }

    imgbuf.save(path).unwrap();
}

For now, shading is minimalistic. We mostly care to display if a point is part of the Mandelbrot set or not using shades of black and white. The nuances based on the number of iterations may not be very visible.

Then, we can use our library in a sample binary:

# bin/mandelbrot.rs
extern crate mandelbrot;
use mandelbrot::{compute_iterations, save_image};

fn main() {
    let width: u32 = 900;
    let height: u32 = 900;

    let xa: f32 = -2.0;
    let xb: f32 = 1.0;
    let ya: f32 = -1.5;
    let yb: f32 = 1.5;
    let max_iterations = 256;

    let nb_iterations = compute_iterations(width, height, xa, xb, ya, yb, max_iterations);
    save_image(
        &nb_iterations,
        width,
        height,
        max_iterations,
        "mandelbrot.png",
    );
}

Then, we can run our program, and it’s reasonnably fast:

$ cargo run --bin mandelbrot
$ cargo run --bin mandelbrot --release
$ cargo build --release && time ./target/release/mandelbrot
    Finished release [optimized] target(s) in 0.01s

real    0m0,054s
user    0m0,438s
sys 0m0,000s

See a typo ? You can suggest a modification on Github.