Skip to content

Commit 5f41b62

Browse files
committed
Added synchronous updating
1 parent 2269850 commit 5f41b62

2 files changed

Lines changed: 218 additions & 2 deletions

File tree

CA.py

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,110 @@ def __init__(
196196
self.synchronous: bool = bool(synchronous)
197197

198198
def update_sync(self) -> None:
199-
"""Synchronous update (not implemented)."""
200-
raise NotImplementedError("Synchronous PP update not implemented")
199+
"""Synchronous (vectorized) update.
200+
201+
Implements a vectorized equivalent of the random-sequential
202+
asynchronous update. Each occupied cell (prey or predator) gets at
203+
most one reproduction attempt: with probability `birth` it chooses a
204+
random neighbor and, if that neighbor in the reference grid has the
205+
required target state (empty for prey, prey for predator), it
206+
becomes a candidate attempt. When multiple reproducers target the
207+
same cell, one attempt is chosen uniformly at random to succeed.
208+
Deaths are applied the same vectorized way as in the async update.
209+
"""
210+
211+
rows, cols = self.grid.shape
212+
grid_ref = self.grid.copy()
213+
214+
# Precompute neighbor shifts and arrays for indexing
215+
if self.neighborhood == "neumann":
216+
shifts = [(-1, 0), (1, 0), (0, -1), (0, 1)]
217+
else:
218+
shifts = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
219+
dr_arr = np.array([s[0] for s in shifts], dtype=int)
220+
dc_arr = np.array([s[1] for s in shifts], dtype=int)
221+
n_shifts = len(shifts)
222+
223+
def _process_reproduction(sources, birth_prob, target_state_required, new_state_val):
224+
"""Handle reproduction attempts from `sources`.
225+
226+
sources: (M,2) array of (r,c) positions in grid_ref
227+
birth_prob: scalar probability that a source attempts reproduction
228+
target_state_required: state value required in grid_ref at target
229+
new_state_val: state to write into self.grid for successful targets
230+
"""
231+
if sources.size == 0:
232+
return
233+
234+
M = sources.shape[0]
235+
# Which sources attempt reproduction
236+
attempt_mask = self.generator.random(M) < float(birth_prob)
237+
if not np.any(attempt_mask):
238+
return
239+
240+
src = sources[attempt_mask]
241+
K = src.shape[0]
242+
243+
# Each attempting source picks one neighbor uniformly
244+
nbr_idx = self.generator.integers(0, n_shifts, size=K)
245+
nr = (src[:, 0] + dr_arr[nbr_idx]) % rows
246+
247+
nc = (src[:, 1] + dc_arr[nbr_idx]) % cols
248+
249+
# Only keep attempts where the reference grid at the target has the required state
250+
valid_mask = (grid_ref[nr, nc] == target_state_required)
251+
if not np.any(valid_mask):
252+
return
253+
254+
nr = nr[valid_mask]
255+
nc = nc[valid_mask]
256+
257+
# Flatten target indices to group collisions
258+
target_flat = (nr * cols + nc).astype(np.int64)
259+
# Sort targets to find groups that target the same cell
260+
order = np.argsort(target_flat)
261+
tf_sorted = target_flat[order]
262+
263+
# unique targets (on the sorted array) with start indices and counts
264+
uniq_targets, idx_start, counts = np.unique(tf_sorted, return_index=True, return_counts=True)
265+
if uniq_targets.size == 0:
266+
return
267+
268+
# For each unique target, pick one attempt uniformly at random
269+
# idx_start gives indices into the sorted array
270+
chosen_sorted_positions = []
271+
for start, cnt in zip(idx_start, counts):
272+
off = int(self.generator.integers(0, cnt))
273+
chosen_sorted_positions.append(start + off)
274+
chosen_sorted_positions = np.array(chosen_sorted_positions, dtype=int)
275+
276+
# Map back to indices in the filtered attempts array
277+
chosen_indices = order[chosen_sorted_positions]
278+
279+
chosen_target_flats = target_flat[chosen_indices]
280+
chosen_rs = (chosen_target_flats // cols).astype(int)
281+
chosen_cs = (chosen_target_flats % cols).astype(int)
282+
283+
# Apply successful births to the main grid
284+
self.grid[chosen_rs, chosen_cs] = new_state_val
285+
286+
# Prey reproduce into empty cells (target state 0 -> new state 1)
287+
prey_sources = np.argwhere(grid_ref == 1)
288+
_process_reproduction(prey_sources, self.params["prey_birth"], 0, 1)
289+
290+
# Predators reproduce into prey cells (target state 1 -> new state 2)
291+
pred_sources = np.argwhere(grid_ref == 2)
292+
_process_reproduction(pred_sources, self.params["predator_birth"], 1, 2)
293+
294+
# Vectorized synchronous deaths (same as async implementation)
295+
rand_prey = self.generator.random(self.grid.shape)
296+
rand_pred = self.generator.random(self.grid.shape)
297+
298+
prey_death_mask = (grid_ref == 1) & (rand_prey < float(self.params["prey_death"])) & (self.grid == 1)
299+
pred_death_mask = (grid_ref == 2) & (rand_pred < float(self.params["predator_death"])) & (self.grid == 2)
300+
301+
self.grid[prey_death_mask] = 0
302+
self.grid[pred_death_mask] = 0
201303

202304
def update_async(self) -> None:
203305
"""Asynchronous (random-sequential) update.

prompts.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,117 @@ class PP(CA):
299299

300300
### changes
301301

302+
## Add synchronous updating
303+
Now we add the asyncrhonous updating, making sure the functionality is the vectorized equivalent of the asynchronous function.
304+
305+
### prompt
306+
Implement the synchronous updating for the PP class as a vectorized (optimized) equivalent to the asynchronous updating. First, create a copy of the current grid. The reproduction rules can be either implemented by using the empty or prey cells as the anchor and looking for prey or predator neighbors that can reproduce into these anchor cells or by using the prey or predator cells as the anchor and looking for empty or prey cells that the anchor cells can reproduce into. Ensure that the probability of a cell being reproduced into increases as more neighboring cells are occupied by the relevant species. However, each prey and predator must only be able to reproduce into a single cell. The count_neighbors function can be utilized if deemed necessary. The rules for the deaths should be the same as the asynchronous updating, as they are already vectorized. Make sure the synchronous updating is an optimized and thus faster equivalent of the asynchronous updating.
307+
308+
### output
309+
def update_sync(self) -> None:
310+
"""Synchronous (vectorized) update.
311+
312+
Implements a vectorized equivalent of the random-sequential
313+
asynchronous update. Each occupied cell (prey or predator) gets at
314+
most one reproduction attempt: with probability `birth` it chooses a
315+
random neighbor and, if that neighbor in the reference grid has the
316+
required target state (empty for prey, prey for predator), it
317+
becomes a candidate attempt. When multiple reproducers target the
318+
same cell, one attempt is chosen uniformly at random to succeed.
319+
Deaths are applied the same vectorized way as in the async update.
320+
"""
321+
322+
rows, cols = self.grid.shape
323+
grid_ref = self.grid.copy()
324+
325+
# Precompute neighbor shifts and arrays for indexing
326+
if self.neighborhood == "neumann":
327+
shifts = [(-1, 0), (1, 0), (0, -1), (0, 1)]
328+
else:
329+
shifts = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
330+
dr_arr = np.array([s[0] for s in shifts], dtype=int)
331+
dc_arr = np.array([s[1] for s in shifts], dtype=int)
332+
n_shifts = len(shifts)
333+
334+
def _process_reproduction(sources, birth_prob, target_state_required, new_state_val):
335+
"""Handle reproduction attempts from `sources`.
336+
337+
sources: (M,2) array of (r,c) positions in grid_ref
338+
birth_prob: scalar probability that a source attempts reproduction
339+
target_state_required: state value required in grid_ref at target
340+
new_state_val: state to write into self.grid for successful targets
341+
"""
342+
if sources.size == 0:
343+
return
344+
345+
M = sources.shape[0]
346+
# Which sources attempt reproduction
347+
attempt_mask = self.generator.random(M) < float(birth_prob)
348+
if not np.any(attempt_mask):
349+
return
350+
351+
src = sources[attempt_mask]
352+
K = src.shape[0]
353+
354+
# Each attempting source picks one neighbor uniformly
355+
nbr_idx = self.generator.integers(0, n_shifts, size=K)
356+
nr = (src[:, 0] + dr_arr[nbr_idx]) % rows
357+
358+
nc = (src[:, 1] + dc_arr[nbr_idx]) % cols
359+
360+
# Only keep attempts where the reference grid at the target has the required state
361+
valid_mask = (grid_ref[nr, nc] == target_state_required)
362+
if not np.any(valid_mask):
363+
return
364+
365+
nr = nr[valid_mask]
366+
nc = nc[valid_mask]
367+
368+
# Flatten target indices to group collisions
369+
target_flat = (nr * cols + nc).astype(np.int64)
370+
# Sort targets to find groups that target the same cell
371+
order = np.argsort(target_flat)
372+
tf_sorted = target_flat[order]
373+
374+
# unique targets (on the sorted array) with start indices and counts
375+
uniq_targets, idx_start, counts = np.unique(tf_sorted, return_index=True, return_counts=True)
376+
if uniq_targets.size == 0:
377+
return
378+
379+
# For each unique target, pick one attempt uniformly at random
380+
# idx_start gives indices into the sorted array
381+
chosen_sorted_positions = []
382+
for start, cnt in zip(idx_start, counts):
383+
off = int(self.generator.integers(0, cnt))
384+
chosen_sorted_positions.append(start + off)
385+
chosen_sorted_positions = np.array(chosen_sorted_positions, dtype=int)
386+
387+
# Map back to indices in the filtered attempts array
388+
chosen_indices = order[chosen_sorted_positions]
389+
390+
chosen_target_flats = target_flat[chosen_indices]
391+
chosen_rs = (chosen_target_flats // cols).astype(int)
392+
chosen_cs = (chosen_target_flats % cols).astype(int)
393+
394+
# Apply successful births to the main grid
395+
self.grid[chosen_rs, chosen_cs] = new_state_val
396+
397+
# Prey reproduce into empty cells (target state 0 -> new state 1)
398+
prey_sources = np.argwhere(grid_ref == 1)
399+
_process_reproduction(prey_sources, self.params["prey_birth"], 0, 1)
400+
401+
# Predators reproduce into prey cells (target state 1 -> new state 2)
402+
pred_sources = np.argwhere(grid_ref == 2)
403+
_process_reproduction(pred_sources, self.params["predator_birth"], 1, 2)
404+
405+
# Vectorized synchronous deaths (same as async implementation)
406+
rand_prey = self.generator.random(self.grid.shape)
407+
rand_pred = self.generator.random(self.grid.shape)
408+
409+
prey_death_mask = (grid_ref == 1) & (rand_prey < float(self.params["prey_death"])) & (self.grid == 1)
410+
pred_death_mask = (grid_ref == 2) & (rand_pred < float(self.params["predator_death"])) & (self.grid == 2)
411+
412+
self.grid[prey_death_mask] = 0
413+
self.grid[pred_death_mask] = 0
414+
415+
### changes

0 commit comments

Comments
 (0)