genetic_algorithms 3.0.0

Library for solving genetic algorithm problems
Documentation
//! Generic configuration validator for all GA engine types.
//!
//! Provides the core validation logic that checks population sizes, operator
//! compatibility, parameter bounds, and feature-flag consistency before the
//! GA run begins. Called automatically during `.build()` on all engines.
//!
//! # Key items
//!
//! | Item | Description |
//! |------|-------------|
//! | [`validate`] | Main validation function checking all configuration fields |
//!
//! [`validate`]: crate::validators::generic_validator::validate

use crate::configuration::{GaConfiguration, ProblemSolving};
use crate::error::GaError;
use crate::operations;
use crate::population::Population;
use crate::traits::{GeneT, LinearChromosome, OperatorCompat};
use crate::chromosomes::ChromosomeLength;
use std::collections::HashSet;

/// Validate a GA configuration and/or population before running.
///
/// Checks population chromosome length consistency, configuration field
/// validity, operator compatibility, and parameter bounds. Returns an
/// error if any validation check fails.
///
/// # Arguments
/// * `configuration` — Optional GA configuration to validate
/// * `population` — Optional population to validate
/// * `_alleles` — Reserved for future allele-based validation (currently unused)
pub fn validate<U>(
    configuration: Option<&GaConfiguration>,
    population: Option<&Population<U>>,
    _alleles: Option<&[U::Gene]>,
) -> Result<(), GaError>
where
    U: LinearChromosome + Send + Sync + 'static + Clone,
    U::Gene: 'static,
{
    //1 We call the condition for checking the length of every chromosome
    if let Some(population) = population {
        same_dna_length(population)?;
    }

    //2 Checks the configuration
    if let Some(configuration) = configuration {
        //2.1- We call the condition for fixed fitness
        if configuration.limit_configuration.problem_solving == ProblemSolving::FixedFitness {
            fitness_target_is_some(
                configuration,
                configuration
                    .limit_configuration
                    .problem_solving
                    .to_string(),
            )?;
        }

        //2.2 Checks the population
        if let Some(population) = population {
            //2.2.1- Checks the conditions for cycle crossover operation
            if configuration.crossover_configuration.method == operations::Crossover::Cycle {
                unique_gene_ids(population)?;
            }
        }

        //2.3 Condition checkers for the adaptive genetic algorithms
        if configuration.adaptive_ga {
            //2.3.1- Checks for the crossover parameters
            aga_crossover_probabilities(configuration)?;
        }

        //2.6 Condition checker for the couples
        number_of_couples_is_set(configuration)?;

        //2.7 Validate ChromosomeLength::Variable bounds
        validate_chromosome_length(configuration)?;

        //2.8 Validate length_penalty is non-negative
        validate_length_penalty(configuration)?;
    }

    Ok(())
}

/// Checks that every chromosome has unique id's within their dna.
///
/// Uses a `HashSet` for O(N) per chromosome instead of O(N²) nested loop.
pub fn unique_gene_ids<U>(population: &Population<U>) -> Result<(), GaError>
where
    U: LinearChromosome + Send + Sync + 'static + Clone,
{
    for (chromosome_number, chromosome) in population.chromosomes.iter().enumerate() {
        let mut seen = HashSet::with_capacity(chromosome.dna().len());
        for (gene_number, gene) in chromosome.dna().iter().enumerate() {
            if !seen.insert(gene.id()) {
                return Err(GaError::ValidationError(format!(
                    "Gene id must be unique within the DNA. The chromosome #{} has a duplicate gene id {} at gene #{}",
                    chromosome_number, gene.id(), gene_number
                )));
            }
        }
    }
    Ok(())
}

/// This function checks that fitness target is not none
pub fn fitness_target_is_some(
    configuration: &GaConfiguration,
    problem_type: String,
) -> Result<(), GaError> {
    if configuration.limit_configuration.fitness_target.is_none() {
        return Err(GaError::ConfigurationError(format!(
            "For {} problems, fitness_target must be set.",
            problem_type
        )));
    }
    Ok(())
}

/// Checks that all the chromosomes have the same dna length.
///
/// Compares each chromosome to the first one in O(N) instead of O(N²).
pub fn same_dna_length<U>(population: &Population<U>) -> Result<(), GaError>
where
    U: LinearChromosome + Send + Sync + 'static + Clone,
{
    let Some(first) = population.chromosomes.first() else {
        return Ok(());
    };
    let expected_len = first.dna().len();
    for (i, chromosome) in population.chromosomes.iter().enumerate().skip(1) {
        let len = chromosome.dna().len();
        if len != expected_len {
            return Err(GaError::ValidationError(format!(
                "All the chromosomes must have the same dna length. Chromosome #0 has a dna with length {} and chromosome #{} has a dna with length {}.",
                expected_len, i, len
            )));
        }
    }
    Ok(())
}

/// Function to check that the chromosome length is not bigger than the alleles
pub fn chromosome_length_not_bigger_than_alleles<U>(
    alleles: &[U::Gene],
    genes_per_chromosome: usize,
) -> Result<(), GaError>
where
    U: LinearChromosome + Send + Sync + 'static + Clone,
{
    if genes_per_chromosome > alleles.len() {
        return Err(GaError::ConfigurationError(
            "The number of genes within a chromosome should not be higher than the different alleles.".to_string()));
    }
    Ok(())
}

/// Checks that for adaptive crossover all the requirements are set
pub fn aga_crossover_probabilities(configuration: &GaConfiguration) -> Result<(), GaError> {
    if configuration
        .crossover_configuration
        .probability_max
        .is_none()
        || configuration
            .crossover_configuration
            .probability_min
            .is_none()
    {
        return Err(GaError::ConfigurationError(
            "For Adaptive Genetic Algorithms, the probability_max and probability_min in the crossover_configuration are mandatory.".to_string()));
    } else if configuration.crossover_configuration.probability_max
        <= configuration.crossover_configuration.probability_min
    {
        return Err(GaError::ConfigurationError(
            "For Adaptive Genetic Algorithms, the probability_max must be greater than probability_min in the crossover_configuration.".to_string()));
    }
    Ok(())
}

/// Function to check that the number of couples is set
pub fn number_of_couples_is_set(configuration: &GaConfiguration) -> Result<(), GaError> {
    if configuration.selection_configuration.number_of_couples == 0 {
        return Err(GaError::ConfigurationError(
            "The number of couples must be set.".to_string(),
        ));
    }
    Ok(())
}

/// Checks that the configured crossover and mutation operators are valid for the
/// chromosome type `U` according to its `OperatorCompat` implementation.
///
/// Returns `Ok(())` if the chromosome type has no restrictions (`None`), or if the
/// configured operator is in the valid set. Returns `Err(GaError::ConfigurationError)`
/// if the configured operator is not in the valid set.
///
/// Called from `Ga::build()` after the existing validator chain, before any run.
pub fn operator_compat_check<U>(configuration: &GaConfiguration) -> Result<(), GaError>
where
    U: LinearChromosome + OperatorCompat + Send + Sync + 'static + Clone,
{
    if let Some(valid) = U::valid_crossovers() {
        if !valid.contains(&configuration.crossover_configuration.method) {
            return Err(GaError::ConfigurationError(format!(
                "Crossover::{:?} is not valid for this chromosome type. Valid crossovers: {:?}",
                configuration.crossover_configuration.method, valid
            )));
        }
    }
    if let Some(valid) = U::valid_mutations() {
        if !valid.contains(&configuration.mutation_configuration.method) {
            return Err(GaError::ConfigurationError(format!(
                "Mutation::{:?} is not valid for this chromosome type. Valid mutations: {:?}",
                configuration.mutation_configuration.method, valid
            )));
        }
    }
    Ok(())
}

/// Validates `ChromosomeLength::Variable { min, max }` bounds.
///
/// Enforces `min >= 1` and `min <= max` to prevent nonsensical configurations
/// that would cause undefined behavior in insertion/deletion mutation operators.
pub fn validate_chromosome_length(configuration: &GaConfiguration) -> Result<(), GaError> {
    match configuration.limit_configuration.chromosome_length {
        ChromosomeLength::Variable { min, max } => {
            if min == 0 {
                return Err(GaError::ConfigurationError(
                    "ChromosomeLength::Variable requires min >= 1, got min = 0".to_string(),
                ));
            }
            if min > max {
                return Err(GaError::ConfigurationError(format!(
                    "ChromosomeLength::Variable requires min <= max, got min = {}, max = {}",
                    min, max
                )));
            }
        }
        ChromosomeLength::Fixed(_) => {}
    }
    Ok(())
}

/// Validates that `length_penalty` is non-negative.
///
/// A negative length penalty would reverse parsimony pressure (penalizing shorter
/// chromosomes instead of longer ones), which contradicts the documented semantics.
pub fn validate_length_penalty(configuration: &GaConfiguration) -> Result<(), GaError> {
    if let Some(penalty) = configuration.length_penalty {
        if penalty < 0.0 {
            return Err(GaError::ConfigurationError(format!(
                "length_penalty must be >= 0.0, got {}",
                penalty
            )));
        }
    }
    Ok(())
}