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}
121
122/// Crossover strategy for genetic algorithm
123#[derive(Debug, Clone, Copy)]
124pub enum CrossoverStrategy {
125    /// Uniform crossover (random gene selection from each parent)
126    Uniform,
127    /// Single-point crossover (split at random point)
128    SinglePoint,
129    /// Two-point crossover (swap middle section)
130    TwoPoint,
131    /// Adaptive crossover (choice based on parent similarity)
132    Adaptive,
133}
134
135/// Mutation strategy for genetic algorithm
136#[derive(Debug, Clone, Copy)]
137pub enum MutationStrategy {
138    /// Flip bits with fixed probability
139    FixedRate(f64),
140    /// Mutate bits with decreasing rate over generations
141    Annealing(f64, f64), // (initial_rate, final_rate)
142    /// Adaptive mutation based on population diversity
143    Adaptive(f64, f64), // (min_rate, max_rate)
144}
145
146impl GASampler {
147    /// Create a new Genetic Algorithm sampler
148    ///
149    /// # Arguments
150    ///
151    /// * `seed` - An optional random seed for reproducibility
152    #[must_use]
153    pub const fn new(seed: Option<u64>) -> Self {
154        Self {
155            seed,
156            max_generations: 1000,
157            population_size: 100,
158        }
159    }
160
161    /// Create a new Genetic Algorithm sampler with custom parameters
162    ///
163    /// # Arguments
164    ///
165    /// * `seed` - An optional random seed for reproducibility
166    /// * `max_generations` - Maximum number of generations to evolve
167    /// * `population_size` - Size of the population
168    #[must_use]
169    pub const fn with_params(
170        seed: Option<u64>,
171        max_generations: usize,
172        population_size: usize,
173    ) -> Self {
174        Self {
175            seed,
176            max_generations,
177            population_size,
178        }
179    }
180
181    /// Set population size
182    pub const fn with_population_size(mut self, size: usize) -> Self {
183        self.population_size = size;
184        self
185    }
186
187    /// Set elite fraction (placeholder method)
188    pub const fn with_elite_fraction(self, _fraction: f64) -> Self {
189        // Note: Elite fraction not currently implemented in struct
190        // This is a placeholder to satisfy compilation
191        self
192    }
193
194    /// Set mutation rate (placeholder method)
195    pub const fn with_mutation_rate(self, _rate: f64) -> Self {
196        // Note: Mutation rate not currently implemented in struct
197        // This is a placeholder to satisfy compilation
198        self
199    }
200
201    /// Create a new enhanced Genetic Algorithm sampler
202    ///
203    /// # Arguments
204    ///
205    /// * `seed` - An optional random seed for reproducibility
206    /// * `max_generations` - Maximum number of generations to evolve
207    /// * `population_size` - Size of the population
208    /// * `crossover` - Crossover strategy to use
209    /// * `mutation` - Mutation strategy to use
210    pub const fn with_advanced_params(
211        seed: Option<u64>,
212        max_generations: usize,
213        population_size: usize,
214        _crossover: CrossoverStrategy, // Saved for future implementation
215        _mutation: MutationStrategy,   // Saved for future implementation
216    ) -> Self {
217        Self {
218            seed,
219            max_generations,
220            population_size,
221        }
222    }
223
224    /// Perform crossover between two parents
225    fn crossover(
226        &self,
227        parent1: &[bool],
228        parent2: &[bool],
229        strategy: CrossoverStrategy,
230        rng: &mut impl Rng,
231    ) -> (Vec<bool>, Vec<bool>) {
232        let n_vars = parent1.len();
233        let mut child1 = vec![false; n_vars];
234        let mut child2 = vec![false; n_vars];
235
236        match strategy {
237            CrossoverStrategy::Uniform => {
238                // Uniform crossover
239                for i in 0..n_vars {
240                    if rng.random_bool(0.5) {
241                        child1[i] = parent1[i];
242                        child2[i] = parent2[i];
243                    } else {
244                        child1[i] = parent2[i];
245                        child2[i] = parent1[i];
246                    }
247                }
248            }
249            CrossoverStrategy::SinglePoint => {
250                // Single-point crossover
251                let crossover_point = rng.random_range(1..n_vars);
252
253                for i in 0..n_vars {
254                    if i < crossover_point {
255                        child1[i] = parent1[i];
256                        child2[i] = parent2[i];
257                    } else {
258                        child1[i] = parent2[i];
259                        child2[i] = parent1[i];
260                    }
261                }
262            }
263            CrossoverStrategy::TwoPoint => {
264                // Two-point crossover
265                let point1 = rng.random_range(1..(n_vars - 1));
266                let point2 = rng.random_range((point1 + 1)..n_vars);
267
268                for i in 0..n_vars {
269                    if i < point1 || i >= point2 {
270                        child1[i] = parent1[i];
271                        child2[i] = parent2[i];
272                    } else {
273                        child1[i] = parent2[i];
274                        child2[i] = parent1[i];
275                    }
276                }
277            }
278            CrossoverStrategy::Adaptive => {
279                // Calculate Hamming distance between parents
280                let mut hamming_distance = 0;
281                for i in 0..n_vars {
282                    if parent1[i] != parent2[i] {
283                        hamming_distance += 1;
284                    }
285                }
286
287                // Normalized distance
288                let similarity = 1.0 - (hamming_distance as f64 / n_vars as f64);
289
290                if similarity > 0.8 {
291                    // Parents are very similar - use uniform with high mixing
292                    for i in 0..n_vars {
293                        if rng.random_bool(0.5) {
294                            child1[i] = parent1[i];
295                            child2[i] = parent2[i];
296                        } else {
297                            child1[i] = parent2[i];
298                            child2[i] = parent1[i];
299                        }
300                    }
301                } else if similarity > 0.4 {
302                    // Moderate similarity - use two-point
303                    let point1 = rng.random_range(1..(n_vars - 1));
304                    let point2 = rng.random_range((point1 + 1)..n_vars);
305
306                    for i in 0..n_vars {
307                        if i < point1 || i >= point2 {
308                            child1[i] = parent1[i];
309                            child2[i] = parent2[i];
310                        } else {
311                            child1[i] = parent2[i];
312                            child2[i] = parent1[i];
313                        }
314                    }
315                } else {
316                    // Low similarity - use single point
317                    let crossover_point = rng.random_range(1..n_vars);
318
319                    for i in 0..n_vars {
320                        if i < crossover_point {
321                            child1[i] = parent1[i];
322                            child2[i] = parent2[i];
323                        } else {
324                            child1[i] = parent2[i];
325                            child2[i] = parent1[i];
326                        }
327                    }
328                }
329            }
330        }
331
332        (child1, child2)
333    }
334
335    /// Mutate an individual
336    fn mutate(
337        &self,
338        individual: &mut [bool],
339        strategy: MutationStrategy,
340        generation: usize,
341        max_generations: usize,
342        diversity: Option<f64>,
343        rng: &mut impl Rng,
344    ) {
345        match strategy {
346            MutationStrategy::FixedRate(rate) => {
347                // Simple fixed mutation rate
348                for bit in individual.iter_mut() {
349                    if rng.random_bool(rate) {
350                        *bit = !*bit;
351                    }
352                }
353            }
354            MutationStrategy::Annealing(initial_rate, final_rate) => {
355                // Annealing mutation (decreasing rate)
356                let progress = generation as f64 / max_generations as f64;
357                let current_rate = (final_rate - initial_rate).mul_add(progress, initial_rate);
358
359                for bit in individual.iter_mut() {
360                    if rng.random_bool(current_rate) {
361                        *bit = !*bit;
362                    }
363                }
364            }
365            MutationStrategy::Adaptive(min_rate, max_rate) => {
366                // Adaptive mutation based on diversity
367                if let Some(diversity) = diversity {
368                    // High diversity -> low mutation rate, low diversity -> high mutation rate
369                    let rate = (max_rate - min_rate).mul_add(1.0 - diversity, min_rate);
370
371                    for bit in individual.iter_mut() {
372                        if rng.random_bool(rate) {
373                            *bit = !*bit;
374                        }
375                    }
376                } else {
377                    // Default to average if no diversity metric available
378                    let rate = f64::midpoint(min_rate, max_rate);
379                    for bit in individual.iter_mut() {
380                        if rng.random_bool(rate) {
381                            *bit = !*bit;
382                        }
383                    }
384                }
385            }
386        }
387    }
388
389    /// Calculate population diversity (normalized hamming distance)
390    fn calculate_diversity(&self, population: &[Vec<bool>]) -> f64 {
391        if population.len() <= 1 {
392            return 0.0;
393        }
394
395        let n_individuals = population.len();
396        let n_vars = population[0].len();
397        let mut sum_distances = 0;
398        let mut pair_count = 0;
399
400        for i in 0..n_individuals {
401            for j in (i + 1)..n_individuals {
402                let mut distance = 0;
403                for k in 0..n_vars {
404                    if population[i][k] != population[j][k] {
405                        distance += 1;
406                    }
407                }
408                sum_distances += distance;
409                pair_count += 1;
410            }
411        }
412
413        // Average normalized Hamming distance
414        if pair_count > 0 {
415            (sum_distances as f64) / (pair_count as f64 * n_vars as f64)
416        } else {
417            0.0
418        }
419    }
420}
421
422impl Sampler for GASampler {
423    fn run_hobo(
424        &self,
425        hobo: &(
426            Array<f64, scirs2_core::ndarray::IxDyn>,
427            HashMap<String, usize>,
428        ),
429        shots: usize,
430    ) -> SamplerResult<Vec<SampleResult>> {
431        // Extract matrix and variable mapping
432        let (tensor, var_map) = hobo;
433
434        // Make sure shots is reasonable
435        let actual_shots = std::cmp::max(shots, 10);
436
437        // Get the problem dimension
438        let n_vars = var_map.len();
439
440        // Map from indices back to variable names
441        let idx_to_var: HashMap<usize, String> = var_map
442            .iter()
443            .map(|(var, &idx)| (idx, var.clone()))
444            .collect();
445
446        // Initialize random number generator
447        let mut rng = if let Some(seed) = self.seed {
448            StdRng::seed_from_u64(seed)
449        } else {
450            let seed: u64 = thread_rng().random();
451            StdRng::seed_from_u64(seed)
452        };
453
454        // Handle small population size cases to avoid empty range errors
455        if self.population_size <= 2 || n_vars == 0 {
456            // Return a simple result for trivial cases
457            let mut assignments = HashMap::new();
458            for var in var_map.keys() {
459                assignments.insert(var.clone(), false);
460            }
461
462            return Ok(vec![SampleResult {
463                assignments,
464                energy: 0.0,
465                occurrences: 1,
466            }]);
467        }
468
469        // For simplicity, if the tensor is 2D, convert to QUBO and use that implementation
470        if tensor.ndim() == 2 && tensor.shape() == [n_vars, n_vars] {
471            // Create a view as a 2D matrix and convert to owned matrix
472            let matrix = tensor
473                .clone()
474                .into_dimensionality::<scirs2_core::ndarray::Ix2>()
475                .map_err(|e| {
476                    super::SamplerError::InvalidModel(format!(
477                        "Failed to convert tensor to 2D matrix: {}",
478                        e
479                    ))
480                })?;
481            let qubo = (matrix, var_map.clone());
482
483            return self.run_qubo(&qubo, shots);
484        }
485
486        // Otherwise, implement the full HOBO genetic algorithm here
487        // Define a function to evaluate the energy of a solution
488        let evaluate_energy = |state: &[bool]| -> f64 { hobo_energy_full_dispatch(state, tensor) };
489
490        // Solution map with frequencies
491        let mut solution_counts: HashMap<Vec<bool>, (f64, usize)> = HashMap::new();
492
493        // Create a minimal, functional GA implementation
494        let pop_size = self.population_size.clamp(10, 100);
495
496        // Initialize random population
497        let mut population: Vec<Vec<bool>> = (0..pop_size)
498            .map(|_| (0..n_vars).map(|_| rng.random_bool(0.5)).collect())
499            .collect();
500
501        // Evaluate initial population
502        let mut fitness: Vec<f64> = population
503            .iter()
504            .map(|indiv| evaluate_energy(indiv))
505            .collect();
506
507        // Find best solution
508        let mut best_solution = population[0].clone();
509        let mut best_fitness = fitness[0];
510
511        for (idx, fit) in fitness.iter().enumerate() {
512            if *fit < best_fitness {
513                best_fitness = *fit;
514                best_solution = population[idx].clone();
515            }
516        }
517
518        // Genetic algorithm loop
519        for _ in 0..30 {
520            // Reduced number of generations for faster results
521            // Create next generation
522            let mut next_population = Vec::with_capacity(pop_size);
523
524            // Elitism - keep best solution
525            next_population.push(best_solution.clone());
526
527            // Fill population with new individuals
528            while next_population.len() < pop_size {
529                // Select parents via tournament selection
530                let parent1_idx = tournament_selection(&fitness, 3, &mut rng);
531                let parent2_idx = tournament_selection(&fitness, 3, &mut rng);
532
533                // Crossover
534                let (mut child1, mut child2) =
535                    simple_crossover(&population[parent1_idx], &population[parent2_idx], &mut rng);
536
537                // Mutation
538                mutate(&mut child1, 0.05, &mut rng);
539                mutate(&mut child2, 0.05, &mut rng);
540
541                // Add children
542                next_population.push(child1);
543                if next_population.len() < pop_size {
544                    next_population.push(child2);
545                }
546            }
547
548            // Evaluate new population
549            population = next_population;
550            fitness = population
551                .iter()
552                .map(|indiv| evaluate_energy(indiv))
553                .collect();
554
555            // Update best solution
556            for (idx, fit) in fitness.iter().enumerate() {
557                if *fit < best_fitness {
558                    best_fitness = *fit;
559                    best_solution = population[idx].clone();
560                }
561            }
562
563            // Update solution counts
564            for (idx, indiv) in population.iter().enumerate() {
565                let entry = solution_counts
566                    .entry(indiv.clone())
567                    .or_insert((fitness[idx], 0));
568                entry.1 += 1;
569            }
570        }
571
572        // Convert solutions to SampleResult
573        let mut results: Vec<SampleResult> = solution_counts
574            .into_iter()
575            .filter_map(|(state, (energy, count))| {
576                // Convert to variable assignments
577                let assignments: HashMap<String, bool> = state
578                    .iter()
579                    .enumerate()
580                    .filter_map(|(idx, &value)| {
581                        idx_to_var
582                            .get(&idx)
583                            .map(|var_name| (var_name.clone(), value))
584                    })
585                    .collect();
586
587                // Skip solutions with missing variable mappings
588                if assignments.len() != state.len() {
589                    return None;
590                }
591
592                Some(SampleResult {
593                    assignments,
594                    energy,
595                    occurrences: count,
596                })
597            })
598            .collect();
599
600        // Sort by energy (best solutions first)
601        // Use unwrap_or for NaN handling - treat NaN as equal to any value
602        results.sort_by(|a, b| {
603            a.energy
604                .partial_cmp(&b.energy)
605                .unwrap_or(std::cmp::Ordering::Equal)
606        });
607
608        // Limit to requested number of shots if we have more
609        if results.len() > actual_shots {
610            results.truncate(actual_shots);
611        }
612
613        Ok(results)
614    }
615
616    fn run_qubo(
617        &self,
618        qubo: &(
619            Array<f64, scirs2_core::ndarray::Ix2>,
620            HashMap<String, usize>,
621        ),
622        shots: usize,
623    ) -> SamplerResult<Vec<SampleResult>> {
624        // Extract matrix and variable mapping
625        let (matrix, var_map) = qubo;
626
627        // Make sure shots is reasonable
628        let actual_shots = std::cmp::max(shots, 10);
629
630        // Get the problem dimension
631        let n_vars = var_map.len();
632
633        // Map from indices back to variable names
634        let idx_to_var: HashMap<usize, String> = var_map
635            .iter()
636            .map(|(var, &idx)| (idx, var.clone()))
637            .collect();
638
639        // Initialize random number generator
640        let mut rng = if let Some(seed) = self.seed {
641            StdRng::seed_from_u64(seed)
642        } else {
643            let seed: u64 = thread_rng().random();
644            StdRng::seed_from_u64(seed)
645        };
646
647        // Handle edge cases
648        if self.population_size <= 2 || n_vars == 0 {
649            let mut assignments = HashMap::new();
650            for var in var_map.keys() {
651                assignments.insert(var.clone(), false);
652            }
653
654            return Ok(vec![SampleResult {
655                assignments,
656                energy: 0.0,
657                occurrences: 1,
658            }]);
659        }
660
661        // Use adaptive strategies by default
662        let crossover_strategy = CrossoverStrategy::Adaptive;
663        let mutation_strategy = MutationStrategy::Annealing(0.1, 0.01);
664        let selection_pressure = 3; // Tournament size
665        let use_elitism = true;
666
667        // Initialize population with random bitstrings
668        let mut population: Vec<Vec<bool>> = (0..self.population_size)
669            .map(|_| (0..n_vars).map(|_| rng.random_bool(0.5)).collect())
670            .collect();
671
672        // Initialize fitness scores (energy values)
673        let mut fitness: Vec<f64> = population
674            .iter()
675            .map(|indiv| calculate_energy(indiv, matrix))
676            .collect();
677
678        // Keep track of best solution in current population
679        let mut best_idx = 0;
680        let mut best_fitness = fitness[0];
681        for (idx, &fit) in fitness.iter().enumerate() {
682            if fit < best_fitness {
683                best_idx = idx;
684                best_fitness = fit;
685            }
686        }
687        let mut best_individual = population[best_idx].clone();
688        let mut best_individual_fitness = best_fitness;
689
690        // Track solutions and their frequencies
691        let mut solution_counts: HashMap<Vec<bool>, usize> = HashMap::new();
692
693        // Main GA loop
694        for generation in 0..self.max_generations {
695            // Calculate population diversity for adaptive operators
696            let diversity = self.calculate_diversity(&population);
697
698            // Create next generation
699            let mut next_population = Vec::with_capacity(self.population_size);
700            let mut next_fitness = Vec::with_capacity(self.population_size);
701
702            // Elitism - copy best individual
703            if use_elitism {
704                next_population.push(best_individual.clone());
705                next_fitness.push(best_individual_fitness);
706            }
707
708            // Fill rest of population through selection, crossover, mutation
709            while next_population.len() < self.population_size {
710                // Tournament selection for parents
711                let parent1_idx = tournament_selection(&fitness, selection_pressure, &mut rng);
712                let parent2_idx = tournament_selection(&fitness, selection_pressure, &mut rng);
713
714                let parent1 = &population[parent1_idx];
715                let parent2 = &population[parent2_idx];
716
717                // Crossover
718                let (mut child1, mut child2) =
719                    self.crossover(parent1, parent2, crossover_strategy, &mut rng);
720
721                // Mutation
722                self.mutate(
723                    &mut child1,
724                    mutation_strategy,
725                    generation,
726                    self.max_generations,
727                    Some(diversity),
728                    &mut rng,
729                );
730                self.mutate(
731                    &mut child2,
732                    mutation_strategy,
733                    generation,
734                    self.max_generations,
735                    Some(diversity),
736                    &mut rng,
737                );
738
739                // Evaluate fitness of new children
740                let child1_fitness = calculate_energy(&child1, matrix);
741                let child2_fitness = calculate_energy(&child2, matrix);
742
743                // Add first child
744                next_population.push(child1);
745                next_fitness.push(child1_fitness);
746
747                // Add second child if there's room
748                if next_population.len() < self.population_size {
749                    next_population.push(child2);
750                    next_fitness.push(child2_fitness);
751                }
752            }
753
754            // Update population
755            population = next_population;
756            fitness = next_fitness;
757
758            // Update best solution
759            best_idx = 0;
760            best_fitness = fitness[0];
761            for (idx, &fit) in fitness.iter().enumerate() {
762                if fit < best_fitness {
763                    best_idx = idx;
764                    best_fitness = fit;
765                }
766            }
767
768            // Update global best if needed
769            if best_fitness < best_individual_fitness {
770                best_individual = population[best_idx].clone();
771                best_individual_fitness = best_fitness;
772            }
773
774            // Update solution counts
775            for individual in &population {
776                *solution_counts.entry(individual.clone()).or_insert(0) += 1;
777            }
778        }
779
780        // Collect results
781        let mut results = Vec::new();
782
783        // Convert the solutions to SampleResult format
784        for (solution, count) in &solution_counts {
785            // Only include solutions that appeared multiple times
786            if *count < 2 {
787                continue;
788            }
789
790            // Calculate energy one more time
791            let energy = calculate_energy(solution, matrix);
792
793            // Convert to variable assignments, skipping any missing mappings
794            let assignments: HashMap<String, bool> = solution
795                .iter()
796                .enumerate()
797                .filter_map(|(idx, &value)| {
798                    idx_to_var
799                        .get(&idx)
800                        .map(|var_name| (var_name.clone(), value))
801                })
802                .collect();
803
804            // Skip solutions with incomplete variable mappings
805            if assignments.len() != solution.len() {
806                continue;
807            }
808
809            // Create result and add to collection
810            results.push(SampleResult {
811                assignments,
812                energy,
813                occurrences: *count,
814            });
815        }
816
817        // Sort by energy (best solutions first)
818        // Use unwrap_or for NaN handling - treat NaN as equal to any value
819        results.sort_by(|a, b| {
820            a.energy
821                .partial_cmp(&b.energy)
822                .unwrap_or(std::cmp::Ordering::Equal)
823        });
824
825        // Trim to requested number of shots
826        if results.len() > actual_shots {
827            results.truncate(actual_shots);
828        }
829
830        Ok(results)
831    }
832}
833
834// Helper function to calculate energy for a solution
835fn calculate_energy(solution: &[bool], matrix: &Array<f64, Ix2>) -> f64 {
836    let n = solution.len();
837    let mut energy = 0.0;
838
839    // Calculate from diagonal terms (linear)
840    for i in 0..n {
841        if solution[i] {
842            energy += matrix[[i, i]];
843        }
844    }
845
846    // Calculate from off-diagonal terms (quadratic)
847    for i in 0..n {
848        if solution[i] {
849            for j in (i + 1)..n {
850                if solution[j] {
851                    energy += matrix[[i, j]];
852                }
853            }
854        }
855    }
856
857    energy
858}
859
860// Helper function for single-point crossover
861fn simple_crossover(
862    parent1: &[bool],
863    parent2: &[bool],
864    rng: &mut impl Rng,
865) -> (Vec<bool>, Vec<bool>) {
866    let n_vars = parent1.len();
867    let mut child1 = vec![false; n_vars];
868    let mut child2 = vec![false; n_vars];
869
870    // Use single-point crossover
871    let crossover_point = if n_vars > 1 {
872        rng.random_range(1..n_vars)
873    } else {
874        0 // Special case for one-variable problems
875    };
876
877    for i in 0..n_vars {
878        if i < crossover_point {
879            child1[i] = parent1[i];
880            child2[i] = parent2[i];
881        } else {
882            child1[i] = parent2[i];
883            child2[i] = parent1[i];
884        }
885    }
886
887    (child1, child2)
888}
889
890// Helper function for mutation
891fn mutate(individual: &mut [bool], rate: f64, rng: &mut impl Rng) {
892    for bit in individual.iter_mut() {
893        if rng.random_bool(rate) {
894            *bit = !*bit;
895        }
896    }
897}
898
899// Helper function for tournament selection
900fn tournament_selection(fitness: &[f64], tournament_size: usize, rng: &mut impl Rng) -> usize {
901    // Handle edge cases
902    assert!(
903        !fitness.is_empty(),
904        "Cannot perform tournament selection on an empty fitness array"
905    );
906
907    if fitness.len() == 1 || tournament_size <= 1 {
908        return 0; // Only one choice available
909    }
910
911    // Ensure tournament_size is not larger than the population
912    let effective_tournament_size = std::cmp::min(tournament_size, fitness.len());
913
914    let mut best_idx = rng.random_range(0..fitness.len());
915    let mut best_fitness = fitness[best_idx];
916
917    for _ in 1..(effective_tournament_size) {
918        let candidate_idx = rng.random_range(0..fitness.len());
919        let candidate_fitness = fitness[candidate_idx];
920
921        // Lower fitness is better (minimization problem)
922        if candidate_fitness < best_fitness {
923            best_idx = candidate_idx;
924            best_fitness = candidate_fitness;
925        }
926    }
927
928    best_idx
929}