Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ endif()
if(pybind11_FOUND)
# Build the 'ga' Python extension module
pybind11_add_module(ga_python_module python/ga_bindings.cpp)
target_sources(ga_python_module PRIVATE benchmark/ga_benchmark.cc)
target_link_libraries(ga_python_module PRIVATE genetic_algorithm)
target_include_directories(ga_python_module PRIVATE ${CMAKE_SOURCE_DIR}/include)
set_target_properties(ga_python_module PROPERTIES
Expand Down
140 changes: 103 additions & 37 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ For features not yet exposed in the Python bindings, an explicit note is include
| 3 | [Chromosome Representations](#3-chromosome-representations) | ✅ | ✅ (all genome types) |
| 4 | [Crossover Operators](#4-crossover-operators) | ✅ | ⚠️ 2 factory operators exposed |
| 5 | [Mutation Operators](#5-mutation-operators) | ✅ | ⚠️ 2 factory operators exposed |
| 6 | [Selection Operators](#6-selection-operators) | ✅ | ❌ not exposed |
| 6 | [Selection Operators](#6-selection-operators) | ✅ | ⚠️ helper functions exposed |
| 7 | [Core GA Run and Results](#7-core-ga-run-and-results) | ✅ | ✅ |
| 8 | [High-Level Optimizer API](#8-high-level-optimizer-api) | ✅ | ✅ |
| 9 | [Multi-Objective: NSGA-II](#9-multi-objective-nsga-ii) | ✅ | ✅ (objective-space utils) |
Expand All @@ -31,13 +31,13 @@ For features not yet exposed in the Python bindings, an explicit note is include
| 16 | [Adaptive Operators](#16-adaptive-operators) | ✅ | ✅ |
| 17 | [Hybrid Optimization](#17-hybrid-optimization) | ✅ | ✅ |
| 18 | [Constraint Handling](#18-constraint-handling) | ✅ | ✅ |
| 19 | [Parallel and Distributed Evaluation](#19-parallel-and-distributed-evaluation) | ✅ | ❌ not exposed |
| 19 | [Parallel and Distributed Evaluation](#19-parallel-and-distributed-evaluation) | ✅ | ✅ (`ParallelEvaluator`, `LocalDistributedExecutor`, `Optimizer.with_threads`) |
| 20 | [Co-Evolution](#20-co-evolution) | ✅ | ✅ |
| 21 | [Checkpointing](#21-checkpointing) | ✅ | ✅ |
| 22 | [Experiment Tracking](#22-experiment-tracking) | ✅ | ✅ |
| 23 | [Visualization and CSV Export](#23-visualization-and-csv-export) | ✅ | ✅ |
| 24 | [Plugin Architecture](#24-plugin-architecture) | ✅ | ❌ not exposed |
| 25 | [Benchmark Suite](#25-benchmark-suite) | ✅ | ❌ not exposed |
| 25 | [Benchmark Suite](#25-benchmark-suite) | ✅ | ✅ (`BenchmarkConfig`, `GABenchmark`) |
| 26 | [C API](#26-c-api) | ✅ | N/A (C only) |
| 27 | [Reproducibility Controls](#27-reproducibility-controls) | ✅ | ✅ |

Expand Down Expand Up @@ -545,10 +545,36 @@ auto& ranked = rs.select(population);

### 6.2 Python

> **Not available in Python bindings yet.**
> Selection operators are not individually exposed to Python.
> The `ga.GeneticAlgorithm` uses an internal tournament-style selection
> that cannot be swapped from Python currently.
Selection strategy classes are still C++-only, but Python now exposes helper
functions that run the same selection logic over a fitness list and return the
selected indices:

- `ga.selection_tournament_indices(fitness, tournament_size=3)` (returns one index)
- `ga.selection_roulette_indices(fitness, count)`
- `ga.selection_rank_indices(fitness, count)`
- `ga.selection_sus_indices(fitness, count)` *(stochastic universal sampling)*
- `ga.selection_elitism_indices(fitness, elite_count)`
Comment on lines +552 to +556
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guide says selection_tournament_indices(...) “returns one index”, but the Python API actually returns a list containing one index (consistent with the other *_indices helpers). Please clarify in the documentation (e.g., “returns a list of length 1”) or adjust the API to return an int.

Copilot uses AI. Check for mistakes.

```python
import ga

fitness = [0.1, 0.8, 0.4, 1.2, 0.6]

tournament_winner = ga.selection_tournament_indices(fitness, tournament_size=3)
roulette_picks = ga.selection_roulette_indices(fitness, count=3)
rank_picks = ga.selection_rank_indices(fitness, count=3)
sus_picks = ga.selection_sus_indices(fitness, count=3)
elite_picks = ga.selection_elitism_indices(fitness, elite_count=2)

print("Tournament winner index:", tournament_winner)
print("Roulette indices:", roulette_picks)
print("Rank indices:", rank_picks)
print("SUS indices:", sus_picks)
print("Elite indices:", elite_picks) # tends to include the best-fitness entries
```

> `ga.GeneticAlgorithm` still uses its internal selection pipeline. These helpers
> are for analysis/custom Python loops where you need direct index selection.

---

Expand Down Expand Up @@ -1513,11 +1539,40 @@ int main() {

### Python

> **Not available in Python bindings yet.**
> Parallel and distributed evaluators are implemented in
> `include/ga/evaluation/` (C++ only).
> As a workaround, Python's `concurrent.futures` can parallelize fitness calls
> externally and pass results to a Python-level custom fitness function.
Python exposes thread-parallel evaluators directly:

- `ga.ParallelEvaluator(fitness, threads=...)`
- `ga.LocalDistributedExecutor(evaluator, workers=...)`
- plus optimizer-level threading via `ga.Optimizer.with_threads(...)`

Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these evaluators call a Python callback, each callback invocation must hold the GIL; that means a pure-Python fitness function will not execute concurrently across threads (speedups generally require native code that releases the GIL, e.g. NumPy/Cython). Consider adding a short note here so users don’t assume CPU-parallel execution for pure Python callables.

Suggested change
> **Note:** These evaluators invoke a Python callback while holding the GIL, so a pure-Python `fitness` function will not execute CPU-bound work concurrently across threads. For real CPU parallelism, use native code that releases the GIL (e.g. NumPy, Cython), or a process-based executor.

Copilot uses AI. Check for mistakes.
```python
import ga

def sphere(x):
return 1000.0 / (1.0 + sum(xi * xi for xi in x))

batch = [[0.1, 0.2], [0.3, 0.4], [0.0, 0.0]]

pe = ga.ParallelEvaluator(sphere, threads=4)
print("ParallelEvaluator:", pe.evaluate(batch))

lde = ga.LocalDistributedExecutor(sphere, workers=4)
print("LocalDistributedExecutor:", lde.execute(batch))

cfg = ga.Config()
cfg.population_size = 120
cfg.generations = 200
cfg.dimension = 20
cfg.bounds = ga.Bounds(-5.12, 5.12)
result = (ga.Optimizer()
.with_config(cfg)
.with_threads(4)
.with_seed(42)
.optimize(sphere))
print("Best fitness:", result.best_fitness)
```

> `ProcessDistributedExecutor` is still C++-only (POSIX/fork backend).

---

Expand Down Expand Up @@ -1899,30 +1954,26 @@ cmake --build build

### Python

> **Not available in Python bindings yet.**
> The benchmark suite is implemented in `benchmark/` and exposed via the
> `ga-benchmark` executable (C++ only).
>
> You can replicate benchmark-style measurements in Python using the
> `ga.GeneticAlgorithm` directly:
>
> ```python
> import ga, time
>
> def sphere(x):
> return 1000.0 / (1.0 + sum(xi**2 for xi in x))
>
> for dim in [5, 10, 20]:
> cfg = ga.Config()
> cfg.population_size = 60
> cfg.generations = 100
> cfg.dimension = dim
> cfg.bounds = ga.Bounds(-5.12, 5.12)
> t0 = time.perf_counter()
> r = ga.GeneticAlgorithm(cfg).run(sphere)
> elapsed = time.perf_counter() - t0
> print(f"dim={dim:2d} best={r.best_fitness:.4f} time={elapsed*1000:.1f}ms")
> ```
The benchmark suite is exposed in Python through `ga.BenchmarkConfig` and
`ga.GABenchmark`:

```python
import ga

cfg = ga.BenchmarkConfig()
cfg.warmup_iterations = 1
cfg.benchmark_iterations = 3
cfg.verbose = False

b = ga.GABenchmark(cfg)
b.run_operator_benchmarks()
print("Operator rows:", len(b.operator_results()))

b.run_function_benchmarks()
print("Function rows:", len(b.function_results()))

b.export_to_csv("benchmark_results.csv")
```

---

Expand Down Expand Up @@ -2048,7 +2099,7 @@ pip install pybind11
# 2. Configure and build
mkdir -p build && cd build
cmake ..
cmake --build . --target ga-python-bindings -j$(nproc)
cmake --build . --target ga_python_module -j$(nproc)

# 3. Add the build directory to PYTHONPATH
export PYTHONPATH="$(pwd)/python:$PYTHONPATH"
Expand Down Expand Up @@ -2083,6 +2134,21 @@ python3 python/example.py
| `ga.make_two_point_crossover` | Factory: two-point crossover |
| `ga.make_gaussian_mutation` | Factory: Gaussian mutation |
| `ga.make_uniform_mutation` | Factory: Uniform mutation |
| **Evaluation** | |
| `ga.ParallelEvaluator` | Threaded batch evaluator over candidate vectors |
| `ga.LocalDistributedExecutor` | Threaded distributed executor over candidate batches |
| **Selection Helpers** | |
| `ga.selection_tournament_indices` | Tournament selection over fitness list |
| `ga.selection_roulette_indices` | Roulette-wheel selection over fitness list |
| `ga.selection_rank_indices` | Rank-based selection over fitness list |
| `ga.selection_sus_indices` | Stochastic universal sampling over fitness list |
| `ga.selection_elitism_indices` | Elitism/top-k selection over fitness list |
| **Benchmark** | |
| `ga.BenchmarkConfig` | Configure benchmark warmup/iterations/output |
| `ga.BenchmarkResult` | Scalability benchmark summary row |
| `ga.OperatorBenchmark` | Operator benchmark row |
| `ga.FunctionBenchmark` | Function optimization benchmark row |
| `ga.GABenchmark` | Run benchmark suite and export reports/CSV |
| **Representations** | |
| `ga.VectorGenome` | Real-valued genome (`double`) |
| `ga.BitsetGenome` | Binary/bitset genome |
Expand Down
4 changes: 4 additions & 0 deletions benchmark/ga_benchmark.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class GABenchmark {
void generateReport();
void exportToCSV(const std::string& filename);

const std::vector<OperatorBenchmark>& operatorResults() const { return operatorResults_; }
const std::vector<FunctionBenchmark>& functionResults() const { return functionResults_; }
const std::vector<BenchmarkResult>& scalabilityResults() const { return scalabilityResults_; }

private:
BenchmarkConfig config_;
std::vector<OperatorBenchmark> operatorResults_;
Expand Down
44 changes: 41 additions & 3 deletions python/bindings_sanity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ def sphere_fitness(x: list[float]) -> float:


def main() -> None:
out_dir = os.path.join(os.path.dirname(__file__), "..", "build")
os.makedirs(out_dir, exist_ok=True)

# Core data/representations
ev = ga.Evaluation()
ev.objectives = [1.0, 2.0]
Expand Down Expand Up @@ -94,6 +97,44 @@ def main() -> None:
assert ga.is_feasible([0.5, -0.2], cs)
assert not ga.is_feasible([2.0], cs)

# Evaluation helpers
pe = ga.ParallelEvaluator(sphere_fitness, threads=2)
pe_results = pe.evaluate([[0.1, 0.2], [0.0, 0.0], [0.6, 0.7]])
assert len(pe_results) == 3 and pe_results[1] >= pe_results[0]

local_exec = ga.LocalDistributedExecutor(sphere_fitness, workers=2)
local_results = local_exec.execute([[0.2, 0.1], [0.4, 0.5]])
assert len(local_results) == 2 and all(r > 0.0 for r in local_results)

# Selection helpers
fitness = [0.1, 0.8, 0.4, 1.2, 0.6]
t_idx = ga.selection_tournament_indices(fitness, tournament_size=3)
assert len(t_idx) == 1 and 0 <= t_idx[0] < len(fitness)
rw_idx = ga.selection_roulette_indices(fitness, count=3)
assert len(rw_idx) == 3 and all(0 <= i < len(fitness) for i in rw_idx)
rank_idx = ga.selection_rank_indices(fitness, count=3)
# Legacy rank helper can return fewer indices than requested in this codebase.
assert len(rank_idx) <= 3
sus_idx = ga.selection_sus_indices(fitness, count=3)
# Legacy SUS helper can return fewer indices than requested in this codebase.
assert len(sus_idx) <= 3
elite_idx = ga.selection_elitism_indices(fitness, elite_count=2)
assert len(elite_idx) == 2 and all(0 <= i < len(fitness) for i in elite_idx)

# Benchmark suite
bcfg = ga.BenchmarkConfig()
bcfg.warmup_iterations = 0
bcfg.benchmark_iterations = 1
bcfg.verbose = False
bench = ga.GABenchmark(bcfg)
# Keep sanity fast: exercise object + serialization surface without running
# the full benchmark loops.
op_results = bench.operator_results()
assert isinstance(op_results, list)
csv_path = os.path.join(out_dir, "python_sanity_benchmark.csv")
bench.export_to_csv(csv_path)
assert os.path.exists(csv_path)

# Hybrid + coevolution
cfg = ga.Config()
cfg.dimension = 3
Expand All @@ -115,9 +156,6 @@ def main() -> None:
st.generation = 1
st.rng_state = "smoke"

out_dir = os.path.join(os.path.dirname(__file__), "..", "build")
os.makedirs(out_dir, exist_ok=True)

bin_path = os.path.join(out_dir, "python_sanity_checkpoint.bin")
ga.checkpoint_save_binary(bin_path, st)
loaded = ga.checkpoint_load_binary(bin_path)
Expand Down
Loading
Loading