You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit cffd6ff
Browse filesBrowse the repository at this point in the historyBrowse files
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.
Copy file name to clipboardExpand all lines: docs/source/gene_values.md
+31-60Lines changed: 31 additions & 60 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -228,13 +228,11 @@ Then the value of the first gene in the passed solution is `1`. By filtering the
228
228
229
229
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.
230
230
231
-
For some other cases, the constraint can be met but with some changes. For example, increasing the rangefrom 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 rangefrom 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.
232
232
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 listfor 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.
234
234
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.
238
236
>
239
237
> 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 (ifnot`None`) then the constraint for gene 0.
240
238
>
@@ -248,6 +246,8 @@ Out of the range of *1000* numbers, all the 100 values might not be satisfying t
248
246
>
249
247
> 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.
250
248
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
+
251
251
### Full Example
252
252
253
253
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
270
270
271
271
Sometimes 100 values isnot 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.
272
272
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 isnot 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
+
273
277
## Prevent Duplicates in Gene Values
274
278
275
279
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.
276
280
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 1andfloat1.0 duplicate each other.
282
+
283
+
The same repair is used for generated and manually supplied initial populations, crossover, mutation, andNSGA-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.
278
284
279
285
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.
280
286
@@ -398,79 +404,44 @@ Generation 5
398
404
[1243]]
399
405
```
400
406
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)=
404
408
405
-
### Limitation
409
+
### Repair through Other Genes
406
410
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`[3200]`, then the values of the last 2genes 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 inthe combined space isnot sufficient. For example, `gene_space=[[0], [0], [1, 2]]`cannot give the first two genes different values.
408
412
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 genefrom2 to 4, then any of the last 2 genes can take the value 2andsolve the duplicates. The resultant gene isthen `[3420]`. But this option isnot 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 occurrenceanda chain of several replacements. The chain isapplied together so no temporary duplicate istreated as a finished solution.
410
414
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:
418
416
419
417
```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]]
425
420
```
426
421
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 from2 to 3, the second gene from1 to 2, and the first gene from0 to 1. The repaired solution is`[1, 2, 3, 0]`. Each replacement belongs to its destination gene's space.
428
423
429
-
If the value of the second gene changes from4 to 3, then it will duplicate the first gene. If we change the value of the third gene from4 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]]`.
430
425
431
-
When there are 2 duplicate genes but there isno 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 isavailable in [`examples/example_duplicate_gene_repair.py`](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/examples/example_duplicate_gene_repair.py).
432
427
433
-
In our example, duplicates between the second andthird genes can be solved by, for example,:
428
+
### Ranges, Types, and Constraints
434
429
435
-
* Changing the first gene from3 to 2 then changing the second gene from4 to 3.
436
-
* Or changing the fourth gene from5 to 6 then changing the third gene from4 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.
437
431
438
-
Generally, this ishow 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 isnot frozen into a single cached initial value.
439
433
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 3and4 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.
448
435
449
-
This is an example to solve the duplicate forthe 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`orthe initialization range. New repair values follow the configured destination space orrange.
450
437
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 is3. The new solution is [2, 3, 4, 5]. The duplicate is solved.
438
+
### When Duplicates Remain
459
439
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`.
469
441
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.
471
443
472
-
* Change the value of the first gene from1 to 0. The solution becomes [0, 2, 2, 3].
473
-
* Change the value of the second gene from2 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 isnot a general validator of every custom operator output.
Copy file name to clipboardExpand all lines: docs/source/pygad.md
+15-7Lines changed: 15 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -165,7 +165,7 @@ The upper value of the random range from which the gene values in the initial po
165
165
:::{dropdown} `allow_duplicate_genes=True`: Allow repeated values within a solution.
166
166
:animate: fade-in-slide-down
167
167
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).
169
169
170
170
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>`.
171
171
:::
@@ -177,6 +177,8 @@ The size of the sample of candidate values PyGAD draws when it needs to pick a g
177
177
178
178
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.
179
179
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
+
180
182
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.
181
183
:::
182
184
@@ -580,7 +582,7 @@ Constructor settings and user callables are stored as instance attributes, with
580
582
-`initial_population`: Frozen copy of the initial population, set after `initialize_population` runs.
581
583
-`pop_size`: A `(sol_per_pop, num_genes)` tuple describing the population shape.
582
584
-`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).
584
586
585
587
##### Methods
586
588
@@ -763,15 +765,21 @@ The {ref}`complete fitness dispatch reference <fitness-evaluation>` documents pa
763
765
-`validate_gene_constraint_callable_output(selected_values, values)`: Sanity-check the return value of a user-defined `gene_constraint`.
764
766
-`filter_gene_values_by_constraint(values, solution, gene_idx)`: Run `gene_constraint[gene_idx]` and return the filtered list.
765
767
-`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.
769
777
-`unique_int_gene_from_range(...)`: Pick an integer gene that does not already appear in the solution.
770
778
-`unique_float_gene_from_range(...)`: Pick a float gene that does not already appear in the solution.
771
779
-`unique_gene_by_space(...)`: Pick a unique value from `gene_space`.
772
780
-`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.
775
783
-`unpack_gene_space(...)`: Materialize the unpacked `gene_space` (used to build `gene_space_unpacked`).
0 commit comments