Friday, June 3, 2022
HomeProgrammingMandelbrot Set, Sport of Life, and Extra – Java, SQL and jOOQ.

Mandelbrot Set, Sport of Life, and Extra – Java, SQL and jOOQ.


The upcoming jOOQ 3.16 will lastly provide help for the varied RDBMS GIS extensions by way of subject #982. That is nice information per se, and will likely be coated in a future weblog publish, when the mixing is prepared. This publish right here is about one thing else.

Including help for such a characteristic is a good supply of procrastination. You see, while you develop a backend library, all you ever do is implement algorithms, API, and different summary stuff. However GIS is completely different. With GIS, a SQL developer like me all of a sudden has entry to a “UI”, and getting access to a “UI” woke the inside programmer little one in me.

I used to be pleasantly shocked that DBeaver, my most well-liked SQL editor, has out of the field help for GIS’s WKT format. E.g. run a question like this:

SELECT st_geomfromtext('polygon((0 0, 2 3, 0 6, -2 3, 0 0))');

The output being

Let’s mess around with GIS

So, naturally, what different factor to do than mess around with it? The obvious subsequent factor to generate could be your favorite brand, which occurs to be very simple to map to polygons. Let’s have a look at just a few GIS options step-by-step. And for this weblog publish, I’ll be utilizing PostGIS, which you will get very simply from docker hub:

WITH
  -- These "sprites" are simply reusable 2D polygons, which I declared
  -- to keep away from having to cope with the prolonged WKT format
  sprites AS (
    SELECT 

      -- We challenge the unique 1x1 sq., and a scaled 4x4 model
      s AS sq.,
      st_scale(s, 4, 4) AS square4

    -- Right here, we're making a sq. referred to as "s"
    FROM (VALUES 
      (st_polygonfromtext('polygon ((0 0, 1 0, 1 1, 0 1, 0 0))'))
    ) t (s)
  )

-- st_union combines 2 polygons right into a multipolygon
SELECT st_union(sq., st_translate(square4, 2, 0))
FROM sprites

The output of the above question is

MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 6 0, 6 4, 2 4, 2 0)))

Or, utilizing DBeaver’s visualisation utility:

The st_union isn’t a lot completely different from another set union. Be aware that I’ve translated the bigger sq. 2 models to the correct, so that they don’t overlap. In any other case, the union would have simply been the larger sq..

A polygon describes a set of factors within the 2D quantity aircraft. Making a union of these two units of factors is pure. We will additionally create a distinction of the 2 squares, as a substitute:

WITH
  sprites AS (
    SELECT 
      s AS sq.,
      st_scale(s, 4, 4) AS square4
    FROM (VALUES 
      (st_polygonfromtext('polygon ((0 0, 1 0, 1 1, 0 1, 0 0))'))
    ) t (s)
  )
SELECT st_difference(square4, sq.)
FROM sprites

Leading to:

POLYGON ((0 4, 4 4, 4 0, 1 0, 1 1, 0 1, 0 4))

Or, visually:

With these easy instruments, we are able to now create all 4 letters of the jOOQ brand. As a reminder, the instruments have been:

  • st_polygonfromtext: Create a polygon from a WKT textual content illustration
  • st_scale: Scale any geometry to a brand new measurement
  • st_translate: Translate any geometry to a brand new place
  • st_union: Mix two geometries (or extra, if used as an combination operate)
  • st_difference: Take away one geometry from one other

The GIS API is huge, each in PostGIS in addition to within the ISO/IEC 13249-3:2016 normal model, however these easy instruments shall suffice for now. Let’s have a look at this question:

WITH
  -- Creating the 2 squares to work with
  sprites AS (
    SELECT 
      s AS sq.,
      st_scale(s, 4, 4) AS square4
    FROM (VALUES 
      (st_polygonfromtext('polygon ((0 0, 1 0, 1 1, 0 1, 0 0))'))
    ) t (s)
  ),
  
  -- All of the 4 letters j, o1, o2, q of the jOOQ brand
  letters AS (
    SELECT
    
      -- The letter j
      st_difference(
        st_difference(
          st_difference(
            square4, 
            st_translate(st_scale(sq., 2, 3), 1, 1)
          ),
          st_translate(st_scale(sq., 1, 2), 0, 2)
        ),
        st_translate(st_scale(sq., 1, 0.5), 3, 2.5)
      ) AS j,
      
      -- The letter o
      st_difference(square4, st_translate(sq., 1, 1)) AS o1,
      
      -- The letter o
      st_difference(square4, st_translate(sq., 2, 2)) AS o2,
      
      -- The letter q
      st_union(
        st_difference(
          square4, 
          st_translate(st_scale(sq., 2, 2), 1, 1)
        ),
        st_translate(st_scale(sq., 1, 2.5), 1.5, -1)
      ) as q
    FROM sprites
  )
SELECT

  -- Mix all of the 4 letters
  st_union(v)
FROM
  letters,
  
  -- Organize the 4 letters subsequent to one another
  LATERAL (VALUES
    (st_translate(j, 0, 5)),
    (st_translate(o1, 5, 5)),
    (o2),
    (st_translate(q, 5, 0))
  ) t (v);

This produces:

Neat, huh?

Subsequent step: The Mandelbrot Set

A pure subsequent step for any procrastinator is to generate the Mandelbrot set. To organize ourselves for what’s behind the Mandelbrot, take a look at this neat video (extra procrastination):

There are other ways to calculate it with SQL, right here’s one which I got here up with:

WITH RECURSIVE

  -- These are the scale which you could mess around with
  dims (r1, r2, i1, i2, s, it, p) AS (
    VALUES (
      
      -- The size of the actual axis
      -2::float, 1::float, 
      
      -- The size of the imaginary axis
      -1.5::float, 1.5::float, 
      
      -- The step measurement on every axis, per pixel or sprite
      0.01::float, 
      
      -- The utmost variety of iterations
      100, 
      
      -- "Infinity", i.e. when to cease
      256.0::float
    )
  ),
  
  -- The sq. once more, as earlier than
  sprites (s) AS (VALUES 
    (st_polygonfromtext('polygon ((0 0, 0 1, 1 1, 1 0, 0 0))'))
  ),
  
  -- The quantity aircraft, as ints
  n1 (r, i) AS (
    SELECT r, i 
    FROM 
      dims, 
      generate_series((r1 / s)::int, (r2 / s)::int) r,
      generate_series((i1 / s)::int, (i2 / s)::int) i
  ),
  
  -- The quantity aircraft as scaled floats
  n2 (r, i) AS (
    SELECT r::float * s::float, i::float * s::float
    FROM dims, n1
  ),
  
  -- The recursive calculation of the Mandelbrot formulation
  -- zn = (zn-1)^2 + c
  l (cr, ci, zr, zi, g, it, p) AS (
    SELECT r, i, 0::float, 0::float, 0, it, p FROM n2, dims
    UNION ALL
    SELECT cr, ci, zr*zr - zi*zi + cr, 2*zr*zi + ci, g + 1, it, p 
    FROM l
    
    -- The recursions stops once we attain the utmost
    WHERE g < it
    
    -- Or, once we attain "infinity"
    AND zr*zr + zi*zi < p
  ),
  
  -- Discover the final calculated worth per level within the
  -- complicated quantity aircraft c (cr, ci), discard the others
  x (cr, ci, zr, zi, g) AS (
    SELECT DISTINCT ON (cr, ci) cr, ci, zr, zi, g
    FROM l
    ORDER BY cr, ci, g DESC
  )
  
-- Flip each calculated level right into a sq.
SELECT 
  st_union(
    st_translate(sprites.s, spherical(cr / dims.s), spherical(ci / dims.s))
  )
FROM x, sprites, dims

-- Retain solely the factors *not* belonging to the Mandelbrot set
-- You can too inverse the equation to retain the factors that
-- belong to the set
WHERE zr*zr + zi*zi > p;

Be aware, I’m utilizing a synthetic “infinity” right here, as a result of:

  1. That speeds issues up with little noticeable distinction at this zoom scale
  2. I couldn’t work out how one can make PostgreSQL overflow float operations to drift infinities, like Java or CockroachDB or different IEEE 754 float implementations do. Any assist appreciated, see this Stack Overflow query.

The output is the well-known form

You’ll be able to mess around with this and use completely different values for “dims” to zoom in, e.g.

dims (r1, r2, i1, i2, s, it, p) as (values (
  (-0.925-0.032)::float, (-0.925+0.032)::float, 
  (0.266-0.032)::float, (0.266+0.032)::float, 
  0.00005::float, 100, 256.0::float)
)

It will generate some actually neat zooms, all with SQL:

Granted, it’s not probably the most environment friendly technique to calculate these items, however that wasn’t the purpose right here, was it?

Sport of Life

I might be nerd-sniped:

And naturally, I needed to settle for that problem too! Now, the Sport of Life is a straightforward “sport” the place we now have the x,y pure quantity aircraft (e.g. “pixels”), and with every coordinate, we affiliate whether or not that “cell” is lifeless or alive. Then, we set up the next algorithm:

  1. Any stay cell with two or three stay neighbours survives.
  2. Any lifeless cell with three stay neighbours turns into a stay cell.
  3. All different stay cells die within the subsequent era. Equally, all different lifeless cells keep lifeless.

No, it’s laborious to “animate” issues in SQL utilizing spatial extensions, in order a workaround, I’ll simply show iterations of 100×100 pixel tiles of the Sport of Life subsequent to one another. The primary iteration is simply the start line, e.g. a random variety of “alive” cells:

WITH RECURSIVE

  -- Once more generate a "sprite"a from a sq. polygon
  sprites (s) AS (VALUES (
    st_polygonfromtext('polygon ((0 0, 0 1, 1 1, 1 0, 0 0))')
  )),

  -- Generate the x, y quantity aircraft and affiliate a boolean worth
  -- with every coordinate, generated randomly.
  -- 10% of all cells are alive
  m (x, y, b) AS (
    SELECT x, y, 
      CASE WHEN random() > 0.9 THEN 1 ELSE 0 END 
    FROM 
      generate_series(1, 100) x, 
      generate_series(1, 100) y
  )
SELECT st_union(st_translate(sprites.s, x::float, y::float))
FROM m, sprites

-- Show solely "alive" cells
WHERE b = 1

It will produce one thing like:

Now, all we now have to do is iterate the sport formulation. Now, recursive SQL has fairly just a few limitations. E.g. I couldn’t get PostgreSQL to combination recursive knowledge, nor self-join the recursive desk to seek out the closest neighbors of any cell. However with window features, that is positively attainable. So right here goes:

WITH RECURSIVE
  sprites (s) AS (VALUES (
    st_polygonfromtext('polygon ((0 0, 0 1, 1 1, 1 0, 0 0))')
  )),
  m (x, y, b) AS (
    SELECT x, y, 
      CASE WHEN random() > 0.9 THEN 1 ELSE 0 END
    FROM 
      generate_series(1, 100) x, 
      generate_series(1, 100) y
  ),
  
  -- That is the recursion of the Sport of Life
  e (x, y, b, g) AS (
  
    -- The recursion begins with the above preliminary knowledge
    SELECT x, y, b, 1 FROM m
    UNION ALL
    SELECT
      e.x, e.y,
      CASE
      
        -- If this cell is alive, and a couple of or 3
        -- neighbors are alive, too, then this
        -- cell stays alive
        WHEN e.b = 1 AND
          sum(e.b) OVER w1
        + sum(e.b) OVER w2
        + sum(e.b) OVER w3 IN (2, 3)
          THEN 1
          
        -- If this cell is lifeless, and three neighbors
        -- are alive, then this cell turns into alive
        WHEN e.b = 0 AND
          sum(e.b) OVER w1
        + sum(e.b) OVER w2
        + sum(e.b) OVER w3 = 3
          THEN 1
          
        -- In any other case, this cell dies or stays lifeless
        ELSE 0
      END,
      e.g + 1
    FROM e
    
    -- The recursion stops after 100 iterations
    WHERE e.g < 100
    WINDOW
    
      -- We order the info set by x, y. In SQL, there is not 
      -- a notion of a 2-dimensional quantity aircraft. We solely
      -- have 1 dimension.
      o AS (ORDER BY x, y),
      
      -- w1 is all of the neighbors of the earlier row within the x, y
      -- aircraft, which is all of the SQL rows which can be 101-99 rows
      -- earlier than the present row.
      w1 AS (o ROWS BETWEEN 101 PRECEDING AND  99 PRECEDING),
      
      -- w2 is all of the neighbors of the present row within the x, y
      -- quantity aircraft, excluding the present cell
      w2 AS (o ROWS BETWEEN   1 PRECEDING AND   1 FOLLOWING 
               EXCLUDE CURRENT ROW),
               
      -- w3 is all of the neighbors of the subsequent row
      w3 AS (o ROWS BETWEEN  99 FOLLOWING AND 101 FOLLOWING)
  )

-- Lastly, mix all of the iterations  
SELECT st_union(st_translate(
  sprites.s, 
  (x + (g - 1) % 10 * 120)::float, 
  (y - (g - 1) / 10 * 120)::float
))
FROM e, sprites
WHERE b = 1
;

Consider the window specs as follows:

----+-------------+-------------+-------------+-------------+----
... | 105: (1, 5) | 106: (1, 6) | 107: (1, 7) | 108: (1, 8) | ...
... | 205: (2, 5) | 206: (2, 6) | 207: (2, 7) | 208: (2, 8) | ...
... | 305: (3, 5) | 306: (3, 6) | 307: (3, 7) | 308: (3, 8) | ...
... | 405: (4, 5) | 406: (4, 6) | 407: (4, 7) | 408: (4, 8) | ...

So, in a 100×100 grid, x=3,y=7 might be encoded as 307, and its neighbors are

  • w1: 206, 207, 208, i.e. between 101 previous and 99 previous of 307
  • w2: 306, 308, i.e. between 1 previous and 1 following of 307
  • w3: 406, 407, 408, i.e. between 99 following and 101 following of 307

The output seems like this:

Or, zooming in on iterations 1-4:

Or 21-24:

That’s actually cool, huh!

Glider Gun

The nerd-snipe was to animate the glider gun sample, which simply means we’ll have to exchange the random era of the primary iteration by one thing fixed.

WITH RECURSIVE
  sprites (s) AS (VALUES (
    st_polygonfromtext('polygon ((0 0, 0 1, 1 1, 1 0, 0 0))')
  )),
  m (x, y, b) AS (
    SELECT x, y, 

      -- Preliminary knowledge right here
      CASE WHEN (x, y) IN (
        (2, 6), (2, 7), (3, 6), (3, 7), (12, 6), (12, 7), (12, 8), 
        (13, 5), (13, 9), (14, 4), (14, 10), (15, 4), (15, 10),
        (16, 7), (17, 5), (17, 9), (18, 6), (18, 7), (18, 8), 
        (19, 7), (22, 4), (22, 5), (22, 6), (23, 4), (23, 5), 
        (23, 6), (24, 3), (24, 7), (26, 2), (26, 3), (26, 7), 
        (26, 8), (36, 4), (36, 5), (37, 4), (37, 5)
      ) THEN 1 ELSE 0 END 
    FROM 
      generate_series(1, 100) x, 
      generate_series(1, 100) y
  ),
  e (x, y, b, g) AS (
    SELECT x, y, b, 1 FROM m
    UNION ALL
    SELECT
      e.x, e.y,
      CASE
        WHEN e.b = 1 AND
          sum(e.b) OVER w1
        + sum(e.b) OVER w2
        + sum(e.b) OVER w3 IN (2, 3)
          THEN 1
        WHEN e.b = 0 AND
          sum(e.b) OVER w1
        + sum(e.b) OVER w2
        + sum(e.b) OVER w3 = 3
          THEN 1
        ELSE 0
      END,
      e.g + 1
    FROM e
    WHERE e.g < 100
    WINDOW
      o AS (ORDER BY x, y),
      w1 AS (o ROWS BETWEEN 101 PRECEDING AND  99 PRECEDING),
      w2 AS (o ROWS BETWEEN   1 PRECEDING AND   1 FOLLOWING 
               EXCLUDE CURRENT ROW),
      w3 AS (o ROWS BETWEEN  99 FOLLOWING AND 101 FOLLOWING)
  )
SELECT st_union(st_translate(
  sprites.s, 
  (x + (g - 1) % 10 * 120)::float, 
  (y - (g - 1) / 10 * 120)::float
))
FROM e, sprites
WHERE b = 1
;

And, as you may see, the gun works:

It began with this:

And ultimately produces the well-known gliders:



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments