Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Conway's Game of Life

A comprehensive, modular Python implementation demonstrating efficient scientific computing techniques.

Python 3.13 Code Style: Ruff License: MIT


NumPy SciPy Numba CuPy Dear PyGui SQLAlchemy Pytest


Table of Contents


Features

  • Efficient Core Engine: Uses scipy.ndimage.convolve for fast neighbor counting.
  • Adaptive Backend: Automatically runs on GPU (cupy) if Nvidia hardware is available, or gracefully falls back to CPU (numpy / numba).
  • Adaptive Multiprocessing: Automatically switches to shared-memory multiprocessing for large grids on the CPU, eliminating the overhead of process creation and array pickling.
  • Real-time GUI & Visual Profiling: Built with Dear PyGui for GPU-accelerated immediate mode rendering. Features dual real-time plots tracking population dynamics and precise computation step-times (ms) to visually profile CPU scheduling performance.
  • Pattern Detection: Detects and logs known patterns (Gliders, Oscillators, Still Lifes) using connected component analysis. The UI automatically bubbles high-priority structures (like Gliders) to the top of logs and highlights them.
  • Data Persistence: Saves execution statistics to a local SQLite database using SQLAlchemy 2.0.

Installation

Ensure you have conda installed. Create and activate the environment:

conda env create -f environment.yml
conda activate game-of-life

Command Cheat Sheet

Here are all the commands you can use to interact with this project:

Installation:

conda env create -f environment.yml
conda activate game-of-life

Running the Simulation (Basic & Advanced):

# Basic run (will auto-generate data/initial.pkl if missing)
python scripts/run_game_of_life.py

# Run with a specific initial state
python scripts/run_game_of_life.py --initial data/initial.pkl

# Run headlessly (no GUI, good for servers)
python scripts/run_game_of_life.py --headless

# Force specific compute backend (auto, cpu, gpu, numba)
python scripts/run_game_of_life.py --backend cpu
python scripts/run_game_of_life.py --backend gpu
python scripts/run_game_of_life.py --backend numba

# Bypass P-core restriction on hybrid CPUs
python scripts/run_game_of_life.py --backend cpu --all-cores

Generating Initial States:

# Generate a random 100x100 grid with 30% density
python scripts/generate_initial_state.py --pattern random --density 0.3 --rows 100 --cols 100 --out data/initial.pkl

# Generate a specific pattern (e.g. glider)
python scripts/generate_initial_state.py --pattern glider --out data/initial.pkl

Testing and Code Quality:

# Master command: fix linting, format code, and run all tests
ruff check --fix . && ruff format . && pytest

# Individual commands
pytest tests/              # Run just the tests
ruff check .               # Run the linter
ruff format --check .      # Run the formatter (dry-run)

Benchmarking:

jupyter nbconvert --to notebook --execute notebooks/performance_analysis.ipynb --inplace

Architecture

  • libs/core/: The simulation engine (Grid, Rules, SimulationEngine). Headless and fully testable.
  • libs/parallel/: The shared-memory multiprocessing logic, topology scanner, and adaptive dispatcher.
  • libs/patterns/: RLE parser, catalog of known patterns, stability tracker, and pattern detector.
  • libs/persistence/: SQLite database models and initial state loading logic.
  • libs/gui/: Dear PyGui application and views.
  • scripts/: Entrypoints for running the simulation and generating initial states.
  • data/: SQLite databases and pickle files for saving/loading data.

Running Tests and Linting

To automatically fix formatting, check code styles, and run the entire test suite in a single pass, run:

ruff check --fix . && ruff format . && pytest

Alternatively, you can run them individually:

pytest tests/              # Run just the tests
ruff check .               # Run the linter
ruff format --check .      # Run the formatter (dry-run)



Implementation Report

Note: This section constitutes the official Implementation Report for the project, detailing the engineering decisions and hardware evaluations.

Architecture Summary

The project follows a flat, clean architecture (libs/, scripts/, data/) designed for testability and separation of concerns. The core engine (libs/core/) operates headlessly and independently of the GUI (libs/gui/) and persistence (libs/persistence/) layers. Pattern detection (libs/patterns/) handles identifying known structures via connected components, and the multiprocessing module (libs/parallel/) provides seamless shared-memory parallelism.

Backend Architectures Explained

To understand how the simulation runs under the hood, here is the exact breakdown of the three independent compute backends:

1. GPU Mode (--backend gpu)

  • What it uses: Exclusively uses CuPy (cupyx.scipy.ndimage).
  • How it works: CuPy is a library designed to be an exact clone of SciPy, but written entirely for graphics cards (CUDA). To simulate the game, 100% of the mathematical cell evaluation is sent to the GPU, completely bypassing the classic CPU SciPy implementation.

2. CPU Mode (--backend cpu)

  • What it uses: Exclusively uses SciPy (scipy.ndimage).
  • How it works: All mathematical work is performed strictly on the main processor (RAM and CPU). If the matrix is large, it distributes the work across multiple cores using multiprocessing and shared memory blocks. It does not touch the GPU at all.

3. Numba Mode (--backend numba)

  • What it uses: Exclusively uses the CPU and native compiled code (JIT). It does not use the GPU.
  • How it works: Numba takes standard Python for loops and, just before execution, translates (compiles) them into ultra-low-level C machine code designed specifically for your processor. By doing so, it bypasses the slow Python interpreter and high-level math libraries (like SciPy), instead squeezing all available CPU cores using pure, native Multithreading.

Hardware Testing Environment & OS Compatibility

Important

OS Compatibility: The entire project was developed and rigorously tested on Linux, but all implementations (both new and existing) are fully cross-platform compatible with Windows.

The performance benchmarks and multi-processing threshold crossover points (as seen in performance_analysis.ipynb) were evaluated on the following hardware configuration:

  • OS: Ubuntu noble 24.04 x86_64 (Fully compatible with Windows 10/11)
  • CPU: 13th Gen Intel(R) Core(TM) i9-13950HX (32 threads @ 5.50 GHz)
  • RAM: 32 GB
  • GPU (Compute): NVIDIA GeForce RTX 4060 Max-Q / Mobile [Discrete]

Challenges and Solutions

1. Efficient Multiprocessing

Challenge: Naive multiprocessing (spawning a Pool and using map per generation) introduces massive overhead from process creation and pickling full NumPy arrays, typically resulting in a net loss of performance for most grid sizes. Solution: Implemented a persistent ProcessPoolExecutor with double-buffered shared memory (multiprocessing.shared_memory). Worker processes attach to the memory blocks once on initialization. For each generation, they only receive small index ranges, read from the current block (with necessary halo rows), compute the rules via scipy.ndimage.convolve, and write directly to the next block. This eliminates process creation and serialization overhead completely. Cleanup Safety: Posix shared memory segments leak if not explicitly unlinked. The implementation guarantees cleanup by using a context manager (SharedGridBuffer) where the owning process uniquely calls unlink(). Thread-Safety: All GUI-driven multiprocessing pool resets (e.g. boundary mode switches) are strictly deferred to the main event loop to prevent RuntimeError concurrency crashes against background rendering threads.

2. Pattern Catalog Transcription Risk

Challenge: Hand-transcribing patterns (like the Pulsar and Pentadecathlon) to RLE or ASCII grids carries a high risk of subtle errors, which could invalidate pattern detection. Solution: Every pattern in the catalog is subjected to an exhaustive unit test (tests/test_rules.py). The test simulates the pattern for its known period and verifies it returns to its exact initial state (or translates properly for spaceships), acting as an absolute safety net against definition bugs.

3. Stability vs. Cycle Detection

Challenge: "Stable states (no changes)" means exact stasis (Still Lifes). Stopping the simulation prematurely on oscillators (like Blinkers) would be incorrect behavior. Solution: Differentiated exact stasis (StabilityTracker), which halts the simulation when current == previous, from periodic cycle detection (CycleTracker), which maintains a history hash buffer to passively log (at DEBUG level) when a cycle is detected without stopping the simulation.

4. Multiprocessing Threshold

Challenge: Finding the actual crossover point where multiprocessing is beneficial. Solution: Built a Jupyter notebook (performance_analysis.ipynb) to benchmark various grid sizes. (The actual threshold must be measured in the grading environment or developer environment and updated in config.yaml). The dispatcher adaptively chooses between single-process and multi-process execution based on this measured threshold.

5. GPU Acceleration & Adaptive Fallback

Challenge: To drastically improve performance for very large grids without disrupting the GUI or testing framework, while supporting systems without GPUs. Solution: Implemented a GpuDispatcher using CuPy (cupy and cupyx.scipy.ndimage.convolve) which computes generation steps entirely on the GPU. The run_game_of_life.py entrypoint defaults to an auto backend, which safely falls back to Numba or NumPy CPU execution if an Nvidia GPU or CuPy dependencies are not detected. A robust fallback mechanism guarantees the application never crashes due to backend initialization failures, automatically down-grading to the next best available engine and notifying the UI.

6. CPU Topology (P-Cores vs E-Cores) & Numba Acceleration

Challenge: On hybrid processors (like Intel 13th Gen), synchronous multiprocessing processes get severely bottlenecked when the OS scheduler assigns chunks to slower E-Cores. The fast P-Cores sleep waiting for the E-Cores to finish ("the weakest link problem"). Solution: Implemented a cross-platform (Linux/Windows) topology scanner (libs/parallel/topology.py) utilizing /sys/devices/system/cpu/ and psutil to isolate High-Performance (P-Cores). Process affinity is strictly bound to P-Cores to maximize synchronous frame rates. Additionally, an opt-in --backend numba flag was added to JIT-compile the Conway loops using LLVM natively in multithreading mode, completely bypassing the IPC overhead of SciPy multiprocessing. A real-time "Performance History" graph in the GUI visually exposes the micro-stutter latency caused by E-Cores versus the flat-line stability of P-Core affinity.

7. Exhaustive Performance Findings

An exhaustive ablation study (notebooks/performance_analysis.ipynb) evaluated all permutations of grid sizes, backends, and CPU core affinities with fixed random seeds for absolute repeatability. Conclusions:

  1. Small Grids (< 50x50): Single Process is strictly optimal due to the absence of Inter-Process Communication (IPC) overhead or PCIe memory transfer latency.
  2. Medium Grids (~500x500): Numba JIT (P-Cores Only) provides the best CPU performance. Numba's native multithreading outperforms scipy multiprocessing by eliminating shared memory duplication and E-Core scheduling penalties.
  3. Massive Grids (1000x1000+): GPU (CuPy) provides the highest performance, computing massive arrays in fractions of a millisecond utilizing massive CUDA data-parallelism.

About

The final work Scientific Programming

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages