Creating Beauty With Cypher reduce()

Director of Engineering, Neo4j
9 min read









public static void generateMandelbrot(BufferedImage target) { final int maxIterations = 100; for (int y = 0; y < target.getHeight(); y++) { for (int x = 0; x < target.getWidth(); x++) { // Convert to a coordinate system with origin in the center double cx = x*4.0/target.getWidth() - 2.0; double cy = y*-4.0/target.getWidth() + 2.0; double zx = 0; double zy = 0; // Run iterations until the point has "escaped" int i; for (i = 0; i < maxIterations; i++) { // The Mandelbrot formula double xtemp = zx*zx - zy*zy + cx; zy = 2.0 * zx * zy + cy; zx = xtemp; // Pythagorean theorem to calculate distance to origin, // but instead of comparing the square root with 2.0, we // compare with the square of 2.0 (i.e. 4.0) if ((zx*zx+zy*zy) > 4.0) { break; } } // Set the color based on the number of iterations we completed if (i == maxIterations) { target.setRGB(x, y, Color.black.getRGB()); } else { target.setRGB(x, y, new Color(155+i, 155+i, 155+i).getRGB()); } } } }This gives you a grayscale Mandelbrot fractal. To get the more colorful variant at the start of the blog, we just need some more elaborate converting of iterations to color at the end of the routine above.

UNWIND range(0, 31) AS y UNWIND range(0, 63) AS x WITH collect({x:x, y:y}) AS grid WITH reduce( agg=[], v IN grid | agg + [{x:v.x, y:v.y, z: CASE reduce( mem=[0.0, 0.0, (v.x-48.0)/32.0, (v.y-16.0)/-12.0, 0], iter IN range(1,100) | [ mem[0]^2 - mem[1]^2 + mem[2], 2.0 * mem[0] * mem[1] + mem[3], mem[2], mem[3], mem[4] + CASE mem[0]^2 + mem[1]^2 <= 4.0 WHEN true THEN 1 ELSE 0 END ])[4] WHEN 100 THEN " " WHEN >5 THEN "*" WHEN >2 THEN "-" ELSE "." END }]) AS xyz RETURN reduce(res = "", point IN xyz | res + CASE point.x=63 WHEN true THEN point.z + "n" ELSE point.z END ) AS resultYou’ll be able to see something that resembles the Mandelbrot already in the browser output, but it will be a bit distorted since it doesn’t use a mono-sized font. But if you copy the result over to your favorite text editor, you’ll see it more clearly.
................................................................ .................-------------------------------................ ..........---------------------------------------------......... .....-----------------------------------*****---------------.... .---------------------------------------******** *-------------- ---------------------------------------***********-------------- -----------------------------------******** ******------------ ------------------------------*********** ********-------- --------------------------***** *********** ****************-- ------------------------******* ** *********- ---------------------*********** ****- ------********************** ***** -----********************** *** ----******* * ******** *** -********* ** *** ********* * ***- ******- ********* * ***- -********* ** *** ----******* * ******** *** -----********************** *** ------********************** ***** ---------------------*********** ****- ------------------------******* ** *********- --------------------------***** *********** ****************-- ------------------------------*********** ********-------- -----------------------------------******** ******------------ ---------------------------------------***********-------------- .---------------------------------------******** *-------------- .....-----------------------------------*****---------------.... ..........---------------------------------------------......... .................-------------------------------................As we see, it makes heavy use of the Cypher function reduce(). This is one of those methods that is a bit complex to comprehend, but once you do, it’s extremely powerful. It iterates over a list, performing an operation on each element, using an accumulator to update the state on each iteration and then returns that accumulator. So we go through our Cypher query one part at a time and drill down into each of those reduce() uses. We start by creating a list of objects holding an x and y coordinate for each “pixel” of our fractal (we call this list grid). Our resolution is 64*32 (we use this aspect ratio because most fonts are twice as high as they are wide, so it produces a somewhat square result like we see above).
UNWIND range(0, 31) AS y UNWIND range(0, 63) AS x WITH collect({x:x, y:y}) AS gridNow we have our first reduce that produces a new list of objects returned as xyz (temporarily called agg as the aggregator in the reduce). Removing the inner part it looks like this:
WITH reduce( agg=[], v IN grid | agg + [{x:v.x, y:v.y, z: ...}]) AS xyzWe iterate over each element in the list grid, producing the new list of objects that also has x and y, but also a z, which is set to the ASCII character for that “pixel.” The inner reduce() that builds up z (shown as ‘…’ above) looks like this:
CASE reduce( mem=[0.0, 0.0, (v.x-48.0)/32.0, (v.y-16.0)/-12.0, 0], iter IN range(1,100) | [ mem[0]^2 - mem[1]^2 + mem[2], 2.0 * mem[0] * mem[1] + mem[3], mem[2], mem[3], mem[4] + CASE mem[0]^2 + mem[1]^2 <= 4.0 WHEN true THEN 1 ELSE 0 END ])[4] WHEN 100 THEN " " WHEN >5 THEN "*" WHEN >2 THEN "-" ELSE "." ENDThis list iterates 100 times (unlike the Java version, we can’t break when the goal is reached, meaning the point has “escaped”). This is done with the iter IN range(1,100) part. As aggregator, it has a list, called mem, of five elements used to hold the variables we need during the Mandelbrot calculation. These five elements are zx (mem[0]), zy (mem[1]), cx (mem[2]), cy (mem[3]), and the number of iterations before it escaped (mem[4]). zx and zy are initiated to 0, while cx and cy is set like this:
cx = (v.x-48.0)/32.0 cy = (v.y-16.0)/-12.0, 0]This is again to move origin toward center and to scale into the appropriate range for Mandelbrot. However, we offset the origin a bit, and we don’t scale uniformly on x and y. The reason is that we want to capture the interesting part of the Mandelbrot, which is between -1.5,1.33 and 0.5,-1.33, and to scale so that it fits with the font shape, which was mentioned above. In each iteration, we simply update mem[0] and mem[1] (zx and zy) with the Mandelbrot equation. Note that we don’t need the xtemp variable we needed in the Java version to prevent zx from changing before zy was calculated. It’s not needed here since Cypher won’t update anything before all parts are calculated. mem[2] and mem[3] (cx and cy) are kept the same in the iterations (c is not changed when iterating, just z). mem[4] is increased by 1 if the Pythagorean theorem shows that we’re within 2.0 from origin, otherwise by 0. So at the end, this will be set to the number of iterations (because if a point has ever been further away that 2.0 it will never come back within range again). This entire reduce() is wrapped inside a CASE on just the last element, mem[4]:
CASE reduce(...)[4] WHEN 100 THEN " " WHEN >5 THEN "*" WHEN >2 THEN "-" ELSE "." ENDThis returns the ASCII character based on how many iterations we achieved. After these nestled reduces, another reduce is used to create the output as a single string, based on the xyz output from the outer reduce:
RETURN reduce(res = "", point IN xyz | res + CASE point.x=63 WHEN true THEN point.z + "n" ELSE point.z END ) AS resultIt iterates over this xyz list, and for each element adding its ASCII character (i.e., z) to the result string, unless it’s the last point of each row (i.e., x = 63), in which case it adds z and a newline. This is now our final Mandelbrot ASCII output. I’d like to thank Håkan Löfqvist and Georgiy Kargapolov for helping me construct the query.
Summary and Questions to Ponder
Now that we have completed the technical part — we have the algorithm completed and dissected, and we know every aspect of the reduce() function — let’s go back and marvel at the beauty and mystery of the Mandelbrot fractal. You can see a version where you can zoom in and look at all the details. Remember that once you’ve zoomed in a bit, you have to increase max iterations in the control panel to the left to see all the details. Why does it work like this? What is it about this simple equation that gives us this beetle-like shape (which is what I’ve always thought it looks like)? Why does it repeat itself in this pattern? And what is it about those repeated circles that make points there unable to escape?
Creating beauty with Cypher reduce() was originally published in Neo4j Developer Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.