forge_core/algo/
lshade.rs1use super::Optimizer;
21use crate::problem::Problem;
22use crate::rng::Rng;
23use crate::solution::{Report, Solution, StopReason};
24use crate::termination::Termination;
25
26#[derive(Debug, Clone, Copy)]
28pub struct LShade {
29 pub init_pop: Option<usize>,
31 pub memory: usize,
33 pub p_best: f64,
36 pub archive_rate: f64,
38 pub seed: u64,
40}
41
42impl Default for LShade {
43 fn default() -> Self {
44 LShade {
45 init_pop: None,
46 memory: 6,
47 p_best: 0.11,
48 archive_rate: 2.6,
49 seed: 42,
50 }
51 }
52}
53
54const N_MIN: usize = 4;
57
58impl Optimizer for LShade {
59 fn with_seed(&self, seed: u64) -> Self {
60 LShade { seed, ..*self }
61 }
62
63 fn optimize(&self, problem: &dyn Problem, term: &Termination) -> Report {
64 crate::problem::validate(problem)
65 .unwrap_or_else(|e| panic!("LShade: invalid problem: {e}"));
66 let bounds = problem.bounds();
67 let dim = bounds.len();
68 let mut rng = Rng::new(self.seed);
69
70 let n_init = self.init_pop.unwrap_or(18 * dim).max(N_MIN);
71 let h = self.memory.max(1);
72 let max_nfe = term.max_evaluations.max(1);
73
74 let eval = |x: &[f64], problem: &dyn Problem| -> f64 {
75 let v = problem.objective(x);
76 if v.is_finite() {
77 v
78 } else {
79 f64::INFINITY
80 }
81 };
82
83 let mut m_cr = vec![0.5f64; h];
86 let mut m_f = vec![0.5f64; h];
87 let mut k_pos = 0usize;
88
89 let mut pop: Vec<Vec<f64>> = Vec::with_capacity(n_init);
91 let mut fit: Vec<f64> = Vec::with_capacity(n_init);
92 let mut archive: Vec<Vec<f64>> = Vec::new();
93 let mut best = Solution {
94 x: vec![0.0; dim],
95 value: f64::INFINITY,
96 };
97 let mut nfe = 0usize;
98 for _ in 0..n_init {
99 if term.reason(nfe, best.value).is_some() {
102 break;
103 }
104 let x: Vec<f64> = bounds
105 .iter()
106 .map(|&(lo, hi)| rng.uniform_in(lo, hi))
107 .collect();
108 let f = eval(&x, problem);
109 nfe += 1;
110 if f < best.value {
111 best = Solution {
112 x: x.clone(),
113 value: f,
114 };
115 }
116 pop.push(x);
117 fit.push(f);
118 }
119 let mut n = pop.len();
120
121 let mut trial = vec![0.0; dim];
122 'outer: while n >= N_MIN && term.reason(nfe, best.value).is_none() {
123 let mut ranked: Vec<usize> = (0..n).collect();
125 ranked.sort_by(|&a, &b| {
126 fit[a]
127 .partial_cmp(&fit[b])
128 .unwrap_or(std::cmp::Ordering::Equal)
129 });
130 let p_num = ((self.p_best * n as f64).round() as usize).clamp(2, n);
131
132 let mut succ_cr: Vec<f64> = Vec::new();
133 let mut succ_f: Vec<f64> = Vec::new();
134 let mut delta: Vec<f64> = Vec::new();
135
136 for i in 0..n {
137 if term.reason(nfe, best.value).is_some() {
138 break 'outer;
139 }
140 let r = rng.index(h);
141 let cr = if m_cr[r].is_nan() {
142 0.0
143 } else {
144 (m_cr[r] + 0.1 * rng.normal()).clamp(0.0, 1.0)
145 };
146 let scale = loop {
148 let v = m_f[r] + 0.1 * cauchy(&mut rng);
149 if v > 0.0 {
150 break v.min(1.0);
151 }
152 };
153
154 let pbest = ranked[rng.index(p_num)];
155 let r1 = loop {
156 let z = rng.index(n);
157 if z != i {
158 break z;
159 }
160 };
161 let na = archive.len();
162 let r2 = loop {
164 let z = rng.index(n + na);
165 if z >= n || (z != i && z != r1) {
166 break z;
167 }
168 };
169 let x_r2: &[f64] = if r2 < n { &pop[r2] } else { &archive[r2 - n] };
170
171 let jrand = rng.index(dim);
173 for j in 0..dim {
174 let (lo, hi) = bounds[j];
175 if rng.uniform() <= cr || j == jrand {
176 let mut v = pop[i][j]
177 + scale * (pop[pbest][j] - pop[i][j])
178 + scale * (pop[r1][j] - x_r2[j]);
179 if v < lo {
181 v = (lo + pop[i][j]) / 2.0;
182 } else if v > hi {
183 v = (hi + pop[i][j]) / 2.0;
184 }
185 trial[j] = v;
186 } else {
187 trial[j] = pop[i][j];
188 }
189 }
190
191 let tf = eval(&trial, problem);
192 nfe += 1;
193 if tf < best.value {
194 best = Solution {
195 x: trial.clone(),
196 value: tf,
197 };
198 }
199 if tf <= fit[i] {
201 if tf < fit[i] {
202 succ_cr.push(cr);
203 succ_f.push(scale);
204 let d = fit[i] - tf;
207 delta.push(if d.is_finite() { d } else { f64::MAX });
208 let arc_size = (self.archive_rate * n as f64).round() as usize;
211 if archive.len() < arc_size.max(1) {
212 archive.push(pop[i].clone());
213 } else if arc_size > 0 {
214 let idx = rng.index(archive.len());
215 archive[idx] = pop[i].clone();
216 }
217 }
218 pop[i].copy_from_slice(&trial);
219 fit[i] = tf;
220 }
221 }
222
223 let total: f64 = delta.iter().sum();
225 if !succ_f.is_empty() && total.is_finite() && total > 0.0 {
226 let w: Vec<f64> = delta.iter().map(|d| d / total).collect();
227 m_f[k_pos] = lehmer(&w, &succ_f);
228 let max_cr = succ_cr.iter().copied().fold(f64::NEG_INFINITY, f64::max);
229 m_cr[k_pos] = if m_cr[k_pos].is_nan() || max_cr == 0.0 {
230 f64::NAN
231 } else {
232 lehmer(&w, &succ_cr)
233 };
234 k_pos = (k_pos + 1) % h;
235 }
236
237 let target =
239 ((N_MIN as f64 - n_init as f64) / max_nfe as f64) * nfe as f64 + n_init as f64;
240 let n_new = (target.round() as usize).clamp(N_MIN, n);
241 if n_new < n {
242 let mut ranked2: Vec<usize> = (0..n).collect();
243 ranked2.sort_by(|&a, &b| {
244 fit[a]
245 .partial_cmp(&fit[b])
246 .unwrap_or(std::cmp::Ordering::Equal)
247 });
248 ranked2.truncate(n_new);
249 let new_pop: Vec<Vec<f64>> = ranked2.iter().map(|&i| pop[i].clone()).collect();
250 let new_fit: Vec<f64> = ranked2.iter().map(|&i| fit[i]).collect();
251 pop = new_pop;
252 fit = new_fit;
253 n = n_new;
254 let arc2 = (self.archive_rate * n as f64).round() as usize;
255 while archive.len() > arc2 {
256 let idx = rng.index(archive.len());
257 archive.swap_remove(idx);
258 }
259 }
260 }
261
262 let stop = term
263 .reason(nfe, best.value)
264 .unwrap_or(StopReason::BudgetExhausted);
265 Report {
266 solution: best,
267 stop,
268 evaluations: nfe,
269 }
270 }
271}
272
273fn lehmer(w: &[f64], s: &[f64]) -> f64 {
275 let num: f64 = w.iter().zip(s).map(|(wk, sk)| wk * sk * sk).sum();
276 let den: f64 = w.iter().zip(s).map(|(wk, sk)| wk * sk).sum();
277 if den != 0.0 {
278 num / den
279 } else {
280 0.5
281 }
282}
283
284fn cauchy(rng: &mut Rng) -> f64 {
286 (std::f64::consts::PI * (rng.uniform() - 0.5)).tan()
287}