0

I'm writing a Java program to display the Mandelbrot set for my introductory programming class. I believe I've got all of the math set up correctly, however when I attempt to draw the fractal I end up getting just a solid color. I've tested the math and it seems like it should be working. I've searched for over an hour, but I haven't found anything that has helped. Below are my classes for complex numbers and actually creating the Mandelbrot set: Complex Numbers

public class ComplexNum {
//Instance Fields
private double realNum; //the real number portion of the complex number
private double imgNum; //the imaginary number portion of the complex number

//NOTE TO SELF: i = sqrt(-1); i^2 = -1; i^3 = -i; i^4 = 1; then the cycle starts over.

//Constructor
/**Creates a complex number of form x+yi, where x and y are both of type double; x represents the real number piece of the
 * complex number, while y represents the imaginary piece.
 * @param realPart -- the double value which is the real piece of the complex number
 * (Precondition: realPart is a real number of type double)
 * @param imaginaryPart -- the double value which represents the imaginary piece of the complex number
 * (Precondition: imaginaryPart is a real number of type double)
 */
public ComplexNum(double realPart, double imaginaryPart){
    realNum = realPart;
    imgNum = imaginaryPart;
}

/**Add two complex numbers by taking the sum of their real and imaginary pieces.
 * (Postcondition: returns the sum of two complex numbers)
 * @param comNum -- the complex number that is to be added together with this one
 * (Precondition: both the complex number you are calling this method on and comNum must have been initialized)
 * @return the sum of two complex numbers
 */
public ComplexNum add(ComplexNum comNum){
    return new ComplexNum(realNum+comNum.getRealPart(), imgNum+comNum.getImgPart());
}

/**Square the complex number and returns the result.
 * (Precondition: the complex number must have been initialized)
 * @return the squared value of the complex number
 */
public ComplexNum squareComplex(){
    double realPiece = realNum*realNum; //this is a normal number
    double imaginaryPiece = (realNum*imgNum)+(imgNum*realNum); //each section in parenthesis has an i attached to it, allowing both sections to be added together
    double iSquaredPiece = imgNum*imgNum; //this now has an i^2

    //The form that the complex number currently: a + b(i) + c(i^2), where b is actually x(i)+y(i) simplified.
    //since i^2 is -1, the iSquaredPiece is actually a real number. Multiply the value by -1, then add it to a,
    //and the true real number piece of the complex number is created.
    realPiece = realPiece + (iSquaredPiece*-1);

    return new ComplexNum(realPiece, imaginaryPiece);
}

/**Allows the real piece of a complex number to be extracted.
 * (Precondition: the complex number must have been initialized)
 * @return the value of the real number piece of the complex number
 */
public double getRealPart(){
    return realNum;
}

/**Allows the imaginary piece of a complex number to be extracted.
 * (Precondition: the complex number must have been initialized)
 * @return the value of the imaginary number piece of the complex number
 */
public double getImgPart(){
    return imgNum;
}

Mandelbrot

public class MandelbrotGenerator {
//Constants
/**The maximum number of times the Mandelbrot calculations will be run on a specific point. If the real and imaginary pieces
 * from each calculation don't exceed 2 within the maximum number of iterations, they are part of the Mandelbrot set.
 */
public static final int MAX_ITERATIONS = 30; //The maximum number of times the calculations will be run on a specific point.
private final double MIN_X = -2.0; //The minimum value of x when graphing the Mandelbrot set
private final double MAX_Y = 2.0; //The maximum value of y when graphing the Mandelbrot set
private final double MANDEL_X_RANGE = 4.0; //The range of x values from -2 to 2 when graphing the Mandelbrot set
private final double MANDEL_Y_RANGE = 4.0; //The range of y values from -2 to 2 when graphing the Mandelbrot set

//Instance Fields
private ComplexNum z; //In the Mandelbrot equation of Z_(n+1)=Z_n^2+C, this is the value of Z_n^2
private ComplexNum c; //In the Mandelbrot equation of Z_(n+1)=Z_n^2+C, this is the value of C
private ComplexNum currentCalc; //In the Mandelbrot equation of Z_(n+1)=Z_n^2+C, this is the value of Z_(n+1)
private int numIterations; //The current number of iterations

//Constructor
/**Create a MandelbrotGenerator object.
 */
public MandelbrotGenerator(){
    z = new ComplexNum(0,0);
    c = new ComplexNum(0,0);
    currentCalc = new ComplexNum(0,0);
    numIterations = 0;
}

//Methods
/**Carry out the Mandelbrot calculation on the point at the (x,y) coordinates specified by the parameters. The return value specifies
 * whether or not this point is within the Mandelbrot set, which is determined by whether or not the values of the real and imaginary
 * pieces of currentCalc, or Z_(n+1) from the Mandelbrot equation, both reach or exceed the value of 2 within a number of iterations
 * less than or equal to MAX_ITERATIONS.
 * (Postcondition: the program will return an int value which can be used to determine whether the input point is within the Mandelbrot set)
 * @param xVal -- the double value of the desired x coordinate
 * (Precondition: xVal is a real number)
 * @param yVal -- the double value of the desired y coordinate
 * (Precondition: yVal is a real number)
 * @return returns the number of iterations needed to meet or exceed the 2 threshold, or the value of MAX_ITERATIONS if the threshold is never met
 */
public int calculateMandelbrot(double xVal, double yVal, double panelWidth, double panelHeight){
    double xCord = convertToMandelX(xVal, panelWidth);
    double yCord = convertToMandelY(yVal, panelHeight);
    c = new ComplexNum(xCord,-yCord);
    for(int iterations = 0; iterations <= MAX_ITERATIONS && Math.abs(currentCalc.getRealPart())+Math.abs(currentCalc.getImgPart())<=4.0; iterations ++){
        numIterations = iterations;
        z = currentCalc;
        currentCalc = z.squareComplex().add(c);

    }
    return numIterations;

}
//I haven't properly commented the two methods below yet, but these
//are used to convert the coordinates of the pixel I'm testing into
//a point on the coordinate plane with x from -2 to 2 and y from
//-2i to 2i, which the Mandelbrot set is within.
//xPixLoc and yPixLoc are the (x,y) coordinates of the pixels from the
//frame, and maxXVal and maxYVal are the (x,y) dimensions of the frame,
//400 in my case. 
public double convertToMandelX(double xPixLoc, double maxXVal){
    double xCoordinate = MIN_X + ((xPixLoc/maxXVal)*MANDEL_X_RANGE);
    return xCoordinate;
}

public double convertToMandelY(double yPixLoc, double maxYVal){
    double yCoordinate = MAX_Y -((yPixLoc/maxYVal)*MANDEL_Y_RANGE);
    return yCoordinate;
}

I've done some JUnit testing and both of the above classes seem to work. There might be a flaw in my tests that have lead to an oversight, but nothing that I can distinguish. It seems to me that my problem is with my actual creating of the image, which is the class below:

VisualComponent (I'm trying to get this working with only two colors first)

public class VisualComponent extends JComponent{
private static final long serialVersionUID = 1L;

//Constants
public static final int DEFAULT_ZOOM_CHANGE = 10;

//Instance Fields
int pnlWidth, pnlHeight; //The width and height of the panel the image will be painted into
BufferedImage fractalImg;
boolean updateImage;

//Constructor
public VisualComponent(int panelWidth, int panelHeight){
    pnlWidth=panelWidth;
    pnlHeight=panelHeight;
    fractalImg = new BufferedImage(panelWidth, panelHeight, BufferedImage.TYPE_INT_RGB);
    updateImage = true;
    //also initialize a default color pallet
}

//Methods
public void paintComponent(Graphics g){
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
    if(updateImage){
        generateMandelbrot();
        updateImage=false;
    }
    g2.drawImage(fractalImg,0,0,this);

}

public void generateMandelbrot(){
    MandelbrotGenerator genImg = new MandelbrotGenerator();
    int iterations=0;
    for(int x=0; x<pnlWidth;x++){
        for(int y=0; y<pnlHeight;y++){
            iterations = genImg.calculateMandelbrot((double)x, (double)y, pnlWidth, pnlHeight);
            System.out.print(iterations);
            if(iterations == MandelbrotGenerator.MAX_ITERATIONS){
                fractalImg.setRGB(x, y, Color.BLACK.getRGB());
            } else {
                fractalImg.setRGB(x, y, Color.WHITE.getRGB());
            }
        }
    }
}

Here's my main method as well:

public class MainTester {
public static void main(String[] args){
    JFrame frame=new JFrame("Test");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(400,400);
    frame.setResizable(false);
    VisualComponent comp = new VisualComponent(400,400);
    frame.add(comp);
    frame.setVisible(true);
}
}

I'm really stumped. What seems to be happening is that when I call calculateMandelbrot(), the return is always the same. Yet in my testing I've found that this is not the case. I don't have much experience using BufferedImages, so perhaps there's a flaw in how I'm using that?

Since I've mentioned it so much, here's the testing code I was using as well. I know that this is really not the proper form, or at least not the form my professor taught, but I was really just focused on trying to find the issue.

public class ComplexNumTest {

@Test
public void testToString() {
    ComplexNum num = new ComplexNum(5,7);
    String res = num.toString();
    assertEquals("failed toString()", "5.0+7.0i", res);
}

@Test
public void testAdd(){
    ComplexNum num = new ComplexNum(5,7);
    ComplexNum num2 = new ComplexNum(5,3);
    ComplexNum num3 = num.add(num2);
    String res = num3.toString();
    assertEquals("failed add()", "10.0+10.0i", res);
    ComplexNum num4 = new ComplexNum(5,-7);
    ComplexNum num5 = new ComplexNum(-3,4);
    ComplexNum num6 = num4.add(num5);
    String res2 = num6.toString();
    assertEquals("failed add()", "2.0+-3.0i", res2);
}

@Test
public void testSquareComplex(){
    ComplexNum num = new ComplexNum(2,2);
    ComplexNum num2 = num.squareComplex();
    String res = num2.toString();
    assertEquals("failed squareComplex()", "0.0+8.0i", res);
    ComplexNum num3 = new ComplexNum(2,-2);
    ComplexNum num4 = num3.squareComplex();
    String res2 = num4.toString();
    assertEquals("failed squareComplex()", "0.0+-8.0i", res2);
    ComplexNum num5 = new ComplexNum(-1,0.5);
    ComplexNum num6 = num5.squareComplex();
    String res3 = num6.toString();
    assertEquals("failed squareComplex()", "0.75+-1.0i", res3);
}

@Test
public void testCalculations(){
    ComplexNum z = new ComplexNum(0,0);
    ComplexNum y = new ComplexNum(-1,0.5);
    ComplexNum a = z.squareComplex().add(y);
    String res = a.toString();
    assertEquals("failed calculations", "-1.0+0.5i", res);
    z = a;
    a = z.squareComplex().add(y);
    res = a.toString();
    assertEquals("failed squareComplex()", "-0.25+-0.5i", res);
}

@Test
public void getNums(){
    ComplexNum z = new ComplexNum(1,3);
    ComplexNum a = new ComplexNum(2,4);
    double y = z.getRealPart()+a.getRealPart();
    String num=y+"";
    assertEquals("failed getRealPart()", "3.0", num);
    y = z.getImgPart()+a.getImgPart();
    num=y+"";
    assertEquals("failed getRealPart()", "7.0", num);
}

@Test
public void testConvertToMandel(){
    MandelbrotGenerator a = new MandelbrotGenerator();
    double check = a.convertToMandelX(200, 400);
    String res = check+"";
    assertEquals("fail", "0.0", res);
    check = a.calculateMandelbrot(200, 200, 400, 400);
    res=check+"";
    assertEquals("fail", "30.0", res);
    boolean working=false;
    if(check==MandelbrotGenerator.MAX_ITERATIONS){
        working=true;
    }
    assertEquals("fail",true,working);
}
}

I hope this wasn't too much code to throw up here all at once. Thanks so much for the help!

ZMartin
  • 21
  • 4
  • There's only one solution that I know of for this current problem: and that's to learn to use a debugger. – Hovercraft Full Of Eels Apr 05 '16 at 01:00
  • Note that you do have one glaring obvious bug. I'm not sure if it's causing your problems, but regardless it does need to be fixed: you're doing major calculations within a paintComponent method, something you should **never** do. Do these in a background thread and then **display** them within paintComponent. – Hovercraft Full Of Eels Apr 05 '16 at 01:02
  • [For example](http://stackoverflow.com/questions/33859923/change-contents-of-bufferedimage-then-update-jframe-to-reflect-it/33860388#33860388). – Hovercraft Full Of Eels Apr 05 '16 at 01:03
  • Thank you for that example. We just barely touched upon debugging and didn't cover threads at all during my course. I'll do some research into both of these and see what I can come up with. I'll also talk to my professor, but the only time I'll have to do that before this is due is Wednesday, which is why I decided to come ask here. – ZMartin Apr 05 '16 at 01:27
  • First I have never seen the M-Set *calculated* using complex types, it is unecessary even though in the complex plane. Secondly, your end condition compares the sum of the real and imaginary parts with 4.0, whereas it should be checking the *modulus* against 2.0, or more simply, the modulus**2 against 4.0. Third, you can divorce the M-Set calculation from the graphics, by making a simple text array of representing the iterations, and print them to console (max iters = 30, so `'0' + iters` would do). Conversely you can check the graphics by plotting a simple disk of radius 0.5 instead of M-Set. – Weather Vane Apr 05 '16 at 16:22
  • Thank you for your response. My professor wanted me to create the ComplexNum class for this project, so I will be keeping that unless I really can't get this to work. Your other advice seems helpful, and the next opportunity I have to sit down and work on my code I will definitely try these solutions. I actually already started to use the strategy with using an array to check the iterations returned, which confirmed that there was something screwy with my calculations even though I was fairly confident that they were correct. Thanks again, hopefully this helps me solve my issues. – ZMartin Apr 05 '16 at 16:49
  • My program development technique is to "test as you go". So in a project involving graphics I would first check it out with some simple lines or shapes in the pixel range of the window, then move on to scaling such items from the M-Set range to the pixel range. Step by step. – Weather Vane Apr 05 '16 at 17:47
  • Yeah, that's a smart technique, which I could definitely learn from. I managed to get it working. I used an array to hold the iterations as suggested in order to remove the calculations from the paintComponent(), and found the errors within my calculations. I'll make an answer to my question with the changes I made. – ZMartin Apr 06 '16 at 00:46

2 Answers2

2

Your problem is that you dont re-initialize the z points so the computation is stumped as you say:

public int calculateMandelbrot(double xVal, double yVal, double panelWidth, double panelHeight){
    z = new ComplexNum(0,0);
    c = new ComplexNum(0,0);
    currentCalc = new ComplexNum(0,0);
    double xCord = convertToMandelX(xVal, panelWidth);
    double yCord = convertToMandelY(yVal, panelHeight);
    c = new ComplexNum(xCord,-yCord);
    for(int iterations = 0; iterations <= MAX_ITERATIONS && Math.abs(currentCalc.getRealPart())+ Math.abs(currentCalc.getImgPart())<=4; iterations ++){
        numIterations = iterations;
        z = currentCalc;
        currentCalc = z.squareComplex().add(c);
//   System.out.println(currentCalc.getRealPart()+" "+currentCalc.getImgPart());

    }
    return numIterations;

}
gpasch
  • 2,672
  • 3
  • 10
  • 12
  • Hmm, I'm sorry, but I don't quite understand. The Mandelbrot calculations require the result of the previous iteration to be used in the next calculation, which is what I was trying to do by setting z=currentCalc; before using it to compute the next calculation. Is there something more specific that I'm missing? – ZMartin Apr 05 '16 at 02:46
  • I come back weeks later and realize that you were right, but I just didn't understand your wording very well. So sorry! I'll edit the answer post with credit to you! – ZMartin Apr 27 '16 at 13:20
1

I managed to get this working. As Hovercraft Full of Eels suggested, I removed the calculations from my paintComponent() method. Instead I have them done in the main method, stored the values into a 2D array as Weather Vane suggested, and modified my VisualComponent class to take in the array as a parameter when calling the constructor. The calculations themselves were also flawed, my initial confidence in them is misplaced. I had a misunderstanding of how to structure the escape condition, as I didn't realized I was supposed to square the real and imaginary parts, then add them and compare to 4. I also did not need to take the absolute value of them at all (since squaring the value will ensure that it is positive). Finally, I was not initializing the complex numbers every time the method was called, which was an error pointed out by gpasch. There was a reason behind this, but in hindsight I was being entirely stupid and thought that there would be multiple MandelbrotGenerator objects and each would only call the method once. Yeah, I was super confused. My working code is as follows:

Mandelbrot (I completely restructured the method which does the calculations):

    //Constants
/**The maximum number of times the Mandelbrot calculations will be run on a specific point. If the real and imaginary pieces
 * from each calculation don't exceed 2 within the maximum number of iterations, they are part of the Mandelbrot set.
 */
public static final int MAX_ITERATIONS = 30; //The maximum number of times the calculations will be run on a specific point.
private final double MIN_X = -2.0; //The minimum value of x when graphing the Mandelbrot set
private final double MAX_Y = 2.0; //The maximum value of y when graphing the Mandelbrot set
private final double MANDEL_X_RANGE = 4.0; //The range of x values from -2 to 2 when graphing the Mandelbrot set
private final double MANDEL_Y_RANGE = 4.0; //The range of y values from -2 to 2 when graphing the Mandelbrot set

//Instance Fields
private ComplexNum z; //In the Mandelbrot equation of Z_(n+1)=Z_n^2+C, this is the value of Z_n^2
private ComplexNum c; //In the Mandelbrot equation of Z_(n+1)=Z_n^2+C, this is the value of C
private ComplexNum currentCalc; //In the Mandelbrot equation of Z_(n+1)=Z_n^2+C, this is the value of Z_(n+1)
private int numIterations; //The current number of iterations

//Constructor
/**Create a MandelbrotGenerator object.
 */
public MandelbrotGenerator(){
    z = new ComplexNum(0,0);
    c = new ComplexNum(0,0);
    currentCalc = new ComplexNum(0,0);
    numIterations = 0;
}

//Methods
/**Carry out the Mandelbrot calculation on the point at the (x,y) coordinates specified by the parameters. The return value specifies
 * whether or not this point is within the Mandelbrot set, which is determined by whether or not the values of the real and imaginary
 * pieces of currentCalc, or Z_(n+1) from the Mandelbrot equation, both reach or exceed the value of 2 within a number of iterations
 * less than or equal to MAX_ITERATIONS.
 * (Postcondition: the program will return an int value which can be used to determine whether the input point is within the Mandelbrot set)
 * @param xVal -- the double value of the desired x coordinate
 * (Precondition: xVal is a real number)
 * @param yVal -- the double value of the desired y coordinate
 * (Precondition: yVal is a real number)
 * @return returns the number of iterations needed to meet or exceed the 2 threshold, or the value of MAX_ITERATIONS if the threshold is never met
 */
public int calculateMandelbrot(double xVal, double yVal, double panelWidth, double panelHeight){
    double xCord = convertToMandelX(xVal, panelWidth);
    double yCord = convertToMandelY(yVal, panelHeight);
    c = new ComplexNum(xCord,-yCord);
    z = new ComplexNum(0,0);
    currentCalc = new ComplexNum(0,0);
    numIterations=0;
    while(numIterations<=MAX_ITERATIONS && Math.pow(currentCalc.getRealPart(),2)+Math.pow(currentCalc.getImgPart(),2)<=4.0){
        numIterations++;
        z = currentCalc;
        currentCalc = z.squareComplex();
        currentCalc = currentCalc.add(c);
    }
    return numIterations;
}

public double convertToMandelX(double xPixLoc, double maxXVal){
    double xCoordinate = MIN_X + ((xPixLoc/maxXVal)*MANDEL_X_RANGE);
    return xCoordinate;
}

public double convertToMandelY(double yPixLoc, double maxYVal){
    double yCoordinate = MAX_Y -((yPixLoc/maxYVal)*MANDEL_Y_RANGE);
    return yCoordinate;
}

Main

public class MainTester {
public static void main(String[] args){
    JFrame frame=new JFrame("Test");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(400,400);
    frame.setResizable(false);
    MandelbrotGenerator genImg = new MandelbrotGenerator();
    int[][] list = new int[400][400];
    int iterations=0;
    for(int x=0; x<400;x++){
        for(int y=0; y<400;y++){
            iterations = genImg.calculateMandelbrot((double)x, (double)y, 400, 400);
            list[x][y]=iterations;
            //System.out.println(list[x][y]);
        }
    }
    VisualComponent comp = new VisualComponent(400,400, list);
    frame.add(comp);
    frame.setVisible(true);
}
}

VisualComponent (my current color selections are arbitrary and were just my own experimentation)

public class VisualComponent extends JComponent{
private static final long serialVersionUID = 1L;

//Constants
public static final int DEFAULT_ZOOM_CHANGE = 10;

//Instance Fields
int pnlWidth, pnlHeight; //The width and height of the panel the image will be painted into
BufferedImage fractalImg;
boolean updateImage;
int[][] fList;

//Constructor
public VisualComponent(int panelWidth, int panelHeight, int[][] list){
    pnlWidth=panelWidth;
    pnlHeight=panelHeight;
    fractalImg = new BufferedImage(panelWidth, panelHeight, BufferedImage.TYPE_INT_ARGB);
    updateImage = true;
    fList=list;
    //also initialize a default color pallet
}

//Methods
public void paintComponent(Graphics g){
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
    Color pixColor;
    for(int x = 0; x<400; x++){
        for(int y=0; y<400; y++){
            if(fList[x][y] >= MandelbrotGenerator.MAX_ITERATIONS){
                fractalImg.setRGB(x, y, Color.BLACK.getRGB());
            } else {
                if(fList[x][y]<=2){
                    pixColor= new Color((42+fList[x][y])%255,0,(80+fList[x][y])%255);
                }else if(fList[x][y]<=3){
                    pixColor= new Color((48+fList[x][y])%255,0,(90+fList[x][y])%255);
                }else {
                    pixColor=new Color((50+fList[x][y])%255,0,(100+fList[x][y])%255);
                }
                fractalImg.setRGB(x, y, pixColor.getRGB());
            }
        }
    }
    g2.drawImage(fractalImg,0,0,this);
}

There were no changes made to my complex number class. Obviously I still need to actually make the program do something besides just generate the base image, but now that I've gotten this all straightened out I think I'll be able to figure that out. Thanks again to Hovercraft Full of Eels and Weather Vane for their helpful comments!

EDIT: I realize in my code posted above that there are some instances where I use 400 instead of a variable which holds the size of the frame. I've already fixed that, just wanted to make sure that it was clear that I realized the oversight. Here's an image of my result

ZMartin
  • 21
  • 4