In this project, we create and display the Mandelbrot set (the black points in the above figure), a math set that satisfies *M* is the Mandelbrot set and *C* is the set of all complex numbers.

(1)
⎧⎪⎨⎪⎩
*M*
= {*c* ∈ *C*|^{ }lim_{n → ∞}|*Z*_{n}| ≠ ∞}
*Z*_{o}
= *c*
*Z*_{n + 1}
= *Z*^{2}_{n} + *c*
,

in which
Creating the Mandelbrot set based on the equations above is impossible, for the ranges of both *n* and |*Z*_{n}| in the first equation are infinity instead of specific numbers. However, we can narrow the ranges down to a reasonable scope for the numerical implementation of the Mandelbrot set. Heuristic experiments and deductions have shown that a number *c* belongs to the Mandelbrot set if the following equations are true:

(2)
⎧⎪⎨⎪⎩
*M*
= {*c* ∈ *C*||*Z*_{n}| ≤ 2, *n* = 0, 1, ⋯1999}
*Z*_{o}
= *c*
*Z*_{n + 1}
= *Z*^{2}_{n} + *c*
.

That is, we can categorize a complex number *c* as a member of the Mandelbrot set if the first equation holds within the first 1999 iterations of the third equation; otherwise, this number does not belong to the Mandelbrot set. Even though the aforementioned specifications change the implementation of the Mandelbrot set into a feasible task, this task is still computationally expensive.

In this project, various libraries are used for the computation and display of the Mandelbrot set: MPI for distributedly computing the Mandelbrot set, OpenGL for displaying the set, POSIX Threads for handling events input by users, such as mouse clicking and region drawing, and STL container for storing intermediate computation results.

To see the entire set, we initially limit the display area (the contents in the window in (a↑)) to be

− 2.0 ≤ *Re*(*c*) ≤ 1.0

and
− 1.2 ≤ *Im*(*c*) ≤ 1.8.

This area is equally discretized by a finite number of samples (512 × 512). A user can zoom in on any local region on the display area to see the details by drawing a square (shown in red) to define the region of interest. While the user clicks on the viewport and drags the mouse cursor over a range, OpenGL Utility Toolkit (GLUT) is responsible for detecting the positions of the mouse cursor in terms of pixels; then, the boundaries and positions of the drawn square will be converted into the corresponding complex coordinates for subsequent calculation and visualization.
When an application is required to yield smooth animations, the OpenGL frame buffer is deliberately configured to have double color buffers (front and back). While the rendering in the front buffer is shown on the screen, the contents that will be subsequently displayed are being rendered in the back buffer. When the contents on the screen need refreshing, the roles of the two buffers are swapped. As such, users can see a smoother animation than is the case when only one color buffer is used.

In this program, we still set double color buffers; however, they are not configured as mentioned above. Instead, they are utilized to improve the rendering efficiency with the trade-off of slight loss of visualization quality while the red square is being drawn. Note that the red square is superimposed on the existing contents in the window, so only the pixels on the boundaries of the square are updated to red. Hence, we 1) disable the swapping between the front and back buffers, 2) use the back buffer to store the existing contents (the rendering without the superimposed red square, as shown in the back buffer of 4↑), 3) copy the contents from the back buffer to the front one, 4) superimpose the red boundaries onto the copied contents, and 5) constantly refresh and display the contents in the front buffer on the screen.

Once the drawing is finished (the user releases the pressed button on the mouse), the program immediately rediscretizes the drawn region into 512 × 512 samples, checks which samples belong to the Mandelbrot set, paints samples with corresponding colors (described later), and refreshes the contents in the window for display. As mentioned earlier, if the corresponding complex coordinates of a sample iteratively satisfy the equations (2↑) for a full 2000 iterations, this sample belongs to the Mandelbrot set and we paint it black. If any of the equations does not hold at the *N*^{th} iteration (*N* < 2000) when the corresponding complex coordinates of a sample are substituted into the equations (2↑), then this sample does not belong to the Mandelbrot set. We paint all samples like this with colors according to various *N* values. To sum up, in the figures above, all points with the same color require the same number of iterations to verify that they do or do not belong to the Mandelbrot set.

The computation cost of validating 512 × 512 samples is high, so once a result is obtained, it is stored in an STL vector (used as a stack) while the user proceeds to zoom in on the complex plane. As such, a user can undo the rendering of the previous zooming level (as changes from Figs. e↑ to g↑) without resorting to recomputation: A region is selected on Fig. e↑; after computation, the result is shown in Fig. f↑. If a user is not satisfied with the region of interest in Fig. e↑, the user can return back to the previous result and select a different region (as shown in Fig. g↑), yielding the result in Fig. h↑.

This figure illustrates how the required computation for validating 512 × 512 complex numbers is distributed to a number of processors. When the program launches, a processor (processor 0 in Fig. 5↑) is responsible for listening to the events created by the user. Hence, processor 0 runs the OpenGL Utility Toolkit (GLUT) using one thread (thread 0 in the figure) to handle the input events. Meanwhile, other processors (processors 1, 2, and 3) simply wait for the data passed from processor 0 using block receives (explained later).

Once a user finishes drawing a red square on the window, processor 0 creates one more thread (thread 1 in the figure) to 1) convert confined image pixels in the drawn square to the corresponding complex numbers, 2) segment the total complex numbers into parts and send each part to a processor for computation through nonblocking sends, 3) attempt to receive the computation results passed from other processors using nonblocking receives, 4) processes one part of complex numbers, and 5) “MPI_Testall” to ensure that results from other processors are successfully received. Meanwhile, processors 1, 2, and 3 process the data sent from processor 0 and send the computation results back to processor 0 using blocking sends.

Note that the blocking send and receive in the implementation of the MPICH2 MPI library (developed by Argonne National Laboratory) use busy-waiting. That is, computation slaves (processors 1--3 in Fig. 5↑ or CPUs 1--7 in Fig. a↓) will be staying in full load while they execute the blocking send and receive of the MPICH2 MPI library to wait for the exchanges of data. The remedy for this unfavorable waste is to use the built-in nonblocking send and receive, a *while loop*, and the *usleep* function to implement a customized blocking send and receive. Fig. b↓ exhibits the usage of CPUs after this strategy is applied to the slave processors. As shown, processors are in full load only when they perform computation. Otherwise, they remain idle.

Using inheritance when coding the program of this project can systematically simplify the structure and increase the reusability of the code. We can set the drawn red square (referred to as “complex region boundaries,” the top block of 7↑) as the base class of the inheritance mechanism and create a derived class accounting for the data passed between processors (referred to as “partial complex region,” the middle block). We can even design a class representing the collection of all distributed data (shown at the bottom) using the inheritance from the partial complex region.