Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mandelbrot Set implementation in Common Lisp

I've been working on an implementation of the Mandelbrot Set in several different languages. I have a working implementation in C++, C#, Java, and Python, but the Common Lisp implementation has some bugs that I just can't figure out. It generates the set but somewhere in the pipeline the set gets distorted. I've tested and know with near certainty that the file I/O CLO isn't the problem - it's unlikely but possible, I've tested it pretty thoroughly.

Note that the intent of these implementations is to benchmark them against one another - so I'm trying to keep the code implementations as similar as possible so they are comparable.

The Mandelbrot set (here generated by the Python implementation):

http://www.freeimagehosting.net/uploads/65cb71a873.png "Mandelbrot Set (Generated by Python)"

But my Common Lisp program generates this:

http://www.freeimagehosting.net/uploads/50bf29bcc9.png "Common Lisp version's Distorted Mandelbrot Set"

The bug is identical in both Clisp and SBCL.

CODE:

Common Lisp:

(defun mandelbrot (real cplx num_iter)
   (if (> (+ (* real real) (* cplx cplx)) 4)
      1
      (let ((tmpreal real) (tmpcplx cplx) (i 1))
         (loop
            (setq tmpcplx (+ (* (* tmpreal tmpcplx) 2) cplx))
            (setq tmpreal (+ (- (* tmpreal tmpreal) (* tmpcplx tmpcplx))
               real))
            (setq i (+ i 1))
            (cond
               ((> (+ (* tmpreal tmpreal)
                  (* tmpcplx tmpcplx)) 4) (return i))
               ((= i num_iter) (return 0)))))))

(defun floordiv (dend sor) (/ (- dend (mod dend sor)) sor))

(defclass xbm () (
   (data :accessor data :initarg :data)
   (dim :reader dim :initarg :dim)
   (arrsize :reader arrsize :initarg :arrsize)))

(defmethod width ((self xbm)) (third (dim self)))

(defmethod height ((self xbm)) (second (dim self)))

(defun generate (width height)
   (let ((dims (list 0 0 0)) (arrsize_tmp 0))
      (setq dims (list 0 0 0))
      (setf (second dims) height)
      (setf (third dims) width)
      (setf (first dims) (floordiv (third dims) 8))
      (unless (= (mod width 8) 0) (setf (first dims) (+ (first dims) 1)))
      (setq arrsize_tmp (* (first dims) (second dims)))
      (make-instance 'xbm
         :data (make-array arrsize_tmp :initial-element 0)
         :dim dims
         :arrsize arrsize_tmp)))

(defun writexbm (self f)
   (with-open-file (stream f :direction :output :if-exists :supersede)
      (let ((fout stream))
         (format fout "#define mandelbrot_width ~d~&" (width self))
         (format fout "#define mandelbrot_height ~d~&" (height self))
         (format fout "#define mandelbrot_x_hot 1~&")
         (format fout "#define mandelbrot_y_hot 1~&")
         (format fout "static char mandelbrot_bits[] = {")
         (let ((i 0))
            (loop
               (if (= (mod i 8) 0)
                  (format fout "~&    ")
                  (format fout " "))
               (format fout "0x~x," (svref (data self) i))
               (unless (< (setf i (+ i 1)) (arrsize self))
                  (return t)))))))

(defmethod setpixel ((self xbm) (x integer) (y integer))
   (if (and (< x (third (dim self))) (< y (second (dim self))))
      (let ((val (+ (floordiv x 8) (* y (first (dim self))))))
         (setf (svref (data self) val) (boole boole-ior (svref (data self) val) (ash 1 (mod x 8)))))))

(defmethod unsetpixel ((self xbm) (x integer) (y integer))
   (if (and (< x (third (dim self))) (< y (second (dim self))))
      (let ((val (+ (floordiv x 8) (* y (first (dim self))))))
         (setf (svref (data self) val) (boole boole-xor (boole boole-ior
            (svref (data self) val) (ash 1 (mod x 8))) (ash 1 (mod x 8)))))))

(defmethod draw_mandelbrot ((xbm xbm) (num_iter integer) (xmin number)
   (xmax number) (ymin number) (ymax number))

   (let ((img_width (width xbm)) (img_height (height xbm)) (xp 0))
      (loop
         (if (< xp img_width)
            (let ((xcoord (+ (* (/ xp img_width) (- xmax xmin)) xmin)) (yp 0))
               (loop
                  (if (< yp img_height)
                     (let (
                        (ycoord (+ (* (/ yp img_height) (- ymax ymin)) ymin)))
                        (let ((val (mandelbrot xcoord ycoord num_iter)))
                           (if (> val 0) (unsetpixel xbm xp yp) (setpixel xbm xp yp)))
                        (setq yp (+ yp 1)))
                     (return 0)))
               (setq xp (+ xp 1)))
            (return 0)))))

(defun main ()
   (let ((maxiter 0) (xmin 0) (xmax 0) (ymin 0) (ymax 0) (file nil) (xsize 0) (ysize 0) (picture nil))
      (format t "maxiter? ")
      (setq maxiter (read))
      (format t "xmin? ")
      (setq xmin (read))
      (format t "xmax? ")
      (setq xmax (read))
      (format t "ymin? ")
      (setq ymin (read))
      (format t "ymax? ")
      (setq ymax (read))
      (format t "file path: ")
      (setq file (read-line))
      (format t "picture width? ")
      (setq xsize (read))
      (format t "picture height? ")
      (setq ysize (read))
      (format t "~&")
      (setq picture (generate xsize ysize))
      (draw_mandelbrot picture maxiter xmin xmax ymin ymax)
      (writexbm picture file)
      (format t "File Written.")
      0))

(main)

And the closest to it is Python:

from xbm import *

def mandelbrot(real_old,cplx_old,i):
   real = float(real_old)
   cplx = float(cplx_old)
   if (real*real+cplx*cplx) > 4:
      return 1
   tmpreal = real
   tmpcplx = cplx   
   for rep in range(1,i):
      tmpb = tmpcplx
      tmpcplx = tmpreal*tmpcplx*2
      tmpreal = tmpreal*tmpreal - tmpb*tmpb
      tmpcplx += cplx
      tmpreal += real
      tmpb = tmpcplx*tmpcplx + tmpreal*tmpreal
      if tmpb > 4:
         return rep+1
   else:
      return 0

def draw_mandelbrot(pic, num_iter, xmin, xmax, ymin, ymax):
   img_width = pic.width()
   img_height = pic.height()
   for xp in range(img_width):
      xcoord = (((float(xp)) / img_width) * (xmax - xmin)) + xmin
      for yp in range(img_height):
         ycoord = (((float(yp)) / img_height) * (ymax - ymin)) + ymin
         val = mandelbrot(xcoord, ycoord, num_iter)
         if (val):
            pic.unsetpixel(xp, yp)
         else:
            pic.setpixel(xp, yp)

def main():
   maxiter = int(raw_input("maxiter? "))
   xmin = float(raw_input("xmin? "))
   xmax = float(raw_input("xmax? "))
   ymin = float(raw_input("ymin? "))
   ymax = float(raw_input("ymax? "))
   file = raw_input("file path: ")
   xsize = int(raw_input("picture width? "))
   ysize = int(raw_input("picture height? "))
   print
   picture = xbm(xsize, ysize)
   draw_mandelbrot(picture, maxiter, xmin, xmax, ymin, ymax)
   picture.writexbm(file)
   print "File Written. "
   return 0;

main()

[xbm.py]

from array import *

class xbm:
   def __init__(self, width, height):
      self.dim = [0, 0, 0]
      self.dim[1] = height
      self.dim[2] = width
      self.dim[0] = self.dim[2] / 8
      if width % 8 != 0:
         self.dim[0] += 1
      self.arrsize = self.dim[0] * self.dim[1]
      self.data = array('B', (0 for x in range(self.arrsize)))
      self.hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
   def __nibbletochar__(self, a):
      if a < 0 or a > 16:
         return '0'
      else:
         return self.hex[a]
   def setpixel(self, x, y):
      if x < self.dim[2] and y < self.dim[1]:
         self.data[(x / 8) + (y * self.dim[0])] |= 1 << (x % 8)
   def unsetpixel(self, x, y):
      if x < self.dim[2] and y < self.dim[1]:
         self.data[(x / 8) + (y * self.dim[0])] |= 1 << (x % 8)
         self.data[(x / 8) + (y * self.dim[0])] ^= 1 << (x % 8)
   def width(self):
      return self.dim[2]
   def height(self):
      return self.dim[1]
   def writexbm(self, f):
      fout = open(f, 'wt')
      fout.write("#define mandelbrot_width ")
      fout.write(str(self.dim[2]))
      fout.write("\n#define mandelbrot_height ")
      fout.write(str(self.dim[1]))
      fout.write("\n#define mandelbrot_x_hot 1")
      fout.write("\n#define mandelbrot_y_hot 1")
      fout.write("\nstatic char mandelbrot_bits[] = {")
      for i in range(self.arrsize):
         if (i % 8 == 0): fout.write("\n\t")
         else: fout.write(" ")
         fout.write("0x")
         fout.write(self.__nibbletochar__(((self.data[i] >> 4) & 0x0F)))
         fout.write(self.__nibbletochar__((self.data[i] & 0x0F)))
         fout.write(",")
      fout.write("\n};\n")
      fout.close();

I can post the C++, C#, or Java code as well need be.

Thanks!

EDIT: Thanks to Edmund's response i found the bug- Just something that slipped through the cracks on porting. Modified code:

(defun mandelbrot (real cplx num_iter)
   (if (> (+ (* real real) (* cplx cplx)) 4)
      1
      (let ((tmpreal real) (tmpcplx cplx) (i 1) (tmpb cplx))
         (loop
            (setq tmpb tmpcplx)
            (setq tmpcplx (+ (* (* tmpreal tmpcplx) 2) cplx))
            (setq tmpreal (+ (- (* tmpreal tmpreal) (* tmpb tmpb))
               real))
            (setq i (+ i 1))
            (cond
               ((> (+ (* tmpreal tmpreal)
                  (* tmpcplx tmpcplx)) 4) (return i))
               ((= i num_iter) (return 0)))))))

(defun floordiv (dend sor) (/ (- dend (mod dend sor)) sor))

(defclass xbm () (
   (data :accessor data :initarg :data)
   (dim :reader dim :initarg :dim)
   (arrsize :reader arrsize :initarg :arrsize)))

(defun width (self) (third (dim self)))

(defun height (self) (second (dim self)))

(defun generate (width height)
   (let ((dims (list 0 0 0)) (arrsize_tmp 0))
      (setq dims (list 0 0 0))
      (setf (second dims) height)
      (setf (third dims) width)
      (setf (first dims) (floordiv (third dims) 8))
      (unless (= (mod width 8) 0) (setf (first dims) (+ (first dims) 1)))
      (setq arrsize_tmp (* (first dims) (second dims)))
      (make-instance 'xbm
         :data (make-array arrsize_tmp :initial-element 0)
         :dim dims
         :arrsize arrsize_tmp)))

(defun writexbm (self f)
   (with-open-file (stream f :direction :output :if-exists :supersede)
      (let ((fout stream))
         (format fout "#define mandelbrot_width ~d~&" (width self))
         (format fout "#define mandelbrot_height ~d~&" (height self))
         (format fout "#define mandelbrot_x_hot 1~&")
         (format fout "#define mandelbrot_y_hot 1~&")
         (format fout "static char mandelbrot_bits[] = {")
         (let ((i 0))
            (loop
               (if (= (mod i 8) 0)
                  (format fout "~&    ")
                  (format fout " "))
               (format fout "0x~x," (svref (data self) i))
               (unless (< (setf i (+ i 1)) (arrsize self))
                  (return t)))))))

(defun setpixel (self x y)
   (if (and (< x (third (dim self))) (< y (second (dim self))))
      (let ((val (+ (floordiv x 8) (* y (first (dim self))))))
         (setf (svref (data self) val) (boole boole-ior (svref (data self) val) (ash 1 (mod x 8)))))))

(defun unsetpixel (self x y)
   (if (and (< x (third (dim self))) (< y (second (dim self))))
      (let ((val (+ (floordiv x 8) (* y (first (dim self))))))
         (setf (svref (data self) val) (boole boole-xor (boole boole-ior
            (svref (data self) val) (ash 1 (mod x 8))) (ash 1 (mod x 8)))))))

(defun draw_mandelbrot (xbm num_iter xmin xmax ymin ymax)

   (let ((img_width (width xbm)) (img_height (height xbm)) (xp 0))
      (loop
         (if (< xp img_width)
            (let ((xcoord (+ (* (/ xp img_width) (- xmax xmin)) xmin)) (yp 0))
               (loop
                  (if (< yp img_height)
                     (let (
                        (ycoord (+ (* (/ yp img_height) (- ymax ymin)) ymin)))
                        (let ((val (mandelbrot xcoord ycoord num_iter)))
                           (if (> val 0) (unsetpixel xbm xp yp) (setpixel xbm xp yp)))
                        (setq yp (+ yp 1)))
                     (return 0)))
               (setq xp (+ xp 1)))
            (return 0)))))

(defun main ()
   (let ((maxiter 0) (xmin 0) (xmax 0) (ymin 0) (ymax 0) (file nil) (xsize 0) (ysize 0) (picture nil))
      (format t "maxiter? ")
      (setq maxiter (read))
      (format t "xmin? ")
      (setq xmin (read))
      (format t "xmax? ")
      (setq xmax (read))
      (format t "ymin? ")
      (setq ymin (read))
      (format t "ymax? ")
      (setq ymax (read))
      (format t "file path: ")
      (setq file (read-line))
      (format t "picture width? ")
      (setq xsize (read))
      (format t "picture height? ")
      (setq ysize (read))
      (format t "~&")
      (setq picture (generate xsize ysize))
      (draw_mandelbrot picture maxiter xmin xmax ymin ymax)
      (writexbm picture file)
      (format t "File Written.")
      0))

(main)

Though the code isn't very LISP-ish (is that a word?) it works. Thanks to all who posted/commented/answered :)

like image 928
Robert Mason Avatar asked Jan 15 '10 21:01

Robert Mason


1 Answers

Some remarks about your code:

  • mandelbrot: lacks declarations, squares are computed twice in the loop

  • mandelbrot: in the computation for TMPREAL you are using the new value of TMPCLX, not the old one

  • You don't want to use METHODS to set pixels. SLOW.

  • FLOORDIV is one of FLOOR or TRUNCATE (depending on what you want) in Common Lisp, see (FLOOR 10 3)

  • use type declarations

  • in writexbm don't repeatedly call DATA and ARRSIZE

  • setpixel, unsetpixel looks very expensive, again repeatedly dereferencing the structure

  • draw-mandelbrot has a lot of repeated computations that can be done once

  • Common Lisp has 2d arrays which simplify the code

  • Common Lisp has complex numbers, which also simplify the code

  • a variable name 'self' makes no sense in Common Lisp. Name it to what it is.

Generally the code is full of waste. It makes little sense to benchmark your code, since it is written in a style that hopefully nobody uses in Common Lisp. Common Lisp has been designed with experience of large mathematical software like Macsyma and allows to write mathematical code in a straight forward way (no objects, just functions over numbers, arrays, ...). The better compilers can take advantage of primitive types, primitive operations and type declarations. Thus the style is different from what one might write in Python (which usually either is object-oriented Python or calls to some C code) or Ruby. In heavy numerical code it is usually not a good idea to have dynamic dispatch like with CLOS. Setting pixels in bitmaps via CLOS calls in a tight LOOP is really something one wants to avoid (unless you know how to optimize it).

The better Lisp compilers will compile the numeric functions to direct machine code. During the compilation they give hints which operations are generic and can't be optimized (until the developer adds more type information). The developer can also 'DISASSEMBLE' the functions and check for code that is generic or does unnecessary function calls. "TIME' gives runtime information and also informs the developer about the amount of memory 'consed'. In numeric code consing of 'floats' is a usual performance problem.

So, to sum up:

  • if you write code and think that it does the same in different languages, when the code looks similar or has a similar structure, this might not be the case - unless you really know both languages and both language implementations.

  • if you write code in one language and port it in a similar style to a different language, you may miss a whole existing culture to write solutions to these kinds of problems in a different way. For example one can write code in C++ in an object-oriented style and port it in a similar way to FORTRAN. But no one writes such code in FORTRAN. Written in FORTRAN style, will typically result in faster code - especially since the compilers are heavily optimized for idiomatic FORTRAN code.

  • "when in rome, speak like the romans"

Example:

in SETPIXEL there is a call to (first (dim self)). Why not make that value a slot in the structure in the first place, instead of doing a list access all the time? But then the value is constant during the computation. Still the structure is passed, and the value is retrieved all the time. Why not just get the value outside the main loop and pass it directly? Instead of doing multiple computations of it?

To give you an idea how code might be written (with type declarations, loops, complex numbers, ...), here is a slightly different version of the mandelbrot computation.

The core algorithm:

(defvar *num-x-cells* 1024)
(defvar *num-y-cells* 1024)
(defvar *depth* 60)


(defun m (&key (left -1.5) (top -1.0) (right 0.5) (bottom 1.0) (depth *depth*))
  (declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
  (loop with delta-x-cell float = (/ (- right left) *num-x-cells*)
        and  delta-y-cell float = (/ (- bottom top) *num-y-cells*)
        and field = (make-array (list *num-x-cells* *num-y-cells*))
        for ix fixnum below *num-x-cells*
        for x float = (+ (* (float ix) delta-x-cell) left)
        do (loop for iy fixnum below *num-y-cells*
                 for y = (+ (* (float iy) delta-y-cell) top)
                 do (loop for i fixnum below depth
                          for z of-type complex = (complex x y)
                          then (+ (complex x y) (* z z))
                          for exit = (> (+ (* (realpart z) (realpart z))
                                           (* (imagpart z) (imagpart z)))
                                        4)
                          finally (setf (aref field ix iy) i)
                          until exit))
        finally (return field)))

Above function returns a 2d array of numbers.

Writing an xbm file:

(defun writexbm (array pathname &key (black *depth*))
  (declare (fixnum black)
           (optimize (speed 3) (safety 2) (debug 0) (space 0)))
  (with-open-file (stream pathname :direction :output :if-exists :supersede)
    (format stream "#define mandelbrot_width ~d~&"  (array-dimension array 0))
    (format stream "#define mandelbrot_height ~d~&" (array-dimension array 1))
    (format stream "#define mandelbrot_x_hot 1~&")
    (format stream "#define mandelbrot_y_hot 1~&")
    (format stream "static char mandelbrot_bits[] = {")
    (loop for j fixnum below (array-dimension array 1) do
          (loop for i fixnum below (truncate (array-dimension array 0) 8)
                for m fixnum = 0 then (mod (1+ m) 8) do
                (when (zerop m) (terpri stream))
                (format stream "0x~2,'0x, "
                        (let ((v 0))
                          (declare (fixnum v))
                          (dotimes (k 8 v)
                            (declare (fixnum k))
                            (setf v (logxor (ash (if (= (aref array
                                                              (+ (* i 8) k) j)
                                                        black)
                                                     1 0)
                                                 k)
                                            v)))))))
    (format stream "~&}~&")))

Above function takes an array and a pathname and writes the array as XBM file. One number 'black' will be 'black' and the other numbers are 'white'

Call

(writexbm (m) "/tmp/m.xbm")
like image 154
9 revs Avatar answered Sep 26 '22 00:09

9 revs