Skip to content

Commit cffd6ff

Browse files
committed
Unify duplicate-gene repair across the GA lifecycle
Repair finite spaces through replacement chains using each destination gene's type, range, and constraints. Reuse the same repair for initialization, operators, callbacks, and population growth while retaining the existing helper signatures. Add regression coverage, restore chain tests, and document sampling limits and the new example.
1 parent 773b2de commit cffd6ff

17 files changed

Lines changed: 1031 additions & 1095 deletions

‎docs/source/gene_values.md‎

Lines changed: 31 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,11 @@ Then the value of the first gene in the passed solution is `1`. By filtering the
228228

229229
Sometimes it is normal for PyGAD to fail to find a gene value that satisfies the constraint. For example, if the possible gene values are only `[20,30,40]` and the gene constraint restricts the values to be greater than 50, then it is impossible to meet the constraint.
230230

231-
For some other cases, the constraint can be met but with some changes. For example, increasing the range from which a value is sampled. If the `gene_space` is used and assigned `range(10)`, then the gene constraint can be met by using `range(50)` so that we can find values greater than 50.
231+
For some other cases, the constraint can be met but with some changes. For example, increasing the range from which a value is sampled. If the `gene_space` is used and assigned `range(10)`, then the gene constraint can be met by using `range(100)` so that we can find values greater than 50.
232232

233-
Even if the gene space is already assigned `range(1000)`, it might still not find values that meet the constraints. This is because PyGAD samples a number of values equal to the `sample_size` parameter which defaults to *100*.
233+
Finite gene spaces, such as `range(1000)`, provide their full candidate list for constraint checks. When candidates come from random sampling, a larger `sample_size` can increase the chance of finding a value that meets a narrow constraint.
234234

235-
Out of the range of *1000* numbers, all the 100 values might not be satisfying the constraint. This issue could be solved by simply assigning a larger value for the `sample_size` parameter.
236-
237-
> PyGAD does not yet handle the **dependencies** among the genes in the `gene_constraint` parameter.
235+
> Initialization and ordinary mutation apply gene constraints sequentially. They do not determine the dependency order among the genes automatically.
238236
>
239237
> This is an example where gene 0 depends on gene 1. To efficiently enforce the constraints, the constraint for gene 1 must be enforced first (if not `None`) then the constraint for gene 0.
240238
>
@@ -248,6 +246,8 @@ Out of the range of *1000* numbers, all the 100 values might not be satisfying t
248246
>
249247
> PyGAD applies constraints sequentially, starting from the first gene to the last. To ensure correct behavior when genes depend on each other, structure your GA problem so that if gene X depends on gene Y, then gene Y appears earlier in the chromosome (solution) than gene X. As a result, its gene constraint will be earlier in the list.
250248

249+
Duplicate repair also checks all constraints against complete candidate solutions before accepting changes. Its additional search for dependent constraints is bounded by `sample_size`; this does not reorder the general initialization or mutation constraint checks.
250+
251251
### Full Example
252252

253253
For a full example, please check the [`examples/example_gene_constraint.py` script](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/examples/example_gene_constraint.py).
@@ -270,11 +270,17 @@ If the objective is to find a unique value or enforce the gene constraint, then
270270

271271
Sometimes 100 values is not enough and PyGAD sometimes fails to find a good value. In this case, it is highly recommended to increase the `sample_size` parameter. This is to create a larger sample to increase the chance of finding a value that meets our objectives.
272272

273+
For duplicate repair, finite spaces are considered in full. These include lists, tuples, NumPy arrays, `range` objects, stepped dictionaries, and integer random ranges. Increasing `sample_size` is useful for continuous candidates and constraints that depend on other genes; it is not needed to explore a larger finite space.
274+
275+
When replacement chains do not satisfy a dependent constraint, PyGAD also tries alternative complete assignments. This additional search considers up to `sample_size * num_genes` tentative gene assignments. A larger value allows more alternatives to be checked. The limit prevents arbitrary constraint functions from requiring an unbounded combinatorial search.
276+
273277
## Prevent Duplicates in Gene Values
274278

275279
In [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0), a new bool parameter called `allow_duplicate_genes` is supported to control whether duplicates are supported in the chromosome or not. In other words, whether 2 or more genes might have the same exact value.
276280

277-
If `allow_duplicate_genes=True` (which is the default case), genes may have the same value. If `allow_duplicate_genes=False`, then no 2 genes will have the same value given that there are enough unique values for the genes.
281+
If `allow_duplicate_genes=True` (which is the default case), genes may have the same value. If `allow_duplicate_genes=False`, PyGAD tries to give each gene a different numeric value within its solution. Duplicates are checked after applying the configured gene types and rounding. Mixed numeric types are compared by their exact stored values; for example, integer 1 and float 1.0 duplicate each other.
282+
283+
The same repair is used for generated and manually supplied initial populations, crossover, mutation, and NSGA-III population growth. It also checks outputs from custom crossover and mutation functions and the `on_crossover` and `on_mutation` callbacks. Duplicate solutions in different population rows are allowed; this parameter controls repeated values within a single row.
278284

279285
The next code gives an example to use the `allow_duplicate_genes` parameter. A callback generation function is implemented to print the population after each generation.
280286

@@ -398,79 +404,44 @@ Generation 5
398404
[1 2 4 3]]
399405
```
400406

401-
You should give enough values for the genes so that PyGAD can find an alternative when a gene value duplicates another gene.
402-
403-
If PyGAD fails to find a unique gene value while there is still room to find one, then set the `sample_size` parameter to a larger value. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information.
407+
(solve-duplicates-using-a-third-gene)=
404408

405-
### Limitation
409+
### Repair through Other Genes
406410

407-
There might be 2 duplicate genes where changing either of the 2 duplicating genes will not solve the problem. For example, if `gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]` and the solution is `[3 2 0 0]`, then the values of the last 2 genes duplicate. There are no possible changes in the last 2 genes to solve the problem.
411+
There must be enough distinct values that can be assigned to the individual genes. Counting all values in the combined space is not sufficient. For example, `gene_space=[[0], [0], [1, 2]]` cannot give the first two genes different values.
408412

409-
This problem can be solved by randomly changing one of the non-duplicating genes to make room for a unique value in one of the 2 duplicating genes. For example, by changing the second gene from 2 to 4, then any of the last 2 genes can take the value 2 and solve the duplicates. The resultant gene is then `[3 4 2 0]`. But this option is not yet supported in PyGAD.
413+
Repair first tries unused values. If a needed value is already used by another gene, PyGAD looks for an alternative for that gene. This can involve either duplicate occurrence and a chain of several replacements. The chain is applied together so no temporary duplicate is treated as a finished solution.
410414

411-
### Solve Duplicates using a Third Gene
412-
413-
When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happens that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene with another. In [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-1-0), the duplicates are solved by looking for a third gene that helps solve them. The following examples explain how it works.
414-
415-
Example 1:
416-
417-
Let's assume that this gene space is used and there is a solution with 2 duplicate genes with the same value 4.
415+
For example:
418416

419417
```python
420-
Gene space: [[2, 3],
421-
[3, 4],
422-
[4, 5],
423-
[5, 6]]
424-
Solution: [3, 4, 4, 5]
418+
gene_space = [[0, 1], [1, 2], [2, 3], [0]]
419+
initial_population = [[0, 1, 2, 0], [0, 1, 2, 0]]
425420
```
426421

427-
By checking the gene space, the second gene can have the values `[3, 4]` and the third gene can have the values `[4, 5]`. To solve the duplicates, we change the value of one of these 2 genes.
422+
The last gene can only keep 0. Repair moves the third gene from 2 to 3, the second gene from 1 to 2, and the first gene from 0 to 1. The repaired solution is `[1, 2, 3, 0]`. Each replacement belongs to its destination gene's space.
428423

429-
If the value of the second gene changes from 4 to 3, then it will duplicate the first gene. If we change the value of the third gene from 4 to 5, then it will duplicate the fourth gene. In short, simply selecting a different value for either the second or third gene will introduce new duplicate genes.
424+
This behavior also handles third-gene repairs, such as changing `[3, 4, 4, 5]` into `[2, 3, 4, 5]` for `gene_space=[[2, 3], [3, 4], [4, 5], [5, 6]]`.
430425

431-
When there are 2 duplicate genes but there is no way to solve their duplicates, then the solution is to change a third gene that makes a room to solve the duplicates between the 2 genes.
426+
A runnable example is available in [`examples/example_duplicate_gene_repair.py`](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/examples/example_duplicate_gene_repair.py).
432427

433-
In our example, duplicates between the second and third genes can be solved by, for example,:
428+
### Ranges, Types, and Constraints
434429

435-
* Changing the first gene from 3 to 2 then changing the second gene from 4 to 3.
436-
* Or changing the fourth gene from 5 to 6 then changing the third gene from 4 to 5.
430+
Each replacement uses its own gene's range, type, precision, and constraint. Initialization uses `init_range_low` and `init_range_high` with replacement. Random and adaptive mutation use `random_mutation_min_val` and `random_mutation_max_val`, respecting `mutation_by_replacement`. SBX crossover and polynomial mutation use their per-gene initialization bounds for repair.
437431

438-
Generally, this is how to solve such duplicates:
432+
An explicit gene-space value replaces the gene regardless of `mutation_by_replacement`. A nested space entry of `None` follows the random-mutation mode. A `None` inside a list, tuple, or array supplies fresh replacement candidates from the relevant initialization or mutation range. It is not frozen into a single cached initial value.
439433

440-
1. For any duplicate gene **GENE1**, select another value.
441-
2. Check which other gene **GENEX** has duplicate with this new value.
442-
3. Find if **GENEX** can have another value that will not cause any more duplicates. If so, go to step 7.
443-
4. If all the other values of **GENEX** will cause duplicates, then try another gene **GENEY**.
444-
5. Repeat steps 3 and 4 until exploring all the genes.
445-
6. If there is no way to solve the duplicates, then we have to keep the duplicate value.
446-
7. If a value for a gene **GENEM** is found that will not cause more duplicates, then use this value for the gene **GENEM**.
447-
8. Replace the value of the gene **GENE1** by the old value of the gene **GENEM**. This solves the duplicates.
434+
Constraints on other genes may depend on a replaced position. PyGAD checks all constraints against a complete repaired solution before accepting it. If the repair would violate another gene's constraint, that assignment is rejected and alternatives are considered.
448435

449-
This is an example to solve the duplicate for the solution `[3, 4, 4, 5]`:
436+
Manually supplied values and values inherited from parents are kept when possible. Supplying a population does not automatically replace every value outside `gene_space` or the initialization range. New repair values follow the configured destination space or range.
450437

451-
1. Let's use the second gene with value 4. Because the space of this gene is `[3, 4]`, then the only other value we can select is 3.
452-
2. The first gene also has the value 3.
453-
3. The first gene has another value 2 that will not cause more duplicates in the solution. Then go to step 7.
454-
4. Skip.
455-
5. Skip.
456-
6. Skip.
457-
7. The value of the first gene 3 will be replaced by the new value 2. The new solution is [2, 4, 4, 5].
458-
8. Replace the value of the second gene 4 by the old value of the first gene which is 3. The new solution is [2, 3, 4, 5]. The duplicate is solved.
438+
### When Duplicates Remain
459439

460-
Example 2:
461-
462-
```python
463-
Gene space: [[0, 1],
464-
[1, 2],
465-
[2, 3],
466-
[3, 4]]
467-
Solution: [1, 2, 2, 3]
468-
```
440+
Finite spaces without dependent constraints are searched completely, including replacement chains. If no unique assignment exists, repair keeps as many distinct values as possible and reports the remaining duplicates with warnings unless `suppress_warnings=True`.
469441

470-
The quick summary is:
442+
Continuous ranges are sampled, so finding every possible value cannot be guaranteed. Rounding or a narrow numeric type can also reduce the number of distinct available values. Increase `sample_size` when continuous sampling or the additional search for dependent constraints needs more candidates or alternatives. If no constraint-valid improvement is found, existing values are retained instead of accepting a repair that violates a constraint.
471443

472-
* Change the value of the first gene from 1 to 0. The solution becomes [0, 2, 2, 3].
473-
* Change the value of the second gene from 2 to 1. The solution becomes [0, 1, 2, 3]. The duplicate is solved.
444+
Changes made directly to the population in other callbacks remain the responsibility of those callbacks. Custom operators still need to generate meaningful values for the problem; duplicate repair is not a general validator of every custom operator output.
474445

475446
## More about the `gene_type` Parameter
476447

‎docs/source/pygad.md‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ The upper value of the random range from which the gene values in the initial po
165165
:::{dropdown} `allow_duplicate_genes=True`: Allow repeated values within a solution.
166166
:animate: fade-in-slide-down
167167

168-
Added in [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0). If `True`, then a solution/chromosome may have duplicate gene values. If `False`, then each gene will have a unique value in its solution.
168+
Added in [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0). If `True`, then a solution/chromosome may have duplicate gene values. If `False`, PyGAD tries to give each gene a different numeric value after conversion and rounding. Repair can follow chains of replacements using each destination gene's space, range, type, precision, and constraint. If no usable alternative is found, duplicates remain with a warning unless warnings are suppressed. See [Prevent Duplicates in Gene Values](https://pygad.readthedocs.io/en/latest/gene_values.html#prevent-duplicates-in-gene-values).
169169

170170
For permutation encodings where every value in `gene_space` is already used, random and adaptive mutation try a compatible swap instead of keeping the selected gene unchanged. The fallback preserves destination gene types, numeric values, gene spaces, uniqueness, and constraints. Each gene can participate in at most one fallback swap per mutation pass. If no compatible partner exists, the gene stays unchanged. See {ref}`Mutation Methods <mutation-methods>`.
171171
:::
@@ -177,6 +177,8 @@ The size of the sample of candidate values PyGAD draws when it needs to pick a g
177177

178178
It is useful when `allow_duplicate_genes=False` or `gene_constraint` is used. If PyGAD cannot find a unique value or a value that meets a constraint, increase this parameter.
179179

180+
Duplicate repair considers finite spaces in full. For constraints depending on other genes, an additional search checks up to `sample_size * num_genes` tentative assignments when replacement chains do not satisfy all constraints.
181+
180182
Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). See the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information.
181183
:::
182184

@@ -580,7 +582,7 @@ Constructor settings and user callables are stored as instance attributes, with
580582
- `initial_population`: Frozen copy of the initial population, set after `initialize_population` runs.
581583
- `pop_size`: A `(sol_per_pop, num_genes)` tuple describing the population shape.
582584
- `gene_type_single`: `True` when every gene shares the same dtype; `False` when `gene_type` is a list/tuple/numpy.ndarray. Added in [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0).
583-
- `gene_space_unpacked`: Unpacked version of `gene_space`. For example, `range(1, 5)` becomes `[1, 2, 3, 4]`; `{'low': 2, 'high': 4}` becomes a finite sample. Added in [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-1-0).
585+
- `gene_space_unpacked`: A snapshot of the converted finite values and continuous samples in `gene_space`. Generation reads the original space so `None` entries remain random. For example, `range(1, 5)` becomes `[1, 2, 3, 4]`; `{'low': 2, 'high': 4}` becomes a finite sample. Added in [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-1-0).
584586

585587
##### Methods
586588

@@ -763,15 +765,21 @@ The {ref}`complete fitness dispatch reference <fitness-evaluation>` documents pa
763765
- `validate_gene_constraint_callable_output(selected_values, values)`: Sanity-check the return value of a user-defined `gene_constraint`.
764766
- `filter_gene_values_by_constraint(values, solution, gene_idx)`: Run `gene_constraint[gene_idx]` and return the filtered list.
765767
- `get_valid_gene_constraint_values(...)`: Sample candidate values until one satisfies the gene constraint.
766-
- `solve_duplicate_genes_randomly(...)`: Resolve duplicate genes by sampling new values from the random range.
767-
- `solve_duplicate_genes_by_space(...)`: Resolve duplicate genes by sampling new values from `gene_space`.
768-
- `solve_duplicates_deeply(...)`: Slow, exhaustive fallback for duplicate resolution.
768+
- `solve_duplicate_genes(solution, build_initial_pop=False, ...)`: Shared repair for a solution using each gene's space, range, type, precision, and constraint. Returns the repaired copy, remaining duplicate indices, and their count. Can follow chains of replacements.
769+
- `solve_duplicate_genes_in_population(population, build_initial_pop=False)`: Convert and round population rows before applying the shared repair.
770+
- `get_duplicate_gene_indices(solution)`: Return the indices after the first occurrence of each repeated value.
771+
- `solution_satisfies_gene_constraints(solution)`: Check every constraint against a complete candidate solution.
772+
- `get_gene_space_values(...)`: Return converted finite candidates or fresh continuous candidates for one gene.
773+
- `is_gene_value_in_space(...)`: Check a swap candidate against the original finite space or continuous bounds.
774+
- `solve_duplicate_genes_randomly(...)`: Compatibility helper for the shared repair using explicit random ranges.
775+
- `solve_duplicate_genes_by_space(...)`: Compatibility helper for the shared repair using `gene_space`.
776+
- `solve_duplicates_deeply(...)`: Compatibility helper for replacement-chain repair. Returns `None` when no duplicates are resolved.
769777
- `unique_int_gene_from_range(...)`: Pick an integer gene that does not already appear in the solution.
770778
- `unique_float_gene_from_range(...)`: Pick a float gene that does not already appear in the solution.
771779
- `unique_gene_by_space(...)`: Pick a unique value from `gene_space`.
772780
- `unique_genes_by_space(...)`: Pick unique values for several genes from `gene_space`.
773-
- `select_unique_value(...)`: Sample one value uniformly at random from a list of candidates.
774-
- `find_two_duplicates(solution)`: Locate the first pair of duplicated indices in a solution.
781+
- `select_unique_value(...)`: Pick an unused candidate when possible, otherwise keep the current value.
782+
- `find_two_duplicates(solution, gene_space_unpacked)`: Find a duplicated gene with alternative values in its space.
775783
- `unpack_gene_space(...)`: Materialize the unpacked `gene_space` (used to build `gene_space_unpacked`).
776784

777785
#### Saving, Loading, and Reporting

0 commit comments

Comments
 (0)