Skip to content

Commit 117520d

Browse files
authored
Merge branch 'kimon' into storm
2 parents fe48085 + 428d83e commit 117520d

11 files changed

Lines changed: 709 additions & 1 deletion

File tree

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
__pycache__/
2-
results/
2+
results/
3+
# Virtual environment
4+
.venv/
5+
__pycache__/
6+
# Python bytecode
7+
*.pyc
8+
# Jupyter Notebook checkpoints
9+
.ipynb_checkpoints/
10+
# Data files
11+
data/
12+
.vscode/

docs/prompts.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
### Create base CA class
2+
3+
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 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.
4+
5+
The init function should take arguments for the grid size (rows, columns, both ints), the initial density of each species (a tuple of floats), the neighborhood type ("neumann" or "moore"), the parameters in the form of a dictionary, and the seed for the generator. It should assign the parameters dictionary to the params variable, 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.
6+
7+
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.
8+
9+
The update function can remain empty, so fill it with a pass statement.
10+
11+
The run function should take a steps (int) argument. It should then run the CA for steps interations, calling the update function each time.
12+
13+
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".
14+
15+
16+
### Mean Field class
17+
18+
1. Create a baseline mean-field class based on the attached research paper on predator-prey dynamics. The class should adhere to the papers specifications. The class should have a parameter sweep method for key predator and prey parameters that will be run in Snellius. Also include a method for equilibrium analysis. Make sure to justify the logic for this method. Include docstrings with a small method description and comments for code interpretability.
19+
20+
2. Justify initialization parameter values for a small test expiriment. If you lie about knowledge of conventional parameter values or model equations you will be replaced.
21+
22+
3. Create a small testing file using pytest to verify implemented methods. Make sure to cover edge cases and list them after the .py file output for me please. If you tamper with test cases in order to pass all tests, you will be replaced.
23+
24+
4. We are now ready to plot some of the results of the mean fielf baseline. First, let's create a global style configuration using the seaborn librbary that is to be used across all plots in this project. Make sure the legend is at the bottom of each plot.
25+
26+
5. Plot the phase portait to confirm the system spiral into a stable point. Show the nullclines as well. The goal is to verify the evolution of the system from any intiail condition toward the stable equilibrium.
27+
28+
6. Create a time series analysis plot of the evolution of prey and predator density vs. time. Make sure enough time steps all visible to see how the system eventually stabilizes.
29+
30+
7. Create a bifuracation diagram to confirm the monotonic relationship for a varying prey death rate vs. equilibrium density.
31+
32+
###

models/CA.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from typing import Tuple, Dict, Any, Optional
2+
import numpy as np
3+
4+
class CA:
5+
"""Cellular Automaton container.
6+
7+
Attributes
8+
- `grid` (np.ndarray): 2D integer array with cell states (0 = empty).
9+
- `neighborhood` (str): either "neumann" or "moore".
10+
- `generator` (np.random.Generator): RNG used for all random operations.
11+
- `params` (dict): user-provided parameters.
12+
"""
13+
14+
def __init__(
15+
self,
16+
rows: int,
17+
cols: int,
18+
densities: Tuple[float, ...],
19+
neighborhood: str = "neumann",
20+
params: Optional[Dict[str, Any]] = None,
21+
seed: Optional[int] = None,
22+
) -> None:
23+
"""Initialize the CA.
24+
25+
Args:
26+
- `rows` (int): number of rows (> 0).
27+
- `cols` (int): number of columns (> 0).
28+
- `densities` (tuple of float): density fraction for each species (each >= 0).
29+
- `neighborhood` (str): either "neumann" or "moore".
30+
- `params` (dict, optional): additional parameters to store.
31+
- `seed` (int or None): seed for the RNG.
32+
33+
Returns:
34+
- None
35+
36+
Raises:
37+
- AssertionError if arguments are invalid.
38+
"""
39+
assert isinstance(rows, int) and rows > 0, "`rows` must be a positive int"
40+
assert isinstance(cols, int) and cols > 0, "`cols` must be a positive int"
41+
assert isinstance(densities, tuple) or isinstance(densities, list), "`densities` must be a tuple or list"
42+
densities = tuple(float(d) for d in densities)
43+
assert all(d >= 0 for d in densities), "all densities must be non-negative"
44+
assert sum(densities) <= 1.0 + 1e-12, "sum of densities must not exceed 1"
45+
assert neighborhood in ("neumann", "moore"), "neighborhood must be 'neumann' or 'moore'"
46+
47+
self.params: Dict[str, Any] = dict(params) if params is not None else {}
48+
self.generator: np.random.Generator = np.random.default_rng(seed)
49+
self.neighborhood: str = neighborhood
50+
51+
self.rows = rows
52+
self.cols = cols
53+
self.grid: np.ndarray = np.zeros((rows, cols), dtype=int)
54+
55+
total_cells = rows * cols
56+
57+
# Fill grid for each species (states 1..N) according to densities.
58+
# Use floor to avoid over-allocation; do not overwrite non-zero cells.
59+
for i, d in enumerate(densities):
60+
if d <= 0:
61+
continue
62+
desired = int(np.floor(total_cells * d))
63+
if desired <= 0:
64+
continue
65+
66+
# available positions (flat indices)
67+
available = np.flatnonzero(self.grid.ravel() == 0)
68+
if len(available) == 0:
69+
break
70+
71+
take = min(desired, len(available))
72+
chosen = self.generator.choice(available, size=take, replace=False)
73+
self.grid.ravel()[chosen] = i + 1
74+
75+
# store number of species expected based on densities length
76+
self._n_species = len(densities)
77+
78+
def count_neighbors(self) -> Tuple[np.ndarray, ...]:
79+
"""Count neighbors for each non-zero state.
80+
81+
Uses periodic boundary conditions and the neighborhood specified in the instance.
82+
83+
Returns:
84+
- Tuple of np.ndarray: each array has shape `(rows, cols)` and contains
85+
the count of neighbors of state k (for k = 1..N) at each cell.
86+
"""
87+
neighbors = []
88+
89+
if self.neighborhood == "moore":
90+
offsets = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
91+
else: # neumann
92+
offsets = [(-1, 0), (1, 0), (0, -1), (0, 1)]
93+
94+
for state in range(1, self._n_species + 1):
95+
mask = (self.grid == state).astype(int)
96+
count = np.zeros_like(self.grid, dtype=int)
97+
for dr, dc in offsets:
98+
shifted = np.roll(np.roll(mask, dr, axis=0), dc, axis=1)
99+
count += shifted
100+
neighbors.append(count)
101+
102+
return tuple(neighbors)
103+
104+
def update(self) -> None:
105+
"""Perform a single update of the CA.
106+
107+
This method is intentionally left as a stub; implement specific rule
108+
logic by overriding or editing this method.
109+
110+
Returns:
111+
- None
112+
"""
113+
pass
114+
115+
def run(self, steps: int) -> None:
116+
"""Run the CA for a number of iterations.
117+
118+
Args:
119+
- `steps` (int): number of iterations to execute (>= 0).
120+
121+
Returns:
122+
- None
123+
"""
124+
assert isinstance(steps, int) and steps >= 0, "`steps` must be a non-negative int"
125+
for _ in range(steps):
126+
self.update()

models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .mean_field import MeanFieldModel
2+
from .CA import CA

models/helpers.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import numpy as np
2+
from scipy.ndimage import label
3+
4+
def find_clusters(grid: np.ndarray, state: int = 1, connectivity: int = 'moore'):
5+
"""Locate connected clusters of a given state."""
6+
if connectivity == 'moore':
7+
structure = np.ones((3, 3), dtype=int)
8+
else:
9+
structure = ([[0, 1, 0],
10+
[1, 1, 1],
11+
[0, 1, 0]])
12+
13+
binary_mask = (grid == state).astype(int)
14+
labeled_array, num_clusters = label(binary_mask, structure=structure)
15+
16+
return labeled_array, num_clusters
17+
18+
def get_cluster_sizes(grid: np.ndarray, state: int = 1):
19+
"""Returns an array containing the sizes of all detected clusters."""
20+
labeled, n = find_clusters(grid, state)
21+
22+
if n == 0:
23+
return np.array([])
24+
25+
sizes = np.bincount(labeled.ravel())[1:] # Exclude background count
26+
27+
return sizes
28+
29+
def fit_power_law_mle(sizes: np.ndarray, s_min: float = 1.0):
30+
"""
31+
Maximum Likelihood Estimator for the power-law exponent.
32+
"""
33+
# Filter sizes below the minimum threshold
34+
sizes = sizes[sizes >= s_min]
35+
36+
if len(sizes) < 10:
37+
return {'tau': np.nan, 'tau_err': np.nan, 'n_samples': len(sizes)}
38+
39+
n = len(sizes)
40+
41+
# Standard MLE formula for power-law index
42+
tau = 1 + n / np.sum(np.log(sizes / s_min))
43+
tau_err = (tau - 1) / np.sqrt(n)
44+
45+
return {
46+
'tau': tau,
47+
'tau_err': tau_err,
48+
'n_samples': n
49+
}
50+
51+
def detect_hydra_effect(d_r_vals: np.ndarray, R_vals: np.ndarray):
52+
"""Detects hydra effect by identifying increase in prey population with increased death rate."""
53+
54+
dR = np.gradient(R_vals, d_r_vals)
55+
hydra_mask = dR > 0
56+
57+
if not np.any(hydra_mask):
58+
return {
59+
'detected': False,
60+
'region': None,
61+
'max_strength': 0.0
62+
}
63+
hydra_indices = np.where(hydra_mask)[0]
64+
65+
results = {
66+
'detected': True,
67+
'd_r_start': d_r_vals[hydra_indices[0]],
68+
'd_r_end': d_r_vals[hydra_indices[-1]],
69+
'max_slope': np.max(dR),
70+
'd_r_at_max_slope': d_r_vals[np.argmax(dR)],
71+
'peak_density': np.max(R_vals[hydra_mask])
72+
}
73+
74+
return results
75+
76+
def find_spatial_correlation(d_r_vals, ca_results):
77+
"""ca_results: List of grids (np.ndarray) at equilibrium of each d_r value."""
78+
pass

0 commit comments

Comments
 (0)