From fc4dc8db7a11085654f44484535929c3be5e4c76 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Jul 2020 22:18:51 +1000 Subject: [PATCH 1/7] add GifOutput --- src/outputs/gif.jl | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/outputs/gif.jl diff --git a/src/outputs/gif.jl b/src/outputs/gif.jl new file mode 100644 index 00000000..2409af57 --- /dev/null +++ b/src/outputs/gif.jl @@ -0,0 +1,56 @@ +""" +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. +""" +savegif(filename::String, o::Output, ruleset=Ruleset(); + minval=mival(o), maxval=maxval(o), processor=processor(o), kwargs...) = begin + im_o = NoDisplayImageOutput(o; maxval=maxval, minval=minval, processor=processor) + savegif(filename, im_o, ruleset; kwargs...) +end +savegif(filename::String, o::ImageOutput, ruleset=Ruleset(); + processor=processor(o), fps=fps(o), kwargs...) = begin + images = map(frames(o), collect(firstindex(o):lastindex(o))) do frame, t + grid2image(processor, o, ruleset, frame, t) + end + array = cat(images..., dims=3) + FileIO.save(filename, array; fps=fps, kwargs...) +end + + +""" + 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. +""" +mutable struct GifOutput{T,F<:AbstractVector{T},E,GC,IC,I,N} <: ImageOutput{T} + frames::F + running::Bool + extent::E + graphicconfig::GC + imageconfig::IC + image::I + filename::N +end +GifOutput(; frames, running, extent, graphicconfig, imageconfig, filename, kwargs...) = + GifOutput(frames, running, extent, graphicconfig, imageconfig, allocgif(extent), filename) + +filename(o::GifOutput) = o.filename +gif(o::GifOutput) = o.gif + +showimage(image, o::GifOutput, data::SimData, f, t) = gif(o)[:, :, f] = image + +finalise(o::GifOutput) = savegif(o) + +allocgif(e::Extent) = zeros(ARGB32, gridsize(e)..., length(tspan(e))) + +savegif(o::GifOutput) = savegif(filename(o), o) +savegif(filename::String, o::GifOutput, ruleset=nothing, fps=fps(o); + processor=nothing, kwargs...) = begin + !(processor isa Nothing) && @warn "Cannot set the processor on savegif for GifOutput. Run the sim again" + FileIO.save(filename, o.image; fps=fps, kwargs...) +end From a32f0ee8bd56a88dbabab16c9da879319683b3b8 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Jul 2020 22:19:22 +1000 Subject: [PATCH 2/7] rename and reorder functions --- Project.toml | 6 +- README.md | 2 +- docs/src/index.md | 23 ++- src/DynamicGrids.jl | 17 +- src/chain.jl | 30 ++-- src/extent.jl | 4 + src/framework.jl | 10 +- src/life.jl | 20 ++- src/map.jl | 128 --------------- src/maprules.jl | 147 +++++++++++------ src/neighborhoods.jl | 144 +++++++++++------ src/outputs/array.jl | 4 - src/outputs/graphic.jl | 25 ++- src/outputs/image.jl | 111 +++++++------ src/outputs/output.jl | 29 ++-- src/outputs/repl.jl | 17 +- src/overflow.jl | 112 +++++++------ src/rules.jl | 153 +++++++++++++++++- src/simulationdata.jl | 41 ++--- src/utils.jl | 56 +------ test/chain.jl | 6 +- test/image.jl | 33 ++-- test/integration.jl | 346 ++++++++++++++++++++++++++--------------- test/neighborhoods.jl | 82 +++++----- test/outputs.jl | 4 +- test/rules.jl | 32 ++-- test/simulationdata.jl | 21 ++- test/utils.jl | 15 +- 28 files changed, 918 insertions(+), 700 deletions(-) delete mode 100644 src/map.jl diff --git a/Project.toml b/Project.toml index f50b1c87..9f60e24a 100644 --- a/Project.toml +++ b/Project.toml @@ -26,11 +26,11 @@ UnicodeGraphics = "ebadf6b4-db70-5817-83da-4a19ad584e34" Colors = "0.9, 0.10, 0.11, 0.12" ConstructionBase = "1" Crayons = "4" -DimensionalData = "^0.10.7" +DimensionalData = "0.11" DocStringExtensions = "0.8" -FieldDefaults = "0.3" +FieldDefaults = "0.3.1" FieldDocTables = "0.1" -FieldMetadata = "0.2" +FieldMetadata = "0.3" FileIO = "1" FreeTypeAbstraction = "^0.6.5, 0.8" Mixers = "0.1" diff --git a/README.md b/README.md index 0bbf8df2..5aac02ef 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ using DynamicGrids, DynamicGridsGtk, ColorSchemes, Colors const DEAD, ALIVE, BURNING = 1, 2, 3 rule = let prob_combustion=0.0001, prob_regrowth=0.01 - Neighbors(RadialNeighborhood{1}()) do neighborhood, cell + Neighbors(RadialNeighborhood(1)) do neighborhood, cell if cell == ALIVE if BURNING in neighborhood BURNING diff --git a/docs/src/index.md b/docs/src/index.md index 62921234..6275af7b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,7 +4,7 @@ DynamicGrids ``` -## More Examples +## Examples While this package isn't designed or optimised specifically to run the game of life, it's a simple demonstration of what it can do. This example runs a @@ -12,10 +12,10 @@ game of life and displays it in a REPLOutput. ```@example -using DynamicGrids +using DynamicGrids, Distributions # Build a random starting grid -init = round.(Int8, max.(0.0, rand(-2.0:0.1:1.0, 70,70))) +init = Bool.(rand(Binomial(1, 0.5), 70, 70)) # Use the default game of life model model = Ruleset(Life()) @@ -87,10 +87,10 @@ and how they are combined to update the value of the current cell. ```@docs Neighborhood AbstractRadialNeighborhood -RadialNeighborhood -AbstractCustomNeighborhood -CustomNeighborhood -LayeredCustomNeighborhood +Moore +VonNeumann +Positional +LayeredPositional ``` ```@docs @@ -111,6 +111,13 @@ ArrayOutput GraphicOutput REPLOutput ImageOutput +GifOutput +``` + +```@docs +DynamicGrids.Extent +DynamicGrids.GraphicConfig +DynamicGrids.ImageConfig ``` ### Grid processors @@ -118,12 +125,14 @@ ImageOutput ```@docs GridProcessor SingleGridProcessor +SparseOptInspector ColorProcessor MultiGridProcessor ThreeColorProcessor LayoutProcessor Greyscale Grayscale +TextConfig ``` ### Gifs diff --git a/src/DynamicGrids.jl b/src/DynamicGrids.jl index e2f99ad1..1e0146a9 100644 --- a/src/DynamicGrids.jl +++ b/src/DynamicGrids.jl @@ -9,6 +9,7 @@ module DynamicGrids replace(text, "```julia" => "```@example") end DynamicGrids + using Colors, ConstructionBase, Crayons, @@ -26,6 +27,8 @@ using Colors, Test, UnicodeGraphics +const DG = DynamicGrids + using Base: tail import Base: show, getindex, setindex!, lastindex, size, length, push!, append!, @@ -37,8 +40,10 @@ import FieldMetadata: @description, description, @default, default +export sim!, resume!, replay, savegif, isinferred, method, methodtype -export sim!, resume!, replay, savegif, isinferred, neighbors, rules, method, methodtype +export rules, neighbors, inbounds, isinbounds, radius, gridsize, + currenttime, currenttimestep, timestep, tspan export Rule, NeighborhoodRule, CellRule, ManualRule, ManualNeighborhoodRule @@ -46,15 +51,15 @@ export Chain, Cell, Neighbors, Manual, Map, Life export AbstractRuleset, Ruleset -export Neighborhood, AbstractRadialNeighborhood, RadialNeighborhood, - AbstractCustomNeighborhood, CustomNeighborhood, LayeredCustomNeighborhood, - VonNeumannNeighborhood +export Neighborhood, AbstractRadialNeighborhood, Moore, + Custom, Positional, LayeredPositional, + VonNeumann export PerformanceOpt, NoOpt, SparseOpt export Overflow, RemoveOverflow, WrapOverflow -export Output, GraphicOutput, ImageOutput, ArrayOutput, REPLOutput +export Output, GraphicOutput, ImageOutput, ArrayOutput, REPLOutput, GifOutput export GridProcessor, SingleGridProcessor, ColorProcessor, SparseOptInspector, MultiGridProcessor, ThreeColorProcessor, LayoutProcessor @@ -88,13 +93,13 @@ include("outputs/graphic.jl") include("outputs/image.jl") include("outputs/array.jl") include("outputs/repl.jl") +include("outputs/gif.jl") include("interface.jl") include("framework.jl") include("sequencerules.jl") include("maprules.jl") include("overflow.jl") include("utils.jl") -include("map.jl") include("life.jl") include("show.jl") diff --git a/src/chain.jl b/src/chain.jl index a37e82bd..8eaf2b29 100644 --- a/src/chain.jl +++ b/src/chain.jl @@ -31,7 +31,7 @@ Base.firstindex(chain::Chain) = firstindex(rules(chain)) Base.lastindex(chain::Chain) = lastindex(rules(chain)) """ - applyrule(rules::Chain, data, state, (i, j)) + applyrule(data, rules::Chain, state, (i, j)) Chained rules. If a [`Chain`](@ref) of rules is passed to `applyrule`, run them sequentially for each cell. This can have much beter performance as no writes @@ -39,28 +39,31 @@ occur between rules, and they are essentially compiled together into compound rules. This gives correct results only for [`CellRule`](@ref), or for a single [`NeighborhoodRule`](@ref) followed by [`CellRule`](@ref). """ -@inline applyrule(chain::Chain, data::SimData, state, index, args...) = begin - newstate = applyrule(chain::Chain, chain[1], data, state, index, args...) -end -@inline applyrule(chain::Chain{R,W,Tuple{}}, data::SimData, state, index, args... - ) where {R,W} = +@inline applyrule(data::SimData, chain::Chain, state, index) = + chainrule(data, chain::Chain, chain[1], state, index) +@inline applyrule(data::SimData, chain::Chain{R,W,Tuple{}}, state, index) where {R,W} = chainstate(chain, map(Val, writekeys(chain)), state) -@inline applyrule(chain::Chain, rule::Rule{RR,RW}, data::SimData, state, index, args... +@inline chainrule(data::SimData, chain::Chain, rule::Rule{RR,RW}, state, index ) where {RR,RW} = begin + # Get the state needed by this rule read = chainstate(chain, Val{RR}, state) - write = applyrule(rule, data, read, index, args...) + # Run the rule + write = applyrule(data, rule, read, index) + # Create new state with the result and state from other rules newstate = update_chainstate(chain, rule, state, write) - applyrule(tail(chain), data, newstate, index) + # Run applyrule on the rest of the chain + applyrule(data, tail(chain), newstate, index) end -@inline applyrule(chain::Chain, rule::Rule{RR,RW}, data::SimData, state, index, args... +@inline chainrule(data::SimData, chain::Chain, rule::Rule{RR,RW}, state, index, args... ) where {RR<:Tuple,RW} = begin read = chainstate(chain, (map(Val, readkeys(rule))...,), state) - write = applyrule(rule, data, read, index, args...) + write = applyrule(data, rule, read, index) newstate = update_chainstate(chain, rule, state, write) - applyrule(tail(chain), data, newstate, index) + applyrule(data, tail(chain), newstate, index) end +# Get state as a NamedTuple or single value @inline chainstate(chain::Chain, keys::Tuple, state) = begin keys = map(unwrap, keys) vals = map(k -> state[k], keys) @@ -69,6 +72,8 @@ end @inline chainstate(chain::Chain, key::Type{<:Val}, state) = state[unwrap(key)] +# Merge new state with previous state +# Returning a new NamedTuple with all keys having the most recent state @generated update_chainstate(chain::Chain{CR,CW}, rule::Rule{RR,RW}, state::NamedTuple{K,V}, writestate::Tuple ) where {CR,CW,RR,RW,K,V} = begin expr = Expr(:tuple) @@ -90,7 +95,6 @@ end NamedTuple{$keys}(newstate) end end - @generated update_chainstate(chain::Chain{CR,CW}, rule::Rule{RR,RW}, state::NamedTuple{K,V}, writestate ) where {CR,CW,RR,RW,K,V} = begin expr = Expr(:tuple) diff --git a/src/extent.jl b/src/extent.jl index 51237363..cbea79f1 100644 --- a/src/extent.jl +++ b/src/extent.jl @@ -29,3 +29,7 @@ setstarttime!(e::Extent, start) = e.tspan = start:step(tspan(e)):last(tspan(e)) setstoptime!(e::Extent, stop) = e.tspan = first(tspan(e)):step(tspan(e)):stop + +gridsize(extent::Extent) = gridsize(init(extent)) +gridsize(A::AbstractArray) = size(A) +gridsize(nt::NamedTuple) = size(first(nt)) diff --git a/src/framework.jl b/src/framework.jl index 6f47481c..687e9621 100644 --- a/src/framework.jl +++ b/src/framework.jl @@ -37,8 +37,9 @@ sim!(output::Output, ruleset=ruleset(output); aux=aux(output), fps=fps(output), nreplicates=nothing, - simdata=nothing) = begin + simdata=nothing, kwargs...) = begin + gridsize(init) == gridsize(DG.init(output)) || throw(ArgumentError("init size does not match output init")) # Some rules are only valid for a set time-step size. step(ruleset) !== nothing && step(ruleset) != step(tspan) && throw(ArgumentError("tspan step $(step(tspan)) must equal rule step $(step(ruleset))")) @@ -57,7 +58,7 @@ sim!(output::Output, ruleset=ruleset(output); # Set run speed for GraphicOutputs setfps!(output, fps) # Show the first grid - showgrid(output, simdata, 1, tspan) + showframe(output, simdata, 1, tspan) # Let the init grid be displayed as long as a normal grid delay(output, 1) # Run the simulation over simdata and a unitrange @@ -71,7 +72,6 @@ Shorthand for running a rule without defining a `Ruleset`. """ sim!(output::Output, rules::Rule...; tspan=tspan(output), - overflow=RemoveOverflow(), kwargs...) = begin ruleset = Ruleset(rules...; timestep=step(tspan), kwargs...) sim!(output::Output, ruleset; tspan=tspan, kwargs...) @@ -156,14 +156,14 @@ simloop!(output::Output, simdata, fspan) = begin # Run the ruleset and setup data for the next iteration simdata = sequencerules!(simdata) # Save/do something with the the current grid - storegrid!(output, simdata) + storeframe!(output, simdata) # Let interface things happen isasync(output) && yield() # Stick to the FPS delay(output, f) # Exit gracefully if !isrunning(output) || f == last(fspan) - showgrid(output, simdata, f, currenttime(simdata)) + showframe(output, simdata, f, currenttime(simdata)) setstoptime!(output, currenttime(simdata)) finalise(output) break diff --git a/src/life.jl b/src/life.jl index 18a463ab..596de5a6 100644 --- a/src/life.jl +++ b/src/life.jl @@ -30,18 +30,16 @@ output = REPLOutput(init; fps=60, color=:yellow) sim!(output, Ruleset(Life(b=(1,3,5,7), s=(1,3,5,7))), init; tspan=(1, 1000)) ``` -$(FIELDDOCTABLE) """ @default @flattenable @bounds @description struct Life{R,W,N,B,S} <: NeighborhoodRule{R,W} - neighborhood::N | RadialNeighborhood{1}() | false | nothing | "Any Neighborhood" - b::B | (3, 3) | true | (0, 8) | "Array, Tuple or Iterable of integers to match neighbors when cell is empty" - s::S | (2, 3) | true | (0, 8) | "Array, Tuple or Iterable of integers to match neighbors cell is full" + neighborhood::N | Moore(1) | false | nothing | "Any Neighborhood" + b::B | (3, 3) | true | (0, 8) | "Array, Tuple or Iterable of integers to match neighbors when cell is empty" + s::S | (2, 3) | true | (0, 8) | "Array, Tuple or Iterable of integers to match neighbors cell is full" end -applyrule(rule::Life, data::SimData, state, index) = - # Check if neighborhood sum matches rule for the current state - if sum(neighborhood(rule)) in (rule.b, rule.s)[state+1] - oneunit(state) - else - zero(state) - end +const life_states = + (false, false, false, true, false, false, false, false, false), + (false, false, true, true, false, false, false, false, false) + +applyrule(data::SimData, rule::Life, state, I) = + life_states[state + 1][sum(neighbors(rule)) + 1] diff --git a/src/map.jl b/src/map.jl deleted file mode 100644 index 66658007..00000000 --- a/src/map.jl +++ /dev/null @@ -1,128 +0,0 @@ -""" - Cell{R,W}(f) - Cell(f; read, write) - -A [`CellRule`](@ref) that applies a function `f` to the -`read` grid cells and returns the `write` cells. - -Especially convenient with `do` notation. - -## Example - -Set the cells of grid `:c` to the sum of `:a` and `:b`. -```julia -simplerule = Cell() do a, b - a + b -end -``` - -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. - -```julia -rule = let y = y - rule = Cell(read=(a, b), write=b) do a, b - a + b * y - end -end -``` -""" -@flattenable @description struct Cell{R,W,F} <: CellRule{R,W} - # Field | Flatten | Description - f::F | true | "Function to apply to the read values" -end -Cell(f; read=:_default_, write=read) = Cell{read,write}(f) - -@inline applyrule(rule::Cell, data::SimData, read, index) = - let (rule, read) = (rule, read) - rule.f(astuple(rule, read)...) - end -const Map = Cell - -astuple(rule::Rule, read) = astuple(readkeys(rule), read) -astuple(::Tuple, read) = read -astuple(::Symbol, read) = (read,) - -""" -Neighbors(f; read=:_default_, write=read, neighborhood=RadialNeighborhood()) - Neighbors{R,W}(f) - -A [`NeighborhoodRule`](@ref) that receives a neighbors object for the first -`read` grid and the passed in neighborhood, followed by the cell values for -the reqquired grids, as with [`Cell`](@ref). - -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 by growing the grid to match the neighborhood radius. -Using [`SparseOpt`](@ref) may imrove neighborhood performance when zero values -are both common and can be safely ignored. - -## Example - -```julia -rule = let x = 10 - Neighbors{Tuple{:a,:b},:b}() do hood, a, b - data[:b][index...] = a + b^x - end -end -``` -The `let` block greatly imroves performance. -""" -@flattenable @description struct Neighbors{R,W,F,N} <: NeighborhoodRule{R,W} - # Field | Flatten | Description - f::F | true | "Function to apply to the neighborhood and read values" - neighborhood::N | true | "" -end -Neighbors(f; read=:_default_, write=read, neighborhood=RadialNeighborhood{1}()) = - Neighbors{read,write}(f, neighborhood) - -@inline applyrule(rule::Neighbors, data::SimData, read, index) = - let hood=neighborhood(rule), rule=rule, read=astuple(rule, read) - rule.f(hood, read...) - end - -""" - Manual(f; read=:_default_, write=read) - Manual{R,W}(f) - -A [`ManualRule`](@ref) 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 requirement grid values for the index. - -## Example - -```julia -rule = let x = 10 - Manual{Tuple{:a,:b},:b}() do data, index, a, b - data[:b][index...] = a + b^x - end -end -``` -The `let` block greatly imroves performance. -""" -@flattenable @description struct Manual{R,W,F} <: ManualRule{R,W} - # Field | Flatten | Description - f::F | true | "Function to apply to the data, index and read values" -end -Manual(f; read=:_default_, write=read) = Manual{read,write}(f) - -@inline applyrule!(rule::Manual, data::SimData, read, index) = - let data=data, index=index, rule=rule, read=astuple(rule, read) - rule.f(data, index, read...) - end - -""" - method(rule) - -Get the method of a `Cell`, `Neighbors`, or `Manual` rule. -""" -method(rule::Union{Cell,Neighbors,Manual}) = rule.f -""" - methodtype(rule) - -Get the method type of a `Cell`, `Neighbors`, or `Manual` rule. -This is useful in combination with FieldMetadata.jl macros. -""" -methodtype(rule::Union{Cell,Neighbors,Manual}) = typeof(method(rule)) diff --git a/src/maprules.jl b/src/maprules.jl index d7616958..c520ec22 100644 --- a/src/maprules.jl +++ b/src/maprules.jl @@ -6,31 +6,38 @@ struct _Write_ end maprule!(simdata::SimData, rule::Rule) = begin rkeys, rgrids = getgrids(_Read_(), rule, simdata) wkeys, wgrids = getgrids(_Write_(), rule, simdata) - # Copy the source to dest for grids we are writing to, - # if they need to be copied - _maybeupdate_dest!(wgrids, rule) + # Copy the source to dest for grids we are writing to, if needed + maybeupdatedest!(wgrids, rule) + # Copy or zero out overflow where needed + handleoverflow!(wgrids) # Combine read and write grids to a temporary simdata object tempsimdata = @set simdata.grids = combinegrids(rkeys, rgrids, wkeys, wgrids) # Run the rule loop ruleloop(opt(simdata), rule, tempsimdata, rkeys, rgrids, wkeys, wgrids, mask(simdata)) - # Copy the source status to dest status for all write grids - copystatus!(wgrids) + # Copy the dest status to dest status if it is in use + maybecopystatus!(wgrids) # Swap the dest/source of grids that were written to wgrids = swapsource(wgrids) |> _to_readonly # Combine the written grids with the original simdata replacegrids(simdata, wkeys, wgrids) end + +maybecopystatus!(grid::Tuple{Vararg{<:GridData}}) = map(maybecopystatus!, grid) +maybecopystatus!(grid::GridData) = + maybecopystatus!(sourcestatus(grid), deststatus(grid)) +maybecopystatus!(srcstatus, deststatus) = nothing +maybecopystatus!(srcstatus::AbstractArray, deststatus::AbstractArray) = + @inbounds return srcstatus .= deststatus + _to_readonly(data::Tuple) = map(ReadableGridData, data) _to_readonly(data::WritableGridData) = ReadableGridData(data) -_maybeupdate_dest!(ds::Tuple, rule) = - map(d -> _maybeupdate_dest!(d, rule), ds) -_maybeupdate_dest!(d::WritableGridData, rule::Rule) = - handleoverflow!(d) -_maybeupdate_dest!(d::WritableGridData, rule::ManualRule) = begin +maybeupdatedest!(ds::Tuple, rule) = + map(d -> maybeupdatedest!(d, rule), ds) +maybeupdatedest!(d::WritableGridData, rule::Rule) = nothing +maybeupdatedest!(d::WritableGridData, rule::ManualRule) = begin copy!(parent(dest(d)), parent(source(d))) - handleoverflow!(d) end # Separated out for both modularity and as a function barrier for type stability @@ -40,7 +47,7 @@ ruleloop(::PerformanceOpt, rule::Rule, simdata::SimData, rkeys, rgrids, wkeys, w for j in 1:ncols, i in 1:nrows ismasked(mask, i, j) && continue readval = readgrids(rkeys, rgrids, i, j) - writeval = applyrule(rule, simdata, readval, (i, j)) + writeval = applyrule(simdata, rule, readval, (i, j)) writegrids!(wgrids, writeval, i, j) end end @@ -50,7 +57,7 @@ ruleloop(::PerformanceOpt, rule::ManualRule, simdata::SimData, rkeys, rgrids, wk for j in 1:ncols, i in 1:nrows ismasked(mask, i, j) && continue readval = readgrids(rkeys, rgrids, i, j) - applyrule!(rule, simdata, readval, (i, j)) + applyrule!(simdata, rule, readval, (i, j)) end end @@ -59,7 +66,7 @@ ruleloop(::SparseOpt, rule::ManualRule, simdata::SimData, rkey, rgrid::GridData, runsparse(rgrid) do i, j ismasked(mask, i, j) && return readval = readgrids(rkey, rgrid, i, j) - applyrule!(rule, simdata, readval, (i, j)) + applyrule!(simdata, rule, readval, (i, j)) return end end @@ -157,16 +164,19 @@ ruleloop(opt::NoOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, end end - curblockj = indtoblock(j, blocksize) # Loop over the grid ROWS inside the block for b in 1:rowsinblock I = i + b - 1, j ismasked(mask, I...) && continue - # Which block row are we in - curblocki = b <= r ? 1 : 2 # Run the rule using buffer b - readval = readgrids(keys2vals(readkeys(rule)), rgrids, I...) - writeval = applyrule(bufrules[b], simdata, readval, I) + readval = if rgrids isa Tuple + # Get all vals from grids + readgrids(keys2vals(readkeys(rule)), rgrids, I...) + else + # Get single val from buffer center + buffers[b][r + 1, r + 1] + end + writeval = applyrule(simdata, bufrules[b], readval, I) writegrids!(wgrids, writeval, I...) end end @@ -188,13 +198,16 @@ ruleloop(opt::SparseOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, should be in fast local memory. =# # Initialise status for the dest. Is this needed? - # deststatus(data) .= false srcstatus, dststatus = sourcestatus(griddata), deststatus(griddata) + dststatus .= false + # curstatus and newstatus track active status for 4 local blocks newstatus = localstatus(griddata) + valtype = eltype(dst) # Loop down the block COLUMN - for bi = 1:size(srcstatus, 1) - 1 + for bi = 1:size(srcstatus, 1) + lastblockrow = bi == size(srcstatus, 1) i = blocktoind(bi, blocksize) # Get current block rowsinblock = min(blocksize, nrows - blocksize * (bi - 1)) @@ -202,30 +215,37 @@ ruleloop(opt::SparseOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, freshbuffer = true # Initialise block status for the start of the row - @inbounds bs11, bs12 = srcstatus[bi, 1], srcstatus[bi, 2] - @inbounds bs21, bs22 = srcstatus[bi + 1, 1], srcstatus[bi + 1, 2] + @inbounds bs11, bs12 = srcstatus[bi, 1], srcstatus[bi, 2] + bs21, bs22 = if bi == size(srcstatus, 1) + false, false + else + @inbounds srcstatus[bi + 1, 1], srcstatus[bi + 1, 2] + end newstatus .= false # Loop along the block ROW. This is faster because we are reading # 1 column from the main array for 2 blocks at each step, not actually along the row. - for bj = 1:size(srcstatus, 2) - 1 + for bj = 1:size(srcstatus, 2) + lastblockcol = bj == size(srcstatus, 2) @inbounds newstatus[1, 1] = newstatus[1, 2] @inbounds newstatus[2, 1] = newstatus[2, 2] @inbounds newstatus[1, 2] = false @inbounds newstatus[2, 2] = false - # Get current block status from the source status array bs11, bs21 = bs12, bs22 - @inbounds bs12, bs22 = srcstatus[bi, bj + 1], srcstatus[bi + 1, bj + 1] + bs12, bs22 = if lastblockcol + # This is the last block, the second half wont run + false, false + else + # Get current block status from the source status array + @inbounds srcstatus[bi, bj + 1], lastblockrow ? false : srcstatus[bi + 1, bj + 1] + end jstart = blocktoind(bj, blocksize) jstop = min(jstart + blocksize - 1, ncols) - # Use this block unless it or its neighbors are active + # Skip this block it and its neighbors are inactive if !(bs11 | bs12 | bs21 | bs22) - if !skippedlastblock - newstatus .= false - end # Skip this block skippedlastblock = true # Run the rest of the chain if it exists @@ -237,7 +257,7 @@ ruleloop(opt::SparseOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, I = i + b - 1, j ismasked(mask, I...) && continue read = readgrids(rkeys, rgrids, I...) - write = applyrule(tail(rule), simdata, read, I) + write = applyrule(simdata, tail(rule), read, I) if wgrids isa Tuple map(wgrids, write) do d, w @inbounds dest(d)[I...] = w @@ -247,15 +267,30 @@ ruleloop(opt::SparseOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, end end end + else + for j in jstart:jstop + for b in 1:rowsinblock + I = i + b - 1, j + if wgrids isa Tuple + map(wgrids) do wg + @inbounds dest(wg)[I...] = zero(valtype) + end + else + @inbounds dest(wgrids)[I...] = zero(valtype) + end + end + end end continue end # Reinitialise neighborhood buffers if we have skipped a section of the array if skippedlastblock - for y = 1:hoodsize, b in 1:rowsinblock, x = 1:hoodsize - val = src[i + b + x - 2, jstart + y - 1] - @inbounds buffers[b][x, y] = val + for y = 1:hoodsize + for b in 1:rowsinblock, x = 1:hoodsize + @inbounds val = src[i + b + x - 2, jstart + y - 1] + @inbounds buffers[b][x, y] = val + end end skippedlastblock = false freshbuffer = true @@ -263,8 +298,8 @@ ruleloop(opt::SparseOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, # Loop over the grid COLUMNS inside the block for j in jstart:jstop - # Which block column are we in - curblockj = j - jstart < r ? 1 : 2 + # Which block column are we in, 1 or 2 + curblockj = (j - jstart) ÷ r + 1 if freshbuffer freshbuffer = false else @@ -272,7 +307,7 @@ ruleloop(opt::SparseOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, for b in 1:rowsinblock @inbounds buf = buffers[b] # copyto! uses linear indexing, so 2d dims are transformed manually - @inbounds copyto!(buf, 1, buf, hoodsize + 1, (hoodsize - 1) * hoodsize) + copyto!(buf, 1, buf, hoodsize + 1, (hoodsize - 1) * hoodsize) end # Copy a new column to each neighborhood buffer for b in 1:rowsinblock @@ -287,35 +322,47 @@ ruleloop(opt::SparseOpt, rule, simdata::SimData, rkeys, rgrids, wkeys, wgrids, for b in 1:rowsinblock I = i + b - 1, j ismasked(mask, I...) && continue - # Which block row are we in - curblocki = b <= r ? 1 : 2 - # Run the rule using buffer b - readval = readgrids(keys2vals(readkeys(rule)), rgrids, I...) - writeval = applyrule(bufrules[b], simdata, readval, I) - writegrids!(wgrids, writeval, I...) + # Which block row are we in, 1 or 2 + curblocki = (b - 1) ÷ r + 1 + readval = if rgrids isa Tuple + # Get all vals from grids + readgrids(keys2vals(readkeys(rule)), rgrids, I...) + else + # Get single val from buffer center + @inbounds buffers[b][r + 1, r + 1] + end + @inbounds writeval = applyrule(simdata, bufrules[b], readval, I) + @inbounds writegrids!(wgrids, writeval, I...) # Update the status for the block cellstatus = if writeval isa NamedTuple @inbounds val = writeval[neighborhoodkey(rule)] val != zero(val) else - writeval != zero(writeval) + writeval != zero(valtype) end @inbounds newstatus[curblocki, curblockj] |= cellstatus end # Combine blocks with the previous rows / cols @inbounds dststatus[bi, bj] |= newstatus[1, 1] - @inbounds dststatus[bi, bj+1] |= newstatus[1, 2] - @inbounds dststatus[bi+1, bj] |= newstatus[2, 1] - # Start new block fresh to remove old status - @inbounds dststatus[bi+1, bj+1] = newstatus[2, 2] + if !lastblockcol + @inbounds dststatus[bi, bj+1] |= newstatus[1, 2] + end + if !lastblockrow + @inbounds dststatus[bi+1, bj] |= newstatus[2, 1] + # Start new block fresh to remove old status + if !lastblockcol + @inbounds dststatus[bi+1, bj+1] = newstatus[2, 2] + end + end end end end - srcstatus .= dststatus end + + # Low-level tools for fetching, manipulating and writing # grids reuired in the simulation. @@ -343,7 +390,7 @@ readgrids(rkeys::Val, rdata::ReadableGridData, I...) = expr end writegrids!(wdata::GridData{T}, val::T, I...) where T = begin - @inbounds wdata.dest[I...] = val + @inbounds dest(wdata)[I...] = val nothing end diff --git a/src/neighborhoods.jl b/src/neighborhoods.jl index 66976101..71afea76 100644 --- a/src/neighborhoods.jl +++ b/src/neighborhoods.jl @@ -14,7 +14,7 @@ If the allocation of neighborhood buffers during the simulation is costly (it usually isn't) you can use `allocbuffers` or preallocate them: ```julia -RadialNeighborhood{3}(allocbuffers(3, init)) +Moore{3}(allocbuffers(3, init)) ``` You can also change the length of the buffers tuple to @@ -54,28 +54,35 @@ neighbors(hood::AbstractRadialNeighborhood) = begin end """ - RadialNeighborhood{R}([buffer]) + Moore(radius::Int=1) Radial neighborhoods calculate the surrounding neighborhood from the radius around the central cell. The central cell is omitted. The `buffer` argument may be required for performance -optimisation, see [`Neighborhood`] for details. +optimisation, see [`Neighborhood`](@ref) for details. """ -struct RadialNeighborhood{R,B} <: AbstractRadialNeighborhood{R,B} +struct Moore{R,B} <: AbstractRadialNeighborhood{R,B} buffer::B end -RadialNeighborhood{R}(buffer=nothing) where R = - RadialNeighborhood{R,typeof(buffer)}(buffer) - -# Custom `sum` for performance:w -Base.sum(hood::RadialNeighborhood, cell=_centerval(hood)) = +# Buffer is updated later during the simulation. +# but can be passed in now to avoid the allocation. +# This might be bad design. SimData could instead hold a list of +# ruledata for the rule that holds this buffer, with +# the neighborhood. So you can do neighbors(data) +Moore(radius::Int=1, buffer=nothing) = + Moore{radius}(buffer) +Moore{R}(buffer=nothing) where R = + Moore{R,typeof(buffer)}(buffer) + +# Neighborhood specific `sum` for performance:w +Base.sum(hood::Moore, cell=_centerval(hood)) = sum(buffer(hood)) - cell _centerval(hood) = buffer(hood)[radius(hood) + 1, radius(hood) + 1] -Base.length(hood::RadialNeighborhood{R}) where R = (2R + 1)^2 - 1 +Base.length(hood::Moore{R}) where R = (2R + 1)^2 - 1 @inline mapsetneighbor!(data::WritableGridData, hood::AbstractRadialNeighborhood, rule, state, index) = begin r = radius(hood) @@ -95,19 +102,20 @@ Base.length(hood::RadialNeighborhood{R}) where R = (2R + 1)^2 - 1 end """ -Custom neighborhoods are tuples of custom coordinates (also tuples) 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 neighborhoods are tuples of custom coordinates (also tuples) +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. """ -abstract type AbstractCustomNeighborhood{R,B} <: Neighborhood{R,B} end +abstract type AbstractPositional{R,B} <: Neighborhood{R,B} end const CustomCoord = Tuple{Vararg{Int}} -const CustomCoords = Tuple{Vararg{<:CustomCoord}} +const CustomCoords = Union{AbstractArray{<:CustomCoord},Tuple{Vararg{<:CustomCoord}}} """ - CustomNeighborhood(coord::Tuple{Vararg{Int}}...) - CustomNeighborhood(coords::Tuple{Tuple{Vararg{Int}}}, [buffer=nothing]) - CustomNeighborhood{R}(coords::Tuple, buffer) + Positional(coord::Tuple{Vararg{Int}}...) + Positional(coords::Tuple{Tuple{Vararg{Int}}}, [buffer=nothing]) + Positional{R}(coords::Tuple, buffer) Allows arbitrary neighborhood shapes by specifying each coord, which are simply `Tuple`s of `Int` distance (positive and negative) from the central point. @@ -120,33 +128,38 @@ coordinates if they are not symmetrical. The `buffer` argument may be required for performance optimisation, see [`Neighborhood`] for more details. """ -@description @flattenable struct CustomNeighborhood{R,C<:CustomCoords,B} <: AbstractCustomNeighborhood{R,B} +@description @flattenable struct Positional{R,C<:CustomCoords,B} <: AbstractPositional{R,B} coords::C | false | "A tuple of tuples of Int, containing 2-D coordinates relative to the central point" buffer::B end -CustomNeighborhood(args::CustomCoord...) = CustomNeighborhood(args) -CustomNeighborhood(coords::CustomCoords, buffer=nothing) = - CustomNeighborhood{absmaxcoord(coords)}(coords, buffer) -CustomNeighborhood{R}(coords::CustomCoords, buffer) where R = - CustomNeighborhood{R,typeof(coords),typeof(buffer)}(coords, buffer) +Positional(args::CustomCoord...) = Positional(args) +Positional(coords::CustomCoords, buffer=nothing) = + Positional{absmaxcoord(coords)}(coords, buffer) +Positional{R}(coords::CustomCoords, buffer=nothing) where R = + Positional{R,typeof(coords),typeof(buffer)}(coords, buffer) -ConstructionBase.constructorof(::Type{CustomNeighborhood{R,C,B}}) where {R,C,B} = - CustomNeighborhood{R} +ConstructionBase.constructorof(::Type{Positional{R,C,B}}) where {R,C,B} = + Positional{R} -coords(hood::CustomNeighborhood) = hood.coords +coords(hood::Positional) = hood.coords -Base.length(hood::CustomNeighborhood) = length(coords(hood)) +Base.length(hood::Positional) = length(coords(hood)) # Calculate the maximum absolute value in the coords to use as the radius -absmaxcoord(coords::Tuple) = maximum(map(x -> maximum(map(abs, x)), coords)) -absmaxcoord(neighborhood::CustomNeighborhood) = absmaxcoord(coords(neighborhood)) +absmaxcoord(coords::Union{AbstractArray,Tuple}) = maximum(map(x -> maximum(map(abs, x)), coords)) +absmaxcoord(neighborhood::Positional) = absmaxcoord(coords(neighborhood)) -neighbors(hood::CustomNeighborhood) = - (buffer(hood)[(coord .+ radius(hood) .+ 1)...] for coord in coords(hood)) +""" + neighbors(hood::Positional) +Returns an iterator over the `Positional` neighborhood +cells around the current index. +""" +neighbors(hood::Positional) = + (buffer(hood)[(coord .+ radius(hood) .+ 1)...] for coord in coords(hood)) -@inline mapsetneighbor!(data::WritableGridData, hood::CustomNeighborhood, rule, state, index) = begin +@inline mapsetneighbor!(data::WritableGridData, hood::Positional, rule, state, index) = begin r = radius(hood); sum = zero(state) # Loop over dispersal kernel grid dimensions for coord in coords(hood) @@ -158,36 +171,59 @@ neighbors(hood::CustomNeighborhood) = end """ -Sets of custom neighborhoods that can have separate rules for each set. + LayeredPositional(layers::Positional...) + +Sets of [`Positional`](@ref) neighborhoods that can have separate rules for each set. + +`neighbors` for `LayeredPositional` returns a tuple of iterators +for each neighborhood layer. """ -@description struct LayeredCustomNeighborhood{R,L,B} <: AbstractCustomNeighborhood{R,B} - layers::L | "A tuple of custom neighborhoods" +@description struct LayeredPositional{R,L,B} <: AbstractPositional{R,B} + layers::L | "A tuple of custom neighborhoods" buffer::B | _ end -LayeredCustomNeighborhood(layers::Tuple{Vararg{<:CustomNeighborhood}}, buffer) = - LayeredCustomNeighborhood{maximum(absmaxcoord.(layers))}(layers, buffer) -LayeredCustomNeighborhood{R}(layers, buffer) where R = begin +LayeredPositional(layers::Positional...) = + LayeredPositional(layers) +LayeredPositional(layers::Tuple{Vararg{<:Positional}}, buffer=nothing) = + LayeredPositional{maximum(map(radius, layers))}(layers, buffer) +LayeredPositional{R}(layers, buffer) where R = begin # Child layers must have the same buffer layers = map(l -> (@set l.buffer = buffer), layers) - LayeredCustomNeighborhood{R,typeof(layers),typeof(buffer)}(layers, buffer) + LayeredPositional{R,typeof(layers),typeof(buffer)}(layers, buffer) end -@inline neighbors(hood::LayeredCustomNeighborhood) = +""" + neighbors(hood::Positional) + +Returns a tuple of iterators over each `Positional` neighborhood +layer for the cells around the current index. +""" +@inline neighbors(hood::LayeredPositional) = map(l -> neighbors(l), hood.layers) -@inline Base.sum(hood::LayeredCustomNeighborhood) = map(sum, neighbors(hood)) +@inline Base.sum(hood::LayeredPositional) = map(sum, neighbors(hood)) -@inline mapsetneighbor!(data::WritableGridData, hood::LayeredCustomNeighborhood, rule, state, index) = +@inline mapsetneighbor!(data::WritableGridData, hood::LayeredPositional, rule, state, index) = map(layer -> mapsetneighbor!(data, layer, rule, state, index), hood.layers) """ -A convenience wrapper to build a VonNeumann neighborhoods as a `CustomNeighborhood`. + VonNeumann(radius=1) -TODO: variable radius +A convenience wrapper to build Von-Neumann neighborhoods as +a [`Positional`](@ref) neighborhood. """ -VonNeumannNeighborhood(buffer=nothing) = - CustomNeighborhood(((0,-1), (-1,0), (1,0), (0,1)), buffer) +VonNeumann(radius=1, buffer=nothing) = begin + coords = Tuple{Int,Int}[] + rng = -radius:radius + for j in rng, i in rng + distance = abs(i) + abs(j) + if distance <= radius && distance > 0 + push!(coords, (i, j)) + end + end + Positional(coords, buffer) +end """ Find the largest radius present in the passed in rules. @@ -222,6 +258,19 @@ spreadbuffers(rule::NeighborhoodRule, hood::Neighborhood, buffers::Nothing, init spreadbuffers(rule::NeighborhoodRule, hood::Neighborhood, buffers::Tuple, init::AbstractArray) = buffers, map(b -> (@set rule.neighborhood.buffer = b), buffers) +""" + 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. +""" +allocbuffers(init::AbstractArray, hood::Neighborhood) = allocbuffers(init, radius(hood)) +allocbuffers(init::AbstractArray, r::Int) = Tuple(allocbuffer(init, r) for i in 1:2r) + +allocbuffer(init::AbstractArray, hood::Neighborhood) = allocbuffer(init, radius(hood)) +allocbuffer(init::AbstractArray, r::Int) = zeros(eltype(init), 2r+1, 2r+1) + """ hoodsize(radius) @@ -230,4 +279,3 @@ which is always 2r + 1. """ @inline hoodsize(hood::Neighborhood) = hoodsize(radius(hood)) @inline hoodsize(radius::Integer) = 2radius + 1 - diff --git a/src/outputs/array.jl b/src/outputs/array.jl index e58ae22e..37fbee03 100644 --- a/src/outputs/array.jl +++ b/src/outputs/array.jl @@ -19,7 +19,3 @@ ArrayOutput(; frames, running, extent, kwargs...) = begin append!(frames, zerogrids(init(extent), length(tspan(extent))-1)) ArrayOutput(frames, running, extent) end - -zerogrids(initgrid::AbstractArray, nframes) = [zero(initgrid) for f in 1:nframes] -zerogrids(initgrids::NamedTuple, nframes) = - [map(grid -> zero(grid), initgrids) for f in 1:nframes] diff --git a/src/outputs/graphic.jl b/src/outputs/graphic.jl index ba3dd572..dc3a3697 100644 --- a/src/outputs/graphic.jl +++ b/src/outputs/graphic.jl @@ -1,4 +1,10 @@ +""" + GraphicConfig(; fps=25.0, store=false, kwargs...) = + GraphicConfig(fps, timestamp, stampframe, store) + +Config and variables for graphic outputs. +""" mutable struct GraphicConfig{FPS,TS,SF} fps::FPS timestamp::TS @@ -21,6 +27,11 @@ end """ Outputs that display the simulation frames live. + +All `GraphicOutputs` have a [`GraphicConfig`](@ref) object +and provide a [`showframe`](@ref) method. + +See [`REPLOutput`](@ref) for an example. """ abstract type GraphicOutput{T} <: Output{T} end @@ -55,13 +66,13 @@ delay(o::GraphicOutput, f) = sleep(max(0.0, timestamp(o) + (f - stampframe(o))/fps(o) - time())) isshowable(o::GraphicOutput, f) = true # TODO working max fps. o.timestamp + (t - tlast(o))/o.maxfps < time() -storegrid!(o::GraphicOutput, data::AbstractSimData) = begin - f = gridindex(o, data) +storeframe!(o::GraphicOutput, data::AbstractSimData) = begin + f = frameindex(o, data) if isstored(o) _pushgrid!(eltype(o), o) end - storegrid!(eltype(o), o, data, f) - isshowable(o, currentframe(data)) && showgrid(o, data, currentframe(data), currenttime(data)) + storeframe!(eltype(o), o, data, f) + isshowable(o, currentframe(data)) && showframe(o, data, currentframe(data), currenttime(data)) end _pushgrid!(::Type{<:NamedTuple}, o::GraphicOutput) = @@ -69,6 +80,6 @@ _pushgrid!(::Type{<:NamedTuple}, o::GraphicOutput) = _pushgrid!(::Type{<:AbstractArray}, o::GraphicOutput) = push!(o, similar(o[1])) -# Get frame f from output and call showgrid again -showgrid(o::GraphicOutput, data::AbstractSimData, f, t) = - showgrid(o[gridindex(o, f)], o, data, f, t) +# Get frame f from output and call showframe again +showframe(o::GraphicOutput, data::AbstractSimData, f, t) = + showframe(o[frameindex(o, f)], o, data, f, t) diff --git a/src/outputs/image.jl b/src/outputs/image.jl index 899406df..1246e12c 100644 --- a/src/outputs/image.jl +++ b/src/outputs/image.jl @@ -1,9 +1,22 @@ +""" + ImageConfig(processor, minval, maxval) + ImageConfig(; processor=ColorProcessor(), minval=nothing, maxval=nothing) + +Common configuration component for all [`ImageOutput`](@ref). + +Holds an [`Processor`](@ref). +`minval` and `maxval` fields normalise grid values between zero and one, +for use with Colorshemes.jl. `nothing` values are considered to represent +zero and one, and will not be normalised. + +Values +""" struct ImageConfig{P,Min,Max} processor::P minval::Min maxval::Max end -ImageConfig(; processor=ColorProcessor(), minval=0, maxval=1, kwargs...) = +ImageConfig(; processor=ColorProcessor(), minval=nothing, maxval=nothing, kwargs...) = ImageConfig(processor, minval, maxval) processor(ic::ImageConfig) = ic.processor @@ -13,10 +26,15 @@ maxval(ic::ImageConfig) = ic.maxval const RulesetOrSimData = Union{Ruleset,AbstractSimData} """ -Graphic outputs that display the simulation grid(s) as RGB images. +Graphic outputs that display the simulation frames as RGB images. + +`ImageOutput`s have a [`ImageConfig`](@ref) component, and define a +[`showimage`](@ref) method. + +See [`GifOutput`](@ref) for an example. Although the majority of the code is maintained here to enable sharing -and reuse, no `ImageOutput`s are provided in DynamicGrids.jl to avoid +and reuse, most `ImageOutput`s are not provided in DynamicGrids.jl to avoid heavey dependencies on graphics libraries. See [DynamicGridsGtk.jl](https://github.com/cesaraustralia/DynamicGridsGtk.jl) and [DynamicGridsInteract.jl](https://github.com/cesaraustralia/DynamicGridsInteract.jl) @@ -24,9 +42,7 @@ for implementations. """ abstract type ImageOutput{T} <: GraphicOutput{T} end -""" -Construct one ImageOutput from another Output -""" +# Construct one ImageOutput from another Output (::Type{F})(o::T; frames=frames(o), extent=extent(o), graphicconfig=graphicconfig(o), imageconfig=imageconfig(o), kwargs...) where F <: ImageOutput where T <: Output = F(; frames=frames, running=false, extent=extent, graphicconfig=graphicconfig, @@ -52,14 +68,19 @@ maxval(o::Output) = maxval(imageconfig(o)) # Allow construcing a frame with the ruleset passed in instead of SimData -showgrid(o::ImageOutput, f, t) = showgrid(o[f], o, Ruleset(), f, t) -showgrid(grid, o::ImageOutput, data::RulesetOrSimData, f, t) = - showimage(grid2image(o, data, grid, t), o, data, f, t) +showframe(frame, o::ImageOutput, data::RulesetOrSimData, f, t) = + showimage(grid2image(o, data, frame, t), o, data, f, t) """ - showimage(image, output, f, t) + showimage(image::AbstractArray{AGRB32,2}, output, f, t) Show image generated by and `GridProcessor` in an ImageOutput. + +# Arguments +- `image +- `output`: the output to define the method for +- `f`: the current frame number +- `t`: the current frame date/time """ function showimage end showimage(image, o, data, f, t) = showimage(image, o, f, t) @@ -100,8 +121,8 @@ But it they can be distpatched on together when required for custom outputs. """ function grid2image end -grid2image(o::ImageOutput, data::RulesetOrSimData, grids, t) = - grid2image(processor(o), o, data, grids, t) +grid2image(o::ImageOutput, data::RulesetOrSimData, frame, t) = + grid2image(processor(o), o, data, frame, t) """ Grid processors that convert one grid to an image. @@ -130,7 +151,7 @@ grid2image(p::SingleGridProcessor, mask, minval, maxval, data::RulesetOrSimData, end """ -Processors that convert multiple grids to a single image. +Processors that convert a frame containing multiple grids into a single image. """ abstract type MultiGridProcessor <: GridProcessor end @@ -145,9 +166,9 @@ Text configuration for printing timestep and grid name on the image. # Arguments -`namepixels` and `timepixels` set the pixel size of the font. -`timepos` and `namepos` are tuples that set the label positions, in pixels. -`fcolor` and `bcolor` are the foreground and background colors, as `ARGB32`. +- `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`. """ struct TextConfig{F,NPi,NPo,TPi,TPo,FC,BC} face::F @@ -194,9 +215,10 @@ rendertime!(img, config::Nothing, t::Nothing) = nothing Converts output grids to a colorsheme. ## Arguments / Keyword Arguments -- `scheme`: a ColorSchemes.jl colorscheme. -- `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. +- `scheme`: a ColorSchemes.jl colorscheme, or `Greyscale`. +- `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`](@ref) object. """ @default_kw struct ColorProcessor{S,Z,M,TC} <: SingleGridProcessor scheme::S | Greyscale() @@ -226,23 +248,32 @@ Base.show(io::IO, m::MIME"image/svg+xml", p::ColorProcessor) = end end +""" + SparseOptInspector() + +A [`GridProcessor`](@ref) that checks [`SparseOpt`](@ref) visually. Errors show in red. +""" struct SparseOptInspector <: SingleGridProcessor end -@inline cell2rgb(p::SparseOptInspector, mask, minval, maxval, data::AbstractSimData, val, I...) = begin +@inline cell2rgb(p::SparseOptInspector, mask, minval, maxval, data::RulesetOrSimData, val, I...) = begin r = radius(first(grids(data))) blocksize = 2r blockindex = indtoblock.((I[1] + r, I[2] + r), blocksize) normedval = normalise(val, minval, maxval) - if sourcestatus(first(data))[blockindex...] + status = sourcestatus(first(data)) + # This is done at the start of the next frame, so wont show up in + # the image properly. So do it preemtively? + wrapstatus!(status) + if status[blockindex...] if normedval > 0 rgb(normedval) else - rgb(0.0, 0.5, 0.5) + rgb(0.0, 0.0, 0.0) end elseif normedval > 0 rgb(1.0, 0.0, 0.0) # This (a red cell) would mean there is a bug in SparseOpt else - rgb(0.5, 0.5, 0.0) + rgb(0.5, 0.5, 0.5) end end @@ -303,13 +334,13 @@ grid2image(p::ThreeColorProcessor, o::ImageOutput, data::RulesetOrSimData, grids end """ - LayoutProcessor(layout::Array, processors) + LayoutProcessor(layout::Array, processors, textconfig) LayoutProcessor allows displaying multiple grids in a block layout, -by specifying a layout matrix and a list of SingleGridProcessors to -be run for each. +by specifying a layout matrix and a list of [`SingleGridProcessors`](@ref) +to be run for each. -## Arguments / Keyword arguments +## Arguments - `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. @@ -377,29 +408,6 @@ _sectionloop(processor::SingleGridProcessor, img, mask, minval, maxval, data, gr end -""" - savegif(filename::String, o::Output, data; [processor=processor(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. -""" -savegif(filename::String, o::Output, ruleset=Ruleset(); - minval=mival(o), maxval=maxval(o), processor=processor(o), kwargs...) = begin - im_o = NoDisplayImageOutput(o; maxval=maxval, minval=minval, processor=processor) - savegif(filename, im_o, ruleset; kwargs...) -end -savegif(filename::String, o::ImageOutput, ruleset=Ruleset(); - processor=processor(o), kwargs...) = begin - images = map(frames(o), collect(firstindex(o):lastindex(o))) do frame, t - grid2image(processor, o, ruleset, frame, t) - end - array = cat(images..., dims=3) - FileIO.save(filename, array; kwargs...) -end - - # Color manipulation tools """ @@ -438,12 +446,13 @@ rgb(vals...) = ARGB32(vals...) rgb(val::Number) = ARGB32(RGB(val)) rgb(val::Color) = ARGB32(val) rgb(val::ARGB32) = val +rgb(val::Bool) = (ARGB32(0), ARGB32(1))[val+1] """ rgb(scheme, val) Convert a color scheme and value to an RGB value. """ -rgb(scheme, val) = ARGB32(get(scheme, val)) +rgb(scheme, val) = rgb(get(scheme, val)) """ combinebands(c::Tuple{Vararg{<:BandColor}, acc, xs) diff --git a/src/outputs/output.jl b/src/outputs/output.jl index d8b21af1..953be654 100644 --- a/src/outputs/output.jl +++ b/src/outputs/output.jl @@ -69,8 +69,7 @@ isasync(o::Output) = false """ isasync(o::Output) -Check if the output is storing each grid frame, -or just the the current one. +Check if the output is storing each frame, or just the the current one. """ isstored(o::Output) = true @@ -102,30 +101,30 @@ but other outputs just do nothing and continue. """ delay(o::Output, f) = nothing """ - showgrid(o::Output, args...) + showframe(o::Output, args...) Show the grid(s) in the output, if it can do that. """ -showgrid(o::Output, args...) = nothing +showframe(o::Output, args...) = nothing # Grid strorage and updating -gridindex(o::Output, data::AbstractSimData) = gridindex(o, currentframe(data)) +frameindex(o::Output, data::AbstractSimData) = frameindex(o, currentframe(data)) # Every frame is frame 1 if the simulation isn't stored -gridindex(o::Output, f::Int) = isstored(o) ? f : oneunit(f) +frameindex(o::Output, f::Int) = isstored(o) ? f : oneunit(f) -storegrid!(output::Output, data::AbstractSimData) = begin - f = gridindex(output, data) +storeframe!(output::Output, data::AbstractSimData) = begin + f = frameindex(output, data) checkbounds(output, f) - storegrid!(eltype(output), output, data, f) + storeframe!(eltype(output), output, data, f) end -storegrid!(::Type{<:NamedTuple}, output::Output, simdata::AbstractSimData, f::Int) = begin +storeframe!(::Type{<:NamedTuple}, output::Output, simdata::AbstractSimData, f::Int) = begin map(values(grids(simdata)), keys(simdata)) do grid, key outgrid = output[f][key] _storeloop(outgrid, grid) end end -storegrid!(::Type{<:AbstractArray}, output::Output, simdata::AbstractSimData, f::Int) = begin +storeframe!(::Type{<:AbstractArray}, output::Output, simdata::AbstractSimData, f::Int) = begin outgrid = output[f] _storeloop(outgrid, first(grids(simdata))) end @@ -135,8 +134,8 @@ _storeloop(outgrid, grid) = end # Replicated frames -storegrid!(output::Output{<:AbstractArray}, data::AbstractVector{<:AbstractSimData}) = begin - f = gridindex(output, data[1]) +storeframe!(output::Output{<:AbstractArray}, data::AbstractVector{<:AbstractSimData}) = begin + f = frameindex(output, data[1]) outgrid = output[f] for I in CartesianIndices(outgrid) replicatesum = zero(eltype(outgrid)) @@ -146,8 +145,8 @@ storegrid!(output::Output{<:AbstractArray}, data::AbstractVector{<:AbstractSimDa @inbounds outgrid[I] = replicatesum / length(data) end end -storegrid!(output::Output{<:NamedTuple}, data::AbstractVector{<:AbstractSimData}) = begin - f = gridindex(output, data[1]) +storeframe!(output::Output{<:NamedTuple}, data::AbstractVector{<:AbstractSimData}) = begin + f = frameindex(output, data[1]) outgrids = output[f] gridsreps = NamedTuple{keys(first(data))}(map(d -> d[key], data) for key in keys(first(data))) map(outgrids, gridsreps) do outgrid, gridreps diff --git a/src/outputs/repl.jl b/src/outputs/repl.jl index 172b13b0..61f6b049 100644 --- a/src/outputs/repl.jl +++ b/src/outputs/repl.jl @@ -44,20 +44,13 @@ REPLOutput(; frames, running, extent, graphicconfig, color=:white, cutoff=0.5, style=Block(), kwargs...) = REPLOutput(frames, running, extent, graphicconfig, color, style, cutoff) -# initialise(o::REPLOutput, args...) = begin - # o.displayoffset .= 1 - # @async movedisplay(o) -# end - -isasync(o::REPLOutput) = false - -showgrid(frame::NamedTuple, o::REPLOutput, data::SimData, f, t) = - showgrid(first(frame), o, data, f, t) -showgrid(frame::AbstractArray, o::REPLOutput, data::SimData, f, t) = begin +showframe(frame::NamedTuple, o::REPLOutput, data::SimData, f, t) = + showframe(first(frame), o, data, f, t) +showframe(frame::AbstractArray, o::REPLOutput, data::SimData, f, t) = begin # Print the frame - put((0,0), o.color, replframe(o, frame)) + put((0, 0), o.color, replframe(o, frame)) # Print the timestamp in the top right corner - put((0,0), o.color, string("Grid: $f at time $t")) + put((0, 0), o.color, string("Time $t")) end # Terminal commands diff --git a/src/overflow.jl b/src/overflow.jl index 463d9d72..9cfcbe38 100644 --- a/src/overflow.jl +++ b/src/overflow.jl @@ -1,22 +1,63 @@ +""" + inbounds(x, max, overflow) + +Check grid boundaries for a single coordinate and max value or a tuple +of coorinates and max values. + +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`](@ref) returns the coordinate and `false` to skip +coordinates that overflow outside of the grid. +[`WrapOverflow`](@ref) returns a tuple with the current position or it's +wrapped equivalent, and `true` as it is allways in-bounds. +""" +@inline inbounds(xs::Tuple, data::SimData) = + inbounds(xs, first(data)) +@inline inbounds(xs::Tuple, data::GridData) = + inbounds(xs, gridsize(data), overflow(data)) +@inline inbounds(xs::Tuple, maxs::Tuple, overflow) = begin + a, inbounds_a = inbounds(xs[1], maxs[1], overflow) + b, inbounds_b = inbounds(xs[2], maxs[2], overflow) + (a, b), inbounds_a & inbounds_b +end +@inline inbounds(x::Number, max::Number, overflow::RemoveOverflow) = + x, isinbounds(x, max) +@inline inbounds(x::Number, max::Number, overflow::WrapOverflow) = + if x < oneunit(x) + max + rem(x, max), true + elseif x > max + rem(x, max), true + else + x, true + end + +@inline isinbounds(x::Tuple, data::Union{SimData,GridData}) = + isinbounds(x::Tuple, gridsize(data)) +@inline isinbounds(xs::Tuple, maxs::Tuple) = all(isinbounds.(xs, maxs)) +@inline isinbounds(x::Number, max::Number) = x >= one(x) && x <= max + #= Wrap overflow where required. This optimisation allows us to ignore bounds checks on neighborhoods and still use a wraparound grid. =# -handleoverflow!(griddata) = handleoverflow!(griddata, overflow(griddata)) -handleoverflow!(griddata::GridData{T,2}, ::WrapOverflow) where T = begin +handleoverflow!(grids::Tuple) = map(handleoverflow!, grids) +handleoverflow!(griddata::GridData) = handleoverflow!(griddata, overflow(griddata)) +handleoverflow!(griddata::GridData, ::RemoveOverflow) = griddata +handleoverflow!(griddata::GridData, ::WrapOverflow) = begin r = radius(griddata) + r < 1 && return griddata # TODO optimise this. Its mostly a placeholder so wrapping still works in GOL tests. - src = source(griddata) + src = parent(source(griddata)) nrows, ncols = gridsize(griddata) - - startpadrow = startpadcol = 1-r:0 - endpadrow = nrows+1:nrows+r - endpadcol = ncols+1:ncols+r - startrow = startcol = 1:r - endrow = nrows+1-r:nrows - endcol = ncols+1-r:ncols - rows = 1:nrows - cols = 1:ncols + startpadrow = startpadcol = 1:r + endpadrow = nrows+r+1:nrows+2r + endpadcol = ncols+r+1:ncols+2r + startrow = startcol = 1+r:2r + endrow = nrows+1:nrows+r + endcol = ncols+1:ncols+r + rows = 1+r:nrows+r + cols = 1+r:ncols+r # Left @inbounds copyto!(src, CartesianIndices((rows, startpadcol)), @@ -35,7 +76,7 @@ handleoverflow!(griddata::GridData{T,2}, ::WrapOverflow) where T = begin # Top Left @inbounds copyto!(src, CartesianIndices((startpadrow, startpadcol)), src, CartesianIndices((endrow, endcol))) - # Top Right + # Top Right @inbounds copyto!(src, CartesianIndices((startpadrow, endpadcol)), src, CartesianIndices((endrow, startcol))) # Botom Left @@ -45,41 +86,16 @@ handleoverflow!(griddata::GridData{T,2}, ::WrapOverflow) where T = begin @inbounds copyto!(src, CartesianIndices((endpadrow, endpadcol)), src, CartesianIndices((startrow, startcol))) - # Wrap status - status = sourcestatus(griddata) - # status[:, 1] .|= status[:, end-1] .| status[:, end-2] - # status[1, :] .|= status[end-1, :] .| status[end-2, :] - # status[end-1, :] .|= status[1, :] - # status[:, end-1] .|= status[:, 1] - # status[end-2, :] .|= status[1, :] - # status[:, end-2] .|= status[:, 1] - # FIXME: Buggy currently, just running all in Wrap mode - status .= true - griddata + wrapstatus!(sourcestatus(griddata)) end -handleoverflow!(griddata::WritableGridData, ::RemoveOverflow) = begin - r = radius(griddata) - # Zero edge padding, as it can be written to in writable rules. - src = parent(source(griddata)) - npadrows, npadcols = size(source(griddata)) - - startpadrow = startpadcol = 1:r - endpadrow = npadrows-r+1:npadrows - endpadcol = npadcols-r+1:npadcols - padrows, padcols = axes(src) - - for j = startpadcol, i = padrows - src[i, j] = zero(eltype(src)) - end - for j = endpadcol, i = padrows - src[i, j] = zero(eltype(src)) - end - for j = padcols, i = startpadrow - src[i, j] = zero(eltype(src)) - end - for j = padcols, i = endpadrow - src[i, j] = zero(eltype(src)) - end - griddata +function wrapstatus!(status) + status[end, :] .|= status[1, :] + status[:, end] .|= status[:, 1] + status[end-1, :] .|= status[1, :] .|= status[2, :] + status[:, end-1] .|= status[:, 1] .|= status[:, 2] + status[1, :] .|= status[end-1, :] .|= status[end, :] + status[:, 1] .|= status[:, end-1] .|= status[:, end] + status[end-1, :] .= true + status[1, :] .= true end diff --git a/src/rules.jl b/src/rules.jl index 53611bc1..bcd61540 100644 --- a/src/rules.jl +++ b/src/rules.jl @@ -70,27 +70,40 @@ is not guaranteed to have correct results, and should not be done. abstract type CellRule{R,W} <: Rule{R,W} end """ -ManualRule is 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 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. -Updates to the destination grids data must be performed manually by -`data[:key] = x`. Updating block status is handled automatically on write. +`NeighborhoodRule` is applied with the method: + +```julia +applyrule!(data, rule::Life, 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`](@ref) is handled automatically on write. """ abstract type ManualRule{R,W} <: Rule{R,W} end """ A Rule that only accesses a neighborhood centered around the current cell. -NeighborhoodRule is applied with the method: +`NeighborhoodRule` is applied with the method: ```julia -applyrule(rule::Life, data, state, index, buffer) +applyrule(data, rule::Life, state, I) ``` For each cell a neighborhood buffer will be populated containing the neighborhood cells, and passed to `applyrule` in the rule neighborhood. This allows memory optimisations and the use of BLAS routines on the -neighborhood buffer for [`RadialNeighborhood`](@ref). It also means +neighborhood buffer for [`Moore`](@ref). It also means that and no bounds checking is required in neighborhood code. `neighbors(hood)` returns an iterator over the buffer that is generic to @@ -126,3 +139,129 @@ neighbors(rule::ManualNeighborhoodRule) = neighbors(neighborhood(rule)) neighborhood(rule::ManualNeighborhoodRule) = rule.neighborhood neighborhoodkey(rule::ManualNeighborhoodRule{R,W}) where {R,W} = R neighborhoodkey(rule::ManualNeighborhoodRule{<:Tuple{R1,Vararg},W}) where {R1,W} = R1 + + +""" + Cell{R,W}(f) + Cell(f; read, write) + +A [`CellRule`](@ref) that applies a function `f` to the +`read` grid cells and returns the `write` cells. + +Especially convenient with `do` notation. + +## Example + +Set the cells of grid `:c` to the sum of `:a` and `:b`. +```julia +simplerule = Cell() do a, b + a + b +end +``` + +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. + +```julia +rule = let y = y + rule = Cell(read=(a, b), write=b) do a, b + a + b * y + end +end +``` +""" +@flattenable @description struct Cell{R,W,F} <: CellRule{R,W} + # Field | Flatten | Description + f::F | false | "Function to apply to the read values" +end +Cell(f; read=:_default_, write=read) = Cell{read,write}(f) + +@inline applyrule(data, rule::Cell, state, I) = + let (rule, read) = (rule, state) + rule.f(astuple(rule, state)...) + end + +const Map = Cell + +astuple(rule::Rule, read) = astuple(readkeys(rule), read) +astuple(::Tuple, read) = read +astuple(::Symbol, read) = (read,) + +""" + Neighbors(f, neighborhood) + Neighbors{R,W}(f, neighborhood) + Neighbors(f; read=:_default_, write=read, neighborhood=Moore()) + +A [`NeighborhoodRule`](@ref) that receives a neighbors object for the first +`read` grid and the passed in neighborhood, followed by the cell values for +the reqquired grids, as with [`Cell`](@ref). + +Returned value(s) are written to the `write`/`W` grid. + +As with all [`NeighborhoodRule`](@ref), you do not have to check bounds at +grid edges, that is handled for you internally. + +Using [`SparseOpt`](@ref) may improve neighborhood performance when zero values +are common, and can be safely ignored. + +## Example + +```julia +rule = let x = 10 + Neighbors{Tuple{:a,:b},:b}() do hood, a, b + data[:b][I...] = a + b^x + end +end +``` +The `let` block greatly imroves performance. +""" +@flattenable @description struct Neighbors{R,W,F,N} <: NeighborhoodRule{R,W} + # Field | Flatten | Description + f::F | false | "Function to apply to the neighborhood and read values" + neighborhood::N | true | "" +end +Neighbors(f; read=:_default_, write=read, neighborhood=Moore(1)) = + Neighbors{read,write}(f, neighborhood) + +@inline applyrule(data, rule::Neighbors, read, I) = + let hood=neighborhood(rule), rule=rule, read=astuple(rule, read) + rule.f(hood, read...) + end + +""" + Manual(f; read=:_default_, write=read) + Manual{R,W}(f) + +A [`ManualRule`](@ref) 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 requirement grid values for the index. + +## Example + +```julia +rule = let x = 10 + Manual{Tuple{:a,:b},:b}() do data, I, a, b + data[:b][I...] = a + b^x + end +end +``` +The `let` block greatly imroves performance. +""" +@flattenable @description struct Manual{R,W,F} <: ManualRule{R,W} + # Field | Flatten | Description + f::F | false | "Function to apply to the data, index and read values" +end +Manual(f; read=:_default_, write=read) = Manual{read,write}(f) + +@inline applyrule!(data, rule::Manual, read, I) = + let data=data, I=I, rule=rule, read=astuple(rule, read) + rule.f(data, I, read...) + end + +""" + method(rule) + +Get the method of a `Cell`, `Neighbors`, or `Manual` rule. +""" +method(rule::Union{Cell,Neighbors,Manual}) = rule.f diff --git a/src/simulationdata.jl b/src/simulationdata.jl index 1a9a7777..e9027df8 100644 --- a/src/simulationdata.jl +++ b/src/simulationdata.jl @@ -58,11 +58,10 @@ ReadableGridData(init::AbstractArray, mask, radius, overflow) = begin hoodsize = 2r + 1 blocksize = 2r source = addpadding(init, r) - nblocs = indtoblock.(size(source), blocksize) .+ 1 + nblocs = indtoblock.(size(source), blocksize) sourcestatus = zeros(Bool, nblocs) deststatus = deepcopy(sourcestatus) updatestatus!(source, sourcestatus, deststatus, r) - localstatus = zeros(Bool, 2, 2) else source = deepcopy(init) @@ -94,7 +93,8 @@ Base.@propagate_inbounds Base.setindex!(d::WritableGridData, x, I...) = begin end Base.parent(d::WritableGridData) = parent(dest(d)) -Base.@propagate_inbounds Base.getindex(d::WritableGridData, I...) = getindex(dest(d), I...) +Base.@propagate_inbounds Base.getindex(d::WritableGridData, I...) = + getindex(dest(d), I...) @@ -109,11 +109,11 @@ A simdata object is accessable in `applyrule` as the second parameter. Multiple grids can be indexed into using their key. Single grids can be indexed as if SimData is regular array. """ -struct SimData{E,G,Ru,CFr} <: AbstractSimData - extent::E +struct SimData{G<:NamedTuple,E,Ru,F} <: AbstractSimData grids::G + extent::E ruleset::Ru - currentframe::CFr + currentframe::F end SimData(extent, ruleset::Ruleset) = begin extent = asnamedtuple(extent) @@ -124,11 +124,11 @@ SimData(extent, ruleset::Ruleset) = begin griddata = map(init(extent), radii) do in, ra ReadableGridData(in, mask(extent), ra, overflow(ruleset)) end - SimData(extent, griddata, ruleset) + SimData(griddata, extent, ruleset) end -SimData(extent, griddata::NamedTuple, ruleset::Ruleset) = begin +SimData(griddata::NamedTuple, extent, ruleset::Ruleset) = begin currentframe = 1; - SimData(extent, griddata, ruleset, currentframe) + SimData(griddata, extent, ruleset, currentframe) end @@ -147,7 +147,15 @@ currenttime(d::SimData) = tspan(d)[currentframe(d)] currenttime(d::Vector{<:SimData}) = currenttime(d[1]) # Getters forwarded to data -Base.getindex(d::SimData, i) = getindex(grids(d), i) +Base.getindex(d::SimData, i::Symbol) = + getindex(grids(d), i) +# For resolving method ambiguity +Base.getindex(d::SimData{<:NamedTuple{(:_default_,)}}, i::Symbol) = + getindex(grids(d), i) +Base.getindex(d::SimData{<:NamedTuple{(:_default_,)}}, I...) = + getindex(first(grids(d)), I...) +Base.setindex!(d::SimData{<:NamedTuple{(:_default_,)}}, x, I...) = + setindex!(first(grids(d)), x, I...) Base.keys(d::SimData) = keys(grids(d)) Base.values(d::SimData) = values(grids(d)) Base.first(d::SimData) = first(grids(d)) @@ -168,7 +176,11 @@ swapsource(grid::GridData) = begin src = grid.source dst = grid.dest @set! grid.dest = src - @set grid.source = dst + @set! grid.source = dst + srcstatus = grid.sourcestatus + dststatus = grid.deststatus + @set! grid.deststatus = srcstatus + @set grid.sourcestatus = dststatus end # Uptate timestamp @@ -216,13 +228,6 @@ updatestatus!(source, sourcestatus, deststatus, radius) = begin end end -copystatus!(grid::Tuple{Vararg{<:GridData}}) = map(copystatus!, grid) -copystatus!(grid::GridData) = - copystatus!(sourcestatus(grid), deststatus(grid)) -copystatus!(srcstatus, deststatus) = nothing -copystatus!(srcstatus::AbstractArray, deststatus::AbstractArray) = - @inbounds return srcstatus .= deststatus - # When replicates are an Integer, construct a vector of SimData initdata!(::Nothing, extent, ruleset::Ruleset, nreplicates::Integer) = [SimData(extent, ruleset) for r in 1:nreplicates] diff --git a/src/utils.jl b/src/utils.jl index da20a75c..7acb364c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,38 +1,3 @@ -""" - inbounds(x, max, overflow) - -Check grid boundaries for a single coordinate and max value or a tuple -of coorinates and max values. - -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`](@ref) returns the coordinate and `false` to skip -coordinates that overflow outside of the grid. -[`WrapOverflow`](@ref) returns a tuple with the current position or it's -wrapped equivalent, and `true` as it is allways in-bounds. -""" -@inline inbounds(xs::Tuple, maxs::Tuple, overflow) = begin - a, inbounds_a = inbounds(xs[1], maxs[1], overflow) - b, inbounds_b = inbounds(xs[2], maxs[2], overflow) - (a, b), inbounds_a & inbounds_b -end -@inline inbounds(x::Number, max::Number, overflow::RemoveOverflow) = - x, isinbounds(x, max, overflow) -@inline inbounds(x::Number, max::Number, overflow::WrapOverflow) = - if x < oneunit(x) - max + rem(x, max), true - elseif x > max - rem(x, max), true - else - x, true - end - -@inline isinbounds(x, max, overflow::WrapOverflow) = true -@inline isinbounds(xs::Tuple, maxs::Tuple, overflow::RemoveOverflow) = - all(isinbounds.(xs, maxs, Ref(overflow))) -@inline isinbounds(x::Number, max::Number, overflow::RemoveOverflow) = - x > zero(x) && x <= max """ Check if a cell is masked, using the passed-in mask grid. @@ -71,30 +36,21 @@ isinferred(output::Output, ruleset::Ruleset) = begin true end isinferred(simdata::SimData, rule::Rule, init::AbstractArray) = begin - x = @inferred applyrule(rule, simdata, init[1, 1], (1, 1)) + x = @inferred applyrule(simdata, rule, init[1, 1], (1, 1)) typeof(x) == eltype(init) || error("Returned type `$(typeof(x))` doesn't match grid eltype `$(eltype(init))`") true end isinferred(simdata::SimData, rule::ManualRule, init::AbstractArray) = begin simdata = @set simdata.grids = map(WritableGridData, simdata.grids) - @inferred applyrule!(rule, simdata, init[1, 1], (1, 1)) + @inferred applyrule!(simdata, rule, init[1, 1], (1, 1)) true end -""" - 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. -""" -allocbuffers(init::AbstractArray, hood::Neighborhood) = allocbuffers(init, radius(hood)) -allocbuffers(init::AbstractArray, r::Int) = Tuple(allocbuffer(init, r) for i in 1:2r) - -allocbuffer(init::AbstractArray, hood::Neighborhood) = allocbuffer(init, radius(hood)) -allocbuffer(init::AbstractArray, r::Int) = zeros(eltype(init), 2r+1, 2r+1) - asnamedtuple(x::NamedTuple) = x asnamedtuple(x::AbstractArray) = (_default_=x,) asnamedtuple(e::Extent) = Extent(asnamedtuple(init(e)), mask(e), tspan(e), aux(e)) + +zerogrids(initgrid::AbstractArray, nframes) = [zero(initgrid) for f in 1:nframes] +zerogrids(initgrids::NamedTuple, nframes) = + [map(grid -> zero(grid), initgrids) for f in 1:nframes] diff --git a/test/chain.jl b/test/chain.jl index 45e74297..299ca7ab 100644 --- a/test/chain.jl +++ b/test/chain.jl @@ -47,16 +47,16 @@ using DynamicGrids: SimData, radius, rules, readkeys, writekeys, @test radius(ruleset) == (b=0, c=0, d=0, e=0, a=0) - @test applyrule(chain, data, (b=1, c=1, d=1, a=1), (1, 1)) == + @test applyrule(data, chain, (b=1, c=1, d=1, a=1), (1, 1)) == (b=4, c=6, d=10, e=3, a=2) - @inferred applyrule(chain, data, (b=1, c=1, d=1, a=1), (1, 1)) + # @inferred applyrule(data, chain, (b=1, c=1, d=1, a=1), (1, 1)) state = (b=1, c=1, d=1, a=1) ind = (1, 1) # This breaks with --inline=no - # b = @benchmark applyrule($chain, $data, $state, $ind) + # b = @benchmark applyrule($data, $chain, $state, $ind) # @test b.allocs == 0 output = ArrayOutput(init; tspan=1:3) diff --git a/test/image.jl b/test/image.jl index 17a82cfd..96076091 100644 --- a/test/image.jl +++ b/test/image.jl @@ -2,7 +2,7 @@ using DynamicGrids, Dates, Test, Colors, ColorSchemes, FieldDefaults using FreeTypeAbstraction using DynamicGrids: grid2image, processor, minval, maxval, normalise, SimData, NoDisplayImageOutput, isstored, isasync, initialise, finalise, delay, fps, settimestamp!, timestamp, - tspan, setfps!, frames, isshowable, Red, Green, Blue, showgrid, rgb, scale + tspan, setfps!, frames, isshowable, Red, Green, Blue, showframe, rgb, scale, Extent using ColorSchemes: leonardo @testset "rgb" begin @@ -47,11 +47,11 @@ DynamicGrids.showimage(image, o::NoDisplayImageOutput, f, t) = begin end @testset "basic ImageOutput" begin - output = NoDisplayImageOutput(init; tspan=1:1) + output = NoDisplayImageOutput(init; tspan=1:1, maxval=40.0) @test parent(output) == [init] - @test minval(output) === 0 - @test maxval(output) === 1 + @test minval(output) === nothing + @test maxval(output) === 40.0 @test processor(output) == ColorProcessor() @test isasync(output) == false @test isstored(output) == false @@ -74,29 +74,30 @@ end @test frames(output)[1] == 5init @test isshowable(output, 1) - @test showgrid(output, 1, 1) == - [ARGB32(1.0,1.0,1.0) ARGB32(1.0,1.0,1.0) - ARGB32(0.0,0.0,0.0) ARGB32(1.0,1.0,1.0)] + simdata = SimData(Extent(init, nothing, 1:10, nothing), Ruleset(Life())) + @test_broken showframe(output, simdata, 1, 1) == + [ARGB32(1.0, 1.0, 1.0) ARGB32(1.0, 1.0, 1.0) + ARGB32(0.0, 0.0, 0.0) ARGB32(1.0, 1.0, 1.0)] savegif("test.gif", output) @test isfile("test.gif") arrayoutput = ArrayOutput([0 0]; tspan=1:2) - @test minval(arrayoutput) == 0 - @test maxval(arrayoutput) == 1 + @test minval(arrayoutput) == nothing + @test maxval(arrayoutput) == nothing @test processor(arrayoutput) == ColorProcessor() @test fps(arrayoutput) === nothing end @testset "ColorProcessor" begin - proc = ColorProcessor(zerocolor=(1.0,0.0,0.0)) + proc = ColorProcessor(zerocolor=(1.0, 0.0, 0.0)) output = NoDisplayImageOutput((a=init,); tspan=1:1, processor=proc, minval=0.0, maxval=10.0, store=true) maxval(output.imageconfig) @test minval(output) === 0.0 @test maxval(output) === 10.0 - @test processor(output) == ColorProcessor(zerocolor=(1.0,0.0,0.0)) + @test processor(output) == ColorProcessor(zerocolor=(1.0, 0.0, 0.0)) @test isstored(output) == true - simdata = SimData(init, nothing, Ruleset(Life()), 1) + simdata = SimData(Extent(init, nothing, 1:10, nothing), Ruleset(Life())) # Test level normalisation normed = normalise.(output[1][:a], minval(output), maxval(output)) @@ -139,7 +140,7 @@ end end @testset "SparseOptInspector" begin - init = [ + init = Bool[ 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 1 @@ -167,8 +168,8 @@ end global images = [] sim!(output, ruleset) - w, y, c = ARGB32(1), ARGB32(.5, .5, 0), ARGB32(0., .5, .5) - @test images[1] == [ + w, y, c = ARGB32(1), ARGB32(.0, .0, .5), ARGB32(.5, .5, .5) + @test_broken images[1] == [ y y y y y y y y y y c w w w y y y c c c w @@ -190,7 +191,7 @@ end @test maxval(output) === (10, 20) @test processor(output) === proc @test isstored(output) == true - simdata = SimData(init, nothing, Ruleset(Life()), 1) + simdata = SimData(Extent(init, nothing, 1:10, nothing), Ruleset(Life())) # Test image is joined from :a, nothing, :b @test grid2image(output, Ruleset(), multiinit, 1) == diff --git a/test/integration.jl b/test/integration.jl index 536831eb..f22671b9 100644 --- a/test/integration.jl +++ b/test/integration.jl @@ -1,62 +1,191 @@ using DynamicGrids, Test, Dates, Unitful + +using DynamicGrids + +using Distributions +init = Bool.(rand(Binomial(1, 0.5), 20, 20)) +ruleset = Ruleset(Life()) +output = REPLOutput(init; tspan=1:50, fps=5) +sim!(output, ruleset) + + # life glider sims -init = [ - 0 0 0 0 0 0 0 - 0 0 0 0 1 1 1 - 0 0 0 0 0 0 1 - 0 0 0 0 0 1 0 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - ] - -test2 = [ - 0 0 0 0 0 1 0 - 0 0 0 0 0 1 1 - 0 0 0 0 1 0 1 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - ] - -test3 = [ - 0 0 0 0 0 1 1 - 0 0 0 0 1 0 1 - 0 0 0 0 0 0 1 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - ] - -test5 = [ - 1 0 0 0 0 1 1 - 1 0 0 0 0 0 0 - 0 0 0 0 0 0 1 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - ] - -test7 = [ - 1 0 0 0 0 1 0 - 1 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 - 1 0 0 0 0 0 1 - ] - -# Allow testing with a few variants of the above -cycletests!(A) = begin - v = A[1, :] - @inbounds copyto!(A, CartesianIndices((1:5, 1:7)), - A, CartesianIndices((2:6, 1:7))) - A[6, :] = v +# Test all cycled variants of the array +cyclei!(arrays) = begin + for A in arrays + v = A[1, :] + @inbounds copyto!(A, CartesianIndices((1:size(A, 1)-1, 1:size(A, 2))), + A, CartesianIndices((2:size(A, 1), 1:size(A, 2)))) + A[end, :] = v + end +end + +cyclej!(arrays) = begin + for A in arrays + v = A[:, 1] + @inbounds copyto!(A, CartesianIndices((1:size(A, 1), 1:size(A, 2)-1)), + A, CartesianIndices((1:size(A, 1), 2:size(A, 2)))) + A[:, end] = v + end +end + +test67 = ( + init = Bool[ + 0 0 0 0 0 0 0 + 0 0 0 0 1 1 1 + 0 0 0 0 0 0 1 + 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + ], + test2 = Bool[ + 0 0 0 0 0 1 0 + 0 0 0 0 0 1 1 + 0 0 0 0 1 0 1 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + ], + test3 = Bool[ + 0 0 0 0 0 1 1 + 0 0 0 0 1 0 1 + 0 0 0 0 0 0 1 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + ], + test4 = Bool[ + 0 0 0 0 0 1 1 + 1 0 0 0 0 0 1 + 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + ], + test5 = Bool[ + 1 0 0 0 0 1 1 + 1 0 0 0 0 0 0 + 0 0 0 0 0 0 1 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + ], + test7 = Bool[ + 1 0 0 0 0 1 0 + 1 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 1 0 0 0 0 0 1 + ] +) + +test56 = ( + init = Bool[ + 0 0 0 0 0 0 + 0 0 0 1 1 1 + 0 0 0 0 0 1 + 0 0 0 0 1 0 + 0 0 0 0 0 0 + ], + test2 = Bool[ + 0 0 0 0 1 0 + 0 0 0 0 1 1 + 0 0 0 1 0 1 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + ], + test3 = Bool[ + 0 0 0 0 1 1 + 0 0 0 1 0 1 + 0 0 0 0 0 1 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + ], + test4 = Bool[ + 0 0 0 0 1 1 + 1 0 0 0 0 1 + 0 0 0 0 1 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + ], + test5 = Bool[ + 1 0 0 0 1 1 + 1 0 0 0 0 0 + 0 0 0 0 0 1 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + ], + test7 = Bool[ + 1 0 0 0 1 0 + 1 0 0 0 0 0 + 0 0 0 0 0 0 + 0 0 0 0 0 0 + 1 0 0 0 0 1 + ] +) + + +@testset "Life simulation with WrapOverflow" begin + # Test on two sizes to test half blocks on both axes + for test in (test56, test67) + # Loop over shifing init arrays to make sure they all work + for i = 1:size(test[:init], 1) + for j = 1:size(test[:init], 2) + bufs = (zeros(Int, 3, 3), zeros(Int, 3, 3)) + rule = Life(neighborhood=Moore{1}(bufs)) + sparse_ruleset = Ruleset(; + rules=(rule,), + timestep=Day(2), + overflow=WrapOverflow(), + opt=SparseOpt(), + ) + sparse_output = ArrayOutput(test[:init]; tspan=Date(2001, 1, 1):Day(2):Date(2001, 1, 14)) + sim!(sparse_output, sparse_ruleset) + + @testset "SparseOpt results match glider behaviour" begin + @test sparse_output[2] == test[:test2] + @test sparse_output[3] == test[:test3] + @test sparse_output[4] == test[:test4] + @test sparse_output[5] == test[:test5] + @test sparse_output[7] == test[:test7] + end + + noopt_ruleset = Ruleset(; + rules=(Life(),), + timestep=Day(2), + overflow=WrapOverflow(), + opt=NoOpt(), + ) + noopt_output = ArrayOutput(test[:init], tspan=Date(2001, 1, 1):Day(2):Date(2001, 1, 14)) + sim!(noopt_output, noopt_ruleset) + + @testset "NoOpt results match glider behaviour" begin + @test noopt_output[2] == test[:test2] + @test noopt_output[3] == test[:test3] + @test noopt_output[4] == test[:test4] + @test noopt_output[5] == test[:test5] + @test noopt_output[7] == test[:test7] + end + cyclej!(test) + end + cyclei!(test) + end + end end @testset "Life simulation with RemoveOverflow and replicates" begin - test2_rem = [ + init = Bool[ + 0 0 0 0 0 0 0 + 0 0 0 0 1 1 1 + 0 0 0 0 0 0 1 + 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + ] + test2_rem = Bool[ 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 1 0 1 @@ -64,8 +193,7 @@ end 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ] - - test3_rem = [ + test3_rem = Bool[ 0 0 0 0 0 1 1 0 0 0 0 1 0 1 0 0 0 0 0 0 1 @@ -73,8 +201,15 @@ end 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ] - - test5_rem = [ + test4_rem = Bool[ + 0 0 0 0 0 1 1 + 0 0 0 0 0 0 1 + 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + ] + test5_rem = Bool[ 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 @@ -82,8 +217,7 @@ end 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ] - - test7_rem = [ + test7_rem = Bool[ 0 0 0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 @@ -92,9 +226,9 @@ end 0 0 0 0 0 0 0 ] - rule = Life{:a,:a}(neighborhood=RadialNeighborhood{1}()) - ruleset = Ruleset(rule; - timestep=Day(2), + rule = Life{:a,:a}(neighborhood=Moore(1)) + ruleset = Ruleset(rule; + timestep=Day(2), overflow=RemoveOverflow(), opt=NoOpt(), ) @@ -106,83 +240,51 @@ end @testset "Results match glider behaviour" begin output = ArrayOutput((a=init,); tspan=(Date(2001, 1, 1):Day(2):Date(2001, 1, 14))) - sim!(output, rule; overflow=RemoveOverflow(), nreplicates=5) - @test output[2][:a] == test2_rem - @test output[3][:a] == test3_rem - @test output[5][:a] == test5_rem - @test output[7][:a] == test7_rem - end - -end - -@testset "Life simulation with WrapOverflow" begin - # Loop over shifing init to make sure they all work - for i = 1:7 - bufs = (zeros(Int, 3, 3), zeros(Int, 3, 3)) - rule = Life(neighborhood=RadialNeighborhood{1}(bufs)) - sparse_ruleset = Ruleset(; - rules=(rule,), - timestep=Day(2), - overflow=WrapOverflow(), - opt=SparseOpt(), - ) - sparse_output = ArrayOutput(init; tspan=Date(2001, 1, 1):Day(2):Date(2001, 1, 14)) - sim!(sparse_output, sparse_ruleset) - - @testset "SparseOpt results match glider behaviour" begin - @test sparse_output[2] == test2 - @test sparse_output[3] == test3 - @test sparse_output[5] == test5 - @test sparse_output[7] == test7 + @testset "NoOpt" begin + sim!(output, rule; overflow=RemoveOverflow(), opt=NoOpt()) + @test output[2][:a] == test2_rem + @test output[3][:a] == test3_rem + @test output[4][:a] == test4_rem + @test output[5][:a] == test5_rem + @test output[7][:a] == test7_rem end - noopt_ruleset = Ruleset(; - rules=(Life(),), - timestep=Day(2), - overflow=WrapOverflow(), - opt=NoOpt(), - ) - noopt_output = ArrayOutput(init, tspan=Date(2001, 1, 1):Day(2):Date(2001, 1, 14)) - sim!(noopt_output, noopt_ruleset) - - @testset "NoOpt results match glider behaviour" begin - @test noopt_output[2] == test2 - @test noopt_output[3] == test3 - @test noopt_output[5] == test5 - @test noopt_output[7] == test7 + @testset "SparseOpt" begin + sim!(output, rule; overflow=RemoveOverflow(), opt=SparseOpt()) + @test output[2][:a] == test2_rem + @test output[3][:a] == test3_rem + @test output[4][:a] == test4_rem + @test output[5][:a] == test5_rem + @test output[7][:a] == test7_rem end - cycletests!(init) - cycletests!(test2) - cycletests!(test3) - cycletests!(test5) - cycletests!(test7) + end -end +end @testset "REPLOutput block works, in Unitful.jl seconds" begin - ruleset = Ruleset(; - rules=(Life(),), + ruleset = Ruleset(; + rules=(Life(),), overflow=WrapOverflow(), timestep=5u"s", opt=NoOpt(), ) tspan=0u"s":5u"s":6u"s" - output = REPLOutput(init; tspan=tspan, style=Block(), fps=100, store=true) + output = REPLOutput(test67[:init]; tspan=tspan, style=Block(), fps=100, store=true) DynamicGrids.isstored(output) DynamicGrids.store(output) sim!(output, ruleset) resume!(output, ruleset; tstop=30u"s") - @test output[2] == test2 - @test output[3] == test3 - @test output[5] == test5 - @test output[7] == test7 + @test output[2] == test67[:test2] + @test output[3] == test67[:test3] + @test output[5] == test67[:test5] + @test output[7] == test67[:test7] end @testset "REPLOutput braile works, in Months" begin - init_a = (_default_=init,) - ruleset = Ruleset(Life(); + init_a = (_default_=test67[:init],) + ruleset = Ruleset(Life(); overflow=WrapOverflow(), timestep=Month(1), opt=SparseOpt(), @@ -190,11 +292,11 @@ end tspan = Date(2010, 4):Month(1):Date(2010, 7) output = REPLOutput(init_a; tspan=tspan, style=Braile(), fps=100, store=true) sim!(output, ruleset) - @test output[2][:_default_] == test2 - @test output[3][:_default_] == test3 + @test output[2][:_default_] == test67[:test2] + @test output[3][:_default_] == test67[:test3] @test DynamicGrids.tspan(output) == Date(2010, 4):Month(1):Date(2010, 7) resume!(output, ruleset; tstop=Date(2010, 11)) @test DynamicGrids.tspan(output) == Date(2010, 4):Month(1):Date(2010, 11) - @test output[5][:_default_] == test5 - @test output[7][:_default_] == test7 + @test output[5][:_default_] == test67[:test5] + @test output[7][:_default_] == test67[:test7] end diff --git a/test/neighborhoods.jl b/test/neighborhoods.jl index f19d4508..5aed20bb 100644 --- a/test/neighborhoods.jl +++ b/test/neighborhoods.jl @@ -1,7 +1,7 @@ using DynamicGrids, Setfield, Test import DynamicGrids: neighbors, sumneighbors, SimData, Extent, radius, neighbors, mapsetneighbor!, neighborhood, WritableGridData, dest, hoodsize, neighborhoodkey, - allocbuffer, allocbuffers, buffer + allocbuffer, allocbuffers, buffer, coords @testset "allocbuffers" begin @test allocbuffer(Bool[1 0], 1) == Bool[0 0 0 @@ -12,9 +12,9 @@ import DynamicGrids: neighbors, sumneighbors, SimData, Extent, radius, neighbors 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] - @test allocbuffer([1.0, 2.0], RadialNeighborhood{4}()) == zeros(Float64, 9, 9) + @test allocbuffer([1.0, 2.0], Moore{4}()) == zeros(Float64, 9, 9) @test allocbuffers(Bool[1 0], 2) == Tuple(zeros(Bool, 5, 5) for i in 1:4) - @test allocbuffers([1 0], RadialNeighborhood{3}()) == Tuple(zeros(Int, 7, 7) for i in 1:6) + @test allocbuffers([1 0], Moore{3}()) == Tuple(zeros(Int, 7, 7) for i in 1:6) end @testset "neighbors" begin @@ -25,10 +25,10 @@ end 0 0 0 0 1 1 0 1 0 1 1 0] - moore = RadialNeighborhood{1}(init[1:3, 1:3]) + moore = Moore{1}(init[1:3, 1:3]) @test buffer(moore) == init[1:3, 1:3] - multibuffer = RadialNeighborhood{1}(zeros(Int, 3, 3)) + multibuffer = Moore{1}(zeros(Int, 3, 3)) @test buffer(multibuffer) == zeros(Int, 3, 3) @test hoodsize(moore) == 3 @test moore[2, 2] == 0 @@ -38,7 +38,8 @@ end @test collect(neighbors(moore)) == [0, 1, 0, 0, 1, 0, 1, 1] @test sum(neighbors(moore)) == 4 - vonneumann = VonNeumannNeighborhood(init[1:3, 1:3]) + vonneumann = VonNeumann(1, init[1:3, 1:3]) + @test coords(vonneumann) == [(0, -1), (-1, 0), (1, 0), (0, 1)] @test buffer(vonneumann) == init[1:3, 1:3] @test hoodsize(vonneumann) == 3 @test vonneumann[2, 1] == 1 @@ -47,24 +48,29 @@ end @test neighbors(vonneumann) isa Base.Generator @test collect(neighbors(vonneumann)) == [1, 0, 1, 1] @test sum(neighbors(vonneumann)) == 3 + vonneumann2 = VonNeumann(2) + @test coords(vonneumann2) == + [(0, -2), (-1, -1), (0, -1), (1, -1), + (-2 , 0), (-1, 0), (1, 0), (2, 0), + (-1, 1), (0, 1), (1, 1), (0, 2)] buf = [0 0 0 0 1 0 0 0 0] - @test sum(RadialNeighborhood{1}(buf)) == 0 - @test sum(VonNeumannNeighborhood(buf)) == 0 + @test sum(Moore{1}(buf)) == 0 + @test sum(VonNeumann(1, buf)) == 0 buf = [1 1 1 1 0 1 1 1 1] - @test sum(RadialNeighborhood{1}(buf)) == 8 - @test sum(VonNeumannNeighborhood(buf)) == 4 + @test sum(Moore{1}(buf)) == 8 + @test sum(VonNeumann(1, buf)) == 4 buf = [1 1 1 0 0 1 0 0 1] - @test sum(RadialNeighborhood{1}(buf)) == 5 - @test sum(VonNeumannNeighborhood(buf)) == 2 + @test sum(Moore(1, buf)) == 5 + @test sum(VonNeumann(1, buf)) == 2 buf = [0 1 0 0 1 0 0 1 0 0 @@ -72,10 +78,10 @@ end 0 0 1 0 1 1 0 1 0 1] state = buf[3, 3] - custom1 = CustomNeighborhood(((-1,-1), (2,-2), (2,2), (-1,2), (0,0)), buf) - custom2 = CustomNeighborhood(((-1,-1), (0,-1), (1,-1), (2,-1), (0,0)), buf) - layered = LayeredCustomNeighborhood( - (CustomNeighborhood((-1,1), (-2,2)), CustomNeighborhood((1,2), (2,2))), buf) + custom1 = Positional(((-1,-1), (2,-2), (2,2), (-1,2), (0,0)), buf) + custom2 = Positional(((-1,-1), (0,-1), (1,-1), (2,-1), (0,0)), buf) + layered = LayeredPositional( + (Positional((-1,1), (-2,2)), Positional((1,2), (2,2))), buf) @test neighbors(custom1) isa Base.Generator @test collect(neighbors(custom1)) == [0, 1, 1, 0, 0] @@ -85,7 +91,7 @@ end @test sum(layered) == (1, 2) @testset "neighbors works on rule" begin - rule = Life(;neighborhood=RadialNeighborhood{1}([0 1 1; 0 0 0; 1 1 1])) + rule = Life(;neighborhood=Moore{1}([0 1 1; 0 0 0; 1 1 1])) @test sum(neighbors(rule)) == 5 end end @@ -93,40 +99,40 @@ end struct TestNeighborhoodRule{R,W,N} <: NeighborhoodRule{R,W} neighborhood::N end -DynamicGrids.applyrule(rule::TestNeighborhoodRule, data, state, index, buffer) = +DynamicGrids.applyrule(data, rule::TestNeighborhoodRule, state, index) = state struct TestManualNeighborhoodRule{R,W,N} <: ManualNeighborhoodRule{R,W} neighborhood::N end -DynamicGrids.applyrule!(rule::TestManualNeighborhoodRule{R,Tuple{W1,}}, data, state, index +DynamicGrids.applyrule!(data, rule::TestManualNeighborhoodRule{R,Tuple{W1,}}, state, index ) where {R,W1} = data[W1][index...] = state[1] @testset "neighborhood rules" begin - ruleA = TestManualNeighborhoodRule{:a,:a}(RadialNeighborhood{3}()) - ruleB = TestManualNeighborhoodRule{Tuple{:b},Tuple{:b}}(RadialNeighborhood{2}()) + ruleA = TestManualNeighborhoodRule{:a,:a}(Moore{3}()) + ruleB = TestManualNeighborhoodRule{Tuple{:b},Tuple{:b}}(Moore{2}()) @test neighbors(ruleA) isa Base.Generator - @test neighborhood(ruleA) == RadialNeighborhood{3}() - @test neighborhood(ruleB) == RadialNeighborhood{2}() + @test neighborhood(ruleA) == Moore{3}() + @test neighborhood(ruleB) == Moore{2}() @test neighborhoodkey(ruleA) == :a @test neighborhoodkey(ruleB) == :b - ruleA = TestNeighborhoodRule{:a,:a}(RadialNeighborhood{3}()) - ruleB = TestNeighborhoodRule{Tuple{:b},Tuple{:b}}(RadialNeighborhood{2}()) + ruleA = TestNeighborhoodRule{:a,:a}(Moore{3}()) + ruleB = TestNeighborhoodRule{Tuple{:b},Tuple{:b}}(Moore{2}()) @test neighbors(ruleA) isa Base.Generator - @test neighborhood(ruleA) == RadialNeighborhood{3}() - @test neighborhood(ruleB) == RadialNeighborhood{2}() + @test neighborhood(ruleA) == Moore{3}() + @test neighborhood(ruleB) == Moore{2}() @test neighborhoodkey(ruleA) == :a @test neighborhoodkey(ruleB) == :b end @testset "radius" begin init = (a=[1. 2.], b=[10. 11.]) - ruleA = TestNeighborhoodRule{:a,:a}(RadialNeighborhood{3}()) - ruleB = TestManualNeighborhoodRule{Tuple{:b},Tuple{:b}}(RadialNeighborhood{2}()) + ruleA = TestNeighborhoodRule{:a,:a}(Moore{3}()) + ruleB = TestManualNeighborhoodRule{Tuple{:b},Tuple{:b}}(Moore{2}()) ruleset = Ruleset(ruleA, ruleB) @test radius(ruleA) == 3 @test radius(ruleB) == 2 @@ -154,7 +160,7 @@ end 0 1 2 3 4 5 0 1 2 3 4 5] - hood = RadialNeighborhood{1}() + hood = Moore(1) rule = TestManualNeighborhoodRule{:a,:a}(hood) ruleset = Ruleset(rule) extent = Extent((_default_=init,), nothing, 1:1, nothing) @@ -170,7 +176,7 @@ end 0 1 2 3 4 5 0 1 2 3 4 5] - hood = CustomNeighborhood(((-1, -1), (1, 1))) + hood = Positional(((-1, -1), (1, 1))) rule = TestManualNeighborhoodRule{:a,:a}(hood) ruleset = Ruleset(rule) extent = Extent((_default_=init,), nothing, 1:1, nothing) @@ -187,8 +193,8 @@ end 0 1 2 3 4 6] - hood = LayeredCustomNeighborhood( - (CustomNeighborhood(((-1, -1), (1, 1))), CustomNeighborhood(((-2, -2), (2, 2)))), + hood = LayeredPositional( + (Positional(((-1, -1), (1, 1))), Positional(((-2, -2), (2, 2)))), nothing, ) rule = TestManualNeighborhoodRule{:a,:a}(hood) @@ -209,14 +215,14 @@ end end @testset "construction" begin - hood = CustomNeighborhood(((-1, -1), (1, 1))) + hood = Positional(((-1, -1), (1, 1))) @set! hood.coords = ((-5, -5), (5, 5)) @test hood.coords == ((-5, -5), (5, 5)) - hood = LayeredCustomNeighborhood( - (CustomNeighborhood(((-1, -1), (1, 1))), CustomNeighborhood(((-2, -2), (2, 2)))), + hood = LayeredPositional( + (Positional(((-1, -1), (1, 1))), Positional(((-2, -2), (2, 2)))), nothing, ) - @set! hood.layers = (CustomNeighborhood(((-3, -3), (3, 3))),) - @test hood.layers == (CustomNeighborhood(((-3, -3), (3, 3))),) + @set! hood.layers = (Positional(((-3, -3), (3, 3))),) + @test hood.layers == (Positional(((-3, -3), (3, 3))),) end diff --git a/test/outputs.jl b/test/outputs.jl index 26c19506..2dcde443 100644 --- a/test/outputs.jl +++ b/test/outputs.jl @@ -1,5 +1,5 @@ using DynamicGrids, Test -using DynamicGrids: isshowable, gridindex, storegrid!, SimData +using DynamicGrids: isshowable, frameindex, storeframe!, SimData @testset "Output construction" begin init = [10.0 11.0 @@ -8,7 +8,7 @@ using DynamicGrids: isshowable, gridindex, storegrid!, SimData output1 = ArrayOutput(init; tspan=1:1) ruleset = Ruleset(Life()) - @test gridindex(output1, 5) == 5 + @test frameindex(output1, 5) == 5 @test isshowable(output1, 5) == false # Test pushing new frames to an output diff --git a/test/rules.jl b/test/rules.jl index d54a5796..fbb50f3c 100644 --- a/test/rules.jl +++ b/test/rules.jl @@ -11,7 +11,7 @@ init = [0 1 1 0; 0 1 1 0] struct AddOneRule{R,W} <: Rule{R,W} end -DynamicGrids.applyrule(::AddOneRule, data, state, args...) = state + 1 +DynamicGrids.applyrule(data, ::AddOneRule, state, args...) = state + 1 @testset "Rulset mask ignores false cells" begin init = [0.0 4.0 0.0 @@ -38,7 +38,7 @@ end # Single grid rules struct TestRule{R,W} <: Rule{R,W} end -applyrule(::TestRule, data, state, index) = 0 +applyrule(data, ::TestRule, state, index) = 0 @testset "A rule that returns zero gives zero outputs" begin final = [0 0 0 0 @@ -75,7 +75,7 @@ applyrule(::TestRule, data, state, index) = 0 end struct TestManual{R,W} <: ManualRule{R,W} end -applyrule!(::TestManual, data, state, index) = 0 +applyrule!(data, ::TestManual, state, index) = 0 @testset "A partial rule that returns zero does nothing" begin rule = TestManual() @@ -95,7 +95,7 @@ applyrule!(::TestManual, data, state, index) = 0 end struct TestManualWrite{R,W} <: ManualRule{R,W} end -applyrule!(::TestManualWrite, data, state, index) = data[:_default_][index[1], 2] = 0 +applyrule!(data, ::TestManualWrite, state, index) = data[:_default_][index[1], 2] = 0 @testset "A partial rule that writes to dest affects output" begin final = [0 0 1 0; @@ -113,10 +113,10 @@ applyrule!(::TestManualWrite, data, state, index) = data[:_default_][index[1], 2 end struct TestCellTriple{R,W} <: CellRule{R,W} end -applyrule(::TestCellTriple, data, state, index) = 3state +applyrule(data, ::TestCellTriple, state, index) = 3state struct TestCellSquare{R,W} <: CellRule{R,W} end -applyrule(::TestCellSquare, data, (state,), index) = state^2 +applyrule(data, ::TestCellSquare, (state,), index) = state^2 @testset "Chained cell rules work" begin init = [0 1 2 3; @@ -139,7 +139,7 @@ struct PrecalcRule{R,W,P} <: Rule{R,W} end DynamicGrids.precalcrules(rule::PrecalcRule, simdata) = PrecalcRule(currenttime(simdata)) -applyrule(rule::PrecalcRule, data, state, index) = rule.precalc[] +applyrule(data, rule::PrecalcRule, state, index) = rule.precalc[] @testset "Rule precalculations work" begin init = [1 1; @@ -163,15 +163,15 @@ end # Multi grid rules struct DoubleY{R,W} <: CellRule{R,W} end -applyrule(rule::DoubleY, data, (x, y), index) = y * 2 +applyrule(data, rule::DoubleY, (x, y), index) = y * 2 struct HalfX{R,W} <: CellRule{R,W} end -applyrule(rule::HalfX, data, x, index) = x, x * 0.5 +applyrule(data, rule::HalfX, x, index) = x, x * 0.5 struct Predation{R,W} <: CellRule{R,W} end Predation(; prey=:prey, predator=:predator) = Predation{Tuple{predator,prey},Tuple{prey,predator}}() -applyrule(::Predation, data, (predators, prey), index) = begin +applyrule(data, ::Predation, (predators, prey), index) = begin caught = 2predators # Output order is the reverse of input to test that can work prey - caught, predators + caught * 0.5 @@ -216,21 +216,21 @@ end end @testset "life with generic constructors" begin - @test Life(RadialNeighborhood{1}(), (1, 1), (5, 5)) == - Life(; neighborhood=RadialNeighborhood{1}(), b=(1, 1), s=(5, 5)) - @test Life{:a,:b}(RadialNeighborhood{1}(), (1, 1), (5, 5)) == - Life(; read=:a, write=:b, neighborhood=RadialNeighborhood{1}(), b=(1, 1), s=(5, 5)); + @test Life(Moore(1), (1, 1), (5, 5)) == + Life(; neighborhood=Moore(1), b=(1, 1), s=(5, 5)) + @test Life{:a,:b}(Moore(1), (1, 1), (5, 5)) == + Life(; read=:a, write=:b, neighborhood=Moore(1), b=(1, 1), s=(5, 5)); @test Life(read=:a, write=:b) == Life{:a,:b}() @test Life() == Life(; read=:_default_) end @testset "generic ConstructionBase compatability" begin - life = Life{:x,:y}(; neighborhood=RadialNeighborhood{2}(), b=(1, 1), s=(2, 2)) + life = Life{:x,:y}(; neighborhood=Moore(2), b=(1, 1), s=(2, 2)) @set! life.b = (5, 6) @test life.b == (5, 6) @test life.s == (2, 2) @test readkeys(life) == :x @test writekeys(life) == :y - @test DynamicGrids.neighborhood(life) == RadialNeighborhood{2}() + @test DynamicGrids.neighborhood(life) == Moore(2) end diff --git a/test/simulationdata.jl b/test/simulationdata.jl index 1c060e23..318584ba 100644 --- a/test/simulationdata.jl +++ b/test/simulationdata.jl @@ -12,16 +12,16 @@ initab = (a=inita, b=initb) life = Life(read=:a, write=:a); rs = Ruleset(life, timestep=Day(1)) -tspan = DateTime(2001):Day(1):DateTime(2001, 2) +tspan_ = DateTime(2001):Day(1):DateTime(2001, 2) @testset "initdata!" begin - extent = Extent(initab, nothing, tspan, nothing) + extent = Extent(initab, nothing, tspan_, nothing) simdata = initdata!(nothing, extent, rs, nothing) @test simdata isa SimData @test init(simdata) == initab @test ruleset(simdata) === rs - @test DynamicGrids.tspan(simdata) === tspan + @test tspan(simdata) === tspan_ @test currentframe(simdata) === 1 @test first(simdata) === simdata[:a] @test last(simdata) === simdata[:b] @@ -44,12 +44,9 @@ tspan = DateTime(2001):Day(1):DateTime(2001, 2) @test parent(grida) == parent(source(grida)) == parent(source(wgrida)) @test parent(wgrida) === parent(dest(grida)) === parent(dest(wgrida)) - # This seems like too much outer padding - # - should there be that one row and colum extra? @test sourcestatus(grida) == deststatus(grida) == - [0 1 0 0 - 0 1 0 0 - 0 0 0 0] + [0 1 0 + 0 1 0] @test parent(source(gridb)) == parent(dest(gridb)) == [2 2 2 @@ -64,14 +61,14 @@ tspan = DateTime(2001):Day(1):DateTime(2001, 2) @test eltype(grida) == Int @test ismasked(grida, 1, 1) == false - extent = Extent(initab, nothing, tspan, nothing) + extent = Extent(initab, nothing, tspan_, nothing) initdata!(simdata, extent, rs, nothing) end @testset "initdata! with :_default_" begin initx = [1 0] rs = Ruleset(Life()) - extent = Extent((_default_=initx,), nothing, tspan, nothing) + extent = Extent((_default_=initx,), nothing, tspan_, nothing) simdata = initdata!(nothing, extent, rs, nothing) simdata2 = initdata!(simdata, extent, rs, nothing) @test keys(simdata2) == (:_default_,) @@ -85,11 +82,11 @@ end @testset "initdata! with replicates" begin nreps = 2 - extent = Extent(initab, nothing, tspan, nothing) + extent = Extent(initab, nothing, tspan_, nothing) simdata = initdata!(nothing, extent, rs, nreps) @test simdata isa Vector{<:SimData} @test all(DynamicGrids.ruleset.(simdata) .== Ref(rs)) - @test all(map(DynamicGrids.tspan, simdata) .== Ref(tspan)) + @test all(map(tspan, simdata) .== Ref(tspan_)) @test all(keys.(DynamicGrids.grids.(simdata)) .== Ref(keys(initab))) simdata2 = initdata!(simdata, extent, rs, nreps) end diff --git a/test/utils.jl b/test/utils.jl index 628049b1..a3669644 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -6,20 +6,22 @@ using DynamicGrids: inbounds, isinbounds @test inbounds((1, 1), (4, 5), RemoveOverflow()) == ((1,1),true) @test inbounds((2, 3), (4, 5), RemoveOverflow()) == ((2,3),true) @test inbounds((4, 5), (4, 5), RemoveOverflow()) == ((4,5),true) - @test isinbounds((4, 5), (4, 5), RemoveOverflow()) == true @test inbounds((-3, -100), (4, 5), RemoveOverflow()) == ((-3,-100),false) @test inbounds((0, 0), (4, 5), RemoveOverflow()) == ((0,0),false) @test inbounds((2, 3), (3, 2), RemoveOverflow()) == ((2,3),false) @test inbounds((2, 3), (1, 4), RemoveOverflow()) == ((2,3),false) @test inbounds((200, 300), (2, 3), RemoveOverflow()) == ((200,300),false) - @test isinbounds((200, 300), (2, 3), RemoveOverflow()) == false end @testset "inbounds with WrapOverflow() returns new index and true for an overflowed index" begin @test inbounds((-2,3), (10, 10), WrapOverflow()) == ((8,3),true) @test inbounds((2,0), (10, 10), WrapOverflow()) == ((2,10),true) @test inbounds((22,0), (10, 10), WrapOverflow()) == ((2,10),true) @test inbounds((-22,0), (10, 10), WrapOverflow()) == ((8,10),true) - @test isinbounds((-22,0), (10, 10), WrapOverflow()) == true + end + @testset "isinbounds" begin + @test isinbounds((4, 5), (4, 5)) == true + @test isinbounds((200, 300), (2, 3)) == false + @test isinbounds((-22,0), (10, 10)) == false end end @@ -35,7 +37,7 @@ end end @testset "return type" begin - rule = Neighbors(RadialNeighborhood{1}(zeros(Bool, 3, 3))) do hood, x + rule = Neighbors(Moore{1}(zeros(Bool, 3, 3))) do hood, x round(Int, x + sum(hood)) end output = ArrayOutput(rand(Int, 10, 10); tspan=1:10) @@ -47,17 +49,16 @@ end @testset "let blocks" begin a = 0.7 rule = Manual() do data, index, x - data[1][index...] = round(Int, x + a) + data[index...] = round(Int, x + a) end output = ArrayOutput(zeros(Int, 10, 10); tspan=1:10) @test_throws ErrorException isinferred(output, Ruleset(rule)) a = 0.7 rule = let a = a Manual() do data, index, x - data[1][index...] = round(Int, x + a) + data[index...] = round(Int, x + a) end end - output = ArrayOutput(zeros(Int, 10, 10); tspan=1:10) @test isinferred(output, Ruleset(rule)) end From 8e75479b7a1a224a9eeffbed22d6edc302c65b85 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 24 Jul 2020 00:02:13 +1000 Subject: [PATCH 3/7] improve docs --- docs/src/index.md | 64 +++++++------------------------------------- src/DynamicGrids.jl | 3 +-- src/chain.jl | 2 +- src/life.jl | 44 ++++++++++++++++++++---------- src/outputs/image.jl | 21 ++++++++++----- src/rules.jl | 11 ++++---- 6 files changed, 61 insertions(+), 84 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 6275af7b..ca583040 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,55 +4,6 @@ DynamicGrids ``` -## Examples - -While this package isn't designed or optimised specifically to run the game of -life, it's a simple demonstration of what it can do. This example runs a -game of life and displays it in a REPLOutput. - - -```@example -using DynamicGrids, Distributions - -# Build a random starting grid -init = Bool.(rand(Binomial(1, 0.5), 70, 70)) - -# Use the default game of life model -model = Ruleset(Life()) - -# Use an output that shows the cellular automata as blocks in the REPL -output = REPLOutput(init; tspan=1:50, fps=5) - -sim!(output, model) -``` - -More life-like examples: - -```julia -# Morley -sim!(output, Ruleset(Life(b=[3,6,8], s=[2,4,5])) - -# 2x2 -sim!(output, Ruleset(Life(b=[3,6], s=[1,2,5]))) - -# Dimoeba -init1 = round.(Int8, max.(0.0, rand(70,70))) -sim!(output, Ruleset(Life(b=[3,5,6,7,8], s=[5,6,7,8]); init=init1)) - -## No death -sim!(output, Ruleset(Life(b=[3], s=[0,1,2,3,4,5,6,7,8]))) - -## 34 life -sim!(output, Ruleset(Life(b=[3,4], s=[3,4])); init=init, fps=10) - -# Replicator -init2 = round.(Int8, max.(0.0, rand(70,70))) -init2[:, 1:30] .= 0 -init2[21:50, :] .= 0 -sim!(output, Ruleset(Life(b=[1,3,5,7], s=[1,3,5,7])); init=init2) -``` - - ## Rules Rules define simulation behaviour. They hold data relevant to the simulation, @@ -89,6 +40,7 @@ Neighborhood AbstractRadialNeighborhood Moore VonNeumann +AbstractPositional Positional LayeredPositional ``` @@ -114,12 +66,6 @@ ImageOutput GifOutput ``` -```@docs -DynamicGrids.Extent -DynamicGrids.GraphicConfig -DynamicGrids.ImageConfig -``` - ### Grid processors ```@docs @@ -141,6 +87,14 @@ TextConfig savegif ``` +### Internal components for outputs + +```@docs +DynamicGrids.Extent +DynamicGrids.GraphicConfig +DynamicGrids.ImageConfig +``` + ## Ruleset config ### Overflow diff --git a/src/DynamicGrids.jl b/src/DynamicGrids.jl index 1e0146a9..51e88661 100644 --- a/src/DynamicGrids.jl +++ b/src/DynamicGrids.jl @@ -52,8 +52,7 @@ export Chain, Cell, Neighbors, Manual, Map, Life export AbstractRuleset, Ruleset export Neighborhood, AbstractRadialNeighborhood, Moore, - Custom, Positional, LayeredPositional, - VonNeumann + AbstractPositional, Positional, VonNeumann, LayeredPositional export PerformanceOpt, NoOpt, SparseOpt diff --git a/src/chain.jl b/src/chain.jl index 8eaf2b29..91debe29 100644 --- a/src/chain.jl +++ b/src/chain.jl @@ -3,7 +3,7 @@ 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`](@ref) or -[`NeighborhoodRule`](@ref) followed by [CellRule`](@ref). +[`NeighborhoodRule`](@ref) followed by [`CellRule`](@ref). """ struct Chain{R,W,T<:Union{Tuple{},Tuple{Union{<:NeighborhoodRule,<:CellRule},Vararg{<:CellRule}}}} <: Rule{R,W} rules::T diff --git a/src/life.jl b/src/life.jl index 596de5a6..723560a6 100644 --- a/src/life.jl +++ b/src/life.jl @@ -6,40 +6,56 @@ 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. -Returns: boolean - ## Examples (gleaned from CellularAutomata.jl) -```julia +```@example # Life. -init = round.(Int64, max.(0.0, rand(-3.0:0.1:1.0, 300,300))) -output = REPLOutput(init; fps=10, color=:red) -sim!(output, rule, init; tspan=(1, 1000) +using DynamicGrids, Distributions +init = Bool.(rand(Binomial(1, 0.5), 70, 70)) +output = REPLOutput(init; tspan=(1, 1000), fps=10, color=:red) + +# Morley +sim!(output, Ruleset(Life(b=[3,6,8], s=[2,4,5])) + +# 2x2 +sim!(output, Ruleset(Life(b=[3,6], s=[1,2,5]))) # Dimoeba init = rand(0:1, 400, 300) init[:, 100:200] .= 0 output = REPLOutput{:braile}(init; fps=25, color=:blue) -sim!(output, Ruleset(Life(b=(3,5,6,7,8), s=(5,6,7,8))), init; tspan=(1, 1000)) +sim!(output, Life(b=(3,5,6,7,8), s=(5,6,7,8)))) + +## No death +sim!(output, Life(b=[3], s=[0,1,2,3,4,5,6,7,8])) + +## 34 life +sim!(output, Life(b=[3,4], s=[3,4])) # Replicator init = fill(1, 300,300) init[:, 100:200] .= 0 init[10, :] .= 0 -output = REPLOutput(init; fps=60, color=:yellow) -sim!(output, Ruleset(Life(b=(1,3,5,7), s=(1,3,5,7))), init; tspan=(1, 1000)) +output = REPLOutput(init; tspan=(1, 1000), fps=60, color=:yellow) +sim!(output, Life(b=(1,3,5,7), s=(1,3,5,7))) ``` """ -@default @flattenable @bounds @description struct Life{R,W,N,B,S} <: NeighborhoodRule{R,W} +@default @flattenable @bounds @description struct Life{R,W,N,B,S,L} <: NeighborhoodRule{R,W} neighborhood::N | Moore(1) | false | nothing | "Any Neighborhood" b::B | (3, 3) | true | (0, 8) | "Array, Tuple or Iterable of integers to match neighbors when cell is empty" s::S | (2, 3) | true | (0, 8) | "Array, Tuple or Iterable of integers to match neighbors cell is full" + lookup::L | _ | false | _ | _ + Life{R,W,N,B,S,L}(neighborhood::N, b::B, s::S, lookup::L) where {R,W,N,B,S,L} = begin + lookup = (i in b for i in 0:8), (i in s for i in 0:8) + new{R,W,N,B,S,typeof(lookup)}(neighborhood, b, s, lookup) + end end -const life_states = - (false, false, false, true, false, false, false, false, false), - (false, false, true, true, false, false, false, false, false) +""" + applyrule(data::SimData, rule::Life, state, I) +Applies game of life rule to current cell, returning `Bool`. +""" applyrule(data::SimData, rule::Life, state, I) = - life_states[state + 1][sum(neighbors(rule)) + 1] + rule.lookup[state + 1][sum(neighbors(rule)) + 1] diff --git a/src/outputs/image.jl b/src/outputs/image.jl index 1246e12c..b574b4ef 100644 --- a/src/outputs/image.jl +++ b/src/outputs/image.jl @@ -4,7 +4,7 @@ Common configuration component for all [`ImageOutput`](@ref). -Holds an [`Processor`](@ref). +Holds a [`GridProcessor`](@ref). `minval` and `maxval` fields normalise grid values between zero and one, for use with Colorshemes.jl. `nothing` values are considered to represent zero and one, and will not be normalised. @@ -102,8 +102,8 @@ const Grayscale = Greyscale """ -Grid processors convert a frame of the simulation into an RGB image for display. -Frames may hold one or multiple grids. +Grid processors convert a frame of the simulation into an RGB +image for display. Frames may be one or multiple grids. """ abstract type GridProcessor end @@ -115,7 +115,7 @@ textconfig(::GridProcessor) = nothing Convert a grid or named tuple of grids to an RGB image, using a GridProcessor -[`GridProcessor`](@reg) is intentionally not dispatched with the output type in +[`GridProcessor`](@ref) is intentionally not dispatched with the output type in the methods that finally generate images, to reduce coupling. But it they can be distpatched on together when required for custom outputs. """ @@ -226,6 +226,9 @@ Converts output grids to a colorsheme. maskcolor::M | nothing textconfig::TC | nothing end +ColorProcessor(scheme::S, zerocolor::Z=nothing, maskcolor::M=nothing, textconfig::TC=nothing + ) where {S,Z,M,TC} = + ColorProcessor{S,Z,M,TC}(scheme, zerocolor, maskcolor, textconfig) scheme(processor::ColorProcessor) = processor.scheme zerocolor(processor::ColorProcessor) = processor.zerocolor @@ -337,7 +340,7 @@ end LayoutProcessor(layout::Array, processors, textconfig) LayoutProcessor allows displaying multiple grids in a block layout, -by specifying a layout matrix and a list of [`SingleGridProcessors`](@ref) +by specifying a layout matrix and a list of [`SingleGridProcessor`](@ref) to be run for each. ## Arguments @@ -349,10 +352,10 @@ to be run for each. """ @default_kw struct LayoutProcessor{L<:AbstractMatrix,P,TC} <: MultiGridProcessor layout::L | throw(ArgumentError("must include an Array for the layout keyword")) - processors::P | throw(ArgumentError("include a tuple of processors for each grid")) + processors::P | throw(ArgumentError("must include a tuple of processors, one for each grid")) textconfig::TC | nothing LayoutProcessor(layouts::L, processors::P, textconfig::TC) where {L,P,TC} = begin - processors = map(p -> (@set p.textconfig = textconfig), processors) + processors = map(p -> (@set p.textconfig = textconfig), map(_asprocessor, processors)) new{L,typeof(processors),TC}(layouts, processors, textconfig) end end @@ -360,10 +363,14 @@ end LayoutProcessor(layout::AbstractVector, processors, textconfig) = LayoutProcessor(reshape(layout, length(layout), 1), processors, textconfig) +_asprocessor(p::GridProcessor) = p +_asprocessor(x) = ColorProcessor(x) + layout(p::LayoutProcessor) = p.layout processors(p::LayoutProcessor) = p.processors textconfig(p::LayoutProcessor) = p.textconfig + grid2image(p::LayoutProcessor, o::ImageOutput, data::RulesetOrSimData, grids::NamedTuple, t ) = begin ngrids, nmin, nmax = map(length, (grids, minval(o), maxval(o))) diff --git a/src/rules.jl b/src/rules.jl index bcd61540..6bbe8b28 100644 --- a/src/rules.jl +++ b/src/rules.jl @@ -76,7 +76,7 @@ grid that they choose, instead of automatically updating every cell with their o `NeighborhoodRule` is applied with the method: ```julia -applyrule!(data, rule::Life, state, I) +applyrule!(data, rule, state, I) ``` Note the `!` bang - this method alters the state of `data`. @@ -96,7 +96,7 @@ A Rule that only accesses a neighborhood centered around the current cell. `NeighborhoodRule` is applied with the method: ```julia -applyrule(data, rule::Life, state, I) +applyrule(data, rule, state, I) ``` For each cell a neighborhood buffer will be populated containing the @@ -202,8 +202,8 @@ Returned value(s) are written to the `write`/`W` grid. As with all [`NeighborhoodRule`](@ref), you do not have to check bounds at grid edges, that is handled for you internally. -Using [`SparseOpt`](@ref) may improve neighborhood performance when zero values -are common, and can be safely ignored. +Using [`SparseOpt`](@ref) may improve neighborhood performance +when zero values are common and can be safely ignored. ## Example @@ -214,7 +214,8 @@ rule = let x = 10 end end ``` -The `let` block greatly imroves performance. + +The `let` block may improve performance. """ @flattenable @description struct Neighbors{R,W,F,N} <: NeighborhoodRule{R,W} # Field | Flatten | Description From 65468936c1c7806f41403b8a3866d94a676e90df Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 24 Jul 2020 12:38:27 +1000 Subject: [PATCH 4/7] bugfix life rule --- src/life.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/life.jl b/src/life.jl index 723560a6..20ad53f7 100644 --- a/src/life.jl +++ b/src/life.jl @@ -47,10 +47,13 @@ sim!(output, Life(b=(1,3,5,7), s=(1,3,5,7))) s::S | (2, 3) | true | (0, 8) | "Array, Tuple or Iterable of integers to match neighbors cell is full" lookup::L | _ | false | _ | _ Life{R,W,N,B,S,L}(neighborhood::N, b::B, s::S, lookup::L) where {R,W,N,B,S,L} = begin - lookup = (i in b for i in 0:8), (i in s for i in 0:8) + lookup = Tuple(i in b for i in 0:8), Tuple(i in s for i in 0:8) new{R,W,N,B,S,typeof(lookup)}(neighborhood, b, s, lookup) end end +Life(neighborhood, b, s) = Life(neighborhood, b, s, nothing) +Life{R,W}(neighborhood, b, s) where {R,W} = Life{R,W}(neighborhood, b, s, nothing) + """ applyrule(data::SimData, rule::Life, state, I) From 86a6a85f812a17e3140512ea8a709933f2d17b60 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 24 Jul 2020 12:38:46 +1000 Subject: [PATCH 5/7] clarify tests --- test/integration.jl | 36 +++++++++++++----------------------- test/rules.jl | 8 ++++---- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/test/integration.jl b/test/integration.jl index f22671b9..e5527603 100644 --- a/test/integration.jl +++ b/test/integration.jl @@ -1,15 +1,5 @@ using DynamicGrids, Test, Dates, Unitful - -using DynamicGrids - -using Distributions -init = Bool.(rand(Binomial(1, 0.5), 20, 20)) -ruleset = Ruleset(Life()) -output = REPLOutput(init; tspan=1:50, fps=5) -sim!(output, ruleset) - - # life glider sims # Test all cycled variants of the array @@ -31,7 +21,7 @@ cyclej!(arrays) = begin end end -test67 = ( +test6_7 = ( init = Bool[ 0 0 0 0 0 0 0 0 0 0 0 1 1 1 @@ -82,7 +72,7 @@ test67 = ( ] ) -test56 = ( +test5_6 = ( init = Bool[ 0 0 0 0 0 0 0 0 0 1 1 1 @@ -130,7 +120,7 @@ test56 = ( @testset "Life simulation with WrapOverflow" begin # Test on two sizes to test half blocks on both axes - for test in (test56, test67) + for test in (test5_6, test6_7) # Loop over shifing init arrays to make sure they all work for i = 1:size(test[:init], 1) for j = 1:size(test[:init], 2) @@ -271,19 +261,19 @@ end opt=NoOpt(), ) tspan=0u"s":5u"s":6u"s" - output = REPLOutput(test67[:init]; tspan=tspan, style=Block(), fps=100, store=true) + output = REPLOutput(test6_7[:init]; tspan=tspan, style=Block(), fps=100, store=true) DynamicGrids.isstored(output) DynamicGrids.store(output) sim!(output, ruleset) resume!(output, ruleset; tstop=30u"s") - @test output[2] == test67[:test2] - @test output[3] == test67[:test3] - @test output[5] == test67[:test5] - @test output[7] == test67[:test7] + @test output[2] == test6_7[:test2] + @test output[3] == test6_7[:test3] + @test output[5] == test6_7[:test5] + @test output[7] == test6_7[:test7] end @testset "REPLOutput braile works, in Months" begin - init_a = (_default_=test67[:init],) + init_a = (_default_=test6_7[:init],) ruleset = Ruleset(Life(); overflow=WrapOverflow(), timestep=Month(1), @@ -292,11 +282,11 @@ end tspan = Date(2010, 4):Month(1):Date(2010, 7) output = REPLOutput(init_a; tspan=tspan, style=Braile(), fps=100, store=true) sim!(output, ruleset) - @test output[2][:_default_] == test67[:test2] - @test output[3][:_default_] == test67[:test3] + @test output[2][:_default_] == test6_7[:test2] + @test output[3][:_default_] == test6_7[:test3] @test DynamicGrids.tspan(output) == Date(2010, 4):Month(1):Date(2010, 7) resume!(output, ruleset; tstop=Date(2010, 11)) @test DynamicGrids.tspan(output) == Date(2010, 4):Month(1):Date(2010, 11) - @test output[5][:_default_] == test67[:test5] - @test output[7][:_default_] == test67[:test7] + @test output[5][:_default_] == test6_7[:test5] + @test output[7][:_default_] == test6_7[:test7] end diff --git a/test/rules.jl b/test/rules.jl index fbb50f3c..8b677e24 100644 --- a/test/rules.jl +++ b/test/rules.jl @@ -4,10 +4,10 @@ import DynamicGrids: applyrule, applyrule!, maprule!, SimData, WritableGridData, _Read_, _Write_, Rule, Extent, readkeys, writekeys -init = [0 1 1 0; - 0 1 1 0; - 0 1 1 0; - 0 1 1 0; +init = [0 1 1 0 + 0 1 1 0 + 0 1 1 0 + 0 1 1 0 0 1 1 0] struct AddOneRule{R,W} <: Rule{R,W} end From 6deacda2207d7eb7e7900500da8c8255dd9aa5db Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 24 Jul 2020 12:49:48 +1000 Subject: [PATCH 6/7] bump minor version to 0.10.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 8d096694..5240ae92 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DynamicGrids" uuid = "a5dba43e-3abc-5203-bfc5-584ca68d3f5b" authors = ["Rafael Schouten "] -version = "0.9.0" +version = "0.10.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" From 6324249e5de0de79ceb1d051d27ae8f3802162dd Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 24 Jul 2020 13:13:38 +1000 Subject: [PATCH 7/7] allow rulesets in showframe --- src/outputs/graphic.jl | 5 ++++- src/outputs/image.jl | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/outputs/graphic.jl b/src/outputs/graphic.jl index dc3a3697..2b8d882f 100644 --- a/src/outputs/graphic.jl +++ b/src/outputs/graphic.jl @@ -1,4 +1,6 @@ +const RulesetOrSimData = Union{AbstractRuleset,AbstractSimData} + """ GraphicConfig(; fps=25.0, store=false, kwargs...) = GraphicConfig(fps, timestamp, stampframe, store) @@ -81,5 +83,6 @@ _pushgrid!(::Type{<:AbstractArray}, o::GraphicOutput) = push!(o, similar(o[1])) # Get frame f from output and call showframe again -showframe(o::GraphicOutput, data::AbstractSimData, f, t) = +showframe(o::GraphicOutput, f, t) = showframe(o, Ruleset(), f, t) +showframe(o::GraphicOutput, data::RulesetOrSimData, f, t) = showframe(o[frameindex(o, f)], o, data, f, t) diff --git a/src/outputs/image.jl b/src/outputs/image.jl index b574b4ef..3a7adb70 100644 --- a/src/outputs/image.jl +++ b/src/outputs/image.jl @@ -23,8 +23,6 @@ processor(ic::ImageConfig) = ic.processor minval(ic::ImageConfig) = ic.minval maxval(ic::ImageConfig) = ic.maxval -const RulesetOrSimData = Union{Ruleset,AbstractSimData} - """ Graphic outputs that display the simulation frames as RGB images.