CellularBase docs
This is the documentation of CellularBase.jl
xxxxxxxxxx
using Plots
Boundary Conditions
Define the kinds of Boundary Conditions
Periodic
FixedMin - Fixed BC with all elements of the min of the
possible_states
FixedMax - Fixed BC with all elements of the max of the
possible_states
Clamp - Nearest cell within the region of interest
xxxxxxxxxx
BoundaryCondition Periodic FixedMin FixedMax Clamp
Neighborhoods
Every neighborhood has a radius
and a set of CartesianIndex
es associated with it. The most general way of evaluating these is by referencing the appropriate property.
cartesians (generic function with 1 method)
xxxxxxxxxx
begin
# Neighborhoods
abstract type AbstractNeighborhood end
radius(neighborhood::AbstractNeighborhood) = neighborhood.radius
cartesians(neighborhood::AbstractNeighborhood) = neighborhood.cartesians
end
VonNeumann Neighborhood
The VonNeumann Neighborhood is the simplest neighborhood which contains
It is defined for multiple dimensions.
A 2D representation of radius(=
xxxxxxxxxx
struct VonNeumannNeighborhood <: AbstractNeighborhood
cartesians::Array{CartesianIndex}
radius::Int
dimensions::Int
function VonNeumannNeighborhood(radius::Int, dimensions::Int)::VonNeumannNeighborhood
arr = CartesianIndex[CartesianIndex(Tuple(zeros(Int, dimensions)))]
for i in 1:radius
for j in 1:dimensions
zs = zeros(Int, dimensions)
zs[j] = i
push!(arr, CartesianIndex(Tuple(zs)))
zs[j] = -i
push!(arr, CartesianIndex(Tuple(zs)))
end
end
new(arr, radius, dimensions)
end
end
Moore Neighborhood
The Moore Neighborhood is a simple neighborhood which contains all neighbours in the
It is defined for multiple dimensions.
A 2D representation of radius(=
xxxxxxxxxx
struct MooreNeighborhood <: AbstractNeighborhood
cartesians::Array{CartesianIndex}
radius::Int
dimensions::Int
function MooreNeighborhood(radius::Int, dimensions::Int)::MooreNeighborhood
new(
CartesianIndices(Tuple(-radius:radius for _ in 1:dimensions)) |>
x -> reshape(x, :),
radius, dimensions
)
end
end
Grids
These are the actual cellular automata which are the aim of this package.
AbstractGrid
is concretized to a working grid which has the real working mechanism
xxxxxxxxxx
abstract type AbstractGrid{T} end
getindex
Base.getindex
is specialized to AbstractGrids to handle Boundary conditions
xxxxxxxxxx
function Base.getindex(grid::AbstractGrid{T}, index::CartesianIndex)::T where {T}
try
state(grid)[index]
catch
if boundary_conditions(grid) == Periodic
grid[CartesianIndex(mod1.(Tuple(index), size(grid)))] # mod-1 arithmetic
elseif boundary_conditions(grid) == FixedMax
maximum(possible_states(grid))
elseif boundary_conditions(grid) == FixedMin
minimum(possible_states(grid))
else
grid[
CartesianIndex(
Tuple(
clamp(Tuple(index)[i], 1, size(grid)[i])
for i in 1:ndims(grid)
)
)
]
end
end
end
State functions
These are functions related to the state of the grid.
Optimization Possibility
newstate
and state!
can often be specialized and optimized to be more efficient and hence throw warnings
xxxxxxxxxx
begin
function state(grid::AbstractGrid{T})::Array{T} where {T}
grid.state
end
function state!(grid::AbstractGrid{T}, newstate)::Nothing where {T}
"Possible unspecialized call"
grid.state = newstate
return nothing
end
function newstate(grid::AbstractGrid{T})::Array{T} where {T}
"Possible unspecialized call"
similar(state(grid))
end
end;
Accessor functions
xxxxxxxxxx
begin
boundary_conditions(grid::AbstractGrid)::BoundaryCondition = grid.bc
neighborhood(grid::AbstractGrid)::Array{CartesianIndex} = cartesians(grid.neighborhood)
function possible_states(grid::AbstractGrid{T})::Array{T} where {T}
grid.possible_states
end
function neighbors(grid::AbstractGrid{T}, i::CartesianIndex)::Array{T} where {T}
neighborindex = neighborhood(grid) .|> x -> x + i
grid[neighborindex]
end
end;
Useful Helpers
xxxxxxxxxx
begin
Base.ndims(grid::AbstractGrid) = ndims(state(grid))
Base.size(grid::AbstractGrid) = size(state(grid))
function Base.getindex(grid::AbstractGrid{T}, indexs::Union{Array{Int},Array{CartesianIndex{N}}})::Array{T} where {T,N}
indexs .|> i -> grid[i]
end
function Base.getindex(grid::AbstractGrid{T}, is::Int...)::T where T
grid[CartesianIndex(is)]
end
Base.CartesianIndices(grid::AbstractGrid)::CartesianIndices = CartesianIndices(state(grid))
end
Evolutions and Simulations
This section defines the most abstract functions which evolve and simulate a Cellular Automaton
Evolutions
evolve!
is a mutator function that evolves the grid by one step. A new state is created by calling newstate
. Each index is iterated through, and the values of the neighbours are sent as parameters to the grid
. The grid then evaluates the next state of the present index, and hence the evolution rules must be coded into the grid
as a functional object
Grid is a Function too!
Every AbstractGrid is also assumed to be a function-like object and the code will throw an error if it is not defined to be as such
The new state is then set back into the grid.
tabular_evolution!
is a helper function that can be used for rules which can be encoded into a table, like Wolfram's CA.
tabular_evolution!
This function can be used in a specialized evolve!
. Send the table via the kwargs
to evolve!
and then unpack to tabular_evolution!
. See caWolfram.jl for an implementation.
tabular_evolution! (generic function with 1 method)
xxxxxxxxxx
begin
function evolve!(grid::AbstractGrid{T}; kwargs...)::Nothing where {T}
_newstate = newstate(grid)
Threads. for i in CartesianIndices(grid)
_newstate[i] = grid(neighbors(grid, i); kwargs...)
end
state!(grid, _newstate)
return nothing
end
function tabular_evolution!(grid::AbstractGrid, table::Dict)::Nothing
newstate = newstate(grid)
for i in CartesianIndices(grid)
newstate[i] = table[grid(neighbors(grid, i))]
end
state!(grid, newstate)
return nothing
end
end
Simulation
The outer most function that will almost always be called unspecialized by the user. The simulate!
function takes a grid and a number of steps to simulate for. The store_results
parameter allows users to not store results after every evolve!
call. postrunhook
is called after every step with parameters as (step, grid, kwargs...)
. All kwargs
are forwarded to evolve!
and postrunhook
.
postrunhook Special Return Values
While postrunhook
can mutate the grid on specific steps, it can also act as a messenger to simulate!
to change its behavior. See below for available messages
List of special messages-
:shortcurcuit
⟹ do not callevolve!
on the grid in the next step. This can be useful if you want to stop evolution for some steps, or if you know that the grid will not change anymore, such as when it has reached a stable state.:interrupt
⟹ return immediately.
Implementation Example
See Term Paper 1#Equilibrium infection distribution for example
simulate! (generic function with 1 method)
xxxxxxxxxx
function simulate!(grid::AbstractGrid{T}, steps::Int; store_results=true, postrunhook=nothing, kwargs...)::Array{Array{T}} where {T}
if store_results results = Array{T}[] end
push!(results, copy(state(grid)))
if !isnothing(postrunhook)
postrunhook(grid, step; kwargs...)
end
ss = false
for step in 1:steps
if (!ss) evolve!(grid; kwargs...) end
if store_results push!(results, copy(state(grid))) end
if !isnothing(postrunhook)
message = postrunhook(grid, step; kwargs...)
ss = message == :shortcurcuit
if message == :interrupt
break
end
end
end
if store_results results else nothing end
end