Skip to main content

graphmind_optimization/algorithms/
cuckoo.rs

1use crate::common::{Individual, OptimizationResult, Problem, SolverConfig};
2use ndarray::Array1;
3use rand::prelude::*;
4use rand_distr::Distribution;
5use std::f64::consts::PI;
6
7pub struct CuckooSolver {
8    pub config: SolverConfig,
9    pub pa: f64, // Probability of discovering an alien egg (abandonment rate)
10}
11
12impl CuckooSolver {
13    pub fn new(config: SolverConfig) -> Self {
14        Self { config, pa: 0.25 }
15    }
16
17    pub fn with_pa(config: SolverConfig, pa: f64) -> Self {
18        Self { config, pa }
19    }
20
21    /// Levy flight random walk
22    fn levy_flight(&self, dim: usize) -> Array1<f64> {
23        // Mantegna's algorithm for Levy flights
24        let beta = 1.5;
25        let sigma_u = ((gamma(1.0 + beta) * (PI * beta / 2.0).sin())
26            / (gamma((1.0 + beta) / 2.0) * beta * 2.0f64.powf((beta - 1.0) / 2.0)))
27        .powf(1.0 / beta);
28        let sigma_v = 1.0;
29
30        let mut step = Array1::zeros(dim);
31        let mut rng = thread_rng();
32
33        for i in 0..dim {
34            let _u: f64 = rng.gen_range(0.0..1.0) * sigma_u; // Standard normal * sigma_u? No, usually Gaussian(0, sigma_u^2)
35                                                             // Simulating Normal distribution
36            let u_n: f64 = rand_distr::Normal::new(0.0, sigma_u)
37                .unwrap()
38                .sample(&mut rng);
39            let v_n: f64 = rand_distr::Normal::new(0.0, sigma_v)
40                .unwrap()
41                .sample(&mut rng);
42
43            let s = u_n / v_n.abs().powf(1.0 / beta);
44            step[i] = s;
45        }
46        step
47    }
48
49    pub fn solve<P: Problem>(&self, problem: &P) -> OptimizationResult {
50        let mut rng = thread_rng();
51        let dim = problem.dim();
52        let (lower, upper) = problem.bounds();
53
54        // 1. Initialize Population (Nests)
55        let mut nests: Vec<Individual> = (0..self.config.population_size)
56            .map(|_| {
57                let mut vars = Array1::zeros(dim);
58                for i in 0..dim {
59                    vars[i] = rng.gen_range(lower[i]..upper[i]);
60                }
61                let fitness = problem.fitness(&vars);
62                Individual::new(vars, fitness)
63            })
64            .collect();
65
66        // Find current best
67        let mut best_idx = 0;
68        for i in 1..self.config.population_size {
69            if nests[i].fitness < nests[best_idx].fitness {
70                best_idx = i;
71            }
72        }
73        let mut best_ind = nests[best_idx].clone();
74
75        let mut history = Vec::with_capacity(self.config.max_iterations);
76
77        for _iter in 0..self.config.max_iterations {
78            history.push(best_ind.fitness);
79
80            // 2. Generate new solutions via Levy Flights
81            for i in 0..self.config.population_size {
82                let step_size = 0.01; // Step scale
83                let levy = self.levy_flight(dim);
84                let current_vars = &nests[i].variables;
85
86                let mut new_vars = Array1::zeros(dim);
87                for j in 0..dim {
88                    // X_new = X + alpha * Levy * (X - X_best) ??
89                    // Standard Cuckoo: X_new = X + alpha * Levy
90                    // Sometimes uses difference from best.
91                    // Let's use simple: X_new = X + alpha * Levy * (X - X_best)
92                    let diff = current_vars[j] - best_ind.variables[j];
93                    let _delta = step_size * levy[j] * (if diff.abs() > 1e-6 { diff } else { 1.0 }); // avoid zero mult?
94                                                                                                     // Actually standard CS: step = alpha * L(s, lambda)
95                                                                                                     // Let's stick to X_new = X + alpha * Levy (+)
96
97                    let delta_simple = step_size * levy[j] * (upper[j] - lower[j]);
98
99                    new_vars[j] = (current_vars[j] + delta_simple).clamp(lower[j], upper[j]);
100                }
101
102                let new_fitness = problem.fitness(&new_vars);
103
104                // Random selection of nest to replace?
105                // Standard: Pick a random nest j, replace if new is better.
106                let j = rng.gen_range(0..self.config.population_size);
107                if new_fitness < nests[j].fitness {
108                    nests[j] = Individual::new(new_vars, new_fitness);
109                    if nests[j].fitness < best_ind.fitness {
110                        best_ind = nests[j].clone();
111                    }
112                }
113            }
114
115            // 3. Abandon worst nests (Alien eggs discovery)
116            // Sort to find worst? Or just random pairwise?
117            // Standard: Sort nests by fitness
118            nests.sort_by(|a, b| a.fitness.partial_cmp(&b.fitness).unwrap());
119
120            // Keep best (elitism), replace fraction pa of the rest (the worst ones)
121            let num_abandon = (self.config.population_size as f64 * self.pa) as usize;
122            let start_abandon_idx = self.config.population_size - num_abandon;
123
124            for i in start_abandon_idx..self.config.population_size {
125                // Generate new solution
126                // Usually via preferential random walk or simple random
127                let mut vars = Array1::zeros(dim);
128
129                // Pick two random nests for mixing
130                let r1 = rng.gen_range(0..self.config.population_size);
131                let r2 = rng.gen_range(0..self.config.population_size);
132
133                for j in 0..dim {
134                    let step = rng.gen::<f64>() * (nests[r1].variables[j] - nests[r2].variables[j]);
135                    vars[j] = (nests[i].variables[j] + step).clamp(lower[j], upper[j]);
136                }
137
138                let fitness = problem.fitness(&vars);
139                nests[i] = Individual::new(vars, fitness);
140
141                if fitness < best_ind.fitness {
142                    best_ind = nests[i].clone();
143                }
144            }
145        }
146
147        OptimizationResult {
148            best_variables: best_ind.variables,
149            best_fitness: best_ind.fitness,
150            history,
151        }
152    }
153}
154
155// Simple gamma function approximation (Lanczos)
156fn gamma(x: f64) -> f64 {
157    // For x ~ 1.5, simple approximation or crate `statrs`
158    // Since we don't have `statrs` in dependencies yet, let's use a hardcoded value for beta=1.5
159    // Gamma(1.5) = sqrt(PI)/2 ~= 0.886227
160    // Gamma(2.5) = 1.5 * Gamma(1.5) ~= 1.32934
161
162    if (x - 1.5).abs() < 1e-6 {
163        return 0.886227;
164    }
165    if (x - 2.5).abs() < 1e-6 {
166        return 1.32934;
167    }
168
169    // Fallback: Stirling's approximation
170    let term1 = (2.0 * PI / x).sqrt();
171    let term2 = (x / std::f64::consts::E).powf(x);
172    term1 * term2
173}