There is an interesting sort of CA that comes when one considers an infinite universe, where the cells have less and less of an effect as their Euclidean Distance becomes greater.

This script runs CA with the following rules:

-The 'heat' or neighbour count of a cell is the total sum of all reciprocals of quarted(^4) euclidean distances to other live cells.
-It chooses to birth or survive cells based on thresholds specified by the rule. For example, ECA:B(1,2)(3,4)/S(3,6) would birth if the heat was between 1 and 2 (including both of those), or 3 and 4, and survive if between 3 and 6.

--This is a preliminary version. If sufficiently interesting behaviours show
--up, it would be nice to get a full simulating algorithm in better-written
--softawre like Golly. Speaking of Golly, make sure you have a version that
--supports Lua, so you can run this script. Stinky.
--TERMS TO KNOW:
--Heat - The number returned when the previous equation is ran on all of the
-- other cells in relation to the given cell (basically what count()
-- does)
local g = golly()
local gp = require "gplus"
local rulesB = {.01, 7}
local rulesS = {}
function count(x1, y1) --determine cell's heat
local pat
local total = 0
if g.getcell(x1, y1) == 1 then --ok i know it's ugly but it saves a lot of processing time than doing a conditional
g.setcell(x1, y1, 0)
pat = g.getcells(g.getrect())
g.setcell(x1, y1, 1)
else
pat = g.getcells(g.getrect())
end
for i = 1, #pat, 2 do
local x2 = pat[i]
local y2 = pat[i+1] --ugly
total = total+1 / ((x2-x1)^2+(y2-y1)^2)^2 --i know i said i was using fourth powers, however the square root and fourth power cancel out leaving just a stray ^2
end
return total
end
function ring(r) --basically, it returns a 'outer moore birth ring' of the given size
local pat = g.getcells(g.getrect())
local x, y, w, h = table.unpack(r) --there a better way to do this?
g.select({x-1, y-1, w+2, h+2})
g.randfill(100)
g.select({x, y, w, h})
g.clear(0)
local nring = g.getcells(g.getrect())
g.putcells(pat)
g.clear(1)
g.select({})
return nring
end
function step()
--hoooo boy i hated writing this
--don't read it if you don't want to get lost
local pat = g.getcells(g.getrect())
if g.getpop() == "0" then
g.exit("All cells are dead.")
return
end
local rulesBclone = rulesB
table.sort(rulesBclone)
local minrulesB = rulesBclone[1] --math.min but with an array
local lx, ly, lw, lh = table.unpack(g.getrect())
while true do --this loop determines how far to check for births
local localmax = 0
local localring = ring({lx, ly, lw, lh})
for i = 1, #localring, 2 do
local localcount = count(localring[i], localring[i+1])
if localcount > localmax then
localmax = localcount
end
end
if localmax < minrulesB then
break
end
lx = lx - 1
ly = ly - 1
lw = lw + 2
lh = lh + 2
end
--gosh that took me months to make, now onto processing
g.select({lx, ly, lw, lh})
g.randfill(100)
g.putcells(pat, 0, 0, 1, 0, 0, 1, --[[ugh why]] "xor")
local area = g.getcells(g.getrect())
g.clear(0)
g.putcells(pat)
--NOW LET'S FINALLY PROCESS BIRTH, aka the reason that hellhole of lines above had to be done
local localcells = {}
for i = 1, #area, 2 do
local localcount = count(area[i], area[i+1])
for j = 1, #rulesB, 2 do
if localcount >= rulesB[j] and localcount <= rulesB[j+1] then
table.insert(localcells, area[i])
table.insert(localcells, area[i+1]) --can't think of a way to insert two at once
end
end
end
--and survival, the easy part
for i = 1, #pat, 2 do
local localcount = count(pat[i], pat[i+1])
for j = 1, #rulesS, 2 do
if localcount >= rulesS[j] and localcount <= rulesS[j+1] then
table.insert(localcells, pat[i])
table.insert(localcells, pat[i+1])
end
end
end
g.select(g.getrect())
g.clear(0)
g.putcells(localcells)
g.select({})
g.setgen(tonumber(g.getgen())+1)
end
while true do
step()
g.update()
step()

There are optimizations, but I will briefly explain some properties I have noticed.

Thresholds

A cell with maximum heat would be surrounded by infinitely many cells. This 'maximum heat' number is finite, but I do not yet know its value. A 511x511 block of cells yields the number 6.0267726589419..., but that's not even close to a good estimate. I have settled on using 7 when engineering day and night-esque rules.

Another threshold that isn't yet known is being able to expand outside its bounding box.

EDIT 11/30/2018: Thanks to Caenbe and Freywa for pointing out that the maximum heat is (2/3)*pi^2*C, where C = Catalan's constant. Wolfram says this is the value to 1000 digits:

Ah, yes, a big problem. I had to choose between either having patterns that can only exist alone on a grid, or having patterns behave way differently when alone. Yes, there are patterns that, due to having maximized or minimized heat within a range, can only exist alone. You could engineer this for any pattern, really. For example, here is a glider (quite vast in its rule range) that can exist only by itself in this rule:

This one may bother calcyman and Apple Bottom and the like. The speed of light in this rule is infinite, so it is way more comfortable to use one cell per generation. To demonstrate it is infinite, a single cell in the rule ECA:B(.01,7)/S (I've nicknamed this family of quite redundant rules 'Target', this would be 'Target.01') turns into this monstrosity in 5 generations:

When you compute the 'total', you need to sort the cells by distance before summing.

I don't know the Lua equivalent of this, but in pseudocode it would be:

Create an empty list.

Iterate over the other cells, appending the values 1/((x2-x1)^2+(y2-y1)^2)^2 to the list.

Sort the list into ascending order.

Traverse the list in order, accumulating a running total.

Unfortunately, in this very particular case, sorting the list in ascending order and then calculating the sum gives the less precise result (which is < 0.85) whereas calculating the list in descending order and then calculating the sum gives the expected result ( == 0.85). It's not too surprising that floating point summations and comparisons can't be relied on here, but it still is annoying. In Python, using math.fsum() gives the expected result for both sort orders, but that probably can't be relied on. It's also possible to use something like math.isclose() as part of the comparison, but it seems to me that for this application there are bound to be cases where the exact count of a particular cell actually is really close to a birth or survival threshold and comparing with isclose() may actually give the wrong result for a particular tolerance value.

I suspect there are two options here:

Use a summing algorithm (such as calcyman's) which gives the same result for all cells whose count should be equal, but accept that sometimes it will be wrong, or

Use a more precise summing algorithm (such as Python's fsum) which probably gives the expected result more often (and consistently), but will be much slower.

For lua, there is this implementation of fsum. The performance hit is significant for a population of 100, and above 1000 cells it's rather horrible.

The latest version of the 5S Project contains over 226,000 spaceships. There is also a GitHub mirror of the collection. Tabulated pages up to period 160 (out of date) are available on the LifeWiki.

Here's a version of the Euclidean CA script which uses the FastFloatSum implementation I linked to in the previous post. I've made a few other changes in an attempt to improve performance for larger patterns, particularly where there are distantly separated parts of the current pattern. Performance for populations > 200 starts to really suffer.

-- EuclideanCA.lua, v0.2
-- This is a preliminary version. If sufficiently interesting behaviours show
-- up, it would be nice to get a full simulating algorithm in better-written
-- software like Golly. Speaking of Golly, make sure you have a version that
-- supports Lua, so you can run this script.
-- TERMS TO KNOW:
-- Weight: The contribution of an On cell to the heat of another cell given
-- by the reciprocal of its euclidean distance to the fourth power
-- Heat: The heat of each cell is calculated by summing the weights
-- of all live cells relative to the current cell
-- Author: Dani, Nov 2018
-- Contributors: Arie Paap
local g = golly()
local gp = require "gplus"
-- Birth and survival ranges
--local rulesB = {.01, 7}
--local rulesS = {}
local rulesB = {.085, .105, .135, .143, 2, 2.15}
local rulesS = {1, 1.02, 2, 2.5}
--[[
Accurate floating point summation. Like Python's fsum.
This version collect partial sums in reverse order, with each entry (except last) used up all 53 bits.
Author: Albert Chan
See http://lua-users.org/wiki/FloatSumFast
--]]
local function fsum(v)
local p, abs = {1}, math.abs -- p[1] == #p
local function fadd(x)
local p, i = p, 2
for j = 2, p[1] do
local y = p[j]
if abs(x) > abs(y) then x, y = y, x end
local hi = x + y
local lo = x - (hi - y)
x = hi
if lo ~= 0 then p[i] = x; x = lo; i = i + 1 end
end
if x ~= x then p[1] = 2 return end -- Inf or NaN
p[1] = i
p[i] = x
end
local function ftotal(clear)
if clear then p[1] = 1 end -- clear all partials
repeat
local n, overlap = p[1], false
local prev = {table.unpack(p, 1, n)}
fadd(0) -- remove partials overlap
if n <= 3 then return p[2] end
for i = 1, n do
if p[i] ~= prev[i] then overlap = true; break end
end
until not overlap
local x, lo, err = table.unpack(p, 2, 4)
if (lo < 0) == (err < 0) then
lo = lo * 2 -- |lo| = 1/2 ULP
local hi = x + lo -- -> x off 1 ULP
if lo == hi - x then x = hi end
end
return x
end
for i = 1, #v do fadd(v[i]) end
return ftotal()
end
-- Table to store cell heat as they are calculated.
local cellcounts = {}
local function getcount(x, y, pat)
g.doevent("") -- Allow Esc to abort
local key = x..' '..y
local c = cellcounts[key]
if not c then
local patweights = {}
for i = 2, #pat, 2 do
local w = 1.0/((pat[i-1]-x)^2 + (pat[i]-y)^2)^2
if w == math.huge then w = 0.0 end
patweights[i//2] = w
end
c = fsum(patweights)
cellcounts[key] = c
end
return c
end
local function clearcounts()
cellcounts = {}
end
-- Expand the current pattern by including all cells within a given distance of any On cell
-- Uses an LtL rule with disc neighbourhood, all Birth and no Survival
-- Returns a cell list containing the additional cells
local neighbourcounts = {8, 20, 36, 68, 96, 136, 176, 224, 292, 348}
local function expand(radius)
local nc = neighbourcounts[radius]
local currpat = g.getcells(g.getrect())
g.setrule(string.format("R%u,C0,M1,S0..0,B1..%u,NC", radius, nc))
g.run(1)
local nring = g.getcells(g.getrect())
g.putcells(currpat)
return nring
end
-- Evolve the current pattern one step in the given ECA rule
local function step()
if g.getpop() == "0" then
g.exit("All cells are dead.")
return
end
local pat = g.getcells(g.getrect())
local gen = tonumber(g.getgen())
local rule = g.getrule()
local algo = g.getalgo()
local minrulesB = math.min(table.unpack(rulesB))
clearcounts() -- clear the cache of cell heats
-- Determine how far out from the current pattern to test for birth
expand(2) -- Assume a ring of at least size 2 is required
while true do
local localmax = 0
local localring = expand(2)
for i = 1, #localring, 2 do
local localcount = getcount(localring[i], localring[i+1], pat)
if localcount > localmax then
localmax = localcount
if localmax >= minrulesB then break end -- Need to expand the ring further
end
end
if localmax < minrulesB then break end
end
-- All cells which need to be checked for birth or survival are now On
-- Remove the cells from this current pattern to get the list of cells to test for birth
g.putcells(pat, 0, 0, 1, 0, 0, 1, "xor")
local area = g.getcells(g.getrect())
-- Process birth
local localcells = {}
for i = 1, #area, 2 do
local localcount = getcount(area[i], area[i+1], pat)
for j = 1, #rulesB, 2 do
if localcount >= rulesB[j] and localcount <= rulesB[j+1] then
localcells[#localcells+1] = area[i]
localcells[#localcells+1] = area[i+1]
end
end
end
-- Process survival
for i = 1, #pat, 2 do
local localcount = getcount(pat[i], pat[i+1], pat)
for j = 1, #rulesS, 2 do
if localcount >= rulesS[j] and localcount <= rulesS[j+1] then
localcells[#localcells+1] = pat[i]
localcells[#localcells+1] = pat[i+1]
end
end
end
-- Update the pattern
g.select(g.getrect())
g.clear(0)
g.setalgo(algo)
g.setrule(rule)
g.putcells(localcells)
g.select({})
g.setgen(gen+1)
end
g.show("Press 'q' to quit.")
while true do
step()
g.update()
local event = g.getevent()
if #event > 0 then
if event:find("^key") then
local _, key, mods = gp.split(event)
if key == "q" then break end
else
g.doevent(event)
end
end
end
g.show("")

Edit: I forgot to mention that a large part of the performance degradation of the Fast Float Sum implementation for large populations was due to the usage of var args. I modified the function to accept an array instead, and this improved the situation, but there's no getting away from the O(N^2) characteristic inherent in the Euclidean CA definition.

The latest version of the 5S Project contains over 226,000 spaceships. There is also a GitHub mirror of the collection. Tabulated pages up to period 160 (out of date) are available on the LifeWiki.

danny wrote:
A cell with maximum heat would be surrounded by infinitely many cells. This 'maximum heat' number is finite, but I do not yet know its value. A 511x511 block of cells yields the number 6.0267726589419..., but that's not even close to a good estimate. I have settled on using 7 when engineering day and night-esque rules.

danny wrote:
A cell with maximum heat would be surrounded by infinitely many cells. This 'maximum heat' number is finite, but I do not yet know its value. A 511x511 block of cells yields the number 6.0267726589419..., but that's not even close to a good estimate. I have settled on using 7 when engineering day and night-esque rules.

danny wrote:Another threshold that isn't yet known is being able to expand outside its bounding box.

Given that we have computed the maximum heat, this is easily worked out. Take a large rectangle of ON cells in a universe of OFF cells and consider the heat experienced by a cell adjacent to this rectangle in the middle of one side (i.e. the yellow cell below):

Now expand the rectangle in three directions, the excluded direction being the direction the marked cell lies in. In the limit you get a half-plane of ON cells, and the heat experienced by any OFF cell adjacent to the ON half-plane is L / 2 − zeta(4) = 1.931082… where L is the maximum heat we worked out earlier. The derivation of this number is simply the sum of the parts in the following diagram:

Thus we have that if there is no birth on heat below 1.931082… the pattern cannot expand beyond its bounding box.

Another observation that may help speed up implementations is that the Moore neighbourhood of a cell already contributes 5 out of the possible 6.027 heat that a cell may experience. Interval arithmetic may be useful to decide early whether a cell is born or survives – sum the heat of cells in the Moore neighbourhood to get H, then only proceed to add the heat of more live cells if the interval [H, H + 1.027) contains a boundary point between birth and non-birth or survival and non-survival.

The following patterns were found in B(1.15,1.2)(1.3,1.4)(1.5,1.6)/S(1.603,2.5)(3,4). Here are p2 c spaceships:

Freywa wrote:Another observation that may help speed up implementations is that the Moore neighbourhood of a cell already contributes 5 out of the possible 6.027 heat that a cell may experience. Interval arithmetic may be useful to decide early whether a cell is born or survives – sum the heat of cells in the Moore neighbourhood to get H, then only proceed to add the heat of more live cells if the interval [H, H + 1.027) contains a boundary point between birth and non-birth or survival and non-survival.

That seems to me like a very good suggestion. I don't know when, but I'll certainly have a go at implementing some variation of that idea if nobody else does.

The latest version of the 5S Project contains over 226,000 spaceships. There is also a GitHub mirror of the collection. Tabulated pages up to period 160 (out of date) are available on the LifeWiki.

wildmyron wrote:Edit: I forgot to mention that a large part of the performance degradation of the Fast Float Sum implementation for large populations was due to the usage of var args. I modified the function to accept an array instead, and this improved the situation, but there's no getting away from the O(N^2) characteristic inherent in the Euclidean CA definition.

For a k-by-k grid, you can get down to O(k^2 log(k)) operations using an FFT-based convolution -- which is better when the pattern is dense than the O(k^4) approach of calculating each cell individually.

Freywa's idea is great -- and I think might be even better for this case when you just want to detect whether the total lies in an interval rather than calculating it exactly.

Indeed, you can implement that by creating a Golly rule which decides whether a cell becomes on, off, or 'insufficient information', and then only process the 'insufficient information' cells.

What do you do with ill crystallographers? Take them to the mono-clinic!