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>(f: &F, constraints: &[IsresConstraint], config: IsresConfig) -> Result<IsresReport>
149where
150 F: Fn(&Array1<f64>) -> f64 + Sync,
151{
152 let n = config.bounds.len();
153 if n == 0 {
154 return Err(DEError::BoundsMismatch {
155 lower_len: 0,
156 upper_len: 0,
157 });
158 }
159 for (i, (lo, hi)) in config.bounds.iter().enumerate() {
160 if lo > hi {
161 return Err(DEError::InvalidBounds {
162 index: i,
163 lower: *lo,
164 upper: *hi,
165 });
166 }
167 }
168 if config.mu < 2 {
169 return Err(DEError::PopulationTooSmall {
170 pop_size: config.mu,
171 });
172 }
173
174 let mu = config.mu;
175 let lambda = if config.lambda == 0 {
176 7 * mu
177 } else {
178 config.lambda
179 };
180 if lambda < mu {
181 return Err(DEError::PopulationTooSmall { pop_size: lambda });
182 }
183
184 let n_f = n as f64;
186 let tau = 1.0 / (2.0 * n_f.sqrt()).sqrt();
187 let tau_prime = 1.0 / (2.0 * n_f).sqrt();
188 let sigma_max: Vec<f64> = config
192 .bounds
193 .iter()
194 .map(|(lo, hi)| ((hi - lo) * 0.2).max(1e-12))
195 .collect();
196
197 let mut rng: StdRng = match config.seed {
198 Some(s) => StdRng::seed_from_u64(s),
199 None => {
200 let mut thread_rng = rand::rng();
201 StdRng::from_rng(&mut thread_rng)
202 }
203 };
204
205 let mut nfev = 0usize;
206 let mut population: Vec<Individual> = Vec::with_capacity(mu);
207
208 let initial_sigma: Vec<f64> = sigma_max
212 .iter()
213 .map(|&sm| (sm / n_f.sqrt()).max(1e-12))
214 .collect();
215 for i in 0..mu {
216 let mut x = Array1::<f64>::zeros(n);
217 if i == 0
219 && let Some(ref seed) = config.x0
220 && seed.len() == n
221 {
222 for j in 0..n {
223 let (lo, hi) = config.bounds[j];
224 x[j] = seed[j].clamp(lo, hi);
225 }
226 } else {
227 for j in 0..n {
228 let (lo, hi) = config.bounds[j];
229 x[j] = lo + rng.random::<f64>() * (hi - lo);
230 }
231 }
232 let sigma = Array1::from(initial_sigma.clone());
233 let fun = f(&x);
234 let mv = max_violation_of(&x, constraints);
235 nfev += 1;
236 population.push(Individual {
237 x,
238 sigma,
239 fun,
240 max_violation: mv,
241 });
242 if nfev >= config.maxeval {
243 break;
244 }
245 }
246 while population.len() < mu {
248 let last = population.last().cloned().unwrap();
249 population.push(last);
250 }
251
252 let mut best = best_individual(&population).clone();
255 let mut nit = 0usize;
256 let mut stagnation_counter = 0usize;
257 let mut last_best_fun = best.fun;
258 let mut terminated_by_stagnation = false;
259
260 'outer: while nfev < config.maxeval {
261 nit += 1;
262
263 let mut offspring: Vec<Individual> = Vec::with_capacity(lambda);
267 for k in 0..lambda {
268 let parent_idx = k % mu;
274 let parent = &population[parent_idx];
275
276 let n_global: f64 = standard_normal(&mut rng);
278 let mut sigma_new = parent.sigma.clone();
279 for j in 0..n {
280 let n_local: f64 = standard_normal(&mut rng);
281 let mult = (tau_prime * n_global + tau * n_local).exp();
282 sigma_new[j] = (parent.sigma[j] * mult).min(sigma_max[j]).max(1e-30);
283 }
284
285 let mut x_new = parent.x.clone();
289 if rng.random::<f64>() < config.gamma && mu >= 3 {
290 let p1 = rng.random_range(0..mu.min(mu / 2 + 1).max(2)); let p2 = rng.random_range(0..mu);
292 if p1 != p2 {
293 let blend: f64 = rng.random::<f64>();
294 for j in 0..n {
295 x_new[j] += blend * (population[p1].x[j] - population[p2].x[j]);
296 }
297 }
298 }
299
300 for j in 0..n {
302 let n_j: f64 = standard_normal(&mut rng);
303 x_new[j] += sigma_new[j] * n_j;
304 }
305
306 for j in 0..n {
308 let (lo, hi) = config.bounds[j];
309 x_new[j] = reflect_into_bounds(x_new[j], lo, hi);
310 }
311
312 let fun = f(&x_new);
313 let mv = max_violation_of(&x_new, constraints);
314 nfev += 1;
315 offspring.push(Individual {
316 x: x_new,
317 sigma: sigma_new,
318 fun,
319 max_violation: mv,
320 });
321 if nfev >= config.maxeval {
322 break;
323 }
324 }
325
326 stochastic_rank(&mut offspring, config.pf, &mut rng);
328
329 offspring.truncate(mu);
331 population = offspring;
332
333 let gen_best = best_individual(&population);
335 if individual_better(gen_best, &best) {
336 best = gen_best.clone();
337 }
338
339 if config.f_tol > 0.0 {
341 let delta = (last_best_fun - best.fun).abs();
342 if delta < config.f_tol {
343 stagnation_counter += 1;
344 if stagnation_counter >= config.stagnation_window {
345 terminated_by_stagnation = true;
346 break 'outer;
347 }
348 } else {
349 stagnation_counter = 0;
350 last_best_fun = best.fun;
351 }
352 }
353 }
354
355 let feasible = best.max_violation <= 0.0;
356 let message = if terminated_by_stagnation {
357 format!(
358 "Converged: best fitness stagnated within tol={:.1e} for {} generations",
359 config.f_tol, config.stagnation_window
360 )
361 } else if nfev >= config.maxeval {
362 format!("Maximum evaluations reached: {}", config.maxeval)
363 } else {
364 "Iteration limit reached".to_string()
365 };
366
367 Ok(IsresReport {
368 x: best.x,
369 fun: best.fun,
370 max_violation: best.max_violation.max(0.0),
371 feasible,
372 success: terminated_by_stagnation,
373 message,
374 nfev,
375 nit,
376 })
377}
378
379fn max_violation_of(x: &Array1<f64>, constraints: &[IsresConstraint]) -> f64 {
381 let mut worst = 0.0_f64;
382 for c in constraints {
383 let v = (c.fun)(x);
384 if v > worst {
385 worst = v;
386 }
387 }
388 worst
389}
390
391fn standard_normal<R: Rng + ?Sized>(rng: &mut R) -> f64 {
395 let u1: f64 = rng.random::<f64>().max(1e-300);
396 let u2: f64 = rng.random::<f64>();
397 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
398}
399
400fn reflect_into_bounds(v: f64, lo: f64, hi: f64) -> f64 {
403 if !v.is_finite() {
404 return (lo + hi) * 0.5;
405 }
406 if v < lo {
407 let r = lo + (lo - v);
408 if r > hi { (lo + hi) * 0.5 } else { r }
409 } else if v > hi {
410 let r = hi - (v - hi);
411 if r < lo { (lo + hi) * 0.5 } else { r }
412 } else {
413 v
414 }
415}
416
417fn stochastic_rank<R: Rng + ?Sized>(pop: &mut [Individual], pf: f64, rng: &mut R) {
422 let n = pop.len();
423 if n < 2 {
424 return;
425 }
426 for _ in 0..n.saturating_sub(1) {
428 let mut swapped = false;
429 for i in 0..(n - 1) {
430 let a = &pop[i];
434 let b = &pop[i + 1];
435 let both_feasible = a.max_violation <= 0.0 && b.max_violation <= 0.0;
436 let both_infeasible = a.max_violation > 0.0 && b.max_violation > 0.0;
437 let compare_by_fun = if both_feasible {
438 true
439 } else if both_infeasible {
440 rng.random::<f64>() < pf
441 } else {
442 false
445 };
446
447 let need_swap = if compare_by_fun {
448 a.fun > b.fun
449 } else {
450 a.max_violation > b.max_violation
451 };
452
453 if need_swap {
454 pop.swap(i, i + 1);
455 swapped = true;
456 }
457 }
458 if !swapped {
459 break;
460 }
461 }
462}
463
464fn best_individual(pop: &[Individual]) -> &Individual {
467 pop.iter()
468 .min_by(|a, b| {
469 if individual_better(a, b) {
470 std::cmp::Ordering::Less
471 } else if individual_better(b, a) {
472 std::cmp::Ordering::Greater
473 } else {
474 std::cmp::Ordering::Equal
475 }
476 })
477 .expect("non-empty population")
478}
479
480fn individual_better(a: &Individual, b: &Individual) -> bool {
483 let a_feas = a.max_violation <= 0.0;
484 let b_feas = b.max_violation <= 0.0;
485 match (a_feas, b_feas) {
486 (true, false) => true,
487 (false, true) => false,
488 (true, true) => a.fun < b.fun,
489 (false, false) => a.max_violation < b.max_violation,
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn sphere_unconstrained() {
499 let f = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
501 let cfg = IsresConfig {
502 bounds: vec![(-5.0, 5.0); 4],
503 mu: 20,
504 lambda: 100,
505 maxeval: 8_000,
506 seed: Some(42),
507 ..Default::default()
508 };
509 let report = isres(&f, &[], cfg).expect("isres failed");
510 assert!(
511 report.fun < 1e-3,
512 "fun = {} should converge near 0",
513 report.fun
514 );
515 for &xi in report.x.iter() {
516 assert!(xi.abs() < 0.05, "xi = {} should be near 0", xi);
517 }
518 }
519
520 #[test]
521 fn sphere_with_inequality() {
522 let f = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
526 let constraints = vec![IsresConstraint {
527 fun: Arc::new(|x: &Array1<f64>| 1.0 - x[0] - x[1]),
528 }];
529 let cfg = IsresConfig {
530 bounds: vec![(-5.0, 5.0); 4],
531 mu: 30,
532 lambda: 200,
533 maxeval: 30_000,
534 seed: Some(7),
535 ..Default::default()
536 };
537 let report = isres(&f, &constraints, cfg).expect("isres failed");
538 assert!(report.feasible, "best should be feasible");
539 assert!(
542 report.fun < 0.7,
543 "fun = {} should converge near 0.5",
544 report.fun
545 );
546 }
547
548 #[test]
549 fn rejects_inverted_bounds() {
550 let f = |x: &Array1<f64>| x[0] * x[0];
551 let cfg = IsresConfig {
552 bounds: vec![(1.0, -1.0)],
553 mu: 10,
554 lambda: 30,
555 maxeval: 100,
556 ..Default::default()
557 };
558 let err = isres(&f, &[], cfg).unwrap_err();
559 matches!(err, DEError::InvalidBounds { .. });
560 }
561
562 #[test]
563 fn rejects_tiny_population() {
564 let f = |x: &Array1<f64>| x[0] * x[0];
565 let cfg = IsresConfig {
566 bounds: vec![(-1.0, 1.0)],
567 mu: 1,
568 lambda: 10,
569 maxeval: 100,
570 ..Default::default()
571 };
572 let err = isres(&f, &[], cfg).unwrap_err();
573 matches!(err, DEError::PopulationTooSmall { .. });
574 }
575
576 #[test]
577 fn deterministic_with_seed() {
578 let f = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
579 let cfg1 = IsresConfig {
580 bounds: vec![(-5.0, 5.0); 3],
581 mu: 15,
582 lambda: 80,
583 maxeval: 2_000,
584 seed: Some(123),
585 ..Default::default()
586 };
587 let cfg2 = cfg1.clone();
588 let r1 = isres(&f, &[], cfg1).unwrap();
589 let r2 = isres(&f, &[], cfg2).unwrap();
590 assert!((r1.fun - r2.fun).abs() < 1e-12, "seeded runs must match");
591 }
592}