Skip to content

Commit 47b0cbe

Browse files
committed
Manually review AGENTS.md
1 parent 7df363c commit 47b0cbe

4 files changed

Lines changed: 78 additions & 76 deletions

File tree

AGENTS.md

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ guidance, API reference, gotchas, and copy-paste templates.
77

88
- [Quick Start](#quick-start)
99
- [Critical: FitnessValue is isize](#critical-fitnessvalue-is-isize)
10-
- [Copy-Paste Templates](#copy-paste-templates) (more in [AGENTS_TEMPLATES.md](AGENTS_TEMPLATES.md))
10+
- [Copy-Paste Templates](#copy-paste-templates) (more in [AGENTS_TEMPLATES.md](https://github.com/basvanwesting/genetic-algorithm/blob/main/AGENTS_TEMPLATES.md))
1111
- [Decision Matrix: Problem Type → Configuration](#decision-matrix-problem-type--configuration)
1212
- [Constructor Parameter Reference](#constructor-parameter-reference)
1313
- [Builder Methods (Evolve)](#builder-methods-evolve)
@@ -29,7 +29,7 @@ Add to your `Cargo.toml`:
2929
genetic_algorithm = "0.27.0"
3030
```
3131

32-
```rust
32+
```rust,ignore
3333
use genetic_algorithm::strategy::evolve::prelude::*;
3434
```
3535

@@ -59,7 +59,7 @@ process dominates any floating-point rounding.
5959

6060
For float-based fitness, scale to isize manually:
6161

62-
```rust
62+
```rust,ignore
6363
// divide by desired precision, then cast
6464
let precision = 1e-5;
6565
Some((score / precision) as FitnessValue)
@@ -75,7 +75,7 @@ Return `None` from `calculate_for_chromosome` to mark a chromosome as invalid
7575

7676
Minimal end-to-end example showing the general flow (genotype → fitness → strategy → result):
7777

78-
```rust
78+
```rust,ignore
7979
use genetic_algorithm::strategy::evolve::prelude::*;
8080
8181
#[derive(Clone, Debug)]
@@ -113,7 +113,7 @@ fn main() {
113113
}
114114
```
115115

116-
See [AGENTS_TEMPLATES.md](AGENTS_TEMPLATES.md) for more examples covering all
116+
See [AGENTS_TEMPLATES.md](https://github.com/basvanwesting/genetic-algorithm/blob/main/AGENTS_TEMPLATES.md) for more examples covering all
117117
genotype types, strategies, and patterns (call_repeatedly, HillClimb, Permutate, etc.).
118118

119119
## Decision Matrix: Problem Type → Configuration
@@ -142,28 +142,28 @@ When implementing `calculate_for_chromosome`, access genes via `chromosome.genes
142142
|---|---|---|
143143
| `BinaryGenotype` | `Vec<bool>` | |
144144
| `ListGenotype<T>` | `Vec<T>` | Default T = usize |
145-
| `UniqueGenotype<T>` | `Vec<T>` | Default T = usize, all values unique |
145+
| `UniqueGenotype<T>` | `Vec<T>` | Default T = usize, positional permutation (swap-only mutation), values do not have to be unique, they are only treated as such |
146146
| `RangeGenotype<T>` | `Vec<T>` | Default T = f32 |
147-
| `MultiListGenotype<T>` | `Vec<T>` | Default T = usize |
148-
| `MultiUniqueGenotype<T>` | `Vec<T>` | Default T = usize, unique within each group |
147+
| `MultiListGenotype<T>` | `Vec<T>` | Default T = usize, each gene has its own set of possible values |
148+
| `MultiUniqueGenotype<T>` | `Vec<T>` | Default T = usize, positional permutation (swap-only mutation) within each group, values do not have to be unique, they are only treated as such |
149149
| `MultiRangeGenotype<T>` | `Vec<T>` | Default T = f32, each gene has its own range |
150150

151151
### Which Strategy?
152152

153153
| Situation | Strategy | Why |
154154
|---|---|---|
155155
| General optimization | `Evolve` | Full GA with crossover + mutation |
156-
| Single permutation (`UniqueGenotype`) | `HillClimb` | Standard crossovers swap genes between parents, breaking the uniqueness invariant. HillClimb uses neighbor generation instead. |
157-
| Multi-group permutation (`MultiUniqueGenotype`) | `Evolve` | Point-based crossovers work at group boundaries without breaking within-group uniqueness. |
158156
| Convex search space | `HillClimb` | Local search suffices |
159-
| Small search space (<1M) | `Permutate` | Exhaustive, 100% guarantee |
157+
| Small search space | `Permutate` | Exhaustive, 100% guarantee |
158+
159+
**Scaling warning:** Permutate evaluates all possible gene combinations in a stream, so no memory issues, but serious duration issues
160160

161161
### Which HillClimb Variant?
162162

163163
| Situation | Variant | Why |
164164
|---|---|---|
165-
| Small genome (<20 genes) | `SteepestAscent` | Evaluates all neighbors, guarantees best local move |
166-
| Large genome (>20 genes) | `Stochastic` (default) | One random neighbor per step, fast iterations |
165+
| Large genome | `Stochastic` (default) | One random neighbor per step, fast iterations |
166+
| Small genome | `SteepestAscent` | Evaluates all neighbors, guarantees best local move |
167167
| Plateau traversal needed | `Stochastic` + high `max_stale_generations` | Stochastic can escape via `replace_on_equal_fitness` |
168168
| Exact local optimum needed | `SteepestAscent` + `call_repeatedly(n)` | Deterministic per start, multiple random restarts |
169169

@@ -176,12 +176,12 @@ with `call_repeatedly` for genomes >20 genes.
176176
| Genotype | Compatible Crossovers | Recommended |
177177
|---|---|---|
178178
| `BinaryGenotype` | All | `CrossoverUniform` or `CrossoverSinglePoint` |
179-
| `ListGenotype<T>` | All | Any (e.g. `CrossoverUniform`, `CrossoverSinglePoint`) |
180-
| `MultiListGenotype<T>` | All | Any (e.g. `CrossoverUniform`, `CrossoverSinglePoint`) |
179+
| `ListGenotype<T>` | All | `CrossoverUniform` or `CrossoverSinglePoint` |
180+
| `MultiListGenotype<T>` | All | `CrossoverUniform` or `CrossoverSinglePoint` |
181181
| `UniqueGenotype<T>` | `CrossoverClone`, `CrossoverRejuvenate` ONLY (others are compile errors) | `CrossoverClone` |
182182
| `MultiUniqueGenotype<T>` | Point-based + `CrossoverClone`, `CrossoverRejuvenate` (gene-based are compile errors) | `CrossoverSinglePoint` |
183-
| `RangeGenotype<T>` | All | Any (e.g. `CrossoverUniform`, `CrossoverSinglePoint`) |
184-
| `MultiRangeGenotype<T>` | All | Any (e.g. `CrossoverUniform`, `CrossoverSingleGene`) |
183+
| `RangeGenotype<T>` | All | `CrossoverUniform` or `CrossoverSinglePoint` |
184+
| `MultiRangeGenotype<T>` | All | `CrossoverUniform` or `CrossoverSinglePoint` |
185185

186186
**Compile-time safety**: `UniqueGenotype` does not implement `SupportsGeneCrossover`
187187
or `SupportsPointCrossover`, so incompatible crossovers are **compile errors**.
@@ -192,16 +192,16 @@ crossovers (`CrossoverUniform`, `CrossoverSingleGene`, `CrossoverMultiGene`) are
192192
compile errors.
193193

194194
**Note on UniqueGenotype + Evolve:** `CrossoverClone` with `UniqueGenotype`
195-
produces valid code but is almost always less efficient than `HillClimb`. Only
196-
use `Evolve` + `CrossoverClone` for `UniqueGenotype` when you need Extensions,
197-
speciation, or `call_repeatedly`.
195+
produces valid code but is almost always less efficient than `HillClimb` +
196+
`call_repeatedly(n)`. Only use `Evolve` + `CrossoverClone` for `UniqueGenotype`
197+
when you need Extensions or speciation.
198198

199199
### Which Select?
200200

201201
| Type | When to use |
202202
|---|---|
203-
| `SelectElite` | Default choice. Deterministic, sorts by fitness. |
204-
| `SelectTournament` | When you want stochastic pressure. Better diversity. |
203+
| `SelectElite` | Deterministic, sorts by fitness. Less diversity. |
204+
| `SelectTournament` | Default choice. When you want stochastic pressure. Better diversity. |
205205

206206
### Which Mutate?
207207

@@ -238,34 +238,34 @@ stale generations before terminating.
238238
### If unsure, start here
239239

240240
For binary/list genotypes:
241-
```rust
241+
```rust,ignore
242242
// also requires: genotype, fitness, target_population_size, ending condition
243243
.with_select(SelectTournament::new(0.5, 0.02, 4))
244244
.with_crossover(CrossoverUniform::new(0.7, 0.8))
245245
.with_mutate(MutateSingleGene::new(0.2))
246246
```
247247

248248
For range/float genotypes (>50 genes, see Troubleshooting for tuning):
249-
```rust
249+
```rust,ignore
250250
// also requires: genotype, fitness, target_population_size, ending condition
251251
.with_select(SelectTournament::new(0.5, 0.02, 4))
252252
.with_crossover(CrossoverUniform::new(0.7, 0.8))
253-
.with_mutate(MutateMultiGene::new(10, 1.0))
253+
.with_mutate(MutateMultiGene::new(10, 0.8))
254254
```
255255

256-
For unique genotypes (permutation problems):
257-
```rust
256+
For unique genotypes (swap-only problems):
257+
```rust,ignore
258258
// also requires: genotype, fitness, target_population_size, ending condition
259259
.with_select(SelectTournament::new(0.5, 0.02, 4))
260260
.with_crossover(CrossoverClone::new(0.7))
261-
.with_mutate(MutateMultiGene::new(2, 0.2))
261+
.with_mutate(MutateSingleGene::new(0.8))
262262
```
263263

264264
## Constructor Parameter Reference
265265

266266
### Select
267267

268-
```rust
268+
```rust,ignore
269269
SelectElite::new(
270270
replacement_rate: f32, // 0.3-0.7 typical. Fraction of population replaced by offspring.
271271
elitism_rate: f32, // 0.01-0.05 typical. Fraction preserved as elite (bypass selection gate).
@@ -280,7 +280,7 @@ SelectTournament::new(
280280

281281
### Crossover
282282

283-
```rust
283+
```rust,ignore
284284
CrossoverUniform::new(
285285
selection_rate: f32, // 0.5-0.8 typical. Fraction of parents selected for reproduction.
286286
crossover_rate: f32, // 0.5-0.9 typical. Probability parent pair actually crosses over (vs clone).
@@ -314,12 +314,15 @@ CrossoverRejuvenate::new(
314314

315315
### Mutate
316316

317-
**Rate guidance depends on genotype size and type — see "Mutation tuning for
318-
large float genomes" in Troubleshooting.** For float genomes >50 genes: use
319-
`MutateMultiGene` with `mutation_probability` near 1.0 and scale
320-
`number_of_mutations` with genome size.
317+
**Rate guidance depends on genotype size and type — see "Mutation tuning" in
318+
Troubleshooting.** For float genomes >50 genes: use `MutateMultiGene` with
319+
`mutation_probability` near 1.0 and scale `number_of_mutations` with genome
320+
size. The "typical" mutation rates assume small genomes. For large genomes
321+
(hundreds to thousands of genes), the effective per-gene mutation rate matters
322+
more than the per-chromosome probability. Think in terms of what fraction of
323+
all genes in the population actually change per generation.
321324

322-
```rust
325+
```rust,ignore
323326
MutateSingleGene::new(
324327
mutation_probability: f32, // 0.05-0.3 typical for binary. See note above for floats.
325328
)
@@ -350,10 +353,10 @@ MutateMultiGeneDynamic::new(
350353

351354
Extensions should not be needed when hyperparameters are properly tuned. They are
352355
a fallback when the population keeps collapsing to clones despite reasonable
353-
mutation/selection settings. Escalation order: `MassDegeneration` (least
354-
disruptive) `MassDeduplication`/`MassExtinction``MassGenesis` (last resort).
356+
mutation/selection settings. Escalation order: `MassDegeneration` (most bang for buck)
357+
`MassDeduplication`/`MassExtinction`/`MassGenesis` (mostly for completeness and the cambrian explosion metaphor).
355358

356-
```rust
359+
```rust,ignore
357360
ExtensionMassExtinction::new(
358361
cardinality_threshold: usize, // Trigger when unique chromosomes drop below this.
359362
survival_rate: f32, // Fraction that survives (random selection + elite).
@@ -362,7 +365,7 @@ ExtensionMassExtinction::new(
362365
// Randomly trims population. Recovery happens naturally through offspring in following generations.
363366
364367
ExtensionMassGenesis::new(
365-
cardinality_threshold: usize, // Trims to only 2 best (Adam & Eve). Most aggressive reset.
368+
cardinality_threshold: usize, // Trims to only 2 unique best (Adam & Eve).
366369
)
367370
// Extreme version of MassExtinction. Population recovers through offspring in following generations.
368371
@@ -414,7 +417,7 @@ Optional:
414417
- `.with_par_fitness(true)` — parallelize fitness calculation
415418
- `.with_fitness_cache(size)` — LRU cache for expensive fitness
416419
- `.with_replace_on_equal_fitness(bool)` — replace best even on equal score (default: true)
417-
- `.with_extension(extension)` — diversity management
420+
- `.with_extension(extension)` — diversity management (fallback, hyperparameters smell)
418421
- `.with_reporter(reporter)` — progress monitoring. Use `EvolveReporterSimple::new(100)`
419422
for progress every 100 generations, `EvolveReporterDuration::new()` for performance
420423
profiling. Each strategy has its own reporter types: `EvolveReporterSimple`/`Duration`,
@@ -498,7 +501,7 @@ implements all three strategy traits. All genotypes are compatible:
498501

499502
Switch strategy with `.with_variant(variant)`:
500503

501-
```rust
504+
```rust,ignore
502505
use genetic_algorithm::strategy::prelude::*;
503506
504507
let builder = StrategyBuilder::new()
@@ -531,7 +534,7 @@ for non-Evolve strategies.
531534

532535
Two patterns:
533536

534-
```rust
537+
```rust,ignore
535538
// Pattern 1: One-shot (build + run in one call)
536539
let evolve = Evolve::builder()
537540
.with_genotype(genotype)
@@ -550,11 +553,11 @@ evolve.call(); // runs separately
550553

551554
Additional Evolve builder call variants (return `(best_run, all_runs)` tuple):
552555
- `.call_repeatedly(n)` — n independent runs, return best + all
553-
- `.call_par_repeatedly(n)` — parallel version
556+
- `.call_par_repeatedly(n)` — parallel version, combine with with_par_fitness(false) to avoid double parallelization
554557
- `.call_speciated(n)` — n species runs, then final run seeded with best genes
555-
- `.call_par_speciated(n)` — parallel version
558+
- `.call_par_speciated(n)` — parallel version, combine with with_par_fitness(false) to avoid double parallelization, but will hurt final run
556559

557-
```rust
560+
```rust,ignore
558561
let (best, _all_runs) = Evolve::builder()
559562
// ... builder steps ...
560563
.call_repeatedly(5)
@@ -569,10 +572,10 @@ N-1 results.
569572
| Variant | When to use |
570573
|---|---|
571574
| `call()` | Default. Sufficient for most problems. |
572-
| `call_repeatedly(n)` | Results vary across runs (local optima). Typical n: 3-10. |
573-
| `call_par_repeatedly(n)` | Parallel version of above. |
574-
| `call_speciated(n)` | Multiple runs seed a final refinement pass. Best for complex combinatorial problems. |
575-
| `call_par_speciated(n)` | Parallel version of above. |
575+
| `call_repeatedly(n)` | Results vary across runs (local optima). Typical n: 10. |
576+
| `call_par_repeatedly(n)` | Parallel version of above. Beware of double parallelization with_par_fitness |
577+
| `call_speciated(n)` | Multiple runs seed a final refinement pass. Best for complex combinatorial problems, where characteristics of alternative refined solutions need combining |
578+
| `call_par_speciated(n)` | Parallel version of above. Beware of double parallelization with_par_fitness |
576579

577580
**Call variant availability by builder:**
578581

@@ -585,7 +588,9 @@ N-1 results.
585588
| `call_par_speciated(n)` | yes | no | no | yes (falls back to `call_par_repeatedly`) |
586589

587590
Only Evolve performs true speciation (seeding a final run with best genes from
588-
prior runs). Each run starts from a random population.
591+
prior runs). Each run before the final run starts from a random population.
592+
593+
Permutate always falls back to `call()` for all variants.
589594

590595
Both `.call()` and `.build()` return `Result<_, TryFromEvolveBuilderError>`.
591596
Builder validation catches: missing required fields and missing ending conditions.
@@ -594,26 +599,26 @@ trait bounds (`SupportsGeneCrossover`, `SupportsPointCrossover`).
594599

595600
## Common Mistakes
596601

597-
```rust
598-
// WRONG: UniqueGenotype + CrossoverUniform = COMPILE ERROR
599-
// FIX: Use CrossoverClone or CrossoverRejuvenate
602+
```text
603+
WRONG: UniqueGenotype + CrossoverUniform = COMPILE ERROR
604+
FIX: Use CrossoverClone or CrossoverRejuvenate
600605
601-
// WRONG: No ending condition = COMPILE/BUILD ERROR
602-
// FIX: Add .with_max_stale_generations(1000)
606+
WRONG: No ending condition = COMPILE/BUILD ERROR
607+
FIX: Add .with_max_stale_generations(1000)
603608
604-
// WRONG: Fitness returns f64 = TYPE ERROR
605-
// FIX: Return Some((score / precision) as FitnessValue)
609+
WRONG: Fitness returns f64 = TYPE ERROR
610+
FIX: Return Some((score / precision) as FitnessValue)
606611
607-
// WRONG: MutateSingleGene(0.2) with 1000+ float genes = DIVERSITY COLLAPSE
608-
// FIX: Use MutateMultiGene with higher mutation count, see Troubleshooting
612+
WRONG: MutateSingleGene(0.2) with 1000+ float genes = DIVERSITY COLLAPSE
613+
FIX: Use MutateMultiGene with higher mutation count, see Troubleshooting
609614
```
610615

611616
## Retrieving Results
612617

613618
These methods are available on all strategy types (`Evolve`, `HillClimb`,
614619
`Permutate`):
615620

616-
```rust
621+
```rust,ignore
617622
// Best genes and fitness score (returns None if no valid fitness was found)
618623
let (best_genes, best_fitness_score) = evolve.best_genes_and_fitness_score().unwrap();
619624
@@ -627,7 +632,7 @@ let best_generation = evolve.best_generation();
627632

628633
## Implementing Custom Fitness
629634

630-
```rust
635+
```rust,ignore
631636
#[derive(Clone, Debug)]
632637
struct MyFitness;
633638
@@ -671,7 +676,7 @@ For simple fitness functions, just ignore the mutability. When using
671676
trait bounds: `Clone + Copy + Send + Sync + Debug`. Additionally, types must
672677
implement `Hash` (for genes hashing) via the `impl_allele!` macro.
673678

674-
```rust
679+
```rust,ignore
675680
use genetic_algorithm::strategy::evolve::prelude::*;
676681
677682
#[derive(Clone, Copy, PartialEq, Hash, Debug)]
@@ -705,7 +710,7 @@ the wrong variant gives a helpful build error.
705710
All genotype builders support these optional settings:
706711

707712
- `.with_genes_hashing(true)` (default) — required for `fitness_cache` and
708-
`ExtensionMassDeduplication`. Auto-disabled by HillClimb (unless `fitness_cache` is set).
713+
`Extension` & `Mutation` cardinality_thresholds. Auto-disabled by HillClimb (unless `fitness_cache` is set).
709714
- `.with_chromosome_recycling(true)` (default) — reuses chromosome memory
710715
allocations. Generally leave at default.
711716

@@ -715,12 +720,12 @@ All genotype builders support these optional settings:
715720
- Increase `target_population_size` (more diversity)
716721
- Increase `mutation_probability` (more exploration)
717722
- Try `MutateSingleGeneDynamic` or `MutateMultiGeneDynamic` (auto-adjusts)
718-
- For `RangeGenotype`/`MultiRangeGenotype`: use `MutationType::StepScaled` for
723+
- For `RangeGenotype`/`MultiRangeGenotype`: use `MutationType::RangeScaled` for
719724
progressive refinement instead of `MutationType::Random`
720-
- Add an extension like `ExtensionMassGenesis` or `ExtensionMassDegeneration` to
725+
- Add an extension like `ExtensionMassDegeneration` to
721726
escape local optima when diversity drops
722727

723-
**Mutation tuning for large float genomes (RangeGenotype/MultiRangeGenotype)?**
728+
**Mutation tuning**
724729

725730
The "typical" mutation rates assume small binary genomes. For large float genomes
726731
(hundreds to thousands of genes), the effective per-gene mutation rate matters
@@ -756,12 +761,12 @@ Combine with `max_stale_generations` to trigger phase transitions automatically
756761

757762
**Runtime too slow?**
758763
- Use `.with_par_fitness(true)` for expensive fitness calculations
759-
- Use `.with_fitness_cache(size)` if many chromosomes share the same genes
764+
- Use `.with_fitness_cache(size)` if many chromosomes share the same genes (is hyperparameter smell though)
760765
- Reduce `target_population_size` if fitness is cheap but framework overhead is high
761766
- For `HillClimb::SteepestAscent`: the neighbourhood can be very large, consider
762767
`Stochastic` variant instead
763768
- `par_fitness` has no effect with `HillClimbVariant::Stochastic` (sequential by
764-
nature). Use `call_par_repeatedly` to parallelize independent Stochastic runs.
769+
nature). Use `call_par_repeatedly` to parallelize independent Stochastic runs. Disable `par_fitness` to avoid double parallelization.
765770

766771
**Getting `None` as best fitness?**
767772
- All chromosomes returned `None` from `calculate_for_chromosome`

AGENTS_TEMPLATES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# AGENTS_TEMPLATES.md — Copy-Paste Templates
22

3-
See [AGENTS.md](AGENTS.md) for decision matrices, constructor parameter reference,
3+
See [AGENTS.md](https://github.com/basvanwesting/genetic-algorithm/blob/main/AGENTS.md) for decision matrices, constructor parameter reference,
44
and troubleshooting.
55

66
## Binary Optimization (Knapsack)

0 commit comments

Comments
 (0)