10
\$\begingroup\$

Seeing as it's October, I thought I'd do the community challenge. Due to my inexperience coding in Groovy, I decided to give Groovy a go. I'm not sure how well I followed Groovy conventions, and the rendering is quite slow. It seem that the slow rendering might have to do with locking when writing to the BufferredImage, or so my profiling seems to suggest.

I'm not concerned about the GUI portion (MandelbrotViewer.groovy); that's just there to make it possible to display the Mandelbrot.

Mandelbrot.groovy

package mandelbrot

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovyx.gpars.GParsPool

import java.awt.*
import java.awt.image.BufferedImage

class Mandelbrot {
    @EqualsAndHashCode
    @ToString
    static class RenderDetails {
        double xMin
        double xMax
        double yMin
        double yMax
        int width
        int height
        int maxIteration
        double bailoutRadius
        def palette
        def interpolationFunction
        boolean interpolate

        public RenderDetails(Map hash) {
            xMin = hash.get("xMin", -2.5) as double
            xMax = hash.get("xMax", 1) as double
            yMin = hash.get("yMin", -1) as double
            yMax = hash.get("yMax", 1) as double
            width = hash["width"] as int
            height = hash["height"] as int
            maxIteration = hash.get("maxIteration", 1000) as int
            bailoutRadius = hash.get("bailoutRadius", 1 << 8) as double
            interpolate = hash.get("interpolate", true)
            interpolationFunction = hash.get("interpolationFunction", Mandelbrot.&linearColorInterpolation)
            palette = hash.get("palette", getDefaultPalette(interpolate ? maxIteration + 2 : maxIteration + 1))
        }
    }

    static BufferedImage render(details) {
        if (!(details instanceof RenderDetails)) {
            details = new RenderDetails(details)
        }
        def retval
        GParsPool.withPool {
            // Note: If this is not declared *here*, GParsPool locks on
            // the image each time we write to a pixel, making the
            // multithreading completely counterproductive
            def image = new BufferedImage(details.width, details.height, BufferedImage.TYPE_INT_ARGB);
            (0..image.width - 1).collect { x ->
                (0..image.height - 1).collect { y ->
                    [x, y]
                }
            }.sumParallel()
                    .eachParallel { px, py ->
                def x = scaleToReals(details.xMin, details.xMax, details.width, px)
                def y = scaleToReals(details.yMin, details.yMax, details.height, py)
                image.setRGB(px as int, py as int, getColor(x, y, details) as int)
            }
            retval = image
        }
        return retval
    }

    def private static getColor(double x, double y, RenderDetails details) {
        def p0 = new Complex(x, y)
        def p = new Complex(x, y)
        def curIteration = 0

        while (p.absSquare() < details.bailoutRadius**2 && curIteration < details.maxIteration) {
            p = p.square() + p0
            curIteration++
        }

        if (details.interpolate) {
            if (curIteration < details.maxIteration) {
                def log_zn = Math.log(p.absSquare()) / 2
                def nu = Math.log(log_zn / Math.log(2)) / Math.log(2)
                curIteration = curIteration + 1 - nu
            } else {
                return details.palette.get(details.palette.size() - 1).getRGB()
            }

            def color1 = details.palette.get(Math.floor(curIteration) as int)
            def color2 = details.palette.get(Math.floor(curIteration + 1) as int)
            return details.interpolationFunction(color1, color2, curIteration % 1)
        } else {
            return details.palette.get(curIteration).getRGB()
        }
    }

    def private static getDefaultPalette(size) {
        def final START = Color.ORANGE
        def final END = Color.BLACK
        def palette = []

        def rgbStep = (START.getRGB() - END.getRGB()) / (size - 1);

        (0..size - 1).each { i ->
            palette.push(
                    new Color(
                            ((START.getRGB() - (int) (rgbStep * i)) & 0xFF0000) >>> 16,
                            ((START.getRGB() - (int) (rgbStep * i)) & 0x00FF00) >>> 8,
                            ((START.getRGB() - (int) (rgbStep * i)) & 0x0000FF) >>> 0
                    )
            )
        }

        return palette
    }

    @CompileStatic
    private static int linearColorInterpolation(Color c1, Color c2, double ratio) {
        new Color(
                (c1.red + (c2.red - c1.red) * ratio) as int,
                (c1.green + (c2.green - c1.green) * ratio) as int,
                (c1.blue + (c2.blue - c1.blue) * ratio) as int
        ).getRGB()
    }

    @CompileStatic
    private static double scaleToReals(double min, double max, double width, double value) {
        min + value / width * (max - min)
    }
}

Complex.groovy

package mandelbrot

import groovy.transform.CompileStatic
import groovy.transform.Immutable
import groovy.transform.ToString

@Immutable
@CompileStatic
@ToString(includes = ["real", "imaginary"])
class Complex {
    public static final Complex ZERO = new Complex(0, 0)
    public static final Complex ONE = new Complex(1, 0)
    public static final Complex I = new Complex(0, 1)
    public static final double EQUALS_TOLERANCE = 1e-5;

    /** The real part of the complex number */
    double real
    /** The imaginary part of the complex number */
    double imaginary

    /** Adds the two complex numbers together */
    Complex plus(Complex other) {
        new Complex(real + other.real, imaginary + other.imaginary)
    }

    /** Subtracts the two complex numbers */
    Complex minus(Complex other) {
        new Complex(real - other.real, imaginary - other.imaginary)
    }

    /** Multiplies the two complex numbers */
    Complex multiply(Complex other) {
        new Complex(real * other.real - imaginary * other.imaginary, real * other.imaginary + imaginary * other.real)
    }
    /** Divides the two complex numbers */
    Complex div(Complex other) {
        new Complex(
                (real * other.real + imaginary * other.imaginary) / (other.real**2 + other.imaginary**2),
                (imaginary * other.real - real * other.imaginary) / (other.real**2 + other.imaginary**2)
        )
    }

    /** negate the complex number */
    Complex negative() {
        return new Complex(-real, -imaginary)
    }

    /** Complex conjugate */
    Complex conj() {
        new Complex(real, -imaginary)
    }

    /** The modulus of the complex number */
    double abs() {
        Math.sqrt(absSquare())
    }

    /** The square of the modulus */
    double absSquare() {
        real * real + imaginary * imaginary
    }

    /** The square of the complex number */
    Complex square() {
        new Complex(real * real - imaginary * imaginary, 2 * real * imaginary)
    }

    /** The reciprocal of the complex number */
    Complex reciprocal() {
        new Complex(real / absSquare(), -imaginary / absSquare())
    }

    Complex power(int n) {
        if (n == -1) {
            return reciprocal()
        } else if (n < 0) {
            return reciprocal()**-n
        } else if (n == 0) {
            return ONE
        } else if (n == 1) {
            return this
        } else if (n == 2) {
            return square()
        } else if (n % 2 == 0) {
            return square()**(n.intdiv(2) as int)
        } else {
            return this * square()**((n - 1).intdiv(2) as int)
        }
    }

    boolean equals(o) {
        if (this.is(o)) return true
        if (!(o instanceof Complex)) return false

        Complex complex = (Complex) o

        if (Math.abs(imaginary - complex.imaginary) > EQUALS_TOLERANCE) return false
        if (Math.abs(real - complex.real) > EQUALS_TOLERANCE) return false

        return true
    }
}

MandelbrotViewer.groovy

package mandelbrot

import javax.swing.*

JFrame frame = new JFrame()
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
frame.iconImage = Mandelbrot.render(
        xMin: -2,
        xMax: 1,
        width: 100,
        height: 100,
        maxIteration: 100,
        bailoutRadius: 2,
        interpolate: false
)

frame.add(
        new JLabel(new ImageIcon(
                Mandelbrot.render(
                        width: 500,
                        height: 500
                )
        ))
)
frame.pack()
frame.locationByPlatform = true
frame.visible = true

Output:

Mandelbrot output - Windows 10

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.