Skip to main content

fugue_evo/fitness/
benchmarks.rs

1//! Benchmark fitness functions
2//!
3//! This module provides standard benchmark functions for testing evolutionary algorithms.
4
5use std::f64::consts::PI;
6
7use crate::fitness::traits::Fitness;
8use crate::genome::bit_string::BitString;
9use crate::genome::real_vector::RealVector;
10use crate::genome::traits::{BinaryGenome, RealValuedGenome};
11
12/// Trait for benchmark functions
13pub trait BenchmarkFunction: Send + Sync {
14    /// Name of the benchmark function
15    fn name(&self) -> &'static str;
16
17    /// Dimensionality of the problem
18    fn dimension(&self) -> usize;
19
20    /// Search space bounds (min, max)
21    fn bounds(&self) -> (f64, f64);
22
23    /// Optimal (minimum) fitness value
24    fn optimal_fitness(&self) -> f64;
25
26    /// Optimal solution (if known)
27    fn optimal_solution(&self) -> Option<Vec<f64>>;
28
29    /// Evaluate the function (returns value to be MINIMIZED)
30    fn evaluate_raw(&self, x: &[f64]) -> f64;
31}
32
33/// Sphere function: f(x) = Σxᵢ²
34///
35/// Unimodal, convex, separable. Optimum at origin.
36#[derive(Clone, Debug)]
37pub struct Sphere {
38    dimension: usize,
39}
40
41impl Sphere {
42    /// Create a new Sphere function
43    pub fn new(dimension: usize) -> Self {
44        Self { dimension }
45    }
46}
47
48impl BenchmarkFunction for Sphere {
49    fn name(&self) -> &'static str {
50        "Sphere"
51    }
52
53    fn dimension(&self) -> usize {
54        self.dimension
55    }
56
57    fn bounds(&self) -> (f64, f64) {
58        (-5.12, 5.12)
59    }
60
61    fn optimal_fitness(&self) -> f64 {
62        0.0
63    }
64
65    fn optimal_solution(&self) -> Option<Vec<f64>> {
66        Some(vec![0.0; self.dimension])
67    }
68
69    fn evaluate_raw(&self, x: &[f64]) -> f64 {
70        x.iter().map(|xi| xi * xi).sum()
71    }
72}
73
74impl Fitness for Sphere {
75    type Genome = RealVector;
76    type Value = f64;
77
78    fn evaluate(&self, genome: &Self::Genome) -> f64 {
79        // Negate for maximization (GA convention)
80        -self.evaluate_raw(genome.genes())
81    }
82}
83
84/// Rastrigin function: f(x) = 10n + Σ(xᵢ² - 10cos(2πxᵢ))
85///
86/// Highly multimodal with many local minima. Optimum at origin.
87#[derive(Clone, Debug)]
88pub struct Rastrigin {
89    dimension: usize,
90}
91
92impl Rastrigin {
93    /// Create a new Rastrigin function
94    pub fn new(dimension: usize) -> Self {
95        Self { dimension }
96    }
97}
98
99impl BenchmarkFunction for Rastrigin {
100    fn name(&self) -> &'static str {
101        "Rastrigin"
102    }
103
104    fn dimension(&self) -> usize {
105        self.dimension
106    }
107
108    fn bounds(&self) -> (f64, f64) {
109        (-5.12, 5.12)
110    }
111
112    fn optimal_fitness(&self) -> f64 {
113        0.0
114    }
115
116    fn optimal_solution(&self) -> Option<Vec<f64>> {
117        Some(vec![0.0; self.dimension])
118    }
119
120    fn evaluate_raw(&self, x: &[f64]) -> f64 {
121        let a = 10.0;
122        let n = x.len() as f64;
123        a * n
124            + x.iter()
125                .map(|xi| xi * xi - a * (2.0 * PI * xi).cos())
126                .sum::<f64>()
127    }
128}
129
130impl Fitness for Rastrigin {
131    type Genome = RealVector;
132    type Value = f64;
133
134    fn evaluate(&self, genome: &Self::Genome) -> f64 {
135        -self.evaluate_raw(genome.genes())
136    }
137}
138
139/// Rosenbrock function: f(x) = Σ[100(xᵢ₊₁-xᵢ²)² + (1-xᵢ)²]
140///
141/// Valley structure, non-separable. Optimum at (1,1,...,1).
142#[derive(Clone, Debug)]
143pub struct Rosenbrock {
144    dimension: usize,
145}
146
147impl Rosenbrock {
148    /// Create a new Rosenbrock function
149    pub fn new(dimension: usize) -> Self {
150        assert!(dimension >= 2, "Rosenbrock requires at least 2 dimensions");
151        Self { dimension }
152    }
153}
154
155impl BenchmarkFunction for Rosenbrock {
156    fn name(&self) -> &'static str {
157        "Rosenbrock"
158    }
159
160    fn dimension(&self) -> usize {
161        self.dimension
162    }
163
164    fn bounds(&self) -> (f64, f64) {
165        (-5.0, 10.0)
166    }
167
168    fn optimal_fitness(&self) -> f64 {
169        0.0
170    }
171
172    fn optimal_solution(&self) -> Option<Vec<f64>> {
173        Some(vec![1.0; self.dimension])
174    }
175
176    fn evaluate_raw(&self, x: &[f64]) -> f64 {
177        x.windows(2)
178            .map(|w| {
179                let xi = w[0];
180                let xi1 = w[1];
181                100.0 * (xi1 - xi * xi).powi(2) + (1.0 - xi).powi(2)
182            })
183            .sum()
184    }
185}
186
187impl Fitness for Rosenbrock {
188    type Genome = RealVector;
189    type Value = f64;
190
191    fn evaluate(&self, genome: &Self::Genome) -> f64 {
192        -self.evaluate_raw(genome.genes())
193    }
194}
195
196/// Ackley function
197///
198/// Nearly flat outer region with many local minima. Optimum at origin.
199#[derive(Clone, Debug)]
200pub struct Ackley {
201    dimension: usize,
202    a: f64,
203    b: f64,
204    c: f64,
205}
206
207impl Ackley {
208    /// Create a new Ackley function with default parameters
209    pub fn new(dimension: usize) -> Self {
210        Self {
211            dimension,
212            a: 20.0,
213            b: 0.2,
214            c: 2.0 * PI,
215        }
216    }
217
218    /// Create with custom parameters
219    pub fn with_params(dimension: usize, a: f64, b: f64, c: f64) -> Self {
220        Self { dimension, a, b, c }
221    }
222}
223
224impl BenchmarkFunction for Ackley {
225    fn name(&self) -> &'static str {
226        "Ackley"
227    }
228
229    fn dimension(&self) -> usize {
230        self.dimension
231    }
232
233    fn bounds(&self) -> (f64, f64) {
234        (-32.768, 32.768)
235    }
236
237    fn optimal_fitness(&self) -> f64 {
238        0.0
239    }
240
241    fn optimal_solution(&self) -> Option<Vec<f64>> {
242        Some(vec![0.0; self.dimension])
243    }
244
245    fn evaluate_raw(&self, x: &[f64]) -> f64 {
246        let n = x.len() as f64;
247        let sum_sq = x.iter().map(|xi| xi * xi).sum::<f64>();
248        let sum_cos = x.iter().map(|xi| (self.c * xi).cos()).sum::<f64>();
249
250        -self.a * (-self.b * (sum_sq / n).sqrt()).exp() - (sum_cos / n).exp()
251            + self.a
252            + std::f64::consts::E
253    }
254}
255
256impl Fitness for Ackley {
257    type Genome = RealVector;
258    type Value = f64;
259
260    fn evaluate(&self, genome: &Self::Genome) -> f64 {
261        -self.evaluate_raw(genome.genes())
262    }
263}
264
265/// Griewank function: f(x) = Σxᵢ²/4000 - Πcos(xᵢ/√i) + 1
266///
267/// Many local minima. Optimum at origin.
268#[derive(Clone, Debug)]
269pub struct Griewank {
270    dimension: usize,
271}
272
273impl Griewank {
274    /// Create a new Griewank function
275    pub fn new(dimension: usize) -> Self {
276        Self { dimension }
277    }
278}
279
280impl BenchmarkFunction for Griewank {
281    fn name(&self) -> &'static str {
282        "Griewank"
283    }
284
285    fn dimension(&self) -> usize {
286        self.dimension
287    }
288
289    fn bounds(&self) -> (f64, f64) {
290        (-600.0, 600.0)
291    }
292
293    fn optimal_fitness(&self) -> f64 {
294        0.0
295    }
296
297    fn optimal_solution(&self) -> Option<Vec<f64>> {
298        Some(vec![0.0; self.dimension])
299    }
300
301    fn evaluate_raw(&self, x: &[f64]) -> f64 {
302        let sum_sq: f64 = x.iter().map(|xi| xi * xi).sum::<f64>() / 4000.0;
303        let prod_cos: f64 = x
304            .iter()
305            .enumerate()
306            .map(|(i, xi)| (xi / ((i + 1) as f64).sqrt()).cos())
307            .product();
308        sum_sq - prod_cos + 1.0
309    }
310}
311
312impl Fitness for Griewank {
313    type Genome = RealVector;
314    type Value = f64;
315
316    fn evaluate(&self, genome: &Self::Genome) -> f64 {
317        -self.evaluate_raw(genome.genes())
318    }
319}
320
321/// Schwefel function
322///
323/// Deceptive - global optimum far from local optima.
324#[derive(Clone, Debug)]
325pub struct Schwefel {
326    dimension: usize,
327}
328
329impl Schwefel {
330    /// Create a new Schwefel function
331    pub fn new(dimension: usize) -> Self {
332        Self { dimension }
333    }
334}
335
336impl BenchmarkFunction for Schwefel {
337    fn name(&self) -> &'static str {
338        "Schwefel"
339    }
340
341    fn dimension(&self) -> usize {
342        self.dimension
343    }
344
345    fn bounds(&self) -> (f64, f64) {
346        (-500.0, 500.0)
347    }
348
349    fn optimal_fitness(&self) -> f64 {
350        0.0
351    }
352
353    fn optimal_solution(&self) -> Option<Vec<f64>> {
354        Some(vec![420.9687; self.dimension])
355    }
356
357    fn evaluate_raw(&self, x: &[f64]) -> f64 {
358        let n = x.len() as f64;
359        418.9829 * n - x.iter().map(|xi| xi * xi.abs().sqrt().sin()).sum::<f64>()
360    }
361}
362
363impl Fitness for Schwefel {
364    type Genome = RealVector;
365    type Value = f64;
366
367    fn evaluate(&self, genome: &Self::Genome) -> f64 {
368        -self.evaluate_raw(genome.genes())
369    }
370}
371
372/// OneMax function for bit strings
373///
374/// Counts the number of 1s in the bit string. Optimum when all bits are 1.
375#[derive(Clone, Debug)]
376pub struct OneMax {
377    length: usize,
378}
379
380impl OneMax {
381    /// Create a new OneMax function
382    pub fn new(length: usize) -> Self {
383        Self { length }
384    }
385
386    /// Get the length
387    pub fn length(&self) -> usize {
388        self.length
389    }
390}
391
392impl Fitness for OneMax {
393    type Genome = BitString;
394    type Value = usize;
395
396    fn evaluate(&self, genome: &Self::Genome) -> usize {
397        genome.count_ones()
398    }
399}
400
401/// LeadingOnes function for bit strings
402///
403/// Counts the number of leading 1s before the first 0.
404#[derive(Clone, Debug)]
405pub struct LeadingOnes {
406    #[allow(dead_code)]
407    length: usize,
408}
409
410impl LeadingOnes {
411    /// Create a new LeadingOnes function
412    pub fn new(length: usize) -> Self {
413        Self { length }
414    }
415}
416
417impl Fitness for LeadingOnes {
418    type Genome = BitString;
419    type Value = usize;
420
421    fn evaluate(&self, genome: &Self::Genome) -> usize {
422        genome.bits().iter().take_while(|&&b| b).count()
423    }
424}
425
426// =============================================================================
427// Multi-Objective Test Problems (ZDT)
428// =============================================================================
429
430/// ZDT1 multi-objective test problem
431///
432/// Two objectives with a convex Pareto front.
433/// Reference: Zitzler, E., Deb, K., & Thiele, L. (2000).
434#[derive(Clone, Debug)]
435pub struct Zdt1 {
436    dimension: usize,
437}
438
439impl Zdt1 {
440    /// Create a new ZDT1 function
441    pub fn new(dimension: usize) -> Self {
442        assert!(dimension >= 2, "ZDT1 requires at least 2 dimensions");
443        Self { dimension }
444    }
445
446    /// Evaluate the function (returns [f1, f2])
447    pub fn evaluate(&self, x: &[f64]) -> [f64; 2] {
448        let n = x.len() as f64;
449        let f1 = x[0];
450        let g = 1.0 + 9.0 * x[1..].iter().sum::<f64>() / (n - 1.0);
451        let f2 = g * (1.0 - (f1 / g).sqrt());
452        [f1, f2]
453    }
454
455    /// Bounds for each variable [0, 1]
456    pub fn bounds(&self) -> (f64, f64) {
457        (0.0, 1.0)
458    }
459
460    /// Get the dimension
461    pub fn dimension(&self) -> usize {
462        self.dimension
463    }
464}
465
466/// ZDT2 multi-objective test problem
467///
468/// Two objectives with a non-convex Pareto front.
469#[derive(Clone, Debug)]
470pub struct Zdt2 {
471    dimension: usize,
472}
473
474impl Zdt2 {
475    /// Create a new ZDT2 function
476    pub fn new(dimension: usize) -> Self {
477        assert!(dimension >= 2, "ZDT2 requires at least 2 dimensions");
478        Self { dimension }
479    }
480
481    /// Evaluate the function (returns [f1, f2])
482    pub fn evaluate(&self, x: &[f64]) -> [f64; 2] {
483        let n = x.len() as f64;
484        let f1 = x[0];
485        let g = 1.0 + 9.0 * x[1..].iter().sum::<f64>() / (n - 1.0);
486        let f2 = g * (1.0 - (f1 / g).powi(2));
487        [f1, f2]
488    }
489
490    /// Bounds for each variable [0, 1]
491    pub fn bounds(&self) -> (f64, f64) {
492        (0.0, 1.0)
493    }
494
495    /// Get the dimension
496    pub fn dimension(&self) -> usize {
497        self.dimension
498    }
499}
500
501/// ZDT3 multi-objective test problem
502///
503/// Two objectives with a disconnected Pareto front.
504#[derive(Clone, Debug)]
505pub struct Zdt3 {
506    dimension: usize,
507}
508
509impl Zdt3 {
510    /// Create a new ZDT3 function
511    pub fn new(dimension: usize) -> Self {
512        assert!(dimension >= 2, "ZDT3 requires at least 2 dimensions");
513        Self { dimension }
514    }
515
516    /// Evaluate the function (returns [f1, f2])
517    pub fn evaluate(&self, x: &[f64]) -> [f64; 2] {
518        let n = x.len() as f64;
519        let f1 = x[0];
520        let g = 1.0 + 9.0 * x[1..].iter().sum::<f64>() / (n - 1.0);
521        let h = 1.0 - (f1 / g).sqrt() - (f1 / g) * (10.0 * PI * f1).sin();
522        let f2 = g * h;
523        [f1, f2]
524    }
525
526    /// Bounds for each variable [0, 1]
527    pub fn bounds(&self) -> (f64, f64) {
528        (0.0, 1.0)
529    }
530
531    /// Get the dimension
532    pub fn dimension(&self) -> usize {
533        self.dimension
534    }
535}
536
537/// Schaffer N.1 multi-objective problem
538///
539/// Simple bi-objective problem with a single variable.
540#[derive(Clone, Debug)]
541pub struct SchafferN1;
542
543impl SchafferN1 {
544    /// Create a new Schaffer N.1 function
545    pub fn new() -> Self {
546        Self
547    }
548
549    /// Evaluate the function (returns [f1, f2])
550    pub fn evaluate(&self, x: f64) -> [f64; 2] {
551        let f1 = x * x;
552        let f2 = (x - 2.0) * (x - 2.0);
553        [f1, f2]
554    }
555
556    /// Bounds for the variable
557    pub fn bounds(&self) -> (f64, f64) {
558        (-10.0, 10.0)
559    }
560}
561
562impl Default for SchafferN1 {
563    fn default() -> Self {
564        Self::new()
565    }
566}
567
568// =============================================================================
569// Additional Single-Objective Functions
570// =============================================================================
571
572/// Levy function
573///
574/// Multimodal with many local minima. Optimum at (1, 1, ..., 1).
575#[derive(Clone, Debug)]
576pub struct Levy {
577    dimension: usize,
578}
579
580impl Levy {
581    /// Create a new Levy function
582    pub fn new(dimension: usize) -> Self {
583        Self { dimension }
584    }
585}
586
587impl BenchmarkFunction for Levy {
588    fn name(&self) -> &'static str {
589        "Levy"
590    }
591
592    fn dimension(&self) -> usize {
593        self.dimension
594    }
595
596    fn bounds(&self) -> (f64, f64) {
597        (-10.0, 10.0)
598    }
599
600    fn optimal_fitness(&self) -> f64 {
601        0.0
602    }
603
604    fn optimal_solution(&self) -> Option<Vec<f64>> {
605        Some(vec![1.0; self.dimension])
606    }
607
608    fn evaluate_raw(&self, x: &[f64]) -> f64 {
609        let w: Vec<f64> = x.iter().map(|xi| 1.0 + (xi - 1.0) / 4.0).collect();
610        let n = w.len();
611
612        let term1 = (PI * w[0]).sin().powi(2);
613
614        let sum: f64 = w[..n - 1]
615            .iter()
616            .map(|wi| (wi - 1.0).powi(2) * (1.0 + 10.0 * (PI * wi + 1.0).sin().powi(2)))
617            .sum();
618
619        let term3 = (w[n - 1] - 1.0).powi(2) * (1.0 + (2.0 * PI * w[n - 1]).sin().powi(2));
620
621        term1 + sum + term3
622    }
623}
624
625impl Fitness for Levy {
626    type Genome = RealVector;
627    type Value = f64;
628
629    fn evaluate(&self, genome: &Self::Genome) -> f64 {
630        -self.evaluate_raw(genome.genes())
631    }
632}
633
634/// Dixon-Price function
635///
636/// Valley structure. Optimum depends on dimension.
637#[derive(Clone, Debug)]
638pub struct DixonPrice {
639    dimension: usize,
640}
641
642impl DixonPrice {
643    /// Create a new Dixon-Price function
644    pub fn new(dimension: usize) -> Self {
645        Self { dimension }
646    }
647}
648
649impl BenchmarkFunction for DixonPrice {
650    fn name(&self) -> &'static str {
651        "Dixon-Price"
652    }
653
654    fn dimension(&self) -> usize {
655        self.dimension
656    }
657
658    fn bounds(&self) -> (f64, f64) {
659        (-10.0, 10.0)
660    }
661
662    fn optimal_fitness(&self) -> f64 {
663        0.0
664    }
665
666    fn optimal_solution(&self) -> Option<Vec<f64>> {
667        // Canonical Dixon-Price global minimizer (1-based index j = 1..d):
668        //     x_j = 2^{ -(2^j - 2) / 2^j }
669        // Here `i` is 0-based, so the 1-based index is j = i + 1 and both the
670        // numerator and denominator exponents must use `i + 1` consistently.
671        let optimal: Vec<f64> = (0..self.dimension)
672            .map(|i| {
673                let two_pow_j = (1u64 << (i + 1)) as f64; // 2^{i+1} = 2^j
674                let exp_num = two_pow_j - 2.0; // 2^j - 2
675                let exp_den = two_pow_j; // 2^j
676                2.0_f64.powf(-exp_num / exp_den)
677            })
678            .collect();
679        Some(optimal)
680    }
681
682    fn evaluate_raw(&self, x: &[f64]) -> f64 {
683        let term1 = (x[0] - 1.0).powi(2);
684
685        let sum: f64 = x
686            .windows(2)
687            .enumerate()
688            .map(|(i, w)| (i + 2) as f64 * (2.0 * w[1] * w[1] - w[0]).powi(2))
689            .sum();
690
691        term1 + sum
692    }
693}
694
695impl Fitness for DixonPrice {
696    type Genome = RealVector;
697    type Value = f64;
698
699    fn evaluate(&self, genome: &Self::Genome) -> f64 {
700        -self.evaluate_raw(genome.genes())
701    }
702}
703
704/// Styblinski-Tang function
705///
706/// Multimodal. Optimum at (-2.903534, ..., -2.903534).
707#[derive(Clone, Debug)]
708pub struct StyblinskiTang {
709    dimension: usize,
710}
711
712impl StyblinskiTang {
713    /// Create a new Styblinski-Tang function
714    pub fn new(dimension: usize) -> Self {
715        Self { dimension }
716    }
717}
718
719impl BenchmarkFunction for StyblinskiTang {
720    fn name(&self) -> &'static str {
721        "Styblinski-Tang"
722    }
723
724    fn dimension(&self) -> usize {
725        self.dimension
726    }
727
728    fn bounds(&self) -> (f64, f64) {
729        (-5.0, 5.0)
730    }
731
732    fn optimal_fitness(&self) -> f64 {
733        // f(optimal) = -39.16617 * dimension
734        -39.16617 * self.dimension as f64
735    }
736
737    fn optimal_solution(&self) -> Option<Vec<f64>> {
738        Some(vec![-2.903534; self.dimension])
739    }
740
741    fn evaluate_raw(&self, x: &[f64]) -> f64 {
742        x.iter()
743            .map(|xi| xi.powi(4) - 16.0 * xi.powi(2) + 5.0 * xi)
744            .sum::<f64>()
745            / 2.0
746    }
747}
748
749impl Fitness for StyblinskiTang {
750    type Genome = RealVector;
751    type Value = f64;
752
753    fn evaluate(&self, genome: &Self::Genome) -> f64 {
754        -self.evaluate_raw(genome.genes())
755    }
756}
757
758// ============================================================================
759// Combinatorial Benchmarks
760// ============================================================================
761
762/// Royal Road function
763///
764/// Tests the "building block hypothesis" - fitness is the count of complete
765/// schemas (contiguous blocks of 1s). Rewards completing full blocks rather
766/// than partial solutions.
767///
768/// For example, with schema_size=8 and num_schemas=4 (32-bit string):
769/// - 11111111 00000000 11111111 00000000 has fitness 2 (two complete blocks)
770/// - 11111110 11111111 11111111 11111111 has fitness 3 (one incomplete)
771#[derive(Clone, Debug)]
772pub struct RoyalRoad {
773    /// Size of each schema (block of consecutive 1s)
774    pub schema_size: usize,
775    /// Number of schemas in the genome
776    pub num_schemas: usize,
777}
778
779impl RoyalRoad {
780    /// Create a new Royal Road function
781    ///
782    /// # Arguments
783    /// * `schema_size` - Number of bits in each schema (typically 8)
784    /// * `num_schemas` - Number of schemas (typically 8)
785    pub fn new(schema_size: usize, num_schemas: usize) -> Self {
786        Self {
787            schema_size,
788            num_schemas,
789        }
790    }
791
792    /// Standard Royal Road configuration (8x8 = 64 bits)
793    pub fn standard() -> Self {
794        Self::new(8, 8)
795    }
796
797    /// Total genome length required
798    pub fn genome_length(&self) -> usize {
799        self.schema_size * self.num_schemas
800    }
801
802    /// Check if a schema (block) is complete (all 1s)
803    fn is_complete_schema(&self, bits: &[bool], schema_index: usize) -> bool {
804        let start = schema_index * self.schema_size;
805        let end = start + self.schema_size;
806
807        if end > bits.len() {
808            return false;
809        }
810
811        bits[start..end].iter().all(|&b| b)
812    }
813
814    /// Count the number of complete schemas
815    pub fn count_complete_schemas(&self, bits: &[bool]) -> usize {
816        (0..self.num_schemas)
817            .filter(|&i| self.is_complete_schema(bits, i))
818            .count()
819    }
820}
821
822impl Fitness for RoyalRoad {
823    type Genome = BitString;
824    type Value = usize;
825
826    fn evaluate(&self, genome: &Self::Genome) -> usize {
827        self.count_complete_schemas(genome.bits())
828    }
829}
830
831/// NK Landscape
832///
833/// A tunable fitness landscape with controllable epistasis (gene interactions).
834/// - N is the genome length
835/// - K is the number of other genes that affect each gene's fitness contribution
836///
837/// Higher K = more rugged landscape with more local optima.
838/// K=0: smooth landscape (separable)
839/// K=N-1: maximally rugged (all genes interact)
840///
841/// The fitness is the average of local fitness contributions.
842#[derive(Clone, Debug)]
843pub struct NkLandscape {
844    /// Genome length (N)
845    n: usize,
846    /// Epistasis degree (K)
847    k: usize,
848    /// Neighbor indices for each gene position
849    neighbors: Vec<Vec<usize>>,
850    /// Fitness contribution lookup tables
851    /// For each gene i, maps (gene i value, neighbor values) -> contribution
852    contributions: Vec<std::collections::HashMap<Vec<bool>, f64>>,
853}
854
855impl NkLandscape {
856    /// Create a new NK Landscape with random fitness contributions
857    ///
858    /// # Arguments
859    /// * `n` - Genome length
860    /// * `k` - Epistasis degree (must be < n)
861    /// * `seed` - Random seed for reproducibility
862    pub fn new(n: usize, k: usize, seed: u64) -> Self {
863        assert!(k < n, "K must be less than N");
864
865        use rand::SeedableRng;
866        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
867
868        // Generate random neighbors for each position
869        let mut neighbors = Vec::with_capacity(n);
870        for i in 0..n {
871            let mut gene_neighbors: Vec<usize> = (0..n).filter(|&j| j != i).collect();
872            // Shuffle and take first k neighbors
873            use rand::seq::SliceRandom;
874            gene_neighbors.shuffle(&mut rng);
875            gene_neighbors.truncate(k);
876            gene_neighbors.sort();
877            neighbors.push(gene_neighbors);
878        }
879
880        // Generate random fitness contributions for each configuration
881        let mut contributions = Vec::with_capacity(n);
882        for _i in 0..n {
883            let num_configs = 1 << (k + 1); // 2^(k+1) configurations
884            let mut table = std::collections::HashMap::with_capacity(num_configs);
885
886            // Generate all possible configurations
887            for config_bits in 0..num_configs {
888                let config: Vec<bool> = (0..=k).map(|j| (config_bits >> j) & 1 == 1).collect();
889                use rand::Rng;
890                table.insert(config, rng.gen::<f64>());
891            }
892
893            contributions.push(table);
894        }
895
896        Self {
897            n,
898            k,
899            neighbors,
900            contributions,
901        }
902    }
903
904    /// Create an NK Landscape with adjacent neighbors
905    /// (each gene interacts with its k nearest neighbors)
906    pub fn with_adjacent_neighbors(n: usize, k: usize, seed: u64) -> Self {
907        assert!(k < n, "K must be less than N");
908
909        use rand::SeedableRng;
910        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
911
912        // Adjacent neighbors
913        let mut neighbors = Vec::with_capacity(n);
914        for i in 0..n {
915            let gene_neighbors: Vec<usize> = (1..=k).map(|offset| (i + offset) % n).collect();
916            neighbors.push(gene_neighbors);
917        }
918
919        // Generate random fitness contributions
920        let mut contributions = Vec::with_capacity(n);
921        for _i in 0..n {
922            let num_configs = 1 << (k + 1);
923            let mut table = std::collections::HashMap::with_capacity(num_configs);
924
925            for config_bits in 0..num_configs {
926                let config: Vec<bool> = (0..=k).map(|j| (config_bits >> j) & 1 == 1).collect();
927                use rand::Rng;
928                table.insert(config, rng.gen::<f64>());
929            }
930
931            contributions.push(table);
932        }
933
934        Self {
935            n,
936            k,
937            neighbors,
938            contributions,
939        }
940    }
941
942    /// Get genome length (N)
943    pub fn genome_length(&self) -> usize {
944        self.n
945    }
946
947    /// Get epistasis degree (K)
948    pub fn epistasis(&self) -> usize {
949        self.k
950    }
951
952    /// Evaluate fitness for a bit string
953    pub fn evaluate_bits(&self, bits: &[bool]) -> f64 {
954        assert!(bits.len() >= self.n);
955
956        let mut total = 0.0;
957
958        for i in 0..self.n {
959            // Build configuration: [gene_i, neighbor_1, neighbor_2, ...]
960            let mut config = Vec::with_capacity(self.k + 1);
961            config.push(bits[i]);
962            for &j in &self.neighbors[i] {
963                config.push(bits[j]);
964            }
965
966            // Look up contribution
967            if let Some(&contribution) = self.contributions[i].get(&config) {
968                total += contribution;
969            }
970        }
971
972        // Return average fitness
973        total / self.n as f64
974    }
975}
976
977impl Fitness for NkLandscape {
978    type Genome = BitString;
979    type Value = f64;
980
981    fn evaluate(&self, genome: &Self::Genome) -> f64 {
982        self.evaluate_bits(genome.bits())
983    }
984}
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989    use crate::fitness::traits::Fitness;
990    use approx::assert_relative_eq;
991
992    // Sphere function tests
993    #[test]
994    fn test_sphere_at_optimum() {
995        let sphere = Sphere::new(3);
996        let optimum = RealVector::new(vec![0.0, 0.0, 0.0]);
997        assert_relative_eq!(sphere.evaluate(&optimum), 0.0);
998    }
999
1000    #[test]
1001    fn test_sphere_non_optimum() {
1002        let sphere = Sphere::new(3);
1003        let point = RealVector::new(vec![1.0, 2.0, 3.0]);
1004        // 1 + 4 + 9 = 14, negated = -14
1005        assert_relative_eq!(sphere.evaluate(&point), -14.0);
1006    }
1007
1008    #[test]
1009    fn test_sphere_metadata() {
1010        let sphere = Sphere::new(5);
1011        assert_eq!(sphere.name(), "Sphere");
1012        assert_eq!(sphere.dimension(), 5);
1013        assert_eq!(sphere.bounds(), (-5.12, 5.12));
1014        assert_relative_eq!(sphere.optimal_fitness(), 0.0);
1015        assert_eq!(sphere.optimal_solution(), Some(vec![0.0; 5]));
1016    }
1017
1018    // Rastrigin function tests
1019    #[test]
1020    fn test_rastrigin_at_optimum() {
1021        let rastrigin = Rastrigin::new(3);
1022        let optimum = RealVector::new(vec![0.0, 0.0, 0.0]);
1023        assert_relative_eq!(rastrigin.evaluate(&optimum), 0.0, epsilon = 1e-10);
1024    }
1025
1026    #[test]
1027    fn test_rastrigin_non_optimum() {
1028        let rastrigin = Rastrigin::new(2);
1029        let point = RealVector::new(vec![1.0, 1.0]);
1030        // At x=1, cos(2π*1) = 1, so each term is 1 - 10*1 = -9
1031        // Total = 10*2 + 2*(-9) = 20 - 18 = 2, but we need to calculate more precisely
1032        let expected = 10.0 * 2.0
1033            + (1.0 * 1.0 - 10.0 * (2.0 * PI * 1.0).cos())
1034            + (1.0 * 1.0 - 10.0 * (2.0 * PI * 1.0).cos());
1035        assert_relative_eq!(rastrigin.evaluate(&point), -expected, epsilon = 1e-10);
1036    }
1037
1038    #[test]
1039    fn test_rastrigin_metadata() {
1040        let rastrigin = Rastrigin::new(10);
1041        assert_eq!(rastrigin.name(), "Rastrigin");
1042        assert_eq!(rastrigin.dimension(), 10);
1043        assert_eq!(rastrigin.bounds(), (-5.12, 5.12));
1044    }
1045
1046    // Rosenbrock function tests
1047    #[test]
1048    fn test_rosenbrock_at_optimum() {
1049        let rosenbrock = Rosenbrock::new(3);
1050        let optimum = RealVector::new(vec![1.0, 1.0, 1.0]);
1051        assert_relative_eq!(rosenbrock.evaluate(&optimum), 0.0, epsilon = 1e-10);
1052    }
1053
1054    #[test]
1055    fn test_rosenbrock_non_optimum() {
1056        let rosenbrock = Rosenbrock::new(2);
1057        let point = RealVector::new(vec![0.0, 0.0]);
1058        // 100*(0 - 0)^2 + (1 - 0)^2 = 1
1059        assert_relative_eq!(rosenbrock.evaluate(&point), -1.0, epsilon = 1e-10);
1060    }
1061
1062    #[test]
1063    fn test_rosenbrock_metadata() {
1064        let rosenbrock = Rosenbrock::new(5);
1065        assert_eq!(rosenbrock.name(), "Rosenbrock");
1066        assert_eq!(rosenbrock.dimension(), 5);
1067        assert_eq!(rosenbrock.bounds(), (-5.0, 10.0));
1068        assert_eq!(rosenbrock.optimal_solution(), Some(vec![1.0; 5]));
1069    }
1070
1071    // Ackley function tests
1072    #[test]
1073    fn test_ackley_at_optimum() {
1074        let ackley = Ackley::new(3);
1075        let optimum = RealVector::new(vec![0.0, 0.0, 0.0]);
1076        assert_relative_eq!(ackley.evaluate(&optimum), 0.0, epsilon = 1e-10);
1077    }
1078
1079    #[test]
1080    fn test_ackley_metadata() {
1081        let ackley = Ackley::new(10);
1082        assert_eq!(ackley.name(), "Ackley");
1083        assert_eq!(ackley.dimension(), 10);
1084        assert_eq!(ackley.bounds(), (-32.768, 32.768));
1085    }
1086
1087    // Griewank function tests
1088    #[test]
1089    fn test_griewank_at_optimum() {
1090        let griewank = Griewank::new(3);
1091        let optimum = RealVector::new(vec![0.0, 0.0, 0.0]);
1092        assert_relative_eq!(griewank.evaluate(&optimum), 0.0, epsilon = 1e-10);
1093    }
1094
1095    #[test]
1096    fn test_griewank_metadata() {
1097        let griewank = Griewank::new(10);
1098        assert_eq!(griewank.name(), "Griewank");
1099        assert_eq!(griewank.dimension(), 10);
1100        assert_eq!(griewank.bounds(), (-600.0, 600.0));
1101    }
1102
1103    // Schwefel function tests
1104    #[test]
1105    fn test_schwefel_metadata() {
1106        let schwefel = Schwefel::new(10);
1107        assert_eq!(schwefel.name(), "Schwefel");
1108        assert_eq!(schwefel.dimension(), 10);
1109        assert_eq!(schwefel.bounds(), (-500.0, 500.0));
1110    }
1111
1112    // OneMax function tests
1113    #[test]
1114    fn test_onemax_all_ones() {
1115        let onemax = OneMax::new(10);
1116        let genome = BitString::ones(10);
1117        assert_eq!(onemax.evaluate(&genome), 10);
1118    }
1119
1120    #[test]
1121    fn test_onemax_all_zeros() {
1122        let onemax = OneMax::new(10);
1123        let genome = BitString::zeros(10);
1124        assert_eq!(onemax.evaluate(&genome), 0);
1125    }
1126
1127    #[test]
1128    fn test_onemax_mixed() {
1129        let onemax = OneMax::new(5);
1130        let genome = BitString::new(vec![true, false, true, false, true]);
1131        assert_eq!(onemax.evaluate(&genome), 3);
1132    }
1133
1134    // LeadingOnes function tests
1135    #[test]
1136    fn test_leadingones_all_ones() {
1137        let lo = LeadingOnes::new(10);
1138        let genome = BitString::ones(10);
1139        assert_eq!(lo.evaluate(&genome), 10);
1140    }
1141
1142    #[test]
1143    fn test_leadingones_all_zeros() {
1144        let lo = LeadingOnes::new(10);
1145        let genome = BitString::zeros(10);
1146        assert_eq!(lo.evaluate(&genome), 0);
1147    }
1148
1149    #[test]
1150    fn test_leadingones_mixed() {
1151        let lo = LeadingOnes::new(5);
1152        let genome = BitString::new(vec![true, true, false, true, true]);
1153        assert_eq!(lo.evaluate(&genome), 2); // First 2 are 1s, then a 0
1154    }
1155
1156    #[test]
1157    fn test_leadingones_starts_with_zero() {
1158        let lo = LeadingOnes::new(5);
1159        let genome = BitString::new(vec![false, true, true, true, true]);
1160        assert_eq!(lo.evaluate(&genome), 0);
1161    }
1162
1163    // ZDT1 tests
1164    #[test]
1165    fn test_zdt1_pareto_front() {
1166        let zdt1 = Zdt1::new(10);
1167        // On the Pareto front, all x_i = 0 for i > 0, and x_0 varies from 0 to 1
1168        let x = vec![0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1169        let [f1, f2] = zdt1.evaluate(&x);
1170
1171        // f1 should equal x_0
1172        assert_relative_eq!(f1, 0.5, epsilon = 1e-10);
1173
1174        // On Pareto front with g=1: f2 = 1 - sqrt(f1)
1175        assert_relative_eq!(f2, 1.0 - f1.sqrt(), epsilon = 1e-10);
1176    }
1177
1178    // ZDT2 tests
1179    #[test]
1180    fn test_zdt2_pareto_front() {
1181        let zdt2 = Zdt2::new(10);
1182        let x = vec![0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1183        let [f1, f2] = zdt2.evaluate(&x);
1184
1185        assert_relative_eq!(f1, 0.5, epsilon = 1e-10);
1186        // On Pareto front with g=1: f2 = 1 - f1^2
1187        assert_relative_eq!(f2, 1.0 - f1 * f1, epsilon = 1e-10);
1188    }
1189
1190    // Schaffer N.1 tests
1191    #[test]
1192    fn test_schaffer_n1() {
1193        let schaffer = SchafferN1::new();
1194        let [f1, f2] = schaffer.evaluate(0.0);
1195        assert_relative_eq!(f1, 0.0, epsilon = 1e-10);
1196        assert_relative_eq!(f2, 4.0, epsilon = 1e-10);
1197    }
1198
1199    // Levy function tests
1200    #[test]
1201    fn test_levy_at_optimum() {
1202        let levy = Levy::new(3);
1203        let optimum = RealVector::new(vec![1.0, 1.0, 1.0]);
1204        assert_relative_eq!(levy.evaluate(&optimum), 0.0, epsilon = 1e-10);
1205    }
1206
1207    // Dixon-Price tests
1208    #[test]
1209    fn test_dixonprice_metadata() {
1210        let dp = DixonPrice::new(5);
1211        assert_eq!(dp.name(), "Dixon-Price");
1212        assert_eq!(dp.dimension(), 5);
1213    }
1214
1215    // regression: EV-15 — optimal_solution() must actually be the global minimizer,
1216    // i.e. evaluate_raw() at the declared optimum must be (numerically) zero.
1217    // The pre-fix exponent used 2^i-2 over 2^{i+1}, yielding f > 0.85 at every
1218    // dimension, so this test fails on the old code.
1219    #[test]
1220    fn test_dixonprice_optimal_solution_is_optimum() {
1221        for dim in 2..=6 {
1222            let dp = DixonPrice::new(dim);
1223            let opt = dp
1224                .optimal_solution()
1225                .expect("Dixon-Price exposes an optimal solution");
1226            let value = dp.evaluate_raw(&opt);
1227            assert!(
1228                value < 1e-12,
1229                "f(optimal_solution()) for dim={dim} was {value}, expected < 1e-12"
1230            );
1231        }
1232    }
1233
1234    // Styblinski-Tang tests
1235    #[test]
1236    fn test_styblinskitang_near_optimum() {
1237        let st = StyblinskiTang::new(2);
1238        let near_opt = RealVector::new(vec![-2.9, -2.9]);
1239        // Should be close to optimal fitness
1240        let fitness = st.evaluate(&near_opt);
1241        let optimal = st.optimal_fitness();
1242        // The returned fitness is negated, so compare negatives
1243        assert!(
1244            fitness > optimal - 1.0,
1245            "Fitness {} should be close to optimal {}",
1246            fitness,
1247            optimal
1248        );
1249    }
1250
1251    // Royal Road tests
1252    #[test]
1253    fn test_royal_road_all_ones() {
1254        let rr = RoyalRoad::new(4, 4); // 16-bit genome
1255        let genome = BitString::ones(16);
1256        let fitness: usize = rr.evaluate(&genome);
1257        assert_eq!(fitness, 4); // All 4 schemas complete
1258    }
1259
1260    #[test]
1261    fn test_royal_road_all_zeros() {
1262        let rr = RoyalRoad::new(4, 4);
1263        let genome = BitString::zeros(16);
1264        let fitness: usize = rr.evaluate(&genome);
1265        assert_eq!(fitness, 0); // No schemas complete
1266    }
1267
1268    #[test]
1269    fn test_royal_road_partial() {
1270        let rr = RoyalRoad::new(4, 4);
1271        // First and third schemas complete
1272        let bits = vec![
1273            true, true, true, true, // Schema 0: complete
1274            false, false, false, false, // Schema 1: empty
1275            true, true, true, true, // Schema 2: complete
1276            true, true, true, false, // Schema 3: incomplete
1277        ];
1278        let genome = BitString::new(bits);
1279        let fitness: usize = rr.evaluate(&genome);
1280        assert_eq!(fitness, 2);
1281    }
1282
1283    #[test]
1284    fn test_royal_road_standard() {
1285        let rr = RoyalRoad::standard();
1286        assert_eq!(rr.genome_length(), 64);
1287        assert_eq!(rr.schema_size, 8);
1288        assert_eq!(rr.num_schemas, 8);
1289    }
1290
1291    // NK Landscape tests
1292    #[test]
1293    fn test_nk_landscape_creation() {
1294        let nk = NkLandscape::new(10, 2, 42);
1295        assert_eq!(nk.genome_length(), 10);
1296        assert_eq!(nk.epistasis(), 2);
1297    }
1298
1299    #[test]
1300    fn test_nk_landscape_deterministic() {
1301        // Same seed should give same landscape
1302        let nk1 = NkLandscape::new(8, 2, 123);
1303        let nk2 = NkLandscape::new(8, 2, 123);
1304
1305        let genome = BitString::new(vec![true, false, true, false, true, false, true, false]);
1306        let f1: f64 = nk1.evaluate(&genome);
1307        let f2: f64 = nk2.evaluate(&genome);
1308
1309        assert_relative_eq!(f1, f2);
1310    }
1311
1312    #[test]
1313    fn test_nk_landscape_fitness_range() {
1314        let nk = NkLandscape::new(10, 3, 42);
1315        let genome = BitString::new(vec![true; 10]);
1316        let fitness: f64 = nk.evaluate(&genome);
1317
1318        // Fitness should be average of contributions, so in [0, 1]
1319        assert!((0.0..=1.0).contains(&fitness));
1320    }
1321
1322    #[test]
1323    fn test_nk_landscape_adjacent() {
1324        let nk = NkLandscape::with_adjacent_neighbors(10, 2, 42);
1325        let genome = BitString::ones(10);
1326        let fitness: f64 = nk.evaluate(&genome);
1327
1328        assert!((0.0..=1.0).contains(&fitness));
1329    }
1330
1331    #[test]
1332    #[should_panic(expected = "K must be less than N")]
1333    fn test_nk_landscape_invalid_k() {
1334        let _nk = NkLandscape::new(5, 5, 42); // K = N is invalid
1335    }
1336}