Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ version = "0.1.3"
ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[weakdeps]
Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671"

[extensions]
CellularAutomataAgentsExt = "Agents"

[compat]
Agents = "7"
Aqua = "0.8"
ConcreteStructs = "0.2"
ForwardDiff = "0.10, 1"
Expand All @@ -18,11 +25,12 @@ Test = "1.10"
julia = "1.10"

[extras]
Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671"
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "SafeTestsets", "Aqua", "ForwardDiff", "JET"]
test = ["Test", "SafeTestsets", "Aqua", "ForwardDiff", "JET", "Agents"]
10 changes: 10 additions & 0 deletions docs/src/api/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,13 @@
spatial_dimensions
lempel_ziv
```

## Extensions

```@docs
cellular_automaton_abm
cellular_automaton_state
```

`cellular_automaton_abm`/`cellular_automaton_state` are implemented in a package
extension and require `Agents` to be loaded.
49 changes: 49 additions & 0 deletions ext/CellularAutomataAgentsExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module CellularAutomataAgentsExt

using CellularAutomata:
CellularAutomata, AbstractCellularAutomatonRule, AbstractBoundaryCondition,
AbstractUpdateScheme, Periodic, Synchronous, next_state
using Agents: Agents, StandardABM, GridSpaceSingle, dummystep
using Random: default_rng

mutable struct __CAField{R, S, B, U, G}
rule::R
state::S
boundary::B
scheme::U
rng::G
end

function __step_ca!(field::__CAField)
field.state = next_state(
field.rule, field.state; boundary = field.boundary, scheme = field.scheme,
rng = field.rng
)
return field.state
end

function CellularAutomata.cellular_automaton_abm(
AgentType, rule::AbstractCellularAutomatonRule, state;
boundary::AbstractBoundaryCondition = Periodic(),
scheme::AbstractUpdateScheme = Synchronous(),
rng = default_rng(),
space = GridSpaceSingle(size(state); periodic = boundary isa Periodic),
agent_step! = dummystep,
model_step! = Returns(nothing),
properties::NamedTuple = NamedTuple(),
kwargs...
)
haskey(properties, :cellular_automaton) &&
throw(ArgumentError("properties already has a `cellular_automaton` key"))
field = __CAField(rule, state, boundary, scheme, rng)
merged_properties = merge((; cellular_automaton = field), properties)
wrapped_model_step! = model -> (__step_ca!(field); model_step!(model); nothing)
return StandardABM(
AgentType, space; properties = merged_properties,
agent_step! = agent_step!, model_step! = wrapped_model_step!, kwargs...
)
end

CellularAutomata.cellular_automaton_state(model) = model.cellular_automaton.state

end # module
2 changes: 2 additions & 0 deletions src/CellularAutomata.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include("rules/cca.jl")
include("rules/tca.jl")
include("rules/life.jl")
include("measures.jl")
include("extensions.jl")

export AbstractCellularAutomaton, AbstractCellularAutomatonRule
export AbstractBoundaryCondition, Periodic, Reflecting, ConstantBoundary
Expand All @@ -28,5 +29,6 @@ export CCA
export TCA
export Life
export lempel_ziv
export cellular_automaton_abm, cellular_automaton_state

end # module
42 changes: 42 additions & 0 deletions src/extensions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
cellular_automaton_abm(AgentType, rule, state; kwargs...)

Build an `Agents.StandardABM` that advances `state` under `rule` once per model step,
alongside independently-scheduled agents of type `AgentType`. Requires `Agents` to be
loaded (implemented in a package extension).

# Arguments

- `AgentType`: agent type living on the model's grid, as required by
`Agents.StandardABM`.
- `rule`: an [`AbstractCellularAutomatonRule`](@ref) advanced each model step.
- `state`: the initial cellular-automaton state array.

# Keywords

- `boundary`: boundary condition passed to [`next_state`](@ref). Defaults to
`Periodic()`.
- `scheme`: update scheme passed to [`next_state`](@ref). Defaults to `Synchronous()`.
- `rng`: random number generator, required by stochastic schemes.
- `space`: an `Agents.AbstractSpace`. Defaults to a `GridSpaceSingle` sized to `state`,
periodic if `boundary isa Periodic`.
- `agent_step!`: per-agent step function, as in `Agents.StandardABM`.
- `model_step!`: additional model step function, called after the cellular automaton
has been advanced. Defaults to a no-op.
- `properties`: a `NamedTuple` of additional model properties, merged with the
cellular-automaton state stored under the `cellular_automaton` key.
- other `kwargs` are passed through to `Agents.StandardABM`.

# Throws

- `ArgumentError`: if `properties` already has a `cellular_automaton` key.
"""
function cellular_automaton_abm end

"""
cellular_automaton_state(model)

Return the current cellular-automaton state array stored in an ABM built by
[`cellular_automaton_abm`](@ref). Requires `Agents` to be loaded.
"""
function cellular_automaton_state end
55 changes: 55 additions & 0 deletions test/agents_ext_test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using CellularAutomata
using Agents
using Test

@agent struct Forager(GridAgent{2}) end
@agent struct Point1D(GridAgent{1}) end

@testset "cellular automaton advances like rollout" begin
glider = [0 0 1 0 0; 0 0 0 1 0; 0 1 1 1 0; 0 0 0 0 0; 0 0 0 0 0]
life = Life((3, (2, 3)))

model = cellular_automaton_abm(
Forager, life, glider;
agent_step! = (agent, model) -> walk!(agent, (rand((-1, 0, 1)), rand((-1, 0, 1))), model)
)
add_agent!(model)
add_agent!(model)

step!(model, 3)

@test cellular_automaton_state(model) == rollout(life, glider, 3)
@test nagents(model) == 2
end

@testset "no agent_step! given, model_step! still advances the automaton" begin
dca = DCA(30)
state = [0, 0, 1, 0, 0]

model = cellular_automaton_abm(Point1D, dca, state)
step!(model, 2)

@test cellular_automaton_state(model) == rollout(dca, state, 2)
end

@testset "extra model_step! runs after the automaton update" begin
dca = DCA(30)
state = [0, 0, 1, 0, 0]
calls = Ref(0)

model = cellular_automaton_abm(
Point1D, dca, state; model_step! = model -> (calls[] += 1)
)
step!(model, 3)

@test calls[] == 3
@test cellular_automaton_state(model) == rollout(dca, state, 3)
end

@testset "rejects a conflicting properties key" begin
dca = DCA(30)
state = [0, 0, 1, 0]
@test_throws ArgumentError cellular_automaton_abm(
Point1D, dca, state; properties = (; cellular_automaton = nothing)
)
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ if GROUP in ("All", "Core")
@safetestset "Life blinker" include("blinker_test.jl")
@safetestset "Neighborhoods" include("neighborhood_test.jl")
end

@testset "Extensions" begin
@safetestset "Agents.jl" include("agents_ext_test.jl")
end
end
Loading