1use nalgebra::DVector;
36
37use crate::fitness::Fitness;
38use crate::rng::Rng;
39
40#[derive(Clone, Debug)]
42pub struct PgpeResult {
43 pub x: Vec<f64>,
45 pub y: f64,
47 pub evaluations: u64,
49 pub iterations: i32,
51 pub stop: i32,
53}
54
55#[derive(Clone, Debug)]
57pub struct PgpeParams {
58 pub popsize: i32,
60 pub max_evaluations: u64,
62 pub stop_fitness: f64,
64 pub lr_decay_steps: i32,
66 pub use_ranking: bool,
68 pub center_learning_rate: f64,
70 pub stdev_learning_rate: f64,
72 pub stdev_max_change: f64,
74 pub b1: f64,
76 pub b2: f64,
78 pub eps: f64,
80 pub decay_coef: f64,
82 pub seed: u64,
84 pub runid: i64,
86}
87
88impl Default for PgpeParams {
89 fn default() -> Self {
90 Self {
91 popsize: 32,
92 max_evaluations: 100_000,
93 stop_fitness: f64::NEG_INFINITY,
94 lr_decay_steps: 1000,
95 use_ranking: true,
96 center_learning_rate: 0.15,
97 stdev_learning_rate: 0.1,
98 stdev_max_change: 0.2,
99 b1: 0.9,
100 b2: 0.999,
101 eps: 1e-8,
102 decay_coef: 1.0,
103 seed: 0,
104 runid: 0,
105 }
106 }
107}
108
109fn sort_index(v: &[f64]) -> Vec<usize> {
110 let mut idx: Vec<usize> = (0..v.len()).collect();
111 idx.sort_by(|&a, &b| v[a].partial_cmp(&v[b]).unwrap_or(std::cmp::Ordering::Equal));
112 idx
113}
114
115struct Adam {
117 x: DVector<f64>,
118 m: DVector<f64>,
119 v: DVector<f64>,
120 b1: f64,
121 b2: f64,
122 eps: f64,
123 center_lr: f64,
124 decay_coef: f64,
125}
126
127impl Adam {
128 fn new(x0: &DVector<f64>, b1: f64, b2: f64, eps: f64, center_lr: f64, decay_coef: f64) -> Self {
129 let dim = x0.len();
130 Adam {
131 x: x0.clone(),
132 m: DVector::zeros(dim),
133 v: DVector::zeros(dim),
134 b1,
135 b2,
136 eps,
137 center_lr,
138 decay_coef,
139 }
140 }
141
142 fn step_size(&self, i: i32) -> f64 {
143 self.center_lr * self.decay_coef.powi(i)
144 }
145
146 fn update(&mut self, i: i32, g: &DVector<f64>) {
147 self.m = g * (1.0 - self.b1) + &self.m * self.b1;
148 self.v = g.map(|v| v * v) * (1.0 - self.b2) + &self.v * self.b2;
149 let bc1 = 1.0 / (1.0 - self.b1.powi(i + 1));
150 let bc2 = 1.0 / (1.0 - self.b2.powi(i + 1));
151 let mhat = &self.m * bc1;
152 let vhat = &self.v * bc2;
153 let step = self.step_size(i);
154 let delta = DVector::from_iterator(
155 self.x.len(),
156 (0..self.x.len()).map(|k| step * mhat[k] / (vhat[k].sqrt() + self.eps)),
157 );
158 self.x -= delta;
159 }
160}
161
162pub struct Pgpe {
164 fitfun: Fitness,
165 rng: Rng,
166 dim: usize,
167 popsize: usize,
168 max_evaluations: u64,
169 stopfitness: f64,
170 lr_decay_steps: i32,
171 use_ranking: bool,
172 stdev_learning_rate: f64,
173 stdev_max_change: f64,
174
175 adam: Adam,
176 center: DVector<f64>,
177 stdev: DVector<f64>,
178 scaled_noises: Vec<DVector<f64>>, pop_x: Vec<DVector<f64>>, best_x: DVector<f64>,
182 best_y: f64,
183 iterations: i32,
184 stop: i32,
185 external_evaluations: u64,
186}
187
188impl Pgpe {
189 pub fn new(mut fitfun: Fitness, guess: &[f64], input_sigma: &[f64], p: &PgpeParams) -> Self {
193 let dim = fitfun.dim();
194 fitfun.reset_evaluations();
195 let mut popsize = if p.popsize > 0 {
196 p.popsize as usize
197 } else {
198 4 * dim
199 };
200 if popsize % 2 == 1 {
201 popsize += 1;
202 }
203 let center = DVector::from_vec(fitfun.encode(guess));
204 let stdev = if input_sigma.len() == 1 {
205 DVector::from_element(dim, input_sigma[0])
206 } else {
207 DVector::from_row_slice(input_sigma)
208 };
209 let adam = Adam::new(
214 ¢er,
215 p.b1,
216 p.b2,
217 p.eps,
218 p.center_learning_rate,
219 p.decay_coef,
220 );
221 Pgpe {
222 dim,
223 popsize,
224 max_evaluations: if p.max_evaluations > 0 {
225 p.max_evaluations
226 } else {
227 50_000
228 },
229 stopfitness: p.stop_fitness,
230 lr_decay_steps: p.lr_decay_steps.max(1),
231 use_ranking: p.use_ranking,
232 stdev_learning_rate: p.stdev_learning_rate.abs(),
233 stdev_max_change: p.stdev_max_change.abs(),
234 adam,
235 center,
236 stdev,
237 scaled_noises: vec![],
238 pop_x: vec![DVector::zeros(dim); popsize],
239 best_x: DVector::zeros(dim),
240 best_y: f64::MAX,
241 iterations: 0,
242 stop: 0,
243 external_evaluations: 0,
244 rng: Rng::new(p.seed.wrapping_add(p.runid as u64)),
245 fitfun,
246 }
247 }
248
249 pub fn dim(&self) -> usize {
251 self.dim
252 }
253 pub fn popsize(&self) -> usize {
255 self.popsize
256 }
257 pub fn stop(&self) -> i32 {
259 self.stop
260 }
261
262 fn ask_encoded(&mut self) -> Vec<DVector<f64>> {
265 let n = self.popsize / 2;
266 self.scaled_noises = (0..n)
267 .map(|_| {
268 let noise =
269 DVector::from_iterator(self.dim, (0..self.dim).map(|_| self.rng.gaussian()));
270 noise.component_mul(&self.stdev)
271 })
272 .collect();
273 let mut xs = Vec::with_capacity(self.popsize);
274 for p in 0..n {
275 xs.push(&self.center + &self.scaled_noises[p]);
276 xs.push(&self.center - &self.scaled_noises[p]);
277 }
278 xs
279 }
280
281 fn ask_pop_internal(&mut self) -> Vec<Vec<f64>> {
283 let xs = self.ask_encoded();
284 self.pop_x = xs
285 .iter()
286 .map(|c| {
287 let feasible = self.fitfun.closest_feasible_normed(c.as_slice());
288 DVector::from_vec(self.fitfun.decode(&feasible))
289 })
290 .collect();
291 self.pop_x.iter().map(|c| c.as_slice().to_vec()).collect()
292 }
293
294 fn process_scores(&self, ys: &[f64]) -> DVector<f64> {
295 if self.use_ranking {
296 let n = ys.len();
297 let order = sort_index(ys);
298 let mut ranks = DVector::zeros(n);
299 for (i, &idx) in order.iter().enumerate() {
300 ranks[idx] = i as f64 / n as f64 - 0.5;
301 }
302 ranks
303 } else {
304 DVector::from_row_slice(ys)
305 }
306 }
307
308 fn reinforce(&self, pop_y: &DVector<f64>) -> (DVector<f64>, DVector<f64>) {
311 let n = self.popsize / 2;
312 let mean_all = pop_y.mean();
313 let mut grad_center = DVector::zeros(self.dim);
314 let mut grad_stdev = DVector::zeros(self.dim);
315 for i in 0..self.dim {
316 let mut gc = 0.0;
317 let mut gs = 0.0;
318 for p in 0..n {
319 let fit1 = pop_y[2 * p];
320 let fit2 = pop_y[2 * p + 1];
321 let score = fit1 - fit2;
322 let avg = 0.5 * (fit1 + fit2);
323 let sn = self.scaled_noises[p][i];
324 gc += sn * score * 0.5;
325 gs += (avg - mean_all) * (sn * sn - self.stdev[i] * self.stdev[i]) / self.stdev[i];
326 }
327 grad_center[i] = gc / n as f64;
328 grad_stdev[i] = gs / n as f64;
329 }
330 (grad_center, grad_stdev)
331 }
332
333 fn update_stdev(&self, grad: &DVector<f64>) -> DVector<f64> {
334 DVector::from_iterator(
335 self.dim,
336 (0..self.dim).map(|i| {
337 let allowed = self.stdev[i].abs() * self.stdev_max_change;
338 let lo = self.stdev[i] - allowed;
339 let hi = self.stdev[i] + allowed;
340 (self.stdev[i] + self.stdev_learning_rate * grad[i]).clamp(lo, hi)
341 }),
342 )
343 }
344
345 fn tell(&mut self, ys: &[f64]) -> i32 {
346 let neg: Vec<f64> = ys.iter().map(|y| -y).collect();
347 let pop_y = self.process_scores(&neg);
348 let mut best_p = 0;
353 for p in 1..self.popsize {
354 if ys[p] < ys[best_p] {
355 best_p = p;
356 }
357 }
358 if ys[best_p] < self.best_y {
359 self.best_y = ys[best_p];
360 self.best_x = self.pop_x[best_p].clone();
361 if self.best_y < self.stopfitness {
362 self.stop = 1;
363 }
364 }
365 let (grad_center, grad_stdev) = self.reinforce(&pop_y);
366 self.adam
367 .update(self.iterations / self.lr_decay_steps, &(-grad_center));
368 self.iterations += 1;
369 self.center = self.adam.x.clone();
370 self.stdev = self.update_stdev(&grad_stdev);
371 self.stop
372 }
373
374 fn make_result(&self, evaluations: u64) -> PgpeResult {
375 PgpeResult {
376 x: self.best_x.as_slice().to_vec(),
377 y: self.best_y,
378 evaluations,
379 iterations: self.iterations,
380 stop: self.stop,
381 }
382 }
383
384 pub fn optimize_batch<F>(&mut self, mut eval_batch: F) -> PgpeResult
386 where
387 F: FnMut(&[Vec<f64>]) -> Vec<f64>,
388 {
389 self.iterations = 0;
390 self.fitfun.reset_evaluations();
391 while self.fitfun.evaluations() < self.max_evaluations
392 && !self.fitfun.terminate()
393 && self.stop == 0
394 {
395 let rows = self.ask_pop_internal();
396 let mut ys = eval_batch(&rows);
397 for v in ys.iter_mut() {
398 if !v.is_finite() {
399 *v = crate::fitness::NAN_REPLACEMENT;
400 }
401 }
402 self.fitfun.incr_evaluations(self.popsize as u64);
403 self.tell(&ys);
404 }
405 self.make_result(self.fitfun.evaluations())
406 }
407
408 pub fn ask_pop(&mut self) -> Vec<Vec<f64>> {
412 self.ask_pop_internal()
413 }
414
415 pub fn tell_pop(&mut self, ys: &[f64]) -> i32 {
418 let stop = self.tell(ys);
419 self.external_evaluations += ys.len() as u64;
420 stop
421 }
422
423 pub fn population(&self) -> Vec<Vec<f64>> {
425 self.pop_x.iter().map(|c| c.as_slice().to_vec()).collect()
426 }
427
428 pub fn result(&self) -> PgpeResult {
430 self.make_result(self.external_evaluations)
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 fn sphere(x: &[f64]) -> f64 {
439 x.iter().map(|v| v * v).sum()
440 }
441
442 #[test]
443 fn converges_on_sphere() {
444 let mut fit = Fitness::bounded(5, 1, &[-5.0; 5], &[5.0; 5]);
446 fit.set_normalize(true);
447 let params = PgpeParams {
448 popsize: 40,
449 max_evaluations: 20000,
450 use_ranking: false,
451 seed: 1,
452 ..Default::default()
453 };
454 let mut opt = Pgpe::new(fit, &[3.0; 5], &[0.5; 5], ¶ms);
455 let r = opt.optimize_batch(|rows| rows.iter().map(|x| sphere(x)).collect());
456 assert!(r.y < 1e-2, "pgpe did not converge: {}", r.y);
457 }
458
459 #[test]
460 fn ask_tell_best_x_is_real_point() {
461 let mut fit = Fitness::bounded(4, 1, &[-5.0; 4], &[5.0; 4]);
462 fit.set_normalize(true);
463 let params = PgpeParams {
464 popsize: 32,
465 use_ranking: false,
466 seed: 2,
467 ..Default::default()
468 };
469 let mut opt = Pgpe::new(fit, &[2.0; 4], &[0.5; 4], ¶ms);
470 for _ in 0..400 {
471 let pop = opt.ask_pop();
472 let ys: Vec<f64> = pop.iter().map(|x| sphere(x)).collect();
473 opt.tell_pop(&ys);
474 }
475 let r = opt.result();
476 assert!((sphere(&r.x) - r.y).abs() < 1e-6 || r.y < 1e-2);
478 assert!(sphere(&r.x) < 1e-1, "best_x not good: {}", sphere(&r.x));
479 }
480
481 #[test]
482 fn ranking_defaults_odd_population_getters_and_nonfinite_scores() {
483 let mut fit = Fitness::bounded(2, 1, &[-1.0; 2], &[1.0; 2]);
484 fit.set_normalize(true);
485 let params = PgpeParams {
486 popsize: 3,
487 max_evaluations: 0,
488 stop_fitness: 1.0e100,
489 lr_decay_steps: 0,
490 use_ranking: true,
491 stdev_learning_rate: -0.1,
492 stdev_max_change: -0.2,
493 seed: 14,
494 ..Default::default()
495 };
496 let mut optimizer = Pgpe::new(fit, &[0.0; 2], &[0.25], ¶ms);
497 assert_eq!(optimizer.dim(), 2);
498 assert_eq!(optimizer.popsize(), 4);
499 assert_eq!(optimizer.stop(), 0);
500 let result = optimizer.optimize_batch(|rows| vec![f64::NAN; rows.len()]);
501 assert_eq!(result.evaluations, 4);
502 assert_eq!(result.stop, 1);
503 assert_eq!(optimizer.population().len(), 4);
504 assert_eq!(optimizer.stop(), 1);
505 }
506}