Skip to content

Commit 1a1a462

Browse files
committed
Improvements
1 parent 628d4cf commit 1a1a462

File tree

5 files changed

+1050
-248
lines changed

5 files changed

+1050
-248
lines changed

cellular_automata/Von_Neumann_CA.py

Lines changed: 0 additions & 94 deletions
This file was deleted.

cellular_automata/von_neumann.py

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
"""
2+
Von Neumann cellular automaton with multi-state fading "heatmap" effect.
3+
4+
This implementation demonstrates a Von Neumann cellular automaton where cells
5+
follow custom birth/survive rules and dead cells fade gradually through multiple
6+
visual states, creating a heatmap-like effect.
7+
8+
Based on Von Neumann cellular automata architecture:
9+
https://en.wikipedia.org/wiki/Von_Neumann_cellular_automaton
10+
11+
Von Neumann neighborhood reference:
12+
https://en.wikipedia.org/wiki/Von_Neumann_neighborhood
13+
14+
Requirements: numpy
15+
"""
16+
17+
import numpy as np
18+
from typing import Set, Tuple, Dict, Optional
19+
20+
21+
def create_random_grid(
22+
rows: int, columns: int, alive_probability: float, seed: Optional[int] = None
23+
) -> np.ndarray:
24+
"""
25+
Create initial grid with randomly distributed alive cells.
26+
27+
Args:
28+
rows: Number of grid rows
29+
columns: Number of grid columns
30+
alive_probability: Probability (0.0-1.0) of each cell being initially alive
31+
seed: Random seed for reproducibility
32+
33+
Returns:
34+
2D numpy array where 1 represents alive cells, 0 represents dead cells
35+
36+
Raises:
37+
ValueError: If alive_probability is not between 0 and 1
38+
ValueError: If rows or columns are not positive integers
39+
40+
Examples:
41+
>>> grid = create_random_grid(3, 3, 0.5, seed=42)
42+
>>> grid.shape
43+
(3, 3)
44+
>>> bool(np.all((grid == 0) | (grid == 1)))
45+
True
46+
>>> grid.dtype
47+
dtype('uint8')
48+
49+
>>> create_random_grid(0, 3, 0.5) # doctest: +IGNORE_EXCEPTION_DETAIL
50+
Traceback (most recent call last):
51+
ValueError: Rows and columns must be positive integers
52+
53+
>>> create_random_grid(3, 3, 1.5) # doctest: +IGNORE_EXCEPTION_DETAIL
54+
Traceback (most recent call last):
55+
ValueError: alive_probability must be between 0.0 and 1.0
56+
"""
57+
if rows <= 0 or columns <= 0:
58+
raise ValueError("Rows and columns must be positive integers")
59+
if not 0.0 <= alive_probability <= 1.0:
60+
raise ValueError("alive_probability must be between 0.0 and 1.0")
61+
62+
rng = np.random.default_rng(seed)
63+
alive_cells = (rng.random((rows, columns)) < alive_probability).astype(np.uint8)
64+
return alive_cells
65+
66+
67+
def count_von_neumann_neighbors(
68+
alive_mask: np.ndarray, use_wraparound: bool = True
69+
) -> np.ndarray:
70+
"""
71+
Count Von Neumann neighbors for each cell (4-directional neighborhood).
72+
73+
The Von Neumann neighborhood consists of the four orthogonally adjacent cells
74+
(up, down, left, right) but excludes diagonal neighbors.
75+
76+
Args:
77+
alive_mask: Binary 2D array where 1 represents alive cells
78+
use_wraparound: If True, edges wrap around (toroidal topology)
79+
80+
Returns:
81+
2D array with neighbor counts (0-4) for each cell
82+
83+
Raises:
84+
ValueError: If alive_mask is not 2D or contains invalid values
85+
86+
Examples:
87+
>>> mask = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=np.uint8)
88+
>>> counts = count_von_neumann_neighbors(mask, use_wraparound=False)
89+
>>> int(counts[1, 1]) # center cell has 0 neighbors (all adjacent are 0)
90+
0
91+
>>> int(counts[0, 1]) # top middle has 3 neighbors (down, left, right are 1)
92+
3
93+
94+
>>> mask_simple = np.array([[1, 1], [1, 0]], dtype=np.uint8)
95+
>>> counts_simple = count_von_neumann_neighbors(mask_simple, use_wraparound=False)
96+
>>> int(counts_simple[0, 0]) # top-left has 2 neighbors (right and down)
97+
2
98+
99+
>>> invalid_mask = np.array([1, 2, 3]) # doctest: +IGNORE_EXCEPTION_DETAIL
100+
>>> count_von_neumann_neighbors(invalid_mask)
101+
Traceback (most recent call last):
102+
ValueError: alive_mask must be a 2D array
103+
"""
104+
if alive_mask.ndim != 2:
105+
raise ValueError("alive_mask must be a 2D array")
106+
if not np.all((alive_mask == 0) | (alive_mask == 1)):
107+
raise ValueError("alive_mask must contain only 0s and 1s")
108+
109+
rows, cols = alive_mask.shape
110+
neighbor_counts = np.zeros((rows, cols), dtype=np.uint8)
111+
112+
if use_wraparound:
113+
# Use rolling for wraparound
114+
up_neighbors = np.roll(alive_mask, -1, axis=0)
115+
down_neighbors = np.roll(alive_mask, 1, axis=0)
116+
left_neighbors = np.roll(alive_mask, -1, axis=1)
117+
right_neighbors = np.roll(alive_mask, 1, axis=1)
118+
neighbor_counts = up_neighbors + down_neighbors + left_neighbors + right_neighbors
119+
else:
120+
# Manually count neighbors without wraparound
121+
for r in range(rows):
122+
for c in range(cols):
123+
count = 0
124+
# Check up
125+
if r > 0 and alive_mask[r-1, c]:
126+
count += 1
127+
# Check down
128+
if r < rows-1 and alive_mask[r+1, c]:
129+
count += 1
130+
# Check left
131+
if c > 0 and alive_mask[r, c-1]:
132+
count += 1
133+
# Check right
134+
if c < cols-1 and alive_mask[r, c+1]:
135+
count += 1
136+
neighbor_counts[r, c] = count
137+
138+
return neighbor_counts
139+
140+
141+
def apply_cellular_automaton_rules(
142+
current_ages: np.ndarray,
143+
birth_neighbor_counts: Set[int],
144+
survival_neighbor_counts: Set[int],
145+
maximum_age: int = 5,
146+
use_wraparound: bool = True,
147+
) -> np.ndarray:
148+
"""
149+
Apply cellular automaton rules to advance the grid by one generation.
150+
151+
Cells are born when they have a neighbor count in birth_neighbor_counts.
152+
Living cells survive when they have a neighbor count in survival_neighbor_counts.
153+
Dead cells age and eventually disappear completely.
154+
155+
Args:
156+
current_ages: 2D array where values represent cell ages (0 = dead, >0 = alive)
157+
birth_neighbor_counts: Set of neighbor counts that cause cell birth
158+
survival_neighbor_counts: Set of neighbor counts that allow cell survival
159+
maximum_age: Maximum age before cell disappears completely
160+
use_wraparound: Whether to use wraparound boundaries
161+
162+
Returns:
163+
New 2D array with updated cell ages after applying rules
164+
165+
Raises:
166+
ValueError: If inputs are invalid
167+
168+
Examples:
169+
>>> ages = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.uint8)
170+
>>> new_ages = apply_cellular_automaton_rules(
171+
... ages, birth_neighbor_counts={2},
172+
... survival_neighbor_counts={2, 3}, use_wraparound=False
173+
... )
174+
>>> bool(new_ages[0, 0] > 0) # corner should be born (2 neighbors: right and down)
175+
True
176+
177+
>>> # Test aging of dead cells
178+
>>> dead_aging = np.array([[2, 0, 0]], dtype=np.uint8) # age 2, no survival
179+
>>> result = apply_cellular_automaton_rules(
180+
... dead_aging, birth_neighbor_counts=set(),
181+
... survival_neighbor_counts=set(), maximum_age=3
182+
... )
183+
>>> bool(result[0, 0] == 3) # should age from 2 to 3
184+
True
185+
186+
>>> apply_cellular_automaton_rules(np.array([1, 2]), {1}, {1}) # doctest: +IGNORE_EXCEPTION_DETAIL
187+
Traceback (most recent call last):
188+
ValueError: current_ages must be a 2D array
189+
"""
190+
if current_ages.ndim != 2:
191+
raise ValueError("current_ages must be a 2D array")
192+
if maximum_age < 1:
193+
raise ValueError("maximum_age must be at least 1")
194+
195+
alive_cells_mask = current_ages > 0
196+
neighbor_counts = count_von_neumann_neighbors(
197+
alive_cells_mask.astype(np.uint8), use_wraparound
198+
)
199+
200+
# Determine which cells are born or survive
201+
birth_mask = (~alive_cells_mask) & np.isin(neighbor_counts, list(birth_neighbor_counts))
202+
survival_mask = alive_cells_mask & np.isin(neighbor_counts, list(survival_neighbor_counts))
203+
204+
new_ages = current_ages.copy()
205+
206+
# Set ages for newly born cells
207+
new_ages[birth_mask] = 1
208+
209+
# Reset age for surviving cells (keeps them visually fresh)
210+
new_ages[survival_mask] = 1
211+
212+
# Age cells that neither survive nor get born
213+
fade_mask = (~birth_mask) & (~survival_mask)
214+
new_ages[fade_mask & (new_ages > 0)] += 1
215+
216+
# Remove cells that have exceeded maximum age
217+
new_ages[new_ages > maximum_age] = 0
218+
219+
return new_ages
220+
221+
222+
def simulate_von_neumann_cellular_automaton(
223+
grid_rows: int = 20,
224+
grid_columns: int = 40,
225+
initial_alive_probability: float = 0.25,
226+
birth_rules: Set[int] = None,
227+
survival_rules: Set[int] = None,
228+
maximum_cell_age: int = 5,
229+
generations: int = 100,
230+
random_seed: Optional[int] = None,
231+
use_wraparound_edges: bool = True,
232+
) -> list[np.ndarray]:
233+
"""
234+
Run a complete Von Neumann cellular automaton simulation.
235+
236+
This function creates an initial random grid and evolves it through multiple
237+
generations according to the specified birth and survival rules.
238+
239+
Args:
240+
grid_rows: Number of rows in the grid
241+
grid_columns: Number of columns in the grid
242+
initial_alive_probability: Initial probability of cells being alive
243+
birth_rules: Set of neighbor counts that cause birth (default: {3})
244+
survival_rules: Set of neighbor counts that allow survival (default: {1, 2})
245+
maximum_cell_age: Maximum age before cells disappear (default: 5)
246+
generations: Number of generations to simulate
247+
random_seed: Seed for random number generation
248+
use_wraparound_edges: Whether to use toroidal topology
249+
250+
Returns:
251+
List of 2D numpy arrays representing each generation
252+
253+
Raises:
254+
ValueError: If parameters are invalid
255+
256+
Examples:
257+
>>> result = simulate_von_neumann_cellular_automaton(
258+
... grid_rows=5, grid_columns=5, generations=3, random_seed=42
259+
... )
260+
>>> len(result) == 3
261+
True
262+
>>> all(grid.shape == (5, 5) for grid in result)
263+
True
264+
265+
>>> simulate_von_neumann_cellular_automaton(generations=0) # doctest: +IGNORE_EXCEPTION_DETAIL
266+
Traceback (most recent call last):
267+
ValueError: generations must be positive
268+
"""
269+
if birth_rules is None:
270+
birth_rules = {3}
271+
if survival_rules is None:
272+
survival_rules = {1, 2}
273+
274+
if generations <= 0:
275+
raise ValueError("generations must be positive")
276+
if grid_rows <= 0 or grid_columns <= 0:
277+
raise ValueError("grid dimensions must be positive")
278+
279+
# Initialize grid
280+
current_grid = create_random_grid(
281+
grid_rows, grid_columns, initial_alive_probability, random_seed
282+
)
283+
284+
generation_history = []
285+
286+
# Run simulation for specified number of generations
287+
for _ in range(generations):
288+
generation_history.append(current_grid.copy())
289+
current_grid = apply_cellular_automaton_rules(
290+
current_grid,
291+
birth_rules,
292+
survival_rules,
293+
maximum_cell_age,
294+
use_wraparound_edges,
295+
)
296+
297+
return generation_history
298+
299+
300+
if __name__ == "__main__":
301+
import doctest
302+
doctest.testmod(verbose=True)

0 commit comments

Comments
 (0)