Skip to content

Commit c93299c

Browse files
committed
Basic CA class
See prompts.md for prompt.
1 parent 7a4f1ba commit c93299c

3 files changed

Lines changed: 298 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
results/

CA.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Cellular automaton base class.
2+
3+
Defines a CA class with initialization, neighbor counting, update (to override),
4+
and run loop. Uses a numpy Generator for all randomness and supports
5+
Neumann and Moore neighborhoods with periodic boundaries.
6+
"""
7+
from typing import Tuple, Dict, Optional
8+
9+
import numpy as np
10+
11+
12+
class CA:
13+
"""Base cellular automaton class.
14+
15+
Attributes
16+
- n_species: number of distinct (non-zero) states
17+
- grid: 2D numpy array containing integers in {0, 1, ..., n_species}
18+
- neighborhood: either "neumann" or "moore"
19+
- generator: numpy.random.Generator used for all randomness
20+
- params: global parameters dict
21+
- cell_params: local (per-cell) parameters dict
22+
"""
23+
24+
def __init__(
25+
self,
26+
rows: int,
27+
cols: int,
28+
densities: Tuple[float, ...],
29+
neighborhood: str,
30+
params: Dict[str, object],
31+
cell_params: Dict[str, object],
32+
seed: Optional[int] = None,
33+
) -> None:
34+
"""Initialize the cellular automaton.
35+
36+
Args:
37+
- rows (int): number of rows (>0)
38+
- cols (int): number of columns (>0)
39+
- densities (tuple of floats): initial density for each species. The
40+
length of this tuple defines `n_species`. Values must be >=0 and sum
41+
to at most 1. Each value gives the fraction of the grid to set to
42+
that species (state values are 1..n_species).
43+
- neighborhood (str): either "neumann" (4-neighbors) or "moore"
44+
(8-neighbors).
45+
- params (dict): global parameters.
46+
- cell_params (dict): local per-cell parameters.
47+
- seed (Optional[int]): seed for the numpy random generator.
48+
49+
Returns: None
50+
"""
51+
assert isinstance(rows, int) and rows > 0, "rows must be positive int"
52+
assert isinstance(cols, int) and cols > 0, "cols must be positive int"
53+
assert isinstance(densities, tuple) and len(densities) > 0, "densities must be a non-empty tuple"
54+
for d in densities:
55+
assert isinstance(d, (float, int)) and d >= 0, "each density must be non-negative"
56+
total_density = float(sum(densities))
57+
assert total_density <= 1.0 + 1e-12, "sum of densities must not exceed 1"
58+
assert neighborhood in ("neumann", "moore"), "neighborhood must be 'neumann' or 'moore'"
59+
60+
self.n_species: int = len(densities)
61+
self.params: Dict[str, object] = dict(params) if params is not None else {}
62+
self.cell_params: Dict[str, object] = dict(cell_params) if cell_params is not None else {}
63+
self.neighborhood: str = neighborhood
64+
self.generator: np.random.Generator = np.random.default_rng(seed)
65+
66+
self.grid: np.ndarray = np.zeros((rows, cols), dtype=int)
67+
68+
total_cells = rows * cols
69+
# Fill grid with species states 1..n_species according to densities.
70+
for i, dens in enumerate(densities):
71+
if dens <= 0:
72+
continue
73+
n_to_fill = int(round(total_cells * float(dens)))
74+
if n_to_fill <= 0:
75+
continue
76+
empty_flat = np.flatnonzero(self.grid.ravel() == 0)
77+
if len(empty_flat) == 0:
78+
break
79+
n_choice = min(n_to_fill, len(empty_flat))
80+
chosen = self.generator.choice(empty_flat, size=n_choice, replace=False)
81+
# assign chosen flattened indices to state i+1
82+
r = chosen // cols
83+
c = chosen % cols
84+
self.grid[r, c] = i + 1
85+
86+
def count_neighbors(self) -> Tuple[np.ndarray, ...]:
87+
"""Count neighbors for each non-zero state.
88+
89+
Returns a tuple of numpy arrays, one array for each state in
90+
`1..n_species`. Each returned array has the same shape as `grid`
91+
and contains the integer number of neighbors of that state for
92+
each cell, using periodic boundaries and the configured
93+
neighborhood type.
94+
95+
Returns:
96+
- tuple of np.ndarray: one array per species (state 1..n_species)
97+
"""
98+
counts = []
99+
# Define neighbor shifts
100+
if self.neighborhood == "neumann":
101+
shifts = [(-1, 0), (1, 0), (0, -1), (0, 1)]
102+
else: # moore
103+
shifts = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
104+
105+
for state in range(1, self.n_species + 1):
106+
mask = (self.grid == state).astype(int)
107+
neigh = np.zeros_like(mask)
108+
for dr, dc in shifts:
109+
neigh += np.roll(np.roll(mask, shift=dr, axis=0), shift=dc, axis=1)
110+
counts.append(neigh)
111+
112+
return tuple(counts)
113+
114+
def update(self) -> None:
115+
"""Perform one update step.
116+
117+
This base implementation must be overridden by subclasses. It raises
118+
NotImplementedError to indicate it should be provided by concrete
119+
models that inherit from `CA`.
120+
121+
Returns: None
122+
"""
123+
raise NotImplementedError("Override update() in a subclass to define CA dynamics")
124+
125+
def run(self, steps: int) -> None:
126+
"""Run the CA for a number of steps.
127+
128+
Args:
129+
- steps (int): number of iterations to run (must be non-negative).
130+
131+
Returns: None
132+
"""
133+
assert isinstance(steps, int) and steps >= 0, "steps must be a non-negative integer"
134+
for _ in range(steps):
135+
self.update()
136+

prompts.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
## Create base CA class
2+
For the start of this project, we need a strong baseline. This prompt creates a basic cellular automaton class with general logic. This can then be filled in by overriding the update function and adding visualizations.
3+
4+
### prompt
5+
Create a cellular automaton class named CA with an init function, a count neighbors function, an update function, and a run. The CA should consist of an int n_species, a numpy array called grid, a string containing the neighborhood type, and a numpy random number generator called generator. Use this generator for all random number generation inside the class. The CA class should also contain a dictionary called params for global parameters and a dictionary called cell_params for local parameters.
6+
7+
The init function should take arguments for the grid size (rows, columns, both ints), the initial density of each species (a tuple of floats of which the length defines n_species), the neighborhood type ("neumann" or "moore"), the global parameters in the form of a dictionary params, the local parameters in the form of a dictionary cell_params, and the seed for the generator. It should assign the global and local parameters to their variables and create the generator object and assign it to the generator variable, as well as create the 2D array of zeros based on the grid size and assign it to the grid variable. This grid should then be filled with states dependent on the density tuple. Iterate over the elements i of this tuple, filling grid_size * density[i] elements of the grid with state i+1. Non-zero cell states should not be overwritten, ensuring that the specified percentage of the grid is filled with that state. It should also check if the neighborhood argument corresponds with a known neighborhood and return an error otherwise.
8+
9+
The count neighbors function should return a tuple of matrices (one for each defined non-zero state) containing the amount of neighbors of that state for each cell. It should use the neighborhood defined in the class. Ensure the logic works for both "neumann" and "moore". Use periodic boundaries.
10+
11+
The update function should be overridden when creating a subclass, so it should raise a NotImplemented error.
12+
13+
The run function should take a steps (int) argument. It should then run the CA for steps interations, calling the update function each time.
14+
15+
Finally, make sure to add an expected type for each argument and define the return types. Add this information, as well as a short description of the function to the docstring. Also add assert statements to ensure arguments "make sense". For example, the sum of densities should not exceed 1 and the rows, cols, densities should all be positive, and the neighborhood should be either "neumann" or "moore".
16+
17+
### output
18+
"""Cellular automaton base class.
19+
20+
Defines a CA class with initialization, neighbor counting, update (to override),
21+
and run loop. Uses a numpy Generator for all randomness and supports
22+
Neumann and Moore neighborhoods with periodic boundaries.
23+
"""
24+
from typing import Tuple, Dict, Optional
25+
26+
import numpy as np
27+
28+
29+
class CA:
30+
"""Base cellular automaton class.
31+
32+
Attributes
33+
- n_species: number of distinct (non-zero) states
34+
- grid: 2D numpy array containing integers in {0, 1, ..., n_species}
35+
- neighborhood: either "neumann" or "moore"
36+
- generator: numpy.random.Generator used for all randomness
37+
- params: global parameters dict
38+
- cell_params: local (per-cell) parameters dict
39+
"""
40+
41+
def __init__(
42+
self,
43+
rows: int,
44+
cols: int,
45+
densities: Tuple[float, ...],
46+
neighborhood: str,
47+
params: Dict[str, object],
48+
cell_params: Dict[str, object],
49+
seed: Optional[int] = None,
50+
) -> None:
51+
"""Initialize the cellular automaton.
52+
53+
Args:
54+
- rows (int): number of rows (>0)
55+
- cols (int): number of columns (>0)
56+
- densities (tuple of floats): initial density for each species. The
57+
length of this tuple defines `n_species`. Values must be >=0 and sum
58+
to at most 1. Each value gives the fraction of the grid to set to
59+
that species (state values are 1..n_species).
60+
- neighborhood (str): either "neumann" (4-neighbors) or "moore"
61+
(8-neighbors).
62+
- params (dict): global parameters.
63+
- cell_params (dict): local per-cell parameters.
64+
- seed (Optional[int]): seed for the numpy random generator.
65+
66+
Returns: None
67+
"""
68+
assert isinstance(rows, int) and rows > 0, "rows must be positive int"
69+
assert isinstance(cols, int) and cols > 0, "cols must be positive int"
70+
assert isinstance(densities, tuple) and len(densities) > 0, "densities must be a non-empty tuple"
71+
for d in densities:
72+
assert isinstance(d, (float, int)) and d >= 0, "each density must be non-negative"
73+
total_density = float(sum(densities))
74+
assert total_density <= 1.0 + 1e-12, "sum of densities must not exceed 1"
75+
assert neighborhood in ("neumann", "moore"), "neighborhood must be 'neumann' or 'moore'"
76+
77+
self.n_species: int = len(densities)
78+
self.params: Dict[str, object] = dict(params) if params is not None else {}
79+
self.cell_params: Dict[str, object] = dict(cell_params) if cell_params is not None else {}
80+
self.neighborhood: str = neighborhood
81+
self.generator: np.random.Generator = np.random.default_rng(seed)
82+
83+
self.grid: np.ndarray = np.zeros((rows, cols), dtype=int)
84+
85+
total_cells = rows * cols
86+
# Fill grid with species states 1..n_species according to densities.
87+
for i, dens in enumerate(densities):
88+
if dens <= 0:
89+
continue
90+
n_to_fill = int(round(total_cells * float(dens)))
91+
if n_to_fill <= 0:
92+
continue
93+
empty_flat = np.flatnonzero(self.grid.ravel() == 0)
94+
if len(empty_flat) == 0:
95+
break
96+
n_choice = min(n_to_fill, len(empty_flat))
97+
chosen = self.generator.choice(empty_flat, size=n_choice, replace=False)
98+
# assign chosen flattened indices to state i+1
99+
r = chosen // cols
100+
c = chosen % cols
101+
self.grid[r, c] = i + 1
102+
103+
def count_neighbors(self) -> Tuple[np.ndarray, ...]:
104+
"""Count neighbors for each non-zero state.
105+
106+
Returns a tuple of numpy arrays, one array for each state in
107+
`1..n_species`. Each returned array has the same shape as `grid`
108+
and contains the integer number of neighbors of that state for
109+
each cell, using periodic boundaries and the configured
110+
neighborhood type.
111+
112+
Returns:
113+
- tuple of np.ndarray: one array per species (state 1..n_species)
114+
"""
115+
rows, cols = self.grid.shape
116+
counts = []
117+
# Define neighbor shifts
118+
if self.neighborhood == "neumann":
119+
shifts = [(-1, 0), (1, 0), (0, -1), (0, 1)]
120+
else: # moore
121+
shifts = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
122+
123+
for state in range(1, self.n_species + 1):
124+
mask = (self.grid == state).astype(int)
125+
neigh = np.zeros_like(mask)
126+
for dr, dc in shifts:
127+
neigh += np.roll(np.roll(mask, shift=dr, axis=0), shift=dc, axis=1)
128+
counts.append(neigh)
129+
130+
return tuple(counts)
131+
132+
def update(self) -> None:
133+
"""Perform one update step.
134+
135+
This base implementation must be overridden by subclasses. It raises
136+
NotImplementedError to indicate it should be provided by concrete
137+
models that inherit from `CA`.
138+
139+
Returns: None
140+
"""
141+
raise NotImplementedError("Override update() in a subclass to define CA dynamics")
142+
143+
def run(self, steps: int) -> None:
144+
"""Run the CA for a number of steps.
145+
146+
Args:
147+
- steps (int): number of iterations to run (must be non-negative).
148+
149+
Returns: None
150+
"""
151+
assert isinstance(steps, int) and steps >= 0, "steps must be a non-negative integer"
152+
for _ in range(steps):
153+
self.update()
154+
155+
156+
### changes
157+
Removed a few unnecessary lines.
158+
159+
## Add update logic
160+
Now the basic predator-prey update rules need to be defined. This is done

0 commit comments

Comments
 (0)