Skip to main content

quantrs2_tytan/sampler/
genetic_algorithm.rs

1//! # Genetic Algorithm (GA) Sampler
2//!
3//! The Genetic Algorithm (GA) sampler evolves a population of binary solutions
4//! through successive generations of selection, crossover, and mutation to find
5//! low-energy configurations of QUBO/HOBO problems.
6//!
7//! ## Algorithm
8//!
9//! Each generation:
10//!
11//! 1. **Evaluation**: compute the QUBO energy for every individual in the population.
12//! 2. **Selection**: tournament selection — two random individuals compete; the
13//!    one with lower energy survives as a parent.
14//! 3. **Crossover**: produce offspring from two parents using one of:
15//!    - *Uniform* — each gene is taken from parent 1 or 2 with equal probability.
16//!    - *Single-point* — split at a random point and swap the tails.
17//!    - *Two-point* — swap the middle segment between two random split points.
18//!    - *Adaptive* — strategy chosen based on Hamming distance of parents.
19//! 4. **Mutation**: flip bits with probability `p_mut` (fixed, annealed, or
20//!    adaptive based on population diversity).
21//! 5. **Elitism**: the best individual of the current generation is always
22//!    preserved in the next generation.
23//!
24//! ## Mathematical Formulation
25//!
26//! Given a QUBO matrix Q, the objective is to minimise:
27//!
28//! ```text
29//! E(x) = Σ_{i,j} Q[i,j] · x[i] · x[j],   x[i] ∈ {0, 1}
30//! ```
31//!
32//! Fitness of individual x equals E(x) (lower is better).
33//!
34//! ## Citation
35//!
36//! Holland, J. H. (1975). *Adaptation in Natural and Artificial Systems*.
37//! University of Michigan Press. ISBN: 978-0-472-08460-9.
38//!
39//! ## When to Use
40//!
41//! - **Best for**: medium-size problems (n ≤ 200) where crossover is constructive.
42//! - **Strengths**: maintains population diversity, explores multiple basins.
43//! - **Limitations**: slower per-iteration than SA; may converge prematurely
44//!   without sufficient population size.
45//!
46//! ## Usage
47//!
48//! ```
49//! use quantrs2_tytan::sampler::{GASampler, Sampler};
50//! use scirs2_core::ndarray::Array;
51//! use std::collections::HashMap;
52//!
53//! // Minimise: -x0 - x1 - x2 + 2*x0*x1 + 2*x0*x2  (independence problem)
54//! let mut q = Array::<f64, _>::zeros((3, 3));
55//! q[[0, 0]] = -1.0;
56//! q[[1, 1]] = -1.0;
57//! q[[2, 2]] = -1.0;
58//! q[[0, 1]] = 2.0;
59//! q[[0, 2]] = 2.0;
60//!
61//! let mut var_map = HashMap::new();
62//! var_map.insert("x0".to_string(), 0);
63//! var_map.insert("x1".to_string(), 1);
64//! var_map.insert("x2".to_string(), 2);
65//!
66//! // Use small population/generations for a fast doc-test
67//! let sampler = GASampler::with_params(Some(42), 20, 20);
68//! let results = sampler.run_qubo(&(q, var_map), 5).expect("GA sampler failed");
69//! assert!(!results.is_empty());
70//! println!("Best energy: {}", results[0].energy);
71//! ```
72
73use scirs2_core::ndarray::{Array, Dimension, Ix2};
74use scirs2_core::random::prelude::*;
75use scirs2_core::random::rngs::StdRng;
76use std::collections::HashMap;
77
78use super::energy::hobo_energy_full_dispatch;
79use super::{SampleResult, Sampler, SamplerResult};
80
81/// Genetic Algorithm Sampler
82///
83/// Evolutionary metaheuristic for QUBO/HOBO optimisation that maintains a
84/// population of binary solutions and evolves them through selection,
85/// crossover (uniform, single-point, two-point, or adaptive), and mutation.
86///
87/// # Example
88///
89/// ```
90/// use quantrs2_tytan::sampler::{GASampler, Sampler};
91/// use scirs2_core::ndarray::Array;
92/// use std::collections::HashMap;
93///
94/// // Minimise: -x0 - x1 - x2 + 2*x0*x1 + 2*x0*x2
95/// let mut q = Array::<f64, _>::zeros((3, 3));
96/// q[[0, 0]] = -1.0;
97/// q[[1, 1]] = -1.0;
98/// q[[2, 2]] = -1.0;
99/// q[[0, 1]] = 2.0;
100/// q[[0, 2]] = 2.0;
101///
102/// let mut var_map = HashMap::new();
103/// var_map.insert("x0".to_string(), 0);
104/// var_map.insert("x1".to_string(), 1);
105/// var_map.insert("x2".to_string(), 2);
106///
107/// // Small population/generations for a fast doc-test
108/// let sampler = GASampler::with_params(Some(42), 20, 20);
109/// let results = sampler.run_qubo(&(q, var_map), 5).expect("GA sampler failed");
110/// assert!(!results.is_empty());
111/// println!("Best energy: {}", results[0].energy);
112/// ```
113pub struct GASampler {
114    /// Random number generator seed
115    seed: Option<u64>,
116    /// Maximum number of generations
117    max_generations: usize,
118    /// Population size
119    population_size: usize,
120    /// Crossover strategy used by `run_qubo`/`run_hobo`
121    crossover_strategy: CrossoverStrategy,
122    /// Mutation strategy used by `run_qubo`/`run_hobo`
123    mutation_strategy: MutationStrategy,
124}
125
126/// Crossover strategy for genetic algorithm
127#[derive(Debug, Clone, Copy)]
128pub enum CrossoverStrategy {
129    /// Uniform crossover (random gene selection from each parent)
130    Uniform,
131    /// Single-point crossover (split at random point)
132    SinglePoint,
133    /// Two-point crossover (swap middle section)
134    TwoPoint,
135    /// Adaptive crossover (choice based on parent similarity)
136    Adaptive,
137}
138
139/// Mutation strategy for genetic algorithm
140#[derive(Debug, Clone, Copy)]
141pub enum MutationStrategy {
142    /// Flip bits with fixed probability
143    FixedRate(f64),
144    /// Mutate bits with decreasing rate over generations
145    Annealing(f64, f64), // (initial_rate, final_rate)
146    /// Adaptive mutation based on population diversity
147    Adaptive(f64, f64), // (min_rate, max_rate)
148}
149
150impl GASampler {
151    /// Create a new Genetic Algorithm sampler
152    ///
153    /// # Arguments
154    ///
155    /// * `seed` - An optional random seed for reproducibility
156    #[must_use]
157    pub const fn new(seed: Option<u64>) -> Self {
158        Self {
159            seed,
160            max_generations: 1000,
161            population_size: 100,
162            crossover_strategy: CrossoverStrategy::Adaptive,
163            mutation_strategy: MutationStrategy::Annealing(0.1, 0.01),
164        }
165    }
166
167    /// Create a new Genetic Algorithm sampler with custom parameters
168    ///
169    /// # Arguments
170    ///
171    /// * `seed` - An optional random seed for reproducibility
172    /// * `max_generations` - Maximum number of generations to evolve
173    /// * `population_size` - Size of the population
174    #[must_use]
175    pub const fn with_params(
176        seed: Option<u64>,
177        max_generations: usize,
178        population_size: usize,
179    ) -> Self {
180        Self {
181            seed,
182            max_generations,
183            population_size,
184            crossover_strategy: CrossoverStrategy::Adaptive,
185            mutation_strategy: MutationStrategy::Annealing(0.1, 0.01),
186        }
187    }
188
189    /// Set population size
190    pub const fn with_population_size(mut self, size: usize) -> Self {
191        self.population_size = size;
192        self
193    }
194
195    /// Set elite fraction (placeholder method)
196    pub const fn with_elite_fraction(self, _fraction: f64) -> Self {
197        // Note: Elite fraction not currently implemented in struct
198        // This is a placeholder to satisfy compilation
199        self
200    }
201
202    /// Set mutation rate (placeholder method)
203    pub const fn with_mutation_rate(self, _rate: f64) -> Self {
204        // Note: Mutation rate not currently implemented in struct
205        // This is a placeholder to satisfy compilation
206        self
207    }
208
209    /// Create a new enhanced Genetic Algorithm sampler
210    ///
211    /// # Arguments
212    ///
213    /// * `seed` - An optional random seed for reproducibility
214    /// * `max_generations` - Maximum number of generations to evolve
215    /// * `population_size` - Size of the population
216    /// * `crossover` - Crossover strategy to use
217    /// * `mutation` - Mutation strategy to use
218    pub const fn with_advanced_params(
219        seed: Option<u64>,
220        max_generations: usize,
221        population_size: usize,
222        crossover: CrossoverStrategy,
223        mutation: MutationStrategy,
224    ) -> Self {
225        Self {
226            seed,
227            max_generations,
228            population_size,
229            crossover_strategy: crossover,
230            mutation_strategy: mutation,
231        }
232    }
233
234    /// Perform crossover between two parents
235    fn crossover(
236        &self,
237        parent1: &[bool],
238        parent2: &[bool],
239        strategy: CrossoverStrategy,
240        rng: &mut impl Rng,
241    ) -> (Vec<bool>, Vec<bool>) {
242        let n_vars = parent1.len();
243        let mut child1 = vec![false; n_vars];
244        let mut child2 = vec![false; n_vars];
245
246        match strategy {
247            CrossoverStrategy::Uniform => {
248                // Uniform crossover
249                for i in 0..n_vars {
250                    if rng.random_bool(0.5) {
251                        child1[i] = parent1[i];
252                        child2[i] = parent2[i];
253                    } else {
254                        child1[i] = parent2[i];
255                        child2[i] = parent1[i];
256                    }
257                }
258            }
259            CrossoverStrategy::SinglePoint => {
260                // Single-point crossover
261                let crossover_point = rng.random_range(1..n_vars);
262
263                for i in 0..n_vars {
264                    if i < crossover_point {
265                        child1[i] = parent1[i];
266                        child2[i] = parent2[i];
267                    } else {
268                        child1[i] = parent2[i];
269                        child2[i] = parent1[i];
270                    }
271                }
272            }
273            CrossoverStrategy::TwoPoint => {
274                // Two-point crossover
275                let point1 = rng.random_range(1..(n_vars - 1));
276                let point2 = rng.random_range((point1 + 1)..n_vars);
277
278                for i in 0..n_vars {
279                    if i < point1 || i >= point2 {
280                        child1[i] = parent1[i];
281                        child2[i] = parent2[i];
282                    } else {
283                        child1[i] = parent2[i];
284                        child2[i] = parent1[i];
285                    }
286                }
287            }
288            CrossoverStrategy::Adaptive => {
289                // Calculate Hamming distance between parents
290                let mut hamming_distance = 0;
291                for i in 0..n_vars {
292                    if parent1[i] != parent2[i] {
293                        hamming_distance += 1;
294                    }
295                }
296
297                // Normalized distance
298                let similarity = 1.0 - (hamming_distance as f64 / n_vars as f64);
299
300                if similarity > 0.8 {
301                    // Parents are very similar - use uniform with high mixing
302                    for i in 0..n_vars {
303                        if rng.random_bool(0.5) {
304                            child1[i] = parent1[i];
305                            child2[i] = parent2[i];
306                        } else {
307                            child1[i] = parent2[i];
308                            child2[i] = parent1[i];
309                        }
310                    }
311                } else if similarity > 0.4 {
312                    // Moderate similarity - use two-point
313                    let point1 = rng.random_range(1..(n_vars - 1));
314                    let point2 = rng.random_range((point1 + 1)..n_vars);
315
316                    for i in 0..n_vars {
317                        if i < point1 || i >= point2 {
318                            child1[i] = parent1[i];
319                            child2[i] = parent2[i];
320                        } else {
321                            child1[i] = parent2[i];
322                            child2[i] = parent1[i];
323                        }
324                    }
325                } else {
326                    // Low similarity - use single point
327                    let crossover_point = rng.random_range(1..n_vars);
328
329                    for i in 0..n_vars {
330                        if i < crossover_point {
331                            child1[i] = parent1[i];
332                            child2[i] = parent2[i];
333                        } else {
334                            child1[i] = parent2[i];
335                            child2[i] = parent1[i];
336                        }
337                    }
338                }
339            }
340        }
341
342        (child1, child2)
343    }
344
345    /// Mutate an individual
346    fn mutate(
347        &self,
348        individual: &mut [bool],
349        strategy: MutationStrategy,
350        generation: usize,
351        max_generations: usize,
352        diversity: Option<f64>,
353        rng: &mut impl Rng,
354    ) {
355        match strategy {
356            MutationStrategy::FixedRate(rate) => {
357                // Simple fixed mutation rate
358                for bit in individual.iter_mut() {
359                    if rng.random_bool(rate) {
360                        *bit = !*bit;
361                    }
362                }
363            }
364            MutationStrategy::Annealing(initial_rate, final_rate) => {
365                // Annealing mutation (decreasing rate)
366                let progress = generation as f64 / max_generations as f64;
367                let current_rate = (final_rate - initial_rate).mul_add(progress, initial_rate);
368
369                for bit in individual.iter_mut() {
370                    if rng.random_bool(current_rate) {
371                        *bit = !*bit;
372                    }
373                }
374            }
375            MutationStrategy::Adaptive(min_rate, max_rate) => {
376                // Adaptive mutation based on diversity
377                if let Some(diversity) = diversity {
378                    // High diversity -> low mutation rate, low diversity -> high mutation rate
379                    let rate = (max_rate - min_rate).mul_add(1.0 - diversity, min_rate);
380
381                    for bit in individual.iter_mut() {
382                        if rng.random_bool(rate) {
383                            *bit = !*bit;
384                        }
385                    }
386                } else {
387                    // Default to average if no diversity metric available
388                    let rate = f64::midpoint(min_rate, max_rate);
389                    for bit in individual.iter_mut() {
390                        if rng.random_bool(rate) {
391                            *bit = !*bit;
392                        }
393                    }
394                }
395            }
396        }
397    }
398
399    /// Calculate population diversity (normalized hamming distance)
400    fn calculate_diversity(&self, population: &[Vec<bool>]) -> f64 {
401        if population.len() <= 1 {
402            return 0.0;
403        }
404
405        let n_individuals = population.len();
406        let n_vars = population[0].len();
407        let mut sum_distances = 0;
408        let mut pair_count = 0;
409
410        for i in 0..n_individuals {
411            for j in (i + 1)..n_individuals {
412                let mut distance = 0;
413                for k in 0..n_vars {
414                    if population[i][k] != population[j][k] {
415                        distance += 1;
416                    }
417                }
418                sum_distances += distance;
419                pair_count += 1;
420            }
421        }
422
423        // Average normalized Hamming distance
424        if pair_count > 0 {
425            (sum_distances as f64) / (pair_count as f64 * n_vars as f64)
426        } else {
427            0.0
428        }
429    }
430}
431
432impl Sampler for GASampler {
433    fn run_hobo(
434        &self,
435        hobo: &(
436            Array<f64, scirs2_core::ndarray::IxDyn>,
437            HashMap<String, usize>,
438        ),
439        shots: usize,
440    ) -> SamplerResult<Vec<SampleResult>> {
441        // Extract matrix and variable mapping
442        let (tensor, var_map) = hobo;
443
444        // Make sure shots is reasonable
445        let actual_shots = std::cmp::max(shots, 10);
446
447        // Get the problem dimension
448        let n_vars = var_map.len();
449
450        // Map from indices back to variable names
451        let idx_to_var: HashMap<usize, String> = var_map
452            .iter()
453            .map(|(var, &idx)| (idx, var.clone()))
454            .collect();
455
456        // Initialize random number generator
457        let mut rng = if let Some(seed) = self.seed {
458            StdRng::seed_from_u64(seed)
459        } else {
460            let seed: u64 = thread_rng().random();
461            StdRng::seed_from_u64(seed)
462        };
463
464        // Handle small population size cases to avoid empty range errors
465        if self.population_size <= 2 || n_vars == 0 {
466            // Return a simple result for trivial cases
467            let mut assignments = HashMap::new();
468            for var in var_map.keys() {
469                assignments.insert(var.clone(), false);
470            }
471
472            return Ok(vec![SampleResult {
473                assignments,
474                energy: 0.0,
475                occurrences: 1,
476            }]);
477        }
478
479        // For simplicity, if the tensor is 2D, convert to QUBO and use that implementation
480        if tensor.ndim() == 2 && tensor.shape() == [n_vars, n_vars] {
481            // Create a view as a 2D matrix and convert to owned matrix
482            let matrix = tensor
483                .clone()
484                .into_dimensionality::<scirs2_core::ndarray::Ix2>()
485                .map_err(|e| {
486                    super::SamplerError::InvalidModel(format!(
487                        "Failed to convert tensor to 2D matrix: {}",
488                        e
489                    ))
490                })?;
491            let qubo = (matrix, var_map.clone());
492
493            return self.run_qubo(&qubo, shots);
494        }
495
496        // Otherwise, implement the full HOBO genetic algorithm here
497        // Define a function to evaluate the energy of a solution
498        let evaluate_energy = |state: &[bool]| -> f64 { hobo_energy_full_dispatch(state, tensor) };
499
500        // Solution map with frequencies
501        let mut solution_counts: HashMap<Vec<bool>, (f64, usize)> = HashMap::new();
502
503        // Create a minimal, functional GA implementation
504        let pop_size = self.population_size.clamp(10, 100);
505
506        // Initialize random population
507        let mut population: Vec<Vec<bool>> = (0..pop_size)
508            .map(|_| (0..n_vars).map(|_| rng.random_bool(0.5)).collect())
509            .collect();
510
511        // Evaluate initial population
512        let mut fitness: Vec<f64> = population
513            .iter()
514            .map(|indiv| evaluate_energy(indiv))
515            .collect();
516
517        // Find best solution
518        let mut best_solution = population[0].clone();
519        let mut best_fitness = fitness[0];
520
521        for (idx, fit) in fitness.iter().enumerate() {
522            if *fit < best_fitness {
523                best_fitness = *fit;
524                best_solution = population[idx].clone();
525            }
526        }
527
528        // Genetic algorithm loop
529        const HOBO_GENERATIONS: usize = 30; // Reduced number of generations for faster results
530        for generation in 0..HOBO_GENERATIONS {
531            // Diversity metric for the Adaptive mutation strategy, computed
532            // the same way as in `run_qubo`.
533            let diversity = self.calculate_diversity(&population);
534
535            // Create next generation
536            let mut next_population = Vec::with_capacity(pop_size);
537
538            // Elitism - keep best solution
539            next_population.push(best_solution.clone());
540
541            // Fill population with new individuals
542            while next_population.len() < pop_size {
543                // Select parents via tournament selection
544                let parent1_idx = tournament_selection(&fitness, 3, &mut rng);
545                let parent2_idx = tournament_selection(&fitness, 3, &mut rng);
546
547                // Crossover, respecting the configured strategy (this used
548                // to always use a hardcoded single-point crossover here,
549                // ignoring `with_advanced_params`).
550                let (mut child1, mut child2) = self.crossover(
551                    &population[parent1_idx],
552                    &population[parent2_idx],
553                    self.crossover_strategy,
554                    &mut rng,
555                );
556
557                // Mutation, respecting the configured strategy (this used
558                // to always use a hardcoded fixed 0.05 rate here).
559                self.mutate(
560                    &mut child1,
561                    self.mutation_strategy,
562                    generation,
563                    HOBO_GENERATIONS,
564                    Some(diversity),
565                    &mut rng,
566                );
567                self.mutate(
568                    &mut child2,
569                    self.mutation_strategy,
570                    generation,
571                    HOBO_GENERATIONS,
572                    Some(diversity),
573                    &mut rng,
574                );
575
576                // Add children
577                next_population.push(child1);
578                if next_population.len() < pop_size {
579                    next_population.push(child2);
580                }
581            }
582
583            // Evaluate new population
584            population = next_population;
585            fitness = population
586                .iter()
587                .map(|indiv| evaluate_energy(indiv))
588                .collect();
589
590            // Update best solution
591            for (idx, fit) in fitness.iter().enumerate() {
592                if *fit < best_fitness {
593                    best_fitness = *fit;
594                    best_solution = population[idx].clone();
595                }
596            }
597
598            // Update solution counts
599            for (idx, indiv) in population.iter().enumerate() {
600                let entry = solution_counts
601                    .entry(indiv.clone())
602                    .or_insert((fitness[idx], 0));
603                entry.1 += 1;
604            }
605        }
606
607        // Convert solutions to SampleResult
608        let mut results: Vec<SampleResult> = solution_counts
609            .into_iter()
610            .filter_map(|(state, (energy, count))| {
611                // Convert to variable assignments
612                let assignments: HashMap<String, bool> = state
613                    .iter()
614                    .enumerate()
615                    .filter_map(|(idx, &value)| {
616                        idx_to_var
617                            .get(&idx)
618                            .map(|var_name| (var_name.clone(), value))
619                    })
620                    .collect();
621
622                // Skip solutions with missing variable mappings
623                if assignments.len() != state.len() {
624                    return None;
625                }
626
627                Some(SampleResult {
628                    assignments,
629                    energy,
630                    occurrences: count,
631                })
632            })
633            .collect();
634
635        // Sort by energy (best solutions first)
636        // Use unwrap_or for NaN handling - treat NaN as equal to any value
637        results.sort_by(|a, b| {
638            a.energy
639                .partial_cmp(&b.energy)
640                .unwrap_or(std::cmp::Ordering::Equal)
641        });
642
643        // Limit to requested number of shots if we have more
644        if results.len() > actual_shots {
645            results.truncate(actual_shots);
646        }
647
648        Ok(results)
649    }
650
651    fn run_qubo(
652        &self,
653        qubo: &(
654            Array<f64, scirs2_core::ndarray::Ix2>,
655            HashMap<String, usize>,
656        ),
657        shots: usize,
658    ) -> SamplerResult<Vec<SampleResult>> {
659        // Extract matrix and variable mapping
660        let (matrix, var_map) = qubo;
661
662        // Make sure shots is reasonable
663        let actual_shots = std::cmp::max(shots, 10);
664
665        // Get the problem dimension
666        let n_vars = var_map.len();
667
668        // Map from indices back to variable names
669        let idx_to_var: HashMap<usize, String> = var_map
670            .iter()
671            .map(|(var, &idx)| (idx, var.clone()))
672            .collect();
673
674        // Initialize random number generator
675        let mut rng = if let Some(seed) = self.seed {
676            StdRng::seed_from_u64(seed)
677        } else {
678            let seed: u64 = thread_rng().random();
679            StdRng::seed_from_u64(seed)
680        };
681
682        // Handle edge cases
683        if self.population_size <= 2 || n_vars == 0 {
684            let mut assignments = HashMap::new();
685            for var in var_map.keys() {
686                assignments.insert(var.clone(), false);
687            }
688
689            return Ok(vec![SampleResult {
690                assignments,
691                energy: 0.0,
692                occurrences: 1,
693            }]);
694        }
695
696        // Use the strategies configured via `with_advanced_params` (or the
697        // Adaptive/Annealing defaults from `new`/`with_params`), instead of
698        // silently ignoring whatever the caller configured.
699        let crossover_strategy = self.crossover_strategy;
700        let mutation_strategy = self.mutation_strategy;
701        let selection_pressure = 3; // Tournament size
702        let use_elitism = true;
703
704        // Initialize population with random bitstrings
705        let mut population: Vec<Vec<bool>> = (0..self.population_size)
706            .map(|_| (0..n_vars).map(|_| rng.random_bool(0.5)).collect())
707            .collect();
708
709        // Initialize fitness scores (energy values)
710        let mut fitness: Vec<f64> = population
711            .iter()
712            .map(|indiv| calculate_energy(indiv, matrix))
713            .collect();
714
715        // Keep track of best solution in current population
716        let mut best_idx = 0;
717        let mut best_fitness = fitness[0];
718        for (idx, &fit) in fitness.iter().enumerate() {
719            if fit < best_fitness {
720                best_idx = idx;
721                best_fitness = fit;
722            }
723        }
724        let mut best_individual = population[best_idx].clone();
725        let mut best_individual_fitness = best_fitness;
726
727        // Track solutions and their frequencies
728        let mut solution_counts: HashMap<Vec<bool>, usize> = HashMap::new();
729
730        // Main GA loop
731        for generation in 0..self.max_generations {
732            // Calculate population diversity for adaptive operators
733            let diversity = self.calculate_diversity(&population);
734
735            // Create next generation
736            let mut next_population = Vec::with_capacity(self.population_size);
737            let mut next_fitness = Vec::with_capacity(self.population_size);
738
739            // Elitism - copy best individual
740            if use_elitism {
741                next_population.push(best_individual.clone());
742                next_fitness.push(best_individual_fitness);
743            }
744
745            // Fill rest of population through selection, crossover, mutation
746            while next_population.len() < self.population_size {
747                // Tournament selection for parents
748                let parent1_idx = tournament_selection(&fitness, selection_pressure, &mut rng);
749                let parent2_idx = tournament_selection(&fitness, selection_pressure, &mut rng);
750
751                let parent1 = &population[parent1_idx];
752                let parent2 = &population[parent2_idx];
753
754                // Crossover
755                let (mut child1, mut child2) =
756                    self.crossover(parent1, parent2, crossover_strategy, &mut rng);
757
758                // Mutation
759                self.mutate(
760                    &mut child1,
761                    mutation_strategy,
762                    generation,
763                    self.max_generations,
764                    Some(diversity),
765                    &mut rng,
766                );
767                self.mutate(
768                    &mut child2,
769                    mutation_strategy,
770                    generation,
771                    self.max_generations,
772                    Some(diversity),
773                    &mut rng,
774                );
775
776                // Evaluate fitness of new children
777                let child1_fitness = calculate_energy(&child1, matrix);
778                let child2_fitness = calculate_energy(&child2, matrix);
779
780                // Add first child
781                next_population.push(child1);
782                next_fitness.push(child1_fitness);
783
784                // Add second child if there's room
785                if next_population.len() < self.population_size {
786                    next_population.push(child2);
787                    next_fitness.push(child2_fitness);
788                }
789            }
790
791            // Update population
792            population = next_population;
793            fitness = next_fitness;
794
795            // Update best solution
796            best_idx = 0;
797            best_fitness = fitness[0];
798            for (idx, &fit) in fitness.iter().enumerate() {
799                if fit < best_fitness {
800                    best_idx = idx;
801                    best_fitness = fit;
802                }
803            }
804
805            // Update global best if needed
806            if best_fitness < best_individual_fitness {
807                best_individual = population[best_idx].clone();
808                best_individual_fitness = best_fitness;
809            }
810
811            // Update solution counts
812            for individual in &population {
813                *solution_counts.entry(individual.clone()).or_insert(0) += 1;
814            }
815        }
816
817        // Collect results
818        let mut results = Vec::new();
819
820        // Convert the solutions to SampleResult format
821        for (solution, count) in &solution_counts {
822            // Only include solutions that appeared multiple times
823            if *count < 2 {
824                continue;
825            }
826
827            // Calculate energy one more time
828            let energy = calculate_energy(solution, matrix);
829
830            // Convert to variable assignments, skipping any missing mappings
831            let assignments: HashMap<String, bool> = solution
832                .iter()
833                .enumerate()
834                .filter_map(|(idx, &value)| {
835                    idx_to_var
836                        .get(&idx)
837                        .map(|var_name| (var_name.clone(), value))
838                })
839                .collect();
840
841            // Skip solutions with incomplete variable mappings
842            if assignments.len() != solution.len() {
843                continue;
844            }
845
846            // Create result and add to collection
847            results.push(SampleResult {
848                assignments,
849                energy,
850                occurrences: *count,
851            });
852        }
853
854        // Sort by energy (best solutions first)
855        // Use unwrap_or for NaN handling - treat NaN as equal to any value
856        results.sort_by(|a, b| {
857            a.energy
858                .partial_cmp(&b.energy)
859                .unwrap_or(std::cmp::Ordering::Equal)
860        });
861
862        // Trim to requested number of shots
863        if results.len() > actual_shots {
864            results.truncate(actual_shots);
865        }
866
867        Ok(results)
868    }
869}
870
871// Helper function to calculate energy for a solution
872fn calculate_energy(solution: &[bool], matrix: &Array<f64, Ix2>) -> f64 {
873    let n = solution.len();
874    let mut energy = 0.0;
875
876    // Calculate from diagonal terms (linear)
877    for i in 0..n {
878        if solution[i] {
879            energy += matrix[[i, i]];
880        }
881    }
882
883    // Calculate from off-diagonal terms (quadratic)
884    for i in 0..n {
885        if solution[i] {
886            for j in (i + 1)..n {
887                if solution[j] {
888                    energy += matrix[[i, j]];
889                }
890            }
891        }
892    }
893
894    energy
895}
896
897// Helper function for tournament selection
898fn tournament_selection(fitness: &[f64], tournament_size: usize, rng: &mut impl Rng) -> usize {
899    // Handle edge cases
900    assert!(
901        !fitness.is_empty(),
902        "Cannot perform tournament selection on an empty fitness array"
903    );
904
905    if fitness.len() == 1 || tournament_size <= 1 {
906        return 0; // Only one choice available
907    }
908
909    // Ensure tournament_size is not larger than the population
910    let effective_tournament_size = std::cmp::min(tournament_size, fitness.len());
911
912    let mut best_idx = rng.random_range(0..fitness.len());
913    let mut best_fitness = fitness[best_idx];
914
915    for _ in 1..(effective_tournament_size) {
916        let candidate_idx = rng.random_range(0..fitness.len());
917        let candidate_fitness = fitness[candidate_idx];
918
919        // Lower fitness is better (minimization problem)
920        if candidate_fitness < best_fitness {
921            best_idx = candidate_idx;
922            best_fitness = candidate_fitness;
923        }
924    }
925
926    best_idx
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932    use crate::sampler::Sampler;
933    use scirs2_core::ndarray::Array2;
934    use std::collections::HashMap;
935
936    #[test]
937    fn test_with_advanced_params_stores_configured_strategies() {
938        let sampler = GASampler::with_advanced_params(
939            Some(1),
940            50,
941            20,
942            CrossoverStrategy::TwoPoint,
943            MutationStrategy::Adaptive(0.01, 0.5),
944        );
945
946        // The old fabricated implementation silently discarded these two
947        // constructor arguments (the struct had no fields to store them in)
948        // and `run_qubo`/`run_hobo` always behaved as if
949        // CrossoverStrategy::Adaptive + MutationStrategy::Annealing(0.1,
950        // 0.01) had been requested, regardless of what was passed here.
951        assert!(matches!(
952            sampler.crossover_strategy,
953            CrossoverStrategy::TwoPoint
954        ));
955        match sampler.mutation_strategy {
956            MutationStrategy::Adaptive(min_rate, max_rate) => {
957                assert!((min_rate - 0.01).abs() < 1e-12);
958                assert!((max_rate - 0.5).abs() < 1e-12);
959            }
960            other => panic!("expected MutationStrategy::Adaptive, got {other:?}"),
961        }
962    }
963
964    #[test]
965    fn test_default_constructors_use_adaptive_annealing_defaults() {
966        let sampler = GASampler::new(Some(1));
967        assert!(matches!(
968            sampler.crossover_strategy,
969            CrossoverStrategy::Adaptive
970        ));
971        match sampler.mutation_strategy {
972            MutationStrategy::Annealing(initial, final_) => {
973                assert!((initial - 0.1).abs() < 1e-12);
974                assert!((final_ - 0.01).abs() < 1e-12);
975            }
976            other => panic!("expected MutationStrategy::Annealing, got {other:?}"),
977        }
978    }
979
980    #[test]
981    fn test_run_qubo_with_custom_strategy_produces_valid_results() {
982        let qubo = Array2::from_shape_fn((4, 4), |(i, j)| if i == j { -1.0 } else { 0.1 });
983        let mut var_map = HashMap::new();
984        for i in 0..4 {
985            var_map.insert(format!("x{i}"), i);
986        }
987
988        let sampler = GASampler::with_advanced_params(
989            Some(7),
990            30,
991            20,
992            CrossoverStrategy::SinglePoint,
993            MutationStrategy::FixedRate(0.2),
994        );
995
996        let results = sampler
997            .run_qubo(&(qubo, var_map), 5)
998            .expect("GA sampler should succeed with a configured strategy");
999        assert!(!results.is_empty());
1000        for result in &results {
1001            assert_eq!(result.assignments.len(), 4);
1002        }
1003    }
1004}