|
| 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