1use crate::fitness::Objective;
38use crate::rng::Rng;
39
40#[derive(Clone, Debug)]
42pub struct DaResult {
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 DaParams {
58 pub max_evaluations: u64,
60 pub use_local_search: bool,
62 pub seed: u64,
64 pub runid: i64,
66}
67
68impl Default for DaParams {
69 fn default() -> Self {
70 Self {
71 max_evaluations: 100_000,
72 use_local_search: true,
73 seed: 0,
74 runid: 0,
75 }
76 }
77}
78
79const BIG_VALUE: f64 = 1e16;
80const TAIL_LIMIT: f64 = 1e8;
81const MIN_VISIT_BOUND: f64 = 1e-10;
82const MAX_REINIT_COUNT: i32 = 1000;
83
84const TEMPERATURE_START: f64 = 5230.0;
85const QV: f64 = 2.62;
86const QA: f64 = -5.0;
87const MAXSTEPS: i32 = 1000;
88const TEMPERATURE_RESTART: f64 = 0.1;
89
90struct Da<'a, O: Objective> {
91 obj: &'a O,
92 dim: usize,
93 has_bounds: bool,
94 lower: Vec<f64>,
95 scale: Vec<f64>,
96 max_evals: u64,
97 eval_counter: u64,
98 use_local_search: bool,
99 rng: Rng,
100
101 fit_best_y: f64,
103 fit_best_x: Vec<f64>,
104
105 factor4_p: f64,
107 factor6: f64,
108
109 ebest: f64,
111 xbest: Vec<f64>,
112 current_energy: f64,
113 current_location: Vec<f64>,
114
115 emin: f64,
117 xmin: Vec<f64>,
118 not_improved_idx: i32,
119 not_improved_max_idx: i32,
120 temperature_step: f64,
121 k: f64,
122 state_improved: bool,
123
124 reinit_failed: bool,
125}
126
127impl<'a, O: Objective> Da<'a, O> {
128 fn new(
129 obj: &'a O,
130 dim: usize,
131 lower: Vec<f64>,
132 upper: Vec<f64>,
133 max_evals: u64,
134 use_local_search: bool,
135 seed: u64,
136 ) -> Self {
137 let has_bounds = !lower.is_empty();
138 let scale: Vec<f64> = if has_bounds {
139 upper.iter().zip(&lower).map(|(u, l)| u - l).collect()
140 } else {
141 vec![1.0; dim]
142 };
143 let factor2 = ((4.0 - QV) * (QV - 1.0).ln()).exp();
145 let factor3 = ((2.0 - QV) * 2.0_f64.ln() / (QV - 1.0)).exp();
146 let factor4_p = std::f64::consts::PI.sqrt() * factor2 / (factor3 * (3.0 - QV));
147 let factor5 = 1.0 / (QV - 1.0) - 0.5;
148 let d1 = 2.0 - factor5;
149 let factor6 = std::f64::consts::PI * (1.0 - factor5)
150 / (std::f64::consts::PI * (1.0 - factor5)).sin()
151 / libm::lgamma(d1).exp();
152 Da {
153 obj,
154 dim,
155 has_bounds,
156 lower,
157 scale,
158 max_evals,
159 eval_counter: 0,
160 use_local_search,
161 rng: Rng::new(seed),
162 fit_best_y: f64::MAX,
163 fit_best_x: vec![],
164 factor4_p,
165 factor6,
166 ebest: f64::MAX,
167 xbest: vec![],
168 current_energy: f64::MAX,
169 current_location: vec![],
170 emin: f64::MAX,
171 xmin: vec![],
172 not_improved_idx: 0,
173 not_improved_max_idx: 1000,
174 temperature_step: 0.0,
175 k: 100.0 * dim as f64,
176 state_improved: false,
177 reinit_failed: false,
178 }
179 }
180
181 fn closest_feasible(&self, x: &[f64]) -> Vec<f64> {
182 if self.has_bounds {
183 x.iter().map(|&v| v.clamp(-1.0, 1.0)).collect()
184 } else {
185 x.to_vec()
186 }
187 }
188
189 fn encode(&self, x: &[f64]) -> Vec<f64> {
190 if self.has_bounds {
191 (0..self.dim)
192 .map(|i| (x[i] - self.lower[i]) / self.scale[i])
193 .collect()
194 } else {
195 x.to_vec()
196 }
197 }
198
199 fn decode(&self, x: &[f64]) -> Vec<f64> {
200 if self.has_bounds {
201 (0..self.dim)
202 .map(|i| x[i] * self.scale[i] + self.lower[i])
203 .collect()
204 } else {
205 x.to_vec()
206 }
207 }
208
209 fn raw_eval(&mut self, x_decoded: &[f64]) -> f64 {
210 self.eval_counter += 1;
211 self.obj.eval_scalar(x_decoded)
212 }
213
214 fn value(&mut self, x: &[f64]) -> f64 {
216 let res = if self.has_bounds {
217 let feas = self.closest_feasible(x);
218 let dec = self.decode(&feas);
219 self.raw_eval(&dec)
220 } else {
221 self.raw_eval(x)
222 };
223 if res < self.fit_best_y {
224 self.fit_best_y = res;
225 self.fit_best_x = x.to_vec();
226 }
227 res
228 }
229
230 fn max_eval_reached(&self) -> bool {
231 self.eval_counter >= self.max_evals
232 }
233
234 fn normal_vec(&mut self) -> Vec<f64> {
235 (0..self.dim).map(|_| self.rng.gaussian()).collect()
236 }
237 fn uniform_vec(&mut self) -> Vec<f64> {
238 (0..self.dim).map(|_| self.rng.uniform01()).collect()
239 }
240
241 fn visit_fn(&mut self, temperature: f64, n: usize) -> Vec<f64> {
244 let x: Vec<f64> = (0..n).map(|_| self.rng.gaussian()).collect();
245 let y: Vec<f64> = (0..n).map(|_| self.rng.gaussian()).collect();
246 let factor1 = (temperature.ln() / (QV - 1.0)).exp();
247 let factor4 = self.factor4_p * factor1;
248 let sigmax = (-(QV - 1.0) * (self.factor6 / factor4).ln() / (3.0 - QV)).exp();
249 (0..n)
250 .map(|i| {
251 let xi = x[i] * sigmax;
252 let den = ((y[i].abs() * (QV - 1.0)).ln() / (3.0 - QV)).exp();
253 xi / den
254 })
255 .collect()
256 }
257
258 fn visiting(&mut self, x: &[f64], step: usize, temperature: f64) -> Vec<f64> {
259 if step < self.dim {
260 let upper_sample = self.rng.uniform01();
261 let lower_sample = self.rng.uniform01();
262 let mut visits = self.visit_fn(temperature, self.dim);
263 for v in visits.iter_mut() {
264 if *v > TAIL_LIMIT {
265 *v = TAIL_LIMIT * upper_sample;
266 } else if *v < -TAIL_LIMIT {
267 *v = -TAIL_LIMIT * lower_sample;
268 }
269 }
270 let mut x_visit: Vec<f64> = (0..self.dim).map(|i| visits[i] + x[i]).collect();
271 for xv in x_visit.iter_mut() {
272 let b = (*xv % 1.0) + 1.0;
273 *xv = b % 1.0;
274 if xv.abs() < MIN_VISIT_BOUND {
275 *xv += 1e-10;
276 }
277 }
278 x_visit
279 } else {
280 let mut x_visit = x.to_vec();
281 let mut visit = self.visit_fn(temperature, 1)[0];
282 if visit > TAIL_LIMIT {
283 visit = TAIL_LIMIT * self.rng.uniform01();
284 } else if visit < -TAIL_LIMIT {
285 visit = -TAIL_LIMIT * self.rng.uniform01();
286 }
287 let index = step - self.dim;
288 x_visit[index] = visit + x[index];
289 let b = (x_visit[index] % 1.0) + 1.0;
290 x_visit[index] = b % 1.0;
291 if x_visit[index].abs() < MIN_VISIT_BOUND {
292 x_visit[index] += MIN_VISIT_BOUND;
293 }
294 x_visit
295 }
296 }
297
298 fn reset_energy(&mut self, x0: &[f64]) {
301 self.current_location = if x0.is_empty() {
302 self.normal_vec()
303 } else {
304 x0.to_vec()
305 };
306 let mut reinit_counter = 0;
307 loop {
308 self.current_energy = self.value(&self.current_location.clone());
309 if self.current_energy >= BIG_VALUE || self.current_energy.is_nan() {
310 if reinit_counter >= MAX_REINIT_COUNT {
311 self.reinit_failed = true;
312 return;
313 }
314 self.current_location = self.uniform_vec();
315 reinit_counter += 1;
316 } else {
317 if self.ebest == f64::MAX && self.xbest.is_empty() {
318 self.ebest = self.current_energy;
319 self.xbest = self.current_location.clone();
320 }
321 return;
322 }
323 }
324 }
325
326 fn accept_reject(&mut self, j: usize, e: f64, x_visit: &[f64]) {
329 let r = self.rng.uniform01();
330 let pqv_temp = (QA - 1.0) * (e - self.current_energy) / (self.temperature_step + 1.0);
331 let pqv = if pqv_temp < 0.0 {
332 0.0
333 } else {
334 (pqv_temp.ln() / (1.0 - QA)).exp()
335 };
336 if r <= pqv {
337 self.current_energy = e;
338 self.current_location = x_visit.to_vec();
339 self.xmin = self.current_location.clone();
340 }
341 if self.not_improved_idx >= self.not_improved_max_idx
342 && (j == 0 || self.current_energy < self.emin)
343 {
344 self.emin = self.current_energy;
345 self.xmin = self.current_location.clone();
346 }
347 }
348
349 fn run_chain(&mut self, step: usize, temperature: f64) {
350 self.temperature_step = temperature / (step as f64 + 1.0);
351 self.not_improved_idx += 1;
352 let iters = self.current_location.len() * 2;
353 for j in 0..iters {
354 if j == 0 {
355 self.state_improved = false;
356 }
357 if step == 0 && j == 0 {
358 self.state_improved = true;
359 }
360 let x_visit = self.visiting(&self.current_location.clone(), j, temperature);
361 let e = self.value(&x_visit);
362 if e < self.current_energy {
363 self.current_energy = e;
364 self.current_location = x_visit.clone();
365 if e < self.ebest {
366 self.ebest = e;
367 self.xbest = x_visit.clone();
368 self.state_improved = true;
369 self.not_improved_idx = 0;
370 }
371 } else {
372 self.accept_reject(j, e, &x_visit);
373 }
374 if self.max_eval_reached() {
375 return;
376 }
377 }
378 }
379
380 fn chain_local_search(&mut self) {
381 if self.state_improved {
382 let (e, x) = self.local_search(&self.xbest.clone());
383 if e < self.ebest {
384 self.not_improved_idx = 0;
385 self.ebest = e;
386 self.xbest = x.clone();
387 self.current_energy = e;
388 self.current_location = x;
389 if self.max_eval_reached() {
390 return;
391 }
392 }
393 }
394 let mut do_ls = false;
395 if self.k < 90.0 * self.dim as f64 {
396 let pls = (self.k * (self.ebest - self.current_energy) / self.temperature_step).exp();
397 if pls >= self.rng.uniform01() {
398 do_ls = true;
399 }
400 }
401 if self.not_improved_idx >= self.not_improved_max_idx {
402 do_ls = true;
403 }
404 if do_ls {
405 let (e, x) = self.local_search(&self.xmin.clone());
406 self.xmin = x.clone();
407 self.emin = e;
408 self.not_improved_idx = 0;
409 self.not_improved_max_idx = self.current_location.len() as i32;
410 if e < self.ebest {
411 self.ebest = e;
412 self.xbest = x.clone();
413 self.current_energy = e;
414 self.current_location = x;
415 }
416 }
417 }
418
419 fn fd_grad(&mut self, arg: &[f64]) -> (Vec<f64>, f64) {
424 let eps = 1e-6;
425 let mut grad = vec![0.0; self.dim];
426 for i in 0..self.dim {
427 let mut x1 = arg.to_vec();
428 let mut x2 = arg.to_vec();
429 let mut e1 = eps;
430 let mut e2 = eps;
431 x1[i] += eps;
432 if x1[i] > 1.0 {
433 x1[i] = 1.0;
434 e1 = 1.0 - arg[i];
435 }
436 x2[i] -= eps;
437 if x2[i] < 0.0 {
438 x2[i] = 0.0;
439 e2 = arg[i];
440 }
441 let f1 = self.value(&x1);
442 let f2 = self.value(&x2);
443 grad[i] = (f1 - f2) / (e1 + e2);
444 }
445 let f = self.value(arg);
446 (grad, f)
447 }
448
449 fn local_search(&mut self, x0: &[f64]) -> (f64, Vec<f64>) {
450 self.fit_best_y = f64::MAX;
453 let mut max_iter = (6 * self.dim) as i32;
454 max_iter = max_iter.clamp(100, 1000);
455
456 let clamp01 = |v: &[f64]| -> Vec<f64> { v.iter().map(|x| x.clamp(0.0, 1.0)).collect() };
457 let mut x = clamp01(&self.closest_feasible(x0));
458 let m = 6usize;
459 let mut s_hist: Vec<Vec<f64>> = Vec::new();
460 let mut y_hist: Vec<Vec<f64>> = Vec::new();
461 let mut rho: Vec<f64> = Vec::new();
462
463 let (mut g, mut f) = self.fd_grad(&x);
464 for _ in 0..max_iter {
465 let pg_norm: f64 = (0..self.dim)
467 .map(|i| {
468 let step = (x[i] - g[i]).clamp(0.0, 1.0) - x[i];
469 step * step
470 })
471 .sum::<f64>()
472 .sqrt();
473 if pg_norm < 1e-10 {
474 break;
475 }
476 let mut q = g.clone();
478 let kh = s_hist.len();
479 let mut alpha = vec![0.0; kh];
480 for i in (0..kh).rev() {
481 let a = rho[i] * dot(&s_hist[i], &q);
482 alpha[i] = a;
483 for j in 0..self.dim {
484 q[j] -= a * y_hist[i][j];
485 }
486 }
487 let gamma = if kh > 0 {
488 let last = kh - 1;
489 dot(&s_hist[last], &y_hist[last]) / dot(&y_hist[last], &y_hist[last])
490 } else {
491 1.0
492 };
493 for qi in q.iter_mut() {
494 *qi *= gamma;
495 }
496 for i in 0..kh {
497 let beta = rho[i] * dot(&y_hist[i], &q);
498 for j in 0..self.dim {
499 q[j] += (alpha[i] - beta) * s_hist[i][j];
500 }
501 }
502 let d: Vec<f64> = q.iter().map(|v| -v).collect();
503
504 let gd = dot(&g, &d);
506 let mut step = 1.0;
507 let mut x_new = x.clone();
508 let mut f_new = f;
509 let mut ok = false;
510 for _ in 0..20 {
511 let cand: Vec<f64> = (0..self.dim)
512 .map(|i| (x[i] + step * d[i]).clamp(0.0, 1.0))
513 .collect();
514 let fc = self.value(&cand);
515 if fc.is_finite() && fc <= f + 1e-4 * step * gd {
516 x_new = cand;
517 f_new = fc;
518 ok = true;
519 break;
520 }
521 step *= 0.5;
522 if self.max_eval_reached() {
523 break;
524 }
525 }
526 if !ok || self.max_eval_reached() {
527 break;
528 }
529
530 let (g_new, _) = self.fd_grad(&x_new);
531 let s: Vec<f64> = (0..self.dim).map(|i| x_new[i] - x[i]).collect();
532 let yv: Vec<f64> = (0..self.dim).map(|i| g_new[i] - g[i]).collect();
533 let sy = dot(&s, &yv);
534 if sy > 1e-12 {
535 if s_hist.len() == m {
536 s_hist.remove(0);
537 y_hist.remove(0);
538 rho.remove(0);
539 }
540 s_hist.push(s);
541 y_hist.push(yv);
542 rho.push(1.0 / sy);
543 }
544 x = x_new;
545 g = g_new;
546 f = f_new;
547 if self.max_eval_reached() {
548 break;
549 }
550 }
551 (self.fit_best_y, self.fit_best_x.clone())
552 }
553
554 fn search(&mut self) {
557 let mut iter = 0i32;
558 let t1 = ((QV - 1.0) * 2.0_f64.ln()).exp() - 1.0;
559 loop {
560 for i in 0..MAXSTEPS {
561 let s = i as f64 + 2.0;
562 let t2 = ((QV - 1.0) * s.ln()).exp() - 1.0;
563 let temperature = TEMPERATURE_START * t1 / t2;
564 iter += 1;
565 if iter >= MAXSTEPS {
566 return;
567 }
568 if temperature < TEMPERATURE_RESTART {
569 self.reset_energy(&[]);
570 if self.reinit_failed {
571 return;
572 }
573 break;
574 }
575 self.run_chain(i as usize, temperature);
576 if self.max_eval_reached() {
577 return;
578 }
579 if self.use_local_search {
580 self.chain_local_search();
581 if self.max_eval_reached() {
582 return;
583 }
584 }
585 }
586 }
587 }
588
589 fn optimize(&mut self, guess: &[f64]) -> DaResult {
590 let enc = self.encode(guess);
591 self.reset_energy(&enc);
592 self.emin = self.current_energy;
593 self.xmin = self.current_location.clone();
594 self.not_improved_max_idx = 1000;
595 if !self.reinit_failed {
596 self.search();
597 }
598 DaResult {
599 x: self.decode(&self.xbest),
600 y: self.ebest,
601 evaluations: self.eval_counter,
602 iterations: 0,
603 stop: if self.reinit_failed { -1 } else { 0 },
604 }
605 }
606}
607
608fn dot(a: &[f64], b: &[f64]) -> f64 {
609 a.iter().zip(b).map(|(x, y)| x * y).sum()
610}
611
612pub fn optimize_da(
614 obj: &impl Objective,
615 guess: &[f64],
616 lower: Vec<f64>,
617 upper: Vec<f64>,
618 p: &DaParams,
619) -> DaResult {
620 let dim = guess.len();
621 let max_evals = if p.max_evaluations == 0 {
622 10_000_000
623 } else {
624 p.max_evaluations
625 };
626 let mut da = Da::new(
627 obj,
628 dim,
629 lower,
630 upper,
631 max_evals,
632 p.use_local_search,
633 p.seed.wrapping_add(p.runid as u64),
634 );
635 da.optimize(guess)
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn sphere(x: &[f64]) -> f64 {
643 x.iter().map(|v| v * v).sum()
644 }
645 fn rosen(x: &[f64]) -> f64 {
646 (0..x.len() - 1)
647 .map(|i| 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2))
648 .sum()
649 }
650
651 fn run(obj: impl Objective, dim: usize, seed: u64, ls: bool) -> DaResult {
652 let params = DaParams {
653 max_evaluations: 40_000,
654 use_local_search: ls,
655 seed,
656 ..Default::default()
657 };
658 optimize_da(
659 &obj,
660 &vec![0.0; dim],
661 vec![-5.0; dim],
662 vec![5.0; dim],
663 ¶ms,
664 )
665 }
666
667 #[test]
668 fn minimizes_sphere_with_local_search() {
669 let r = run(sphere as fn(&[f64]) -> f64, 4, 1, true);
670 assert!(r.y < 1e-6, "sphere not solved: {}", r.y);
671 assert!((sphere(&r.x) - r.y).abs() < 1e-6);
672 }
673
674 #[test]
675 fn minimizes_sphere_without_local_search() {
676 let r = run(sphere as fn(&[f64]) -> f64, 4, 2, false);
677 assert!(r.y < 1e-2, "sphere (no ls) too large: {}", r.y);
678 }
679
680 #[test]
681 fn minimizes_rosenbrock() {
682 let r = run(rosen as fn(&[f64]) -> f64, 3, 3, true);
683 assert!(r.y < 1e-2, "rosenbrock not solved: {}", r.y);
684 }
685
686 #[test]
687 fn unbounded_nonfinite_objective_reports_reinitialization_failure() {
688 let result = optimize_da(
689 &(|_: &[f64]| f64::NAN),
690 &[0.0],
691 Vec::new(),
692 Vec::new(),
693 &DaParams {
694 max_evaluations: 0,
695 use_local_search: false,
696 seed: 4,
697 runid: -1,
698 },
699 );
700 assert_eq!(result.stop, -1);
701 assert!(result.evaluations > 1_000);
702 }
703
704 #[test]
705 fn finite_difference_handles_both_box_boundaries() {
706 let objective = sphere as fn(&[f64]) -> f64;
707 let mut optimizer = Da::new(&objective, 2, vec![0.0; 2], vec![1.0; 2], 100, false, 5);
708 let (gradient, value) = optimizer.fd_grad(&[0.0, 1.0]);
709 assert!(gradient.iter().all(|component| component.is_finite()));
710 assert_eq!(value, 1.0);
711 }
712}