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}