Drawing the Mandelbrot set in Ruby and Haskell

Drawing the Mandelbrot set in Ruby and Haskell

Generating the wonderful Mandelbrot set in Ruby and Haskell – it’s amazing how simple math can give birth to such a sophisticated shape. It’s elegance fits Ruby and Haskell well. Come closer and see the wonders!

I just came across one of my favourie books which I read when I was a kid: ‘The emperor’s new mind‘ by Roger Penrose. I remember I was very enthusiastic reading it and became amazed by fractals – especially the good old Mandelbrot set. I wasn’t really into programming back then – I was like 15, only starting out with *blush* Pascal, but now I’m equipped with two wonderful languages, so I decided to code my childhood dream.

Some very basic maths for the things to come:
The Mandelbrot set is a set of complex numbers for which the simple iterative polynomial z(0) = 0; z(n+1) = z(n)^2 + c remains bounded.
It’s the boundary of the set which is a fractal, an ever-zoomable, self-repeating source of wonders. The complex plane is a two dimensional plane on which the x-axis is the real part and the y-axis is the imaginary part of any complex number (at least let’s make this our convention), thus every complex number defines a point on this plane.

After some trial and error, the following area of the complex plane is best used for the well-known generic Mandelbrot fractal:
x: (-2.0) – 0.5
y: (-1.0) – 1.0

The algorithm is this: for every point (complex number) inside this area we check if the iterative polynomial remains bounded. Of course we can’t check up to infinity, so we need to define some maximum iteration limit, say 500 (for our console plotting, a much lower value would be good enough). If the magnitude of the 500th iteration of the polynomial is lower than 2.0 (a hand-picked but close-enough upper limit), then the given point is part of the Mandelbrot set.

Let’s code a quick console plotter in ruby:

require 'complex'
 
def mandelbrot(a)
  Array.new(500,a).inject(a) { |z,c| z*z + c }
end
 
(1.0).step(-1,-0.05) do |y|
  (-2.0).step(0.5,0.0315) do |x|
    print mandelbrot(Complex(x,y)).abs < 2 ? '*' : ' '
  end
  puts
end

Pretty straightforward, I think. All numbers are hard-coded for simplicity (iteration step size is calculated based on the x-y range and the number of characters in our terminal). One neat trick is the iteration itself: Enumerable#inject is used instead of classic looping. I just love ruby’s functional side. Array.new(500,a) generates an array by repeating ‘a’ 500 times. Inject is fold in a disguise, reducing the array to one element by the operations given in the block.

It produces the following cool little output:


Mandelbrot console

Pretty nice for starters. Let’s see the Haskell version.

import Data.Complex
 
mandelbrot a = iterate (\z -> z^2 + a) a !! 500
 
main = mapM_ putStrLn [[if magnitude (mandelbrot (x :+ y)) < 2 then '*' else ' '
                           | x <- [-2, -1.9685 .. 0.5]]
                           | y <- [1, 0.95 .. -1]]

Some definitions here:

  • ‘iterate’ by definition: iterate f x returns an infinite list of repeated applications of f to x: iterate f x == [x, f x, f (f x), ...]
    Quite clear, I think – just what we need.
  • list !! n: take the nth item from the list
  • mapM_ by definition: Evaluate each action in the sequence from left to right, and ignore the results. What it does is actually ‘executing’ the operations on its right without using their results – outputting the mandelbrot set itself.

I have one more thing up in my sleeve: a full-fledged ruby drawing version using RMagick, so here it is without comments, use it to your liking:

require 'rubygems'
require 'complex'
require 'RMagick'
include Magick
 
# this is the interesting area of the complex plane
X_START = -2.0
X_END = 0.5
Y_START = -1.0
Y_END = 1.0
 
# wanted image dimensions
WIDTH = 800.0
HEIGHT = 600.0
 
# set this to higher values and sleep well :)
ITERATIONS = 200
 
STEP_X = (X_END - X_START) / WIDTH
STEP_Y = (Y_END - Y_START) / HEIGHT
 
def mandelbrot(a)
  Array.new(ITERATIONS, a).inject(a) { |z,c| z*z + c }
end
 
complex_plane = Image.new(WIDTH.to_i, HEIGHT.to_i)
 
MIN_X = 
x_pixel = 0
y_pixel = 0 
 
x = X_START
y = Y_START
 
while y < Y_END
  x_pixel = 0
  x = X_START
  while x < X_END
    mandelbrot(Complex(x,y)).abs < 2 ? Draw.new.fill('#000000').point(x_pixel,y_pixel).draw(complex_plane) : nil
    x_pixel += 1
    x += STEP_X
  end
  y_pixel += 1
  y += STEP_Y
end
 
complex_plane.write("mandelbrot.png")

I hope you liked this quick and simple post :) I hacked this together in 30 minutes, so I know it’s not high quality code, but I did it for fun mostly.

 

Related posts:

  1. Ruby vs. Haskell – project Euler #25 deathmatch
  2. Functional ruby – a simple example

5 Comments 5 Tweets

18 comments

  1. >> list !! n: take the first n items from the list

    No it doesn’t. It takes the n-item from the list. To take the first n items you should use take.

    [1..3] !! 2 -> 3
    take 2 [1..3] -> [1,2]

    [Reply]

    ochronus Reply:

    @Diego, thanks, it was a blooper, of course I meant nth item :) It’s corrected now

    [Reply]

  2. Beautiful :) I wish I understood the "inject" syntax… but unfortunately, I do not. I’m pretty new to Ruby.

    This comment was originally posted on Reddit

    [Reply]

  3. It’s like ‘collapsing’ the array, squashing it into one value. A simpler example: irb(main):006:0> [1,2,3,4].inject {|a,b| puts "#{a};#{b}" ; a+b} 1;2 3;3 6;4 => 10 I hope it’s clearer now

    This comment was originally posted on Reddit

    [Reply]

  4. also for a better reference, read this: http://en.wikipedia.org/wiki/Fold_(higher-order_function)

    This comment was originally posted on Reddit

    [Reply]

  5. Not really :( I’m still rather confused. Maybe you could just use 1 variable instead of a,b ?

    This comment was originally posted on Reddit

    [Reply]

  6. You have a mistake on the inner while loop. You increment y_pixel when you should increment x_pixel

    [Reply]

    ochronus Reply:

    @Patrick, Thanks, nice catch! Corrected.

    [Reply]

  7. No, the point is that after step #1, a holds the current aggregated value

    This comment was originally posted on Reddit

    [Reply]

  8. Excellent site you have here, beautiful stuff. BTW – Thanks for the comment on my site. :)

    [Reply]

    ochronus Reply:

    @Joey Primiani, thanks for the kind words :)

    [Reply]

  9. Some small changes that make it colored:

    COLORS = 0xfffff / ITERATIONS

    def escapes?(c)
    z = 0
    ITERATIONS.times do |x|
    return x if z.abs > 2
    z = z*z + c
    end
    false
    end

    And change the central part to:

    escape = escapes?(Complex(x,y))
    Draw.new.fill(“#%06x” % (escape * COLORS)).point(x_pixel,y_pixel).draw(complex_plane) if escape

    [Reply]

    ochronus Reply:

    @Roger Braun, thanks, it’s a very nice tip!

    [Reply]

Leave a Reply

Additional comments powered by BackType