1use ndarray::Array1;
37use rand::rngs::StdRng;
38use rand::{Rng, SeedableRng};
39use std::sync::Arc;
40
41use crate::error::{DEError, Result};
42
43pub type IsresConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
45
46#[derive(Clone)]
48pub struct IsresConstraint {
49 pub fun: IsresConstraintFn,
52}
53
54#[derive(Clone)]
56pub struct IsresConfig {
57 pub bounds: Vec<(f64, f64)>,
59 pub x0: Option<Array1<f64>>,
61 pub mu: usize,
63 pub lambda: usize,
65 pub maxeval: usize,
68 pub pf: f64,
72 pub gamma: f64,
74 pub seed: Option<u64>,
76 pub f_tol: f64,
80 pub stagnation_window: usize,
83}
84
85impl Default for IsresConfig {
86 fn default() -> Self {
87 Self {
88 bounds: Vec::new(),
89 x0: None,
90 mu: 30,
91 lambda: 0, maxeval: 10_000,
93 pf: 0.45,
94 gamma: 0.85,
95 seed: None,
96 f_tol: 1e-8,
97 stagnation_window: 50,
98 }
99 }
100}
101
102#[derive(Clone)]
104pub struct IsresReport {
105 pub x: Array1<f64>,
107 pub fun: f64,
109 pub max_violation: f64,
111 pub feasible: bool,
113 pub success: bool,
116 pub message: String,
118 pub nfev: usize,
120 pub nit: usize,
122}
123
124impl std::fmt::Debug for IsresReport {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.debug_struct("IsresReport")
127 .field("x_len", &self.x.len())
128 .field("fun", &self.fun)
129 .field("max_violation", &self.max_violation)
130 .field("feasible", &self.feasible)
131 .field("success", &self.success)
132 .field("message", &self.message)
133 .field("nfev", &self.nfev)
134 .field("nit", &self.nit)
135 .finish()
136 }
137}
138
139#[derive(Clone)]
140struct Individual {
141 x: Array1<f64>,
142 sigma: Array1<f64>,
143 fun: f64,
144 max_violation: f64,
145}
146
147pub fn isres<F>(
149 f: &F,
150 constraints: &[IsresConstraint],
151 config: IsresConfig,
152) -> Result<IsresReport>
153where
154 F: Fn(&Array1<f64>) -> f64 + Sync,
155{
156 let n = config.bounds.len();
157 if n == 0 {
158 return Err(DEError::BoundsMismatch {
159 lower_len: 0,
160 upper_len: 0,
161 });
162 }
163 for (i, (lo, hi)) in config.bounds.iter().enumerate() {
164 if lo > hi {
165 return Err(DEError::InvalidBounds {
166 index: i,
167 lower: *lo,
168 upper: *hi,
169 });
170 }
171 }
172 if config.mu < 2 {
173 return Err(DEError::PopulationTooSmall { pop_size: config.mu });
174 }
175
176 let mu = config.mu;
177 let lambda = if config.lambda == 0 { 7 * mu } else { config.lambda };
178 if lambda < mu {
179 return Err(DEError::PopulationTooSmall { pop_size: lambda });
180 }
181
182 let n_f = n as f64;
184 let tau = 1.0 / (2.0 * n_f.sqrt()).sqrt();
185 let tau_prime = 1.0 / (2.0 * n_f).sqrt();
186 let sigma_max: Vec<f64> = config
190 .bounds
191 .iter()
192 .map(|(lo, hi)| ((hi - lo) * 0.2).max(1e-12))
193 .collect();
194
195 let mut rng: StdRng = match config.seed {
196 Some(s) => StdRng::seed_from_u64(s),
197 None => {
198 let mut thread_rng = rand::rng();
199 StdRng::from_rng(&mut thread_rng)
200 }
201 };
202
203 let mut nfev = 0usize;
204 let mut population: Vec<Individual> = Vec::with_capacity(mu);
205
206 let initial_sigma: Vec<f64> = sigma_max
210 .iter()
211 .map(|&sm| (sm / n_f.sqrt()).max(1e-12))
212 .collect();
213 for i in 0..mu {
214 let mut x = Array1::<f64>::zeros(n);
215 if i == 0
217 && let Some(ref seed) = config.x0
218 && seed.len() == n
219 {
220 for j in 0..n {
221 let (lo, hi) = config.bounds[j];
222 x[j] = seed[j].clamp(lo, hi);
223 }
224 } else {
225 for j in 0..n {
226 let (lo, hi) = config.bounds[j];
227 x[j] = lo + rng.random::<f64>() * (hi - lo);
228 }
229 }
230 let sigma = Array1::from(initial_sigma.clone());
231 let fun = f(&x);
232 let mv = max_violation_of(&x, constraints);
233 nfev += 1;
234 population.push(Individual {
235 x,
236 sigma,
237 fun,
238 max_violation: mv,
239 });
240 if nfev >= config.maxeval {
241 break;
242 }
243 }
244 while population.len() < mu {
246 let last = population.last().cloned().unwrap();
247 population.push(last);
248 }
249
250 let mut best = best_individual(&population).clone();
253 let mut nit = 0usize;
254 let mut stagnation_counter = 0usize;
255 let mut last_best_fun = best.fun;
256 let mut terminated_by_stagnation = false;
257
258 'outer: while nfev < config.maxeval {
259 nit += 1;
260
261 let mut offspring: Vec<Individual> = Vec::with_capacity(lambda);
265 for k in 0..lambda {
266 let parent_idx = k % mu;
272 let parent = &population[parent_idx];
273
274 let n_global: f64 = standard_normal(&mut rng);
276 let mut sigma_new = parent.sigma.clone();
277 for j in 0..n {
278 let n_local: f64 = standard_normal(&mut rng);
279 let mult = (tau_prime * n_global + tau * n_local).exp();
280 sigma_new[j] = (parent.sigma[j] * mult).min(sigma_max[j]).max(1e-30);
281 }
282
283 let mut x_new = parent.x.clone();
287 if rng.random::<f64>() < config.gamma && mu >= 3 {
288 let p1 = rng.random_range(0..mu.min(mu / 2 + 1).max(2)); let p2 = rng.random_range(0..mu);
290 if p1 != p2 {
291 let blend: f64 = rng.random::<f64>();
292 for j in 0..n {
293 x_new[j] += blend * (population[p1].x[j] - population[p2].x[j]);
294 }
295 }
296 }
297
298 for j in 0..n {
300 let n_j: f64 = standard_normal(&mut rng);
301 x_new[j] += sigma_new[j] * n_j;
302 }
303
304 for j in 0..n {
306 let (lo, hi) = config.bounds[j];
307 x_new[j] = reflect_into_bounds(x_new[j], lo, hi);
308 }
309
310 let fun = f(&x_new);
311 let mv = max_violation_of(&x_new, constraints);
312 nfev += 1;
313 offspring.push(Individual {
314 x: x_new,
315 sigma: sigma_new,
316 fun,
317 max_violation: mv,
318 });
319 if nfev >= config.maxeval {
320 break;
321 }
322 }
323
324 stochastic_rank(&mut offspring, config.pf, &mut rng);
326
327 offspring.truncate(mu);
329 population = offspring;
330
331 let gen_best = best_individual(&population);
333 if individual_better(gen_best, &best) {
334 best = gen_best.clone();
335 }
336
337 if config.f_tol > 0.0 {
339 let delta = (last_best_fun - best.fun).abs();
340 if delta < config.f_tol {
341 stagnation_counter += 1;
342 if stagnation_counter >= config.stagnation_window {
343 terminated_by_stagnation = true;
344 break 'outer;
345 }
346 } else {
347 stagnation_counter = 0;
348 last_best_fun = best.fun;
349 }
350 }
351 }
352
353 let feasible = best.max_violation <= 0.0;
354 let message = if terminated_by_stagnation {
355 format!(
356 "Converged: best fitness stagnated within tol={:.1e} for {} generations",
357 config.f_tol, config.stagnation_window
358 )
359 } else if nfev >= config.maxeval {
360 format!("Maximum evaluations reached: {}", config.maxeval)
361 } else {
362 "Iteration limit reached".to_string()
363 };
364
365 Ok(IsresReport {
366 x: best.x,
367 fun: best.fun,
368 max_violation: best.max_violation.max(0.0),
369 feasible,
370 success: terminated_by_stagnation,
371 message,
372 nfev,
373 nit,
374 })
375}
376
377fn max_violation_of(x: &Array1<f64>, constraints: &[IsresConstraint]) -> f64 {
379 let mut worst = 0.0_f64;
380 for c in constraints {
381 let v = (c.fun)(x);
382 if v > worst {
383 worst = v;
384 }
385 }
386 worst
387}
388
389fn standard_normal<R: Rng + ?Sized>(rng: &mut R) -> f64 {
393 let u1: f64 = rng.random::<f64>().max(1e-300);
394 let u2: f64 = rng.random::<f64>();
395 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
396}
397
398fn reflect_into_bounds(v: f64, lo: f64, hi: f64) -> f64 {
401 if !v.is_finite() {
402 return (lo + hi) * 0.5;
403 }
404 if v < lo {
405 let r = lo + (lo - v);
406 if r > hi { (lo + hi) * 0.5 } else { r }
407 } else if v > hi {
408 let r = hi - (v - hi);
409 if r < lo { (lo + hi) * 0.5 } else { r }
410 } else {
411 v
412 }
413}
414
415fn stochastic_rank<R: Rng + ?Sized>(pop: &mut [Individual], pf: f64, rng: &mut R) {
420 let n = pop.len();
421 if n < 2 {
422 return;
423 }
424 for _ in 0..n.saturating_sub(1) {
426 let mut swapped = false;
427 for i in 0..(n - 1) {
428 let a = &pop[i];
432 let b = &pop[i + 1];
433 let both_feasible = a.max_violation <= 0.0 && b.max_violation <= 0.0;
434 let both_infeasible = a.max_violation > 0.0 && b.max_violation > 0.0;
435 let compare_by_fun = if both_feasible {
436 true
437 } else if both_infeasible {
438 rng.random::<f64>() < pf
439 } else {
440 false
443 };
444
445 let need_swap = if compare_by_fun {
446 a.fun > b.fun
447 } else {
448 a.max_violation > b.max_violation
449 };
450
451 if need_swap {
452 pop.swap(i, i + 1);
453 swapped = true;
454 }
455 }
456 if !swapped {
457 break;
458 }
459 }
460}
461
462fn best_individual(pop: &[Individual]) -> &Individual {
465 pop.iter()
466 .min_by(|a, b| {
467 if individual_better(a, b) {
468 std::cmp::Ordering::Less
469 } else if individual_better(b, a) {
470 std::cmp::Ordering::Greater
471 } else {
472 std::cmp::Ordering::Equal
473 }
474 })
475 .expect("non-empty population")
476}
477
478fn individual_better(a: &Individual, b: &Individual) -> bool {
481 let a_feas = a.max_violation <= 0.0;
482 let b_feas = b.max_violation <= 0.0;
483 match (a_feas, b_feas) {
484 (true, false) => true,
485 (false, true) => false,
486 (true, true) => a.fun < b.fun,
487 (false, false) => a.max_violation < b.max_violation,
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn sphere_unconstrained() {
497 let f = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
499 let cfg = IsresConfig {
500 bounds: vec![(-5.0, 5.0); 4],
501 mu: 20,
502 lambda: 100,
503 maxeval: 8_000,
504 seed: Some(42),
505 ..Default::default()
506 };
507 let report = isres(&f, &[], cfg).expect("isres failed");
508 assert!(
509 report.fun < 1e-3,
510 "fun = {} should converge near 0",
511 report.fun
512 );
513 for &xi in report.x.iter() {
514 assert!(xi.abs() < 0.05, "xi = {} should be near 0", xi);
515 }
516 }
517
518 #[test]
519 fn sphere_with_inequality() {
520 let f = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
524 let constraints = vec![IsresConstraint {
525 fun: Arc::new(|x: &Array1<f64>| 1.0 - x[0] - x[1]),
526 }];
527 let cfg = IsresConfig {
528 bounds: vec![(-5.0, 5.0); 4],
529 mu: 30,
530 lambda: 200,
531 maxeval: 30_000,
532 seed: Some(7),
533 ..Default::default()
534 };
535 let report = isres(&f, &constraints, cfg).expect("isres failed");
536 assert!(report.feasible, "best should be feasible");
537 assert!(
540 report.fun < 0.7,
541 "fun = {} should converge near 0.5",
542 report.fun
543 );
544 }
545
546 #[test]
547 fn rejects_inverted_bounds() {
548 let f = |x: &Array1<f64>| x[0] * x[0];
549 let cfg = IsresConfig {
550 bounds: vec![(1.0, -1.0)],
551 mu: 10,
552 lambda: 30,
553 maxeval: 100,
554 ..Default::default()
555 };
556 let err = isres(&f, &[], cfg).unwrap_err();
557 matches!(err, DEError::InvalidBounds { .. });
558 }
559
560 #[test]
561 fn rejects_tiny_population() {
562 let f = |x: &Array1<f64>| x[0] * x[0];
563 let cfg = IsresConfig {
564 bounds: vec![(-1.0, 1.0)],
565 mu: 1,
566 lambda: 10,
567 maxeval: 100,
568 ..Default::default()
569 };
570 let err = isres(&f, &[], cfg).unwrap_err();
571 matches!(err, DEError::PopulationTooSmall { .. });
572 }
573
574 #[test]
575 fn deterministic_with_seed() {
576 let f = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
577 let cfg1 = IsresConfig {
578 bounds: vec![(-5.0, 5.0); 3],
579 mu: 15,
580 lambda: 80,
581 maxeval: 2_000,
582 seed: Some(123),
583 ..Default::default()
584 };
585 let cfg2 = cfg1.clone();
586 let r1 = isres(&f, &[], cfg1).unwrap();
587 let r2 = isres(&f, &[], cfg2).unwrap();
588 assert!((r1.fun - r2.fun).abs() < 1e-12, "seeded runs must match");
589 }
590}