Repository navigation
Allow complex gene types #322
Description
Activity
Hey @ahmedfgad I would like to work on this issue, therefore there are some information i need beforehand to implement this enhancement in a way that aligns with the ideas of the software. I made a quick overview on the Problem and the issues that the implementation might run into.
Objective
Allow the user to work with Lists,Arrays,Tuples as gene types.
As a result a chromosome will contain genes with multiple values.
Proposed Architecture
First before such implementations can happen it is important to maintain the current logic of the software. That means we want all the mutation and crossover methods to keep on working. In order to achieve this the complex genes must be treated similiarly to the genes that are not complex, e.g the way it is right now. In particular this means that we keep atomic crossovers as they are right now and lists will always be seen as a whole gene. To achieve this it makes the most sense to implement a function that takes the solutions and slices them into segments, while keeping a gene_structure parameter that holds the lengths of all complex genes. This way we flatten the list and can keep using the current logic. For example
Example Mapping
gene_structure = [2, 3, 1] (Represents a coordinate, a color, and a scale factor).
| Step | Representation | Data Type |
|---|---|---|
| Engine State | [10, 20, 255, 128, 0, 1.5] |
np.ndarray (float64) |
| Slicing Logic | [0:2], [2:5], [5:6] |
Index Tuples |
| User View | [[10, 20], [255, 128, 0], [1.5]] |
list of np.views |
Benefits:
- Memory Zero-Copy: Uses NumPy Views, not copies.
- Speed: Maintains full NumPy vectorization for internal GA operations.
3. Atomic Crossover (The Structural Protection)
We restrict crossover points to segment boundaries.
This way we can ensure that the random index for a crossover will take the list as a whole, because it treats the segments as one block.
- Implementation: The GA picks split points only from the pre-calculated list of indices provided by
gene_structureor the index Tuples. - Result: Functional blocks stay intact across generations, preserving the relationship between internal elements.
This implementation makes sense, because this way we treat Lists/Arrays/tuples as gene types.
4. Hierarchical Mutation (The Optimization Engine)
Right now the lists are never optimized, since the values of the original lists will never change due to them being the gene types of the genes.
In theory there are a couple of ways to ensure optimization. First would be not to treat them as gene types and allow crossover points withing the segments, which would directly contradict the issues objective of implementing lists as gene types by definition of genes. The other way to optimize them would be through mutation.
To ensure the internal values of these blocks are still optimized, we introduce a dual-level mutation strategy:
- Block-Level Mutation: Replaces the entire list-gene for broad exploration. This would be similiar to a random mutation and can be further extended to the different mutations (mutation_probs_by_space, mutation_process_gene_value, ...). Obviously this needs to be done with respect to the gene space.
- Element-Level Mutation: Targets a specific index within a list-gene for fine-tuning (exploitation). For this all that needs to be dont is ignoring the sliced segments.
Fitness Function
Since the user always makes up the fitness function by himself it is important to keep in mind that the user uses a mental model of lists as gene types compared to the flattened lists that is actually used in computation. Therefore it is important to make sure that the fitness function the user writes on the lists will be applied correctly on the flattened chromosome. For the user the most convenient method to tackle this problem would be to transform the lists to the structured view of his initial mental model. We are using NumPy Views and a fitness wrapper for this task. This guarantees:
- Zero-copy data handling.
- No significant memory overhead (O(k) instead of O(n)).
- Automatic cleanup after each fitness call.
def fitness_wrapper(self, solution, solution_idx):
# Instead of [solution[s:e] for s, e in self.slice_indices]
# We use a generator ( ) instead of a list [ ]
structured_gen = (solution[s:e] for s, e in self.slice_indices)
# Call the fitness function of the user on his mental model (structured_gen)
return self.fitness_func(self, structured_gen, solution_idx)self refers to the ga_instance in here.
Conclusion
With this implementation we would only need to make slight changes and can keep most of the logic the same. We would just introduce a new gene_structure parameter to tell the program, that it now works with a chromosome that contains Objects, and let the user decide in what way he wants to protect these blocks (atomic_crossover, decides wheter a crossover is allowed withing a list). The mutation_type hierarchial let the user give control how he wants to handle the mutation of lists. This is important especially when the lists are protected by the atomic crossover rule.
ga_instance = pygad.GA(
num_generations=100,
sol_per_pop=20,
gene_structure=[2, 3, 1], # Map for complex genes
atomic_crossover=True, # Protect blocks
mutation_type="hierarchical",
)I would like to hear your thoughts on this implementation idea and would like to help you with the implementation in form of a PR.
Some Use Cases need more complex gene types than int or float. E.g. Lists/Arrays/tuples would be very useful.