2.5. Part 5: A More Complex Example: Mandelbrot

Warning

If you want to run the Mandelbrot example on OSG with Condor, please refer to the OSG-specific instructions: Part 5: A More Complex Example: Mandelbrot (OSG VERSION).

In this example, we split up the calculation of a Mandelbrot set into several tiles, submit a job for each tile using the SAGA Job API, retrieve the tiles using the SAGA File API and stitch together the final image from the individual tiles. This example shows how SAGA can be used to create more complex application workflows that involve multiple aspects of the API.

2.5.1. Hands-On: Distributed Mandelbrot Fractals

In order for this example to work, we need to install an additional Python module, the Python Image Library (PIL). This is done via pip:

pip install Pillow

Next, we need to download the Mandelbrot fractal generator itself as well as the shell wrapper scrip. It is really just a very simple python script that, if invoked on the command line, outputs a full or part of a Mandelbrot fractal as a PNG image. Download the scripts into your $HOME directory:

curl --insecure -Os https://raw.githubusercontent.com/radical-cybertools/radical.saga/devel/examples/tutorial/mandelbrot/mandelbrot.py
curl --insecure -Os https://raw.githubusercontent.com/radical-cybertools/radical.saga/devel/examples/tutorial/mandelbrot/mandelbrot.sh

You can give mandelbrot.py a test-drive locally by calculating a single-tiled 1024x1024 Mandelbrot fractal:

python mandelbrot.py 1024 1024 0 1024 0 1024 frac.gif

In your $HOME directory, open a new file saga_mandelbrot.py with your favorite editor and paste the following script (or download it directly from here).:


__author__    = "Ole Weidner"
__copyright__ = "Copyright 2012-2013, The SAGA Project"
__license__   = "MIT"


import os
import sys
import time

from PIL import Image

import radical.saga as rs


#-----------------------------------------------------------------------------
#

# Change REMOTE_HOST to the machine you want to run this on.
# You might have to change the URL scheme below for REMOTE_JOB_ENDPOINT
# accordingly.
REMOTE_HOST = "localhost"  # try this with different hosts

# This refers to your working directory on 'REMOTE_HOST'. If you use a\
# cluster for 'REMOTE_HOST', make sure this points to a shared filesystem.
REMOTE_DIR = "/tmp/"  # change this to your home directory

# If you change 'REMOTE_HOST' above, you might have to change 'ssh://' to e.g.,
# 'pbs+ssh://', 'sge+ssh://', depdending on the type of service endpoint on
# that particualr host.
REMOTE_JOB_ENDPOINT = "ssh://" + REMOTE_HOST

# At the moment radical.saga only provides an sftp file adaptor, so changing
# the URL scheme here wouldn't make any sense.
REMOTE_FILE_ENDPOINT = "sftp://" + REMOTE_HOST + "/" + REMOTE_DIR

# the dimension (in pixel) of the whole fractal
imgx = 2048
imgy = 2048

# the number of tiles in X and Y direction
tilesx = 2
tilesy = 2

#-----------------------------------------------------------------------------
#
if __name__ == "__main__":
    try:
        # Your ssh identity on the remote machine
        ctx = rs.Context("ssh")
        #ctx.user_id = ""

        session = rs.Session()
        session.add_context(ctx)

        # list that holds the jobs
        jobs = []

        # create a working directory in /scratch
        dirname = '%s/mbrot/' % (REMOTE_FILE_ENDPOINT)
        workdir = rs.filesystem.Directory(dirname, rs.filesystem.CREATE,
                                            session=session)

        # copy the executable and warpper script to the remote host
        mbwrapper = rs.filesystem.File('file://localhost/%s/mandelbrot.sh' % os.getcwd())
        mbwrapper.copy(workdir.get_url())
        mbexe = rs.filesystem.File('file://localhost/%s/mandelbrot.py' % os.getcwd())
        mbexe.copy(workdir.get_url())

        # the saga job services connects to and provides a handle
        # to a remote machine. In this case, it's your machine.
        # fork can be replaced with ssh here:
        jobservice = rs.job.Service(REMOTE_JOB_ENDPOINT, session=session)

        for x in range(0, tilesx):
            for y in range(0, tilesy):

                # describe a single Mandelbrot job. we're using the
                # directory created above as the job's working directory
                outputfile = 'tile_x%s_y%s.gif' % (x, y)
                jd = rs.job.Description()
                #jd.queue             = "development"
                jd.wall_time_limit   = 10
                jd.total_cpu_count   = 1
                jd.working_directory = workdir.get_url().path
                jd.executable        = 'sh'
                jd.arguments         = ['mandelbrot.sh', imgx, imgy,
                                        int(imgx/tilesx*x), int(imgx/tilesx*(x+1)),
                                        int(imgy/tilesy*y), int(imgy/tilesy*(y+1)),
                                        outputfile]
                # create the job from the description
                # above, launch it and add it to the list of jobs
                job = jobservice.create_job(jd)
                job.run()
                jobs.append(job)
                print(' * Submitted %s. Output will be written to: %s' % (job.id, outputfile))

        # wait for all jobs to finish
        while len(jobs) > 0:
            for job in jobs:
                jobstate = job.get_state()
                print(' * Job %s status: %s' % (job.id, jobstate))
                if jobstate in [rs.job.DONE, rs.job.FAILED]:
                    jobs.remove(job)
            print("")
            time.sleep(5)

        # copy image tiles back to our 'local' directory
        for image in workdir.list('*.gif'):
            print(' * Copying %s/%s/%s back to %s' % (REMOTE_FILE_ENDPOINT,
                                                      workdir.get_url(),
                                                      image, os.getcwd()))
            workdir.copy(image, 'file://localhost/%s/' % os.getcwd())

        # stitch together the final image
        fullimage = Image.new('RGB', (imgx, imgy), (255, 255, 255))
        print(' * Stitching together the whole fractal: mandelbrot_full.gif')
        for x in range(0, tilesx):
            for y in range(0, tilesy):
                partimage = Image.open('tile_x%s_y%s.gif' % (x, y))
                fullimage.paste(partimage,
                                (int(imgx/tilesx*x), int(imgy/tilesy*y),
                                 int(imgx/tilesx*(x+1)), int(imgy/tilesy*(y+1))))
        fullimage.save("mandelbrot_full.gif", "GIF")
        sys.exit(0)

    except rs.SagaException as ex:
        # Catch all saga exceptions
        print("An exception occured: (%s) %s " % (ex.type, (str(ex))))
        # Trace back the exception. That can be helpful for debugging.
        print(" \n*** Backtrace:\n %s" % ex.traceback)
        sys.exit(-1)

    except KeyboardInterrupt:
    # ctrl-c caught: try to cancel our jobs before we exit
        # the program, otherwise we'll end up with lingering jobs.
        for job in jobs:
            job.cancel()
        sys.exit(-1)

Look at the code and change the constants at the very top accordingly. Then run it. The output should look something like this:

python saga_mandelbrot.py

* Submitted [ssh://india.futuregrid.org]-[4073]. Output will be written to: tile_x0_y0.gif
* Submitted [ssh://india.futuregrid.org]-[4094]. Output will be written to: tile_x0_y1.gif
* Submitted [ssh://india.futuregrid.org]-[4116]. Output will be written to: tile_x1_y0.gif
* Submitted [ssh://india.futuregrid.org]-[4144]. Output will be written to: tile_x1_y1.gif
* Job [ssh://india.futuregrid.org]-[4073] status: Running
* Job [ssh://india.futuregrid.org]-[4094] status: Running
* Job [ssh://india.futuregrid.org]-[4116] status: Running
* Job [ssh://india.futuregrid.org]-[4144] status: Running

* Job [ssh://india.futuregrid.org]-[4073] status: Running
* Job [ssh://india.futuregrid.org]-[4094] status: Running
* Job [ssh://india.futuregrid.org]-[4116] status: Running
* Job [ssh://india.futuregrid.org]-[4144] status: Running

* Job [ssh://india.futuregrid.org]-[4073] status: Done
* Job [ssh://india.futuregrid.org]-[4116] status: Running
* Job [ssh://india.futuregrid.org]-[4144] status: Running

* Job [ssh://india.futuregrid.org]-[4094] status: Done
* Job [ssh://india.futuregrid.org]-[4144] status: Done

* Job [ssh://india.futuregrid.org]-[4116] status: Done

* Copying sftp://india.futuregrid.org//N/u/oweidner/sftp://india.futuregrid.org//N/u/oweidner/mbrot//tile_x0_y0.gif back to /Users/oweidner/MB
* Copying sftp://india.futuregrid.org//N/u/oweidner/sftp://india.futuregrid.org//N/u/oweidner/mbrot//tile_x0_y1.gif back to /Users/oweidner/MB
* Copying sftp://india.futuregrid.org//N/u/oweidner/sftp://india.futuregrid.org//N/u/oweidner/mbrot//tile_x1_y0.gif back to /Users/oweidner/MB
* Copying sftp://india.futuregrid.org//N/u/oweidner/sftp://india.futuregrid.org//N/u/oweidner/mbrot//tile_x1_y1.gif back to /Users/oweidner/MB
* Stitching together the whole fractal: mandelbrot_full.gif

Open mandelbrot_full.gif with your favorite image editor. It should look like the image below. The different tile*.gif files (open them if you want) were computed on ‘REMOTE_HOST’, transfered back and stitched together as the full image.

../_images/mandelbrot_full.gif