Skip to content

Allow complex gene types #322

Description

@milannal1m

Some Use Cases need more complex gene types than int or float. E.g. Lists/Arrays/tuples would be very useful.

Activity

jkhanGitHub commented on Dec 24, 2025

@jkhanGitHub

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_structure or 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:

  1. 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.
  2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions