Skip to content

Commit 8b07313

Browse files
committed
Manual review of generated documentation
1 parent 79e814d commit 8b07313

5 files changed

Lines changed: 95 additions & 13 deletions

File tree

AGENTS.md

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ When implementing `calculate_for_chromosome`, access genes via `chromosome.genes
8888

8989
**WARNING**: `UniqueGenotype` will cause a **runtime panic** with gene-based or
9090
point-based crossovers. Use `CrossoverClone` (clones parents, relies on mutation
91-
for diversity) or `CrossoverRejuvenate` (generates fresh random chromosomes).
91+
for diversity) or `CrossoverRejuvenate` (like Clone but optimized for less memory copying).
9292
`MultiUniqueGenotype` supports point-based crossovers (`CrossoverSinglePoint`,
9393
`CrossoverMultiPoint`) but panics on gene-based ones (`CrossoverUniform`,
9494
`CrossoverSingleGene`, `CrossoverMultiGene`).
@@ -157,15 +157,18 @@ CrossoverClone::new(
157157
)
158158

159159
CrossoverRejuvenate::new(
160-
selection_rate: f32, // No actual crossover, generates fresh random chromosomes.
160+
selection_rate: f32, // Like Clone but drops non-selected first, then refills. Less memory copying.
161161
)
162162
```
163163

164164
### Mutate
165165

166+
**Rate guidance depends on genotype size and type — see "Mutation tuning for
167+
large float genomes" in Troubleshooting.**
168+
166169
```rust
167170
MutateSingleGene::new(
168-
mutation_probability: f32, // 0.05-0.3 typical. Probability per chromosome.
171+
mutation_probability: f32, // 0.05-0.3 typical for binary. See note above for floats.
169172
)
170173

171174
MutateMultiGene::new(
@@ -195,23 +198,27 @@ MutateMultiGeneDynamic::new(
195198
```rust
196199
ExtensionMassExtinction::new(
197200
cardinality_threshold: usize, // Trigger when unique chromosomes drop below this.
198-
survival_rate: f32, // Fraction that survives extinction.
199-
elitism_rate: f32, // Fraction of elite preserved.
201+
survival_rate: f32, // Fraction that survives (random selection + elite).
202+
elitism_rate: f32, // Fraction of elite preserved before random reduction.
200203
)
204+
// Randomly trims population. Recovery happens naturally through offspring in following generations.
201205

202206
ExtensionMassGenesis::new(
203-
cardinality_threshold: usize, // Replace all non-elite with fresh random chromosomes.
207+
cardinality_threshold: usize, // Trims to only 2 best (Adam & Eve). Most aggressive reset.
204208
)
209+
// Extreme version of MassExtinction. Population recovers through offspring in following generations.
205210

206211
ExtensionMassDegeneration::new(
207212
cardinality_threshold: usize,
208-
number_of_rounds: usize, // Rounds of random mutation applied.
209-
elitism_rate: f32,
213+
number_of_mutations: usize, // Number of gene mutations applied per chromosome.
214+
elitism_rate: f32, // Fraction of elite preserved before mutation.
210215
)
216+
// Only extension that actually mutates genes. No population trim, same size throughout.
211217

212218
ExtensionMassDeduplication::new(
213-
cardinality_threshold: usize, // Replace duplicate chromosomes with fresh ones.
219+
cardinality_threshold: usize, // Trims to only unique chromosomes (by genes hash).
214220
)
221+
// Removes duplicates. Population recovers through offspring in following generations.
215222
```
216223

217224
## Builder Methods (Evolve)
@@ -599,6 +606,40 @@ fn main() {
599606
- Add an extension like `ExtensionMassGenesis` or `ExtensionMassDegeneration` to
600607
escape local optima when diversity drops
601608

609+
**Mutation tuning for large float genomes (RangeGenotype/MultiRangeGenotype)?**
610+
611+
The "typical" mutation rates assume small binary genomes. For large float genomes
612+
(hundreds to thousands of genes), the effective per-gene mutation rate matters
613+
more than the per-chromosome probability. Think in terms of what fraction of all
614+
genes in the population actually change per generation.
615+
616+
- **Binary genes:** mutation flips 0↔1, which is a large relative change. Low
617+
rates (1-5% per chromosome) suffice.
618+
- **Float genes:** mutation nudges a continuous value. Each mutation has less
619+
relative impact, so you need far more mutations to maintain diversity.
620+
621+
Concrete example for a 2000-gene float genome, population 100:
622+
- `MutateSingleGene(0.2)` → 1 gene × 20% of offspring = effective 0.01% of all
623+
genes change per generation. **Population will collapse to near-clones.**
624+
- `MutateMultiGene(10, 1.0)`~5.5 genes × 100% of offspring = effective 0.28%
625+
of all genes change per generation. **Maintains diversity.**
626+
627+
Rule of thumb for float genomes: target 0.1%-1.0% effective per-gene mutation
628+
rate across the population. Use `MutateMultiGene` with high `mutation_probability`
629+
and scale `number_of_mutations` with genome size.
630+
631+
**But high mutation prevents convergence.** Use scaled mutation types to get both
632+
exploration early and convergence late:
633+
- `MutationType::RangeScaled(vec![100.0, 100.0, 50.0, 10.0, 1.0])` — starts
634+
with full-range Random mutations (100% of allele range = effectively Random),
635+
then progressively narrows the mutation bandwidth. Best for float genomes:
636+
wide exploration phases first, then tight range-bound convergence.
637+
- `MutationType::StepScaled(vec![10.0, 1.0, 0.1])` — fixed step sizes that
638+
decrease through phases. Better for grid-like or discrete problems.
639+
640+
Combine with `max_stale_generations` to trigger phase transitions automatically
641+
(advances to next phase when fitness plateaus).
642+
602643
**Runtime too slow?**
603644
- Use `.with_par_fitness(true)` for expensive fitness calculations
604645
- Use `.with_fitness_cache(size)` if many chromosomes share the same genes
@@ -610,6 +651,9 @@ fn main() {
610651
- All chromosomes returned `None` from `calculate_for_chromosome`
611652
- Check your fitness function's validity constraints — they may be too strict
612653
- Increase population size so some valid solutions appear in the initial population
654+
- Prefer large penalties over `None`: returning `Some(very_bad_score)` lets the
655+
algorithm converge incrementally out of invalid space, while `None` provides
656+
no gradient signal and ranks last unconditionally
613657

614658
## Gotchas
615659

src/crossover/rejuvenate.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ impl<G: EvolveGenotype> Crossover for Rejuvenate<G> {
4747
}
4848

4949
impl<G: EvolveGenotype> Rejuvenate<G> {
50-
/// Create a new Rejuvenate crossover strategy. No actual gene exchange occurs; offspring
51-
/// are generated with fresh random genes. Useful for maintaining diversity.
50+
/// Create a new Rejuvenate crossover strategy. Like Clone but optimized for less memory
51+
/// copying: drops non-selected parents first, clones top parents to refill, then resets
52+
/// age on the original selected parents. Allowed for unique genotypes.
5253
/// * `selection_rate` - fraction of parents selected for reproduction (0.5-0.8 typical)
5354
pub fn new(selection_rate: f32) -> Self {
5455
Self {

src/extension/mass_deduplication.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ impl<G: EvolveGenotype> Extension for MassDeduplication<G> {
5656

5757
impl<G: EvolveGenotype> MassDeduplication<G> {
5858
/// Create a new MassDeduplication extension. Triggers when population diversity drops below threshold.
59-
/// Replaces duplicate chromosomes with fresh random ones.
59+
/// Trims population to only unique chromosomes (by genes hash). Population recovers through
60+
/// offspring in following generations.
6061
/// * `cardinality_threshold` - trigger when unique chromosomes drop below this count
6162
pub fn new(cardinality_threshold: usize) -> Self {
6263
Self {

src/extension/mass_genesis.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ impl<G: EvolveGenotype> Extension for MassGenesis<G> {
6060

6161
impl<G: EvolveGenotype> MassGenesis<G> {
6262
/// Create a new MassGenesis extension. Triggers when population diversity drops below threshold.
63-
/// Replaces all non-elite chromosomes with fresh random ones.
63+
/// Trims population to only 2 best chromosomes (Adam & Eve). Population recovers through
64+
/// offspring in following generations.
6465
/// * `cardinality_threshold` - trigger when unique chromosomes drop below this count
6566
pub fn new(cardinality_threshold: usize) -> Self {
6667
Self {

workspace.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,38 @@ I don't feel comfortable adding dedup because it works as the underlying reason
2020
- Rejected: Would double-increment age for existing custom crossover implementations that already call it — a silent behavioral break.
2121

2222
Both were ruled out as "not worth the breaking change" given the existing validation/documentation already handles them adequately.
23+
24+
## TODO
25+
26+
## MAYBE
27+
* Apply precision (to f32/f64) during hashing in order to converge and hit
28+
staleness when nearing the required precision level (maybe per scale?)
29+
* Crossover calls reset_metadata, but sometimes the parens are equal and
30+
sometimes when they are not the difference isn't crossed over. In both
31+
conditions, you create a child equal to an existing parent. This leads to a
32+
cache hit when calculating the fitness again. That is confusing. Extensions
33+
can mutate chromosomes which calls reset_metadata (fitness is reset). Now
34+
crossover follows and clones parents without a fitness, into new children
35+
without fitness, this will lead to a cache miss and a cache hit when
36+
calculating the fitness
37+
* Consider dropping .with_genes_hashing() and always set to true, because it is
38+
needed for proper GA functionality regardless the overhead
39+
* Consider dropping .with_chromosome_recycling() and always set to false
40+
(stripping the recycling completely), because it is complicated and risky for
41+
custom Crossover implementations and maybe framework overhead simply doesn't
42+
matter as much with regards to Fitness overhead
43+
* Target cardinality range for Mutate Dynamic to avoid constant switching (noisy in reporting events)
44+
* Add scaling helper function
45+
* Add simulated annealing strategy
46+
* Add Roulette selection with and without duplicates (with fitness ordering)
47+
* Add OrderOne crossover for UniqueGenotype?
48+
* Order Crossover (OX): Simple and works well for many permutation problems.
49+
* Partially Mapped Crossover (PMX): Preserves more of the parent's structure but is slightly more complex.
50+
* Cycle Crossover (CX): Ensures all genes come from one parent, useful for strict preservation of order.
51+
* Edge Crossover (EX): Preserves adjacency relationships, suitable for Traveling Salesman Problem or similar.
52+
* Add WholeArithmetic crossover for RangeGenotype?
53+
* Add CountTrueWithWork instead of CountTrueWithSleep for better benchmarks?
54+
* StrategyBuilder, with_par_fitness_threshold, with_permutate_threshold?
55+
* Add target fitness score to Permutate? Seems illogical, but would be symmetrical. Don't know yet
56+
* Add negative selection-rate to encode in-place crossover? But do keep the old
57+
extend with best-parents with the pre v0.20 selection-rate behaviour which was crucial for evolve_nqueens

0 commit comments

Comments
 (0)