Build Status codecov.io Aqua.jl Quality Assurance

DynamicGrids is a generalised framework for building high-performance grid-based spatial simulations, including cellular automata, but also allowing a wider range of behaviours like random jumps and interactions between multiple grids. It is extended by Dispersal.jl for modelling organism dispersal processes.

Dispersal quarantine

A dispersal simulation with quarantine interactions, using Dispersal.jl, custom rules and the GtkOuput from DynamicGridsGtk. Note that this is indicative of the real-time frame-rate on a laptop.

A DynamicGrids.jl simulation is run with a script like this one running the included game of life model Life():

using DynamicGrids, Crayons

init = rand(Bool, 150, 200)
output = REPLOutput(init; tspan=1:200, fps=30, color=Crayon(foreground=:red, background=:black, bold=true))
sim!(output, Life())

REPL life

A game of life simulation being displayed directly in a terminal.


The framework is highly customisable, but there are some central ideas that define how a simulation works: grids, rules, and outputs.


Simulation grids may be any single AbstractArray or a NamedTuple of multiple AbstractArray. Usually grids contain values of Number, but other types are possible. Grids are updated by Rules that are run for every cell, at every timestep.

The init grid/s contain whatever initialisation data is required to start a simulation: the array type, size and element type, as well as providing the initial conditions:

init = rand(Float32, 100, 100)

An init grid can be attached to an Output:

output = ArrayOutput(init; tspan=1:100)

or passed in to sim!, where it will take preference over the init attached to the Output, but must be the same type and size:

sim!(output, ruleset; init=init)

For multiple grids, init is a NamedTuple of equal-sized arrays matching the names given to each Ruleset :

init = (predator=rand(100, 100), prey=(rand(100, 100))

Handling and passing of the correct grids to a Rule is automated by DynamicGrids.jl. Rules specify which grids they require in what order using the first two (R and W) type parameters, or read and write keyword arguments.

Dimensional or spatial init grids from DimensionalData.jl of GeoData.jl will propagate through the model to return output with explicit dimensions. This will plot correctly as a map using Plots.jl, to which shape files and observation points can be easily added.

Non-Number Grids

Grids containing custom and non-Number types are possible, with some caveats. They must define Base.zero for their element type, and should be a bitstype for performance. Tuple does not define zero. Array is not a bitstype, and does not define zero. SArray from StaticArrays.jl is both, and can be used as the contents of a grid. Custom structs that defne zero should also work.

However, for any multi-values grid element type, you will need to define a method of DynamicGrids.rgb that returns an ARGB32 for them to work in ImageOutputs, and isless for the REPLoutput to work.


Rules hold the parameters for running a simulation, and are applied in applyrule method that is called for each of the active cells in the grid. Rules come in a number of flavours (outlined in the docs), which allow assumptions to be made about running them that can greatly improve performance. Rules can be collected in a Ruleset, with some additional arguments to control the simulation:

ruleset = Ruleset(Life(2, 3); opt=SparseOpt())

Multiple rules can be combined in a Ruleset. Each rule will be run for the whole grid, in sequence, using appropriate optimisations depending on the parent types of each rule:

ruleset = Ruleset(rule1, rule2; timestep=Day(1), opt=SparseOpt())

For better performance (often ~2x or more), models included in a Chain object will be combined into a single model, using only one array read and write. This optimisation is limited to CellRule, or a NeighborhoodRule followed by CellRule. If the @inline compiler macro is used on all applyrule methods, all rules in a Chain will be compiled together into a single, efficient function call.

ruleset = Ruleset(rule1, Chain(rule2, rule3, rule4))


Outputs are ways of storing or viewing a simulation. They can be used interchangeably depending on your needs: ArrayOutput is a simple storage structure for high performance-simulations. As with most outputs, it is initialised with the init array, but in this case it also requires the number of simulation frames to preallocate before the simulation runs.

output = ArrayOutput(init; tspan=1:10)

The REPLOutput shown above is a GraphicOutput that can be useful for checking a simulation when working in a terminal or over ssh:

output = REPLOutput(init; tspan=1:100)

ImageOutput is the most complex class of outputs, allowing full color visual simulations using ColorSchemes.jl. It can also display multiple grids using color composites or layouts, as shown above in the quarantine simulation.

DynamicGridsInteract.jl provides simulation interfaces for use in Juno, Jupyter, web pages or electron apps, with live interactive control over parameters. DynamicGridsGtk.jl is a simple graphical output for Gtk. These packages are kept separate to avoid dependencies when being used in non-graphical simulations.

Outputs are also easy to write, and high performance applications may benefit from writing a custom output to reduce memory use. Performance of DynamicGrids.jl is dominated by cache interactions, so reducing memory use has positive effects.


This example implements a very simple forest fire model:

using DynamicGrids, DynamicGridsGtk, ColorSchemes, Colors

const DEAD, ALIVE, BURNING = 1, 2, 3

rule = let prob_combustion=0.0001, prob_regrowth=0.01
    Neighbors(Moore(1)) do neighborhood, cell
        if cell == ALIVE
            if BURNING in neighborhood
                rand() <= prob_combustion ? BURNING : ALIVE
        elseif cell in BURNING
            rand() <= prob_regrowth ? ALIVE : DEAD

# Set up the init array and output (using a Gtk window)
init = fill(ALIVE, 400, 400)
processor = ColorProcessor(scheme=ColorSchemes.rainbow, zerocolor=RGB24(0.0))
output = GtkOutput(init; tspan=1:200, fps=25, minval=DEAD, maxval=BURNING, processor=processor)

# Run the simulation
sim!(output, rule)

# Save the output as a gif
savegif("forestfire.gif", output)


Timing the simulation for 200 steps, the performance is quite good:

output = ArrayOutput(init; tspan=1:200)
@time sim!(output, ruleset)
 1.384755 seconds (640 allocations: 2.569 MiB)


Agents.jl can also do cellular-automata style simulations. The design of Agents.jl is to iterate over a list of agents, instead of broadcasting over an array of cells. This approach is well suited to when you need to track the movement and details about individual agents throughout the simulation.

However, for simple grid models where you don't need to track individuals, like the forest fire model above, DynamicGrids.jl is two orders of magnitude faster than Agents.jl, and provides better visualisation tools. If you are doing grid-based simulation and you don't need to track individual agents, DynamicGrids.jl is probably the best tool. For other use cases, try Agents.jl.


Running simulations

sim!(output, [ruleset::Ruleset=ruleset(output)];

Runs the simulation, passing the destination aray to the passed in output for each time-step.


  • output: An Output to store grids or display them on the screen.
  • ruleset: A Ruleset containing one or more Rules. If the output has a Ruleset attached, it will be used.

Keyword Arguments

Theses are the taken from the output argument by default.

  • init: optional array or NamedTuple of arrays.
  • mask: a Bool array matching the init array size. false cells do not run.
  • aux: a NamedTuple of auxilary data to be used by rules.
  • tspan: a tuple holding the start and end of the timespan the simulaiton will run for.
  • fps: the frames per second to display. Will be taken from the output if not passed in.
  • nreplicates: the number of replicates to combine in stochastic simulations
  • simdata: a SimData object. Keeping it between simulations can reduce memory allocation when that is important.
sim!(output, rules::Rule...; kwargs...)

Run a simulation passing in rules without defining a Ruleset.

resume!(output::GraphicOutput, ruleset::Ruleset=ruleset(output);

Restart the simulation from where you stopped last time. For arguments see sim!. The keyword arg tstop can be used to extend the length of the simulation.


  • output: An Output to store grids or display them on the screen.
  • ruleset: A Ruleset containing one ore more Rules. These will each be run in sequence.

Keyword Arguments (optional

  • init: an optional initialisation array
  • tstop: the new stop time for the simulation. Taken from the output length by default.
  • fps: the frames per second to display. Taken from the output by default.
  • nreplicates: the number of replicates to combine in stochastic simulations
  • simdata: a SimData object. Keeping it between simulations can improve performance when that is important


Rules define simulation behaviour. They hold data relevant to the simulation, and trigger dispatch of particular applyrule or applyrule! methods. Rules can be chained together arbitrarily to make composite simulations across any number of grids.

Ruleset(rules...; overflow=RemoveOverflow(), opt=NoOpt(), cellsize=1, timestep=nothing)

A container for holding a sequence of Rules and simulation details like overflow handing and optimisation. Rules will be run in the order they are passed, ie. Ruleset(rule1, rule2, rule3).

Keyword Arguments

  • opt: a PerformanceOpt to specificy optimisations like SparseOpt. Defaults to NoOpt.
  • overflow: what to do with overflow of grid edges. Options are RemoveOverflow() or WrapOverflow(), defaulting to RemoveOverflow.
  • cellsize: size of cells.
  • timestep: fixed timestep where this is reuired for some rules. eg. Month(1) or 1u"s".

A rule contains all the information required to run a rule in a simulation, given an initial array. Rules can be chained together sequentially.

By default the output of the rule for a Rule is automatically written to the current cell in the grid.

Rules are applied to the grid using the applyrule method.


A Rule that only writes and uses a state from single cell of the read grids, and has its return value written back to the same cell(s).

This limitation can be useful for performance optimisation, such as wrapping rules in Chain so that no writes occur between rules.

CellRule is applied with the method:

applyrule(data::SimData, rule::YourCellRule, state, I)

As the cell index is provided in applyrule, you can look up an aux array using aux(data)[:auxname][I...] to access cell-specific parameters for your rule.

Cell(f; read, write)

A CellRule that applies a function f to the read grid cells and returns the write cells.

Especially convenient with do notation.


Set the cells of grid :c to the sum of :a and :b:

simplerule = Cell() do a, b
    a + b

If you need to use multiple grids (a and b), use the read and write arguments. If you want to use external variables, wrap the whole thing in a let block, for performance.

rule = let y = y
    rule = Cell(read=(a, b), write=b) do a, b
        a + b * y 

A Rule that only accesses a neighborhood centered around the current cell. NeighborhoodRule is applied with the method:

applyrule(data::SimData, rule::YourNeighborhoodRule, state, I)

NeighborhoodRule must have a neighborhood field, that holds a Neighborhood object. neighbors(rule) returns an iterator over the surrounding cell pattern defined by the Neighborhood.

For each cell in the grids the neighborhood buffer will be updated for use in the applyrule method, managed to minimise array reads.

This allows memory optimisations and the use of BLAS routines on the neighborhood buffer for Moore neighborhoods. It also means that and no bounds checking is required in neighborhood code.

For neighborhood rules with multiple read grids, the first is always the one used for the neighborhood, the others are passed in as additional state for the cell. Any grids can be written to, but only for the current cell.

Neighbors(f, neighborhood)
Neighbors{R,W}(f, neighborhood)
Neighbors(f; read=:_default_, write=read, neighborhood=Moore())

A NeighborhoodRule that receives a neighbors object for the first read grid and the passed in neighborhood, followed by the cell values for the required grids, as with Cell.

Returned value(s) are written to the write/W grid.

As with all NeighborhoodRule, you do not have to check bounds at grid edges, that is handled for you internally.

Using SparseOpt may improve neighborhood performance when zero values are common and can be safely ignored.


rule = let x = 10
    Neighbors{Tuple{:a,:b},:b}() do hood, a, b
        data[:b][I...] = a + b^x

The let block may improve performance.

Life(neighborhood, birth=3, sustain=(2, 3))

Rule for game-of-life style cellular automata. This is a demonstration of Cellular Automata more than a seriously optimised game of life rule.

Cells becomes active if it is empty and the number of neightbors is a number in the b array, and remains active the cell is active and the number of neightbors is in the s array.

Examples (gleaned from CellularAutomata.jl)

using DynamicGrids, Distributions
# Use `Binomial` to tweak the density random true values
init = Bool.(rand(Binomial(1, 0.5), 70, 70))
output = REPLOutput(init; tspan=1:100, fps=25, color=:red)

# Morley
sim!(output, Ruleset(Life(birth=[3, 6, 8], sustain=[2, 4, 5])))

# 2x2
sim!(output, Ruleset(Life(birth=[3, 6], sustain=[1, 2, 5])))

# Dimoeba
init = rand(Bool, 400, 300)
init[:, 100:200] .= 0
output = REPLOutput(init; tspan=1:100, fps=25, color=:blue, style=Braile())
sim!(output,  Life(birth=(3, 5, 6, 7, 8),  sustain=(5, 6, 7, 8)))

## No death
sim!(output,  Life(birth=(3, ),  sustain=(0, 1, 2, 3, 4, 5, 6, 7, 8)))

## 34 life
sim!(output, Life(birth=(3, 4), sustain=(3, 4)))

# Replicator
init = fill(true, 300,300)
init[:, 100:200] .= false
init[10, :] .= 0
output = REPLOutput(init; tspan=1:100, fps=25, color=:yellow)
sim!(output,  Life(birth=(1, 3, 5, 7),  sustain=(1, 3, 5, 7)))



ManualRule is the supertype for rules that manually write to whichever cells of the grid that they choose, instead of automatically updating every cell with their output.

ManualRule is applied with the method:

applyrule!(data::SimData, rule::YourManualRule, state, I)

Note the ! bang - this method alters the state of data.

Updates to the destination grids data are performed manually by data[:key][I...] += x, or data[I...] += x if no grid names are used.

Direct assignments with = will produce bugs, as the same grid cell may also be written to elsewhere.

Updating the block status of SparseOpt is handled automatically on write.

Manual(f; read=:_default_, write=read) 

A ManualRule to manually write to the array where you need to. f is passed an indexable data object, and the index of the current cell, followed by the required grid values for the index.


rule = let x = 10
    Manual{Tuple{:a,:b},:b}() do data, I, a, b
        data[:b][I...] = a + b^x

The let block greatly improves performance.


A Rule that only writes to its neighborhood, defined by its radius distance from the current point.

ManualNeighborhood rules must return their radius with a radius() method, although by default this will be called on the result of neighborhood(rule).

TODO: performance optimisations with a neighborhood buffer, simular to NeighborhoodRule but for writing.


Chains allow chaining rules together to be completed in a single processing step, without intermediate reads or writes from grids.

They are potentially compiled together into a single function call, especially if you use @inline on all applyrule methods. Chain can hold either all CellRule or NeighborhoodRule followed by CellRule.

ManualRule can't be used in Chain, as it doesn't have a return value.

Chain rule diagram


Rule methods and helpers

applyrule(data, rule::Rule, state, index)

Apply a rule to the cell state and return values to write to the grid/s.

This is called in maprule! methods during the simulation, not by the user. Custom Rule implementations must define this method.


  • data : SimData
  • rule : Rule
  • state: the value(s) of the current cell
  • index: a (row, column) tuple of Int for the current cell coordinates - t: the current time step

Returns the values) to be written to the current cell(s).

applyrule!(data, rule::ManualRule, state, index)

Apply a rule to the cell state and manually write to the grid data array. Used in all rules inheriting from ManualRule.

This is called in internal maprule! methods during the simulation, not by the user. Custom ManualRule implementations must define this method.


see applyrule

precalcrules(rule::Rule, simdata::SimData)

Precalculates rule at each timestep, if there are any fields that need to be updated over time. Rules are usually immutable (it's faster), so return a whole new rule object with changes you need applied. They will be discarded, and rule will always be the original object passed in.

Setfield.jl and Flatten.jl may help for this.

The default action is to return the existing rule without change.

isinferred(output::Output, ruleset::Ruleset)
isinferred(output::Output, rules::Rule...)

Test if a custom rule is inferred and the return type is correct when applyrule or applyrule! is run.

Type-stability can give orders of magnitude improvements in performance.


Data objects and methods for use in applyrule

SimData(extent::Extent, ruleset::Ruleset)

Simulation dataset to hold all intermediate arrays, timesteps and frame numbers for the current frame of the simulation.

A simdata object is accessable in applyrule as the first parameter.

Multiple grids can be indexed into using their key. In single grid simulations SimData can be indexed directly as if it is a Matrix.


Retrieve the current timestep from a SimData object.

This may be different from the timestep. If the simulation is in Month, currenttimestep will return Seconds for the length of the specific month.


Retrieve the current simulation time from a SimData object.

This will be in whatever type/units you specify in tspan.

frameindex(o::Output, data::SimData)

Get the index of the current frame in the output.

Every frame has an index of 1 if the simulation isn't stored

inbounds(xs::Tuple, data)

Check grid boundaries for a coordinate before writing in ManualRule.

Returns a tuple containing the coordinate(s) followed by a boolean true if the cell is in bounds, false if not.

Overflow of type RemoveOverflow returns the coordinate and false to skip coordinates that overflow outside of the grid.

WrapOverflow returns a tuple with the current position or it's wrapped equivalent, and true as it is allways in-bounds.



Neighborhoods define a pattern of cells surrounding the current cell, and how they are combined to update the value of the current cell.


Neighborhoods define the pattern of surrounding cells in the "neighborhood" of the current cell. The neighbors function returns the surrounding cells as an iterable.

The main kinds of neighborhood are demonstrated below:


If the allocation of neighborhood buffers during the simulation is costly (it usually isn't) you can use allocbuffers or preallocate them:

Moore{3}(allocbuffers(3, init))

You can also change the length of the buffers tuple to experiment with cache performance.


Moore neighborhoods define the neighborhood as all cells within a horizontal or vertical distance of the central cell. The central cell is omitted.

The buffer argument may be required for performance optimisation, see Neighborhood for details.


Neighborhoods are tuples or vectors of custom coordinates tuples that are specified in relation to the central point of the current cell. They can be any arbitrary shape or size, but should be listed in column-major order for performance.

Positional(coords::Tuple{Tuple{Vararg{Int}}}, [buffer=nothing])
Positional{R}(coords::Tuple, buffer)

Neighborhoods that can take arbitrary shapes by specifying each coordinate, as Tuple{Int,Int} of the row/column distance (positive and negative) from the central point.

The neighborhood radius is calculated from the most distance coordinate. For simplicity the buffer read from the main grid is a square with sides 2r + 1 around the central point, and is not shrunk or offset to match the coordinates if they are not symmetrical.

The buffer argument may be required for performance optimisation, see [Neighborhood] for more details.


Sets of Positional neighborhoods that can have separate rules for each set.

neighbors for LayeredPositional returns a tuple of iterators for each neighborhood layer.


Methods for use with neighborhood rules and neighborhoods

radius(rule, [key])

Return the radius of a rule or ruleset if it has one, otherwise zero.


Returns an iteraterable generator over all cells in the neighborhood.

Custom Neighborhoods must define this method.

sumneighbors(hood::Neighborhood, state)

Sums all cells in the neighborhood. This is identical to running sum(neighbors(hood)) but it can be more efficient than as it may use matrix algra libraries for sum, instead of regular sum over an iterator.

setneighbor!(data, neighborhood, rule, state, hood_index, dest_index)

Set value of a cell in the neighborhood. Called in mapsetneighbor!.

allocbuffers(init::AbstractArray, hood::Neighborhood)
allocbuffers(init::AbstractArray, radius::Int)

Allocate buffers for the Neighborhood. The init array should be of the same type as the grid the neighborhood runs on.


Get the size of a neighborhood dimension from its radius, which is always 2r + 1.



Output Types and Constructors


Outputs are store or display simulation results, usually as a vector of grids, one for each timestep - but they may also sum, combine or otherwise manipulate the simulation grids to improve performance, reduce memory overheads or similar.

Simulation outputs are decoupled from simulation behaviour, and in many cases can be used interchangeably.

ArrayOutput(init; tspan::AbstractRange)

A simple output that stores each step of the simulation in a vector of arrays.


  • init: initialisation Array or NamedTuple of Array

Keyword Argument:

  • tspan: AbstractRange timespan for the simulation
REPLOutput(init; tspan, fps=25.0, store=false, color=:white, cutoff=0.5, style=Block())

An output that is displayed directly in the REPL. It can either store or discard simulation frames.


  • init: initialisation Array or NamedTuple of arrays.

Keyword Arguments:

  • tspan: AbstractRange timespan for the simulation
  • fps::Real: frames per second to display the simulation
  • store::Bool: whether ot store the simulation frames for later use
  • color: a color from Crayons.jl
  • cutoff::Real: the cutoff point to display a full or empty cell. Default is 0.5
  • style::CharStyle: Block() or Braile() style printing. Braile uses 1/4 the screen space.

The default option is :block.

GifOutput(init; filename, tspan, fps=25.0, store=false, 
          processor=ColorProcessor(), minval=nothing, maxval=nothing)

Output that stores the simulation as images and saves a Gif file on completion.


Output methods

Grid processors

abstract type GridProcessor

Grid processors convert a frame of the simulation into an RGB image for display. Frames may be one or multiple grids.

abstract type SingleGridProcessor <: GridProcessor

Grid processors that convert one grid into an image array.

The first grid will be displayed if a SingleGridProcessor is used with a NamedTuple of grids.

ColorProcessor(; scheme=Greyscale(), zerocolor=nothing, maskcolor=nothing)

Converts output grids to a colorsheme.

Arguments / Keyword Arguments

  • scheme: a ColorSchemes.jl colorscheme, Greyscale or object that defines Base.get(obj, val) and returns a Color or a value that can be converted to Color using ARGB32(val).
  • zerocolor: a Color to use when values are zero, or nothing to ignore.
  • maskcolor: a Color to use when cells are masked, or nothing to ignore.
  • textconfig: a TextConfig object.
ThreeColorProcessor(; colors=(Red(), Green(), Blue()), zerocolor=nothing, maskcolor=nothing)

Assigns Red(), Blue(), Green() or nothing to any number of dynamic grids in any order. Duplicate colors will be summed. The final color sums are combined into a composite color image for display.

Arguments / Keyword Arguments

  • colors: a tuple or Red(), Green(), Blue(), or nothing matching the number of grids.
  • zerocolor: an RGB color to use when values are zero, or nothing to ignore.
  • maskcolor: an RGB color to use when cells are masked, or nothing to ignore.
LayoutProcessor(layout::Array, processors::Matrix, textconfig::TextConfig)

LayoutProcessor allows displaying multiple grids in a block layout, by specifying a layout matrix and a list of SingleGridProcessor to be run for each.


  • layout: A Vector or Matrix containing the keys or numbers of grids in the locations to display them. nothing, missing or 0 values will be skipped.
  • processors: tuple of SingleGridProcessor, one for each grid in the simulation. Can be nothing or any other value for grids not in layout.
  • textconfig : [TextConfig] object for printing time and grid name labels.
Greyscale(min=nothing, max=nothing)

Default colorscheme. Better performance than using a Colorschemes.jl scheme as there is array access or interpolation.

min and max are values between 0.0 and 1.0 that define the range of greys used.

TextConfig(; font::String, namepixels=14, timepixels=14,
           namepos=(timepixels+namepixels, timepixels),
           timepos=(timepixels, timepixels),
           fcolor=ARGB32(1.0), bcolor=ARGB32(RGB(0.0), 1.0),)
TextConfig(face, namepixels, namepos, timepixels, timepos, fcolor, bcolor)

Text configuration for printing timestep and grid name on the image.


  • namepixels and timepixels: set the pixel size of the font.
  • timepos and namepos: tuples that set the label positions, in pixels.
  • fcolor and bcolor: the foreground and background colors, as ARGB32.



savegif(filename::String, o::Output, data; processor=processor(o), fps=fps(o), [kwargs...])

Write the output array to a gif. You must pass a processor keyword argument for any Output objects not in ImageOutput (which allready have a processor attached).

Saving very large gifs may trigger a bug in Imagemagick.


Internal components and methods for outputs

These are used for defining your own outputs and GridProcessors, not for general scripting.

Extent(init, mask, aux, tspan, tstopped)
Extent(; init, mask=nothing, aux=nothing, tspan, kwargs...)

Container for extensive variables: spatial and timeseries data. These are kept separate from rules to allow application of rules to alternate spatial and temporal contexts.

Extent is not usually constructed directly by users, but it can be passed to Output constructors instead of init, mask, aux and tspan.

GraphicConfig(; fps=25.0, store=false, kwargs...) =
GraphicConfig(fps, timestamp, stampframe, store)

Config and variables for graphic outputs.

ImageConfig(processor, minval, maxval) 
ImageConfig(; processor=ColorProcessor(), minval=nothing, maxval=nothing)

Common configuration component for all ImageOutput.

processor is any GridProcessor. minval and maxval fields normalise grid values between zero and one, for use with Colorshemes.jl. nothing values are considered to represent zero or one respectively for minval and maxval, and will not be normalised.

showimage(image::AbstractArray{AGRB32,2}, output::ImageOutput, f, t)

Show image generated by and GridProcessor in an ImageOutput.


  • image: An array of Color
  • output: the output to define the method for
  • f: the current frame number
  • t: the current frame date/time
delay(o::Output, f)

Graphic outputs delay the simulations to match some fps rate, but other outputs just do nothing and continue.

grid2image(o::ImageOutput, data::SimData, grids, f, t)
grid2image(p::GridProcessor, o::ImageOutput, data::SimData, grids, f, t)

Convert a grid or NamedRuple of grids to an RGB image, using a GridProcessor. But it they can be dispatched on together when required for custom outputs.

rendertext!(img, config::TextConfig, name, t)

Render time name and t as text onto the image, following config settings.

normalise(x, min, max)

Set a value to be between zero and one, before converting to Color. min and max of nothing are assumed to be 0 and 1.


Convert a number, tuple or color to an ARGB32 value.

rgb(scheme, val)

Convert a color scheme and value to an RGB value.


Ruleset config


struct WrapOverflow <: Overflow

Wrap cordinates that overflow boundaries back to the opposite side of the grid.



struct NoOpt <: PerformanceOpt

Run the simulation without performance optimisations besides basic high performance programming.

This is still very fast, but not intelligent about the work that it does.

struct SparseOpt <: PerformanceOpt

An optimisation that ignores all zero values in the grid.

For low-density simulations performance may improve by orders of magnitude, as only used cells are run.

This is complicated for optimising neighborhoods - they must run if they contain just one non-zero cell.

This is best demonstrated with this simulation, where the grey areas do not run except where the neighborhood partially hangs over an area that is not grey.

SparseOpt demonstration


Internal data handling

SimData and GridData objects are used to manage the simulation and provide rules with any data they need.

These methods and objects are all subject to change until version 1.0.

ReadableGridData(init::AbstractArray, mask, radius, overflow)

Simulation data and storage passed to rules for each timestep.


Passed to rules <: ManualRule, and can be written to directly as an array. This handles updates to SparseOpt() and writing to the correct source/dest array.

runsim!(output::Output, args...)

Simulation runner. Runs a simulation synchonously or asynchonously depending on the return value of isasync(output) - which may be a fixed trait or a field value depending on the output type.

This allows interfaces with interactive components to update during the simulations.

simloop!(output::Output, simdata::SimData, fspan::UnitRange)

Loop over the frames in fspan, running the ruleset and displaying the output.

Operations on outputs and rulesets are allways mutable and in-place.

Operations on Rules and SimData objects are in a functional style, as they are used in inner loops where immutability improves performance.


Sequence rules over the SimData object, calling maprule! for each individual Rule.

If a Vector of SimData is used replicates will be run with Threads.@threads.

TODO: use the new threading method.

maprule!(simdata::SimData, rule::Rule)

Map a rule over the grids it reads from and updating the grids it writes to.

This is broken into a setup method and an application method to introduce a function barrier, for type stability.

optmap(f, ::SparseOpt, rdata::GridOrGridTuple, wdata::GridOrGridTuple)

Maps rules over grids with sparse block optimisation. Inactive blocks do not run. This can lead to order of magnitude performance improvments in sparse simulations where large areas of the grid are filled with zeros.

optmap(f, ::NoOpt, rgrids::GridOrGridTuple, wgrids::GridOrGridTuple)

Maps rule applicator over the grid with no optimisation

readgrids(rkeys, rgrids, I...)

Read values from grid/s at index I. This occurs for every cell for every rule, so has to be very fast.

Returns a single value or NamedTuple of values.

writegrids(rkeys, rgrids, I...)

Write values to grid/s at index I. This occurs for every cell for every rule, so has to be very fast.

Returns a single value or NamedTuple of values.

ismasked(data, I...)

Check if a cell is masked, using the mask array.

Used used internally during simulations to skip masked cells.

If mask was not passed to the Output constructor or sim! it defaults to nothing and false is always returned.

getredgrids(context, rule::Rule, simdata::AbstractSimData)

Retrieves GridData from a SimData object to match the requirements of a Rule.

Returns a Tuple holding the key or Tuple of keys, and grid or Tuple of grids.

combinegrids(rkey, rgrids, wkey, wgrids)

Combine grids into a new NamedTuple of grids depending on the read and write keys required by a rule.

replacegrids(simdata::AbstractSimData, newkeys, newgrids)

Replace grids in a NamedTuple with new grids where required.

update_chainstate(rule::Rule, state::NamedTuple, writestate)

Merge new state with previous state.

Returns a new NamedTuple with all keys having the most recent state