CellularBase docs
This is the documentation of CellularBase.jl
xxxxxxxxxxusing PlotsBoundary Conditions
Define the kinds of Boundary Conditions
Periodic
FixedMin - Fixed BC with all elements of the min of the
possible_statesFixedMax - Fixed BC with all elements of the max of the
possible_statesClamp - Nearest cell within the region of interest
xxxxxxxxxx BoundaryCondition Periodic FixedMin FixedMax ClampNeighborhoods
Every neighborhood has a radius and a set of CartesianIndexes associated with it. The most general way of evaluating these is by referencing the appropriate property.
cartesians (generic function with 1 method)xxxxxxxxxxbegin # Neighborhoods abstract type AbstractNeighborhood end radius(neighborhood::AbstractNeighborhood) = neighborhood.radius cartesians(neighborhood::AbstractNeighborhood) = neighborhood.cartesiansendVonNeumann Neighborhood
The VonNeumann Neighborhood is the simplest neighborhood which contains
It is defined for multiple dimensions.
A 2D representation of radius(=
xxxxxxxxxxstruct 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) endendMoore 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(=
xxxxxxxxxxstruct 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 ) endendGrids
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
xxxxxxxxxxabstract type AbstractGrid{T} endgetindex
Base.getindex is specialized to AbstractGrids to handle Boundary conditions
xxxxxxxxxxfunction 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 endendState 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
xxxxxxxxxxbegin 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)) endend;Accessor functions
xxxxxxxxxxbegin 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] endend;Useful Helpers
xxxxxxxxxxbegin 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))endEvolutions 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)xxxxxxxxxxbegin 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 endendSimulation
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)xxxxxxxxxxfunction 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 endend