Skip to content

Commit 5e83800

Browse files
Merge pull request #31 from MartinuzziFrancesco/fm/st
feat: add stochastic updates
2 parents 80e8792 + 8ef035a commit 5e83800

10 files changed

Lines changed: 198 additions & 25 deletions

File tree

Project.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
name = "CellularAutomata"
22
uuid = "878138dc-5b27-11ea-1a71-cb95d38d6b29"
33
authors = ["Francesco Martinuzzi"]
4-
version = "0.1.1"
4+
version = "0.1.2"
55

66
[deps]
77
ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471"
8+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
89

910
[compat]
1011
Aqua = "0.8"
@@ -20,9 +21,8 @@ julia = "1.10"
2021
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
2122
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
2223
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
23-
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2424
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
2525
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2626

2727
[targets]
28-
test = ["Test", "SafeTestsets", "Random", "Aqua", "ForwardDiff", "JET"]
28+
test = ["Test", "SafeTestsets", "Aqua", "ForwardDiff", "JET"]

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@
4141
CellularAutomata.jl offers lightweight and ready to use implementations of
4242
cellular automata, one and two dimensional.
4343

44+
## Features
45+
46+
- **Discrete CA** (`DCA`): elementary, general (any radius/state count,
47+
symmetric or asymmetric neighborhoods), and totalistic rules, following
48+
Wolfram's *A New Kind of Science* numbering.
49+
- **Continuous CA** (`CCA`): real-valued totalistic rules.
50+
- **Life-like CA** (`Life`): Conway's Game of Life and any birth/survival
51+
variant, in the Golly notation.
52+
- **Boundary conditions**: `Periodic`, `Reflecting`, `ConstantBoundary`.
53+
- **Update schemes**: `Synchronous` (default) and `Stochastic(rate)` for
54+
per-cell probabilistic updates, given an explicit `rng`.
55+
- **Functional core** (`next_state`, `rollout`): non-mutating, AD/GPU-friendly
56+
building blocks for custom or neural cellular automata, alongside the
57+
stateful `CellularAutomaton` wrapper.
58+
- **Analysis**: `evolution_history`, `lempel_ziv` complexity, and other
59+
accessor functions.
60+
4461
## Installation
4562
CellularAutomata.jl is registered on the general registry. For the installation use either of:
4663

docs/src/ad_design.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,29 @@ without changing the existing discrete-rule API.
88
## Functional core API
99

1010
```julia
11-
next_state(rule, state; boundary=Periodic()) -> state
12-
rollout(rule, state, steps; boundary=Periodic(), save=false) -> result
11+
next_state(rule, state; boundary=Periodic(), scheme=Synchronous(), rng=nothing) -> state
12+
rollout(rule, state, steps; boundary=Periodic(), scheme=Synchronous(),
13+
rng=nothing, save=false) -> result
1314
```
1415

15-
`next_state` is a pure function: it does not mutate `state` or captured parameter
16-
arrays. `rollout` is the orchestration layer. It returns the final state by default,
17-
which is the preferred path inside a loss function. With `save=true`, it returns the
18-
initial state and all subsequent states with time on the last axis.
16+
`next_state` does not mutate `state` or captured parameter arrays. `rollout` is the
17+
orchestration layer. It returns the final state by default, which is the preferred
18+
path inside a loss function. With `save=true`, it returns the initial state and all
19+
subsequent states with time on the last axis.
1920
`CellularAutomaton` remains a compatibility wrapper and preserves its original
2021
history layout.
2122

2223
Boundary behavior is represented by small types — `Periodic`, `Reflecting`, and
2324
`ConstantBoundary` — rather than being embedded in every rule implementation.
2425
Neighborhood extraction is likewise shared across rules.
2526

27+
Update schemes follow the same pattern: `Synchronous` (default) applies every
28+
cell's transition, while `Stochastic(rate)` keeps a cell's previous value unless
29+
a randomly drawn per-cell mask selects it. Stochastic updates require an explicit
30+
`rng` argument threaded through `next_state`/`rollout`; drawing a mask advances
31+
that RNG. The sampled mask is treated as data by the transition, so gradients do
32+
not differentiate through the random draw or the discrete selection decision.
33+
2634
## State and rule representation
2735

2836
Discrete automata accept vectors and matrices. Differentiable rules need a documented
@@ -45,9 +53,9 @@ end
4553

4654
spatial_dimensions(::NeuralRule) = 2
4755

48-
function CellularAutomata.next_state(rule::NeuralRule, x; boundary=Periodic())
56+
function CellularAutomata.__step(rule::NeuralRule, x, boundary)
4957
features = rule.perceive(x, boundary)
50-
return x + rule.update(features)
58+
return x + rule.update(features) # proposed next state
5159
end
5260
```
5361

@@ -59,10 +67,10 @@ that narrow boundary.
5967

6068
## Migration sequence
6169

62-
1. **Implemented:** pure `next_state`, functional `rollout`, explicit boundary types,
63-
shared neighborhood access, compatibility through `CellularAutomaton`, and
64-
multi-step ForwardDiff coverage. Lookup-based DCA/TCA and thresholded Life rules
65-
remain intentionally nondifferentiable.
70+
1. **Implemented:** non-mutating `next_state`, functional `rollout`, explicit
71+
boundary types, shared neighborhood access, compatibility through
72+
`CellularAutomaton`, and multi-step ForwardDiff coverage. Lookup-based DCA/TCA
73+
and thresholded Life rules remain intentionally nondifferentiable.
6674
2. Test reverse-mode AD and GPU arrays after selecting the package's supported ML
6775
stack.
6876
3. Add a generic continuous multidimensional local rule with a documented

docs/src/api/general.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
CellularAutomaton
55
next_state
66
rollout
7+
AbstractUpdateScheme
8+
Synchronous
9+
Stochastic
710
cellular_automaton_rule
811
evolution_history
912
generation_count

src/CellularAutomata.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module CellularAutomata
22

33
using ConcreteStructs: @concrete
4+
using Random: AbstractRNG
45

56
include("generics.jl")
67
include("cellular_automaton.jl")
@@ -12,6 +13,7 @@ include("measures.jl")
1213

1314
export AbstractCellularAutomaton, AbstractCellularAutomatonRule
1415
export AbstractBoundaryCondition, Periodic, Reflecting, ConstantBoundary
16+
export AbstractUpdateScheme, Synchronous, Stochastic
1517
export AbstractDiscreteCellularAutomatonRule
1618
export AbstractContinuousCellularAutomatonRule
1719
export AbstractTotalisticCellularAutomatonRule

src/cellular_automaton.jl

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ Supertype for cellular automata that retain an evolution history.
66
abstract type AbstractCellularAutomaton end
77

88
"""
9-
next_state(rule, state; boundary=Periodic())
9+
next_state(rule, state; boundary=Periodic(), scheme=Synchronous(), rng=nothing)
1010
11-
Apply one synchronous cellular-automaton transition without mutating `state`.
12-
Rules may specialize this function to define differentiable transitions.
11+
Apply one cellular-automaton transition without mutating `state`. Rule subtypes
12+
define their transition by implementing
13+
`CellularAutomata.__step(rule, state, boundary)`; this wrapper then applies the
14+
selected update scheme.
1315
1416
# Arguments
1517
@@ -20,6 +22,11 @@ Rules may specialize this function to define differentiable transitions.
2022
2123
- `boundary`: Boundary condition used outside the state array.
2224
Defaults to `Periodic()`.
25+
- `scheme`: Update scheme controlling which cells apply the transition.
26+
Defaults to `Synchronous()`; use `Stochastic(rate)` for a per-cell random mask.
27+
- `rng`: Random number generator used by stochastic schemes. It must be supplied
28+
explicitly with `Stochastic` and its state advances when the mask is drawn.
29+
It is not consulted by `Synchronous`.
2330
2431
# Examples
2532
@@ -38,23 +45,33 @@ julia> next_state(DCA(30), [0, 0, 1, 0, 0])
3845
function next_state(
3946
rule::AbstractCellularAutomatonRule,
4047
state;
41-
boundary::AbstractBoundaryCondition = Periodic()
48+
boundary::AbstractBoundaryCondition = Periodic(),
49+
scheme::AbstractUpdateScheme = Synchronous(),
50+
rng::Union{Nothing, AbstractRNG} = nothing
4251
)
4352
__validate_state(rule, state)
44-
return __step(rule, state, boundary)
53+
__validate_scheme_rng(scheme, rng)
54+
new_state = __step(rule, state, boundary)
55+
return __apply_scheme(scheme, rng, state, new_state)
4556
end
4657

4758
@concrete struct __Transition
4859
rule
4960
boundary
61+
scheme
62+
rng
5063
end
5164

5265
function (transition::__Transition)(state, _)
53-
return next_state(transition.rule, state; boundary = transition.boundary)
66+
return next_state(
67+
transition.rule, state;
68+
boundary = transition.boundary, scheme = transition.scheme, rng = transition.rng
69+
)
5470
end
5571

5672
"""
57-
rollout(rule, initial_state, steps; boundary=Periodic(), save=false)
73+
rollout(rule, initial_state, steps; boundary=Periodic(), scheme=Synchronous(),
74+
rng=nothing, save=false)
5875
5976
Apply `steps` transitions of `rule` to `initial_state`. By default only the final
6077
state is returned, which is the preferred path inside a loss function. With
@@ -70,6 +87,10 @@ on the last axis.
7087
# Keyword arguments
7188
7289
- `boundary`: Boundary condition used by each transition. Defaults to `Periodic()`.
90+
- `scheme`: Update scheme applied at every step. Defaults to `Synchronous()`.
91+
- `rng`: Random number generator passed to every step. It must be supplied
92+
explicitly with `Stochastic`; the same object is reused and its state advances
93+
across steps. It is not consulted by `Synchronous`.
7394
- `save`: Retain the initial state and every subsequent state. Defaults to `false`.
7495
7596
# Examples
@@ -96,11 +117,14 @@ function rollout(
96117
initial_state,
97118
steps::Integer;
98119
boundary::AbstractBoundaryCondition = Periodic(),
120+
scheme::AbstractUpdateScheme = Synchronous(),
121+
rng::Union{Nothing, AbstractRNG} = nothing,
99122
save::Bool = false
100123
)
101124
steps >= 0 || throw(ArgumentError("steps must be nonnegative"))
102125
isempty(initial_state) && throw(ArgumentError("initial_state cannot be empty"))
103-
transition = __Transition(rule, boundary)
126+
__validate_scheme_rng(scheme, rng)
127+
transition = __Transition(rule, boundary, scheme, rng)
104128
if save
105129
states = accumulate(transition, 1:steps; init = initial_state)
106130
return stack(Iterators.flatten(((initial_state,), states)))

src/generics.jl

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
AbstractCellularAutomatonRule
33
44
Supertype for cellular-automaton transition rules. Subtypes must implement
5-
`__step(rule, state, boundary)` returning the next state without mutating `state`,
6-
or specialize [`next_state`](@ref) directly.
5+
`CellularAutomata.__step(rule, state, boundary)` returning the proposed next state
6+
without mutating `state`. [`next_state`](@ref) applies validation and the selected
7+
update scheme around that transition.
78
"""
89
abstract type AbstractCellularAutomatonRule end
910

@@ -79,6 +80,71 @@ function Base.show(io::IO, boundary::ConstantBoundary)
7980
return print(io, "ConstantBoundary(", boundary.value, ")")
8081
end
8182

83+
"""
84+
AbstractUpdateScheme
85+
86+
Supertype for cellular-automaton update schemes, controlling which cells apply a
87+
transition each generation.
88+
"""
89+
abstract type AbstractUpdateScheme end
90+
91+
"""
92+
Synchronous()
93+
94+
Apply the transition to every cell each generation. This is the default update
95+
scheme and matches classical (non-stochastic) cellular automata.
96+
"""
97+
struct Synchronous <: AbstractUpdateScheme end
98+
99+
"""
100+
Stochastic(rate)
101+
102+
Apply the transition to each cell independently with probability `rate`; cells
103+
that are not selected keep their previous value. An explicit `rng` must be passed
104+
to [`next_state`](@ref) or [`rollout`](@ref). Drawing the per-cell mask advances
105+
the state of that random number generator.
106+
107+
# Examples
108+
109+
```jldoctest
110+
julia> using CellularAutomata, Random
111+
112+
julia> next_state(DCA(30), [0, 0, 1, 0, 0]; scheme=Stochastic(0.5), rng=Xoshiro(1))
113+
5-element Vector{Int64}:
114+
0
115+
1
116+
1
117+
0
118+
0
119+
```
120+
"""
121+
struct Stochastic{T <: Real} <: AbstractUpdateScheme
122+
rate::T
123+
function Stochastic{T}(rate::T) where {T <: Real}
124+
0 <= rate <= 1 || throw(ArgumentError("rate must be between 0 and 1"))
125+
return new{T}(rate)
126+
end
127+
end
128+
129+
Stochastic(rate::T) where {T <: Real} = Stochastic{T}(rate)
130+
131+
__validate_scheme_rng(::Synchronous, rng) = nothing
132+
__validate_scheme_rng(::Stochastic, ::AbstractRNG) = nothing
133+
function __validate_scheme_rng(::Stochastic, ::Nothing)
134+
throw(ArgumentError("an explicit rng is required when using Stochastic"))
135+
end
136+
137+
__apply_scheme(::Synchronous, rng, state, new_state) = new_state
138+
139+
function __apply_scheme(::Stochastic, ::Nothing, state, new_state)
140+
throw(ArgumentError("an explicit rng is required when using Stochastic"))
141+
end
142+
143+
function __apply_scheme(scheme::Stochastic, rng::AbstractRNG, state, new_state)
144+
mask = rand(rng, size(state)...) .< scheme.rate
145+
return ifelse.(mask, new_state, state)
146+
end
147+
82148
@inline __boundary_index(i, n, ::Periodic) = mod1(i, n)
83149
@inline function __boundary_index(i, n, ::Reflecting)
84150
n == 1 && return 1

test/regression_test.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ end
3434
CCA(0.1),
3535
Life(((3,), (2, 3))),
3636
ConstantBoundary(0.0),
37+
Synchronous(),
38+
Stochastic(0.5),
3739
)
3840
for object in objects
3941
@test all(isconcretetype, fieldtypes(typeof(object)))

test/runtests.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ if GROUP in ("All", "Core")
3535
@safetestset "Edge cases and hierarchy" include("regression_test.jl")
3636
end
3737

38+
@testset "Update schemes" begin
39+
@safetestset "Synchronous and Stochastic" include("update_scheme_test.jl")
40+
end
41+
3842
@testset "Life-like" begin
3943
@safetestset "Life glider" include("glider_test.jl")
4044
@safetestset "Life blinker" include("blinker_test.jl")

test/update_scheme_test.jl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using CellularAutomata
2+
using Random
3+
using Test
4+
5+
@testset "Stochastic rate validation" begin
6+
@test_throws ArgumentError Stochastic(-0.1)
7+
@test_throws ArgumentError Stochastic(1.1)
8+
@test_throws ArgumentError next_state(DCA(30), [0, 0, 1]; scheme = Stochastic(0.5))
9+
@test_throws ArgumentError rollout(DCA(30), [0, 0, 1], 0; scheme = Stochastic(0.5))
10+
end
11+
12+
@testset "Stochastic edge rates match Synchronous" begin
13+
rule = DCA(30)
14+
state = [0, 0, 1, 0, 0]
15+
@test next_state(rule, state; scheme = Stochastic(0.0), rng = Xoshiro(1)) == state
16+
@test next_state(rule, state; scheme = Stochastic(1.0), rng = Xoshiro(1)) ==
17+
next_state(rule, state)
18+
end
19+
20+
@testset "custom rule transitions still apply schemes" begin
21+
struct TestRule <: AbstractCellularAutomatonRule end
22+
CellularAutomata.__step(::TestRule, state, boundary) = one.(state)
23+
24+
state = zeros(Int, 4)
25+
@test next_state(TestRule(), state) == ones(Int, 4)
26+
@test next_state(
27+
TestRule(), state; scheme = Stochastic(0.0), rng = Xoshiro(1)
28+
) == state
29+
end
30+
31+
@testset "Stochastic is reproducible given the same rng" begin
32+
rule = DCA(30)
33+
state = [0, 0, 1, 0, 0]
34+
result = next_state(rule, state; scheme = Stochastic(0.5), rng = Xoshiro(1))
35+
@test result == next_state(rule, state; scheme = Stochastic(0.5), rng = Xoshiro(1))
36+
end
37+
38+
@testset "rollout threads a single rng across steps" begin
39+
rule = DCA(30)
40+
state = [0, 0, 1, 0, 0]
41+
42+
rng = Xoshiro(1)
43+
first_step = next_state(rule, state; scheme = Stochastic(0.5), rng = rng)
44+
second_step = next_state(rule, first_step; scheme = Stochastic(0.5), rng = rng)
45+
46+
@test rollout(rule, state, 2; scheme = Stochastic(0.5), rng = Xoshiro(1)) == second_step
47+
end

0 commit comments

Comments
 (0)