Skip to main content

fcmaes_core/
biteopt.rs

1// Numeric kernels index several parallel arrays by a shared loop counter, where
2// range loops read more clearly than zipped iterators.
3#![allow(clippy::needless_range_loop, clippy::manual_memcpy)]
4
5//! BiteOpt, Aleksey Vaneev's adaptive derivative-free optimizer.
6//!
7//! The implementation includes the LCG-hash `BiteRnd`, 58-bit integer-mantissa
8//! parameters, adaptive sparse selectors, dynamic and diverging populations,
9//! all primary generators, CSpherOpt, sequential Nelder-Mead, deep
10//! multi-population solution exchange, and delayed-feedback batch ask/tell.
11//!
12//! # Reference
13//!
14//! A. Vaneev, [BiteOpt algorithm description and reference
15//! implementation](https://github.com/avaneev/biteopt).
16//!
17//! # Example
18//!
19//! ```
20//! use fcmaes_core::{optimize_bite, BiteParams};
21//!
22//! let sphere = |x: &[f64]| x.iter().map(|v| v * v).sum::<f64>();
23//! let params = BiteParams {
24//!     max_evaluations: 2_000,
25//!     seed: 42,
26//!     ..Default::default()
27//! };
28//! let result = optimize_bite(&sphere, &[-5.0; 3], &[5.0; 3], None, &params, 1);
29//! assert!(result.y.is_finite());
30//! ```
31
32use std::collections::VecDeque;
33
34use crate::fitness::Objective;
35
36const INT_MANT_BITS: u32 = 58;
37const INT_MANT_MULT: i64 = 1i64 << INT_MANT_BITS;
38const INT_MANT_MASK: i64 = INT_MANT_MULT - 1;
39const BAD_COST: f64 = 1e300;
40const MAX_DEEP_OPTIMIZERS: i32 = 36;
41
42#[inline]
43fn sanitize_cost(cost: f64) -> f64 {
44    if cost.is_finite() { cost } else { BAD_COST }
45}
46
47#[inline]
48fn objective_cost(objective: &dyn Objective, values: &[f64]) -> f64 {
49    sanitize_cost(objective.eval_scalar(values))
50}
51
52/// Validate the dimensions and numeric domain required by BiteOpt.
53///
54/// # Errors
55///
56/// Returns an error if the bound slices are empty or of unequal length, if any
57/// bound is non-finite or does not satisfy `lower <= upper`, if `init` is
58/// supplied with the wrong length or non-finite values, if the parameters are
59/// outside their valid ranges, or if the population count `m` exceeds 36.
60pub fn validate_bite_inputs(
61    lower: &[f64],
62    upper: &[f64],
63    init: Option<&[f64]>,
64    params: &BiteParams,
65    depth: i32,
66) -> Result<(), String> {
67    if lower.is_empty() || lower.len() != upper.len() {
68        return Err("bounds must be non-empty and have equal lengths".into());
69    }
70    for (&lo, &hi) in lower.iter().zip(upper) {
71        if !lo.is_finite() || !hi.is_finite() || lo >= hi || !(hi - lo).is_finite() {
72            return Err("bounds must contain finite intervals with lower < upper".into());
73        }
74    }
75    if let Some(values) = init {
76        if values.len() != lower.len() {
77            return Err("initial guess must match the bounds dimension".into());
78        }
79        if values.iter().any(|value| !value.is_finite()) {
80            return Err("initial guess must contain only finite values".into());
81        }
82    }
83    if params.stop_fitness.is_nan() {
84        return Err("stop_fitness must not be NaN".into());
85    }
86    if (1..4).contains(&params.popsize) {
87        return Err("popsize must be non-positive (automatic) or at least 4".into());
88    }
89    if depth > MAX_DEEP_OPTIMIZERS {
90        return Err(format!(
91            "M must not exceed {MAX_DEEP_OPTIMIZERS} deep optimizers"
92        ));
93    }
94    Ok(())
95}
96
97/// A generated-but-not-yet-applied candidate (frozen population state).
98struct Candidate {
99    enc: Vec<i64>,
100    real: Vec<f64>,
101    sels: Vec<SelUse>,
102    is_init: bool,
103    /// Set by `generateSolPar`: the parallel optimizer already evaluated it.
104    precomputed_cost: Option<f64>,
105}
106
107/// Which population a generator draws from.
108#[derive(Clone, Copy)]
109enum PopSel {
110    Main,
111    Par(usize),
112    ParOpt,
113    ParOpt2,
114}
115
116/// Outcome of a BiteOpt run.
117#[derive(Clone, Debug)]
118pub struct BiteResult {
119    /// Best decision vector found.
120    pub x: Vec<f64>,
121    /// Objective value at [`x`](Self::x).
122    pub y: f64,
123    /// Number of objective evaluations charged to the run.
124    pub evaluations: u64,
125    /// Number of candidates incorporated into the optimizer state.
126    pub iterations: i32,
127    /// Termination code: `1` for target fitness and `2` for stall.
128    pub stop: i32,
129}
130
131/// Tunable inputs for [`optimize_bite`].
132#[derive(Clone, Debug)]
133pub struct BiteParams {
134    /// Population size; non-positive values select `9 + 3 × dimension`.
135    pub popsize: i32,
136    /// Maximum number of objective evaluations.
137    pub max_evaluations: u64,
138    /// Stop after finding an objective value strictly below this threshold.
139    pub stop_fitness: f64,
140    /// Stall multiplier; zero disables stall termination.
141    pub stall_criterion: i32,
142    /// Seed for the optimizer's random stream.
143    pub seed: u64,
144    /// Additional run identifier mixed into [`seed`](Self::seed).
145    pub runid: i64,
146}
147
148impl Default for BiteParams {
149    fn default() -> Self {
150        Self {
151            popsize: 0,
152            max_evaluations: 100_000,
153            stop_fitness: f64::NEG_INFINITY,
154            stall_criterion: 0,
155            seed: 0,
156            runid: 0,
157        }
158    }
159}
160
161// ---------------------------------------------------------------------------
162// PRNG (faithful port of CBiteRnd)
163// ---------------------------------------------------------------------------
164
165/// Deterministic random generator used by BiteOpt's reference algorithm.
166///
167/// Most applications should seed [`BiteOpt`] rather than use this low-level
168/// generator directly.
169pub struct BiteRnd {
170    seed: u64,
171    lcg: u64,
172    hash: u64,
173    bit_pool: u64,
174    bits_left: i32,
175}
176
177impl BiteRnd {
178    /// Create and warm up a generator from `seed`.
179    pub fn new(seed: u64) -> Self {
180        let mut r = BiteRnd {
181            seed,
182            lcg: 0,
183            hash: 0,
184            bit_pool: 0,
185            bits_left: 0,
186        };
187        for _ in 0..5 {
188            r.advance();
189        }
190        r
191    }
192
193    #[inline]
194    fn advance(&mut self) -> u64 {
195        self.seed = self
196            .seed
197            .wrapping_mul(self.lcg.wrapping_mul(2).wrapping_add(1));
198        let rs = self.seed.rotate_left(32);
199        self.hash = self.hash.wrapping_add(rs).wrapping_add(0xAAAAAAAAAAAAAAAA);
200        self.lcg = self
201            .lcg
202            .wrapping_add(self.seed)
203            .wrapping_add(0x5555555555555555);
204        self.seed ^= self.hash;
205        self.lcg ^ rs
206    }
207
208    #[inline]
209    /// Draw a uniform value in `[0, 1)`.
210    pub fn get(&mut self) -> f64 {
211        (self.advance() >> (64 - 53)) as f64 * (-53f64).exp2()
212    }
213    #[inline]
214    /// Draw an integer by scaling a uniform variate into `[0, n)`.
215    pub fn get_int(&mut self, n: i32) -> i32 {
216        (self.get() * n as f64) as i32
217    }
218    #[inline]
219    fn get_sqr(&mut self) -> f64 {
220        let v = self.get();
221        v * v
222    }
223    #[inline]
224    /// Draw an integer in `[0, n)` biased toward zero by squaring the variate.
225    pub fn get_sqr_int(&mut self, n: i32) -> i32 {
226        (self.get_sqr() * n as f64) as i32
227    }
228    fn get_pow(&mut self, p: f64) -> f64 {
229        let v = self.get();
230        // These are the powers used by BiteOpt's hot paths. Matching the
231        // upstream algebra avoids a comparatively expensive generic pow().
232        match p {
233            0.25 => v.sqrt().sqrt(),
234            0.5 => v.sqrt(),
235            1.0 => v,
236            1.5 => v * v.sqrt(),
237            1.75 => {
238                let sv = v.sqrt();
239                v * sv * sv.sqrt()
240            }
241            2.0 => v * v,
242            3.0 => v * v * v,
243            4.0 => {
244                let v2 = v * v;
245                v2 * v2
246            }
247            _ => v.powf(p),
248        }
249    }
250    #[inline]
251    /// Draw an integer in `[0, n)` after raising the uniform variate to `p`.
252    pub fn get_pow_int(&mut self, p: f64, n: i32) -> i32 {
253        (self.get_pow(p) * n as f64) as i32
254    }
255    #[inline]
256    /// Draw the next raw 64-bit generator output.
257    pub fn get_raw(&mut self) -> u64 {
258        self.advance()
259    }
260    #[inline]
261    /// Draw from the symmetric triangular distribution on `(-1, 1)`.
262    pub fn get_tpdf(&mut self) -> f64 {
263        let v1 = (self.advance() >> (64 - 53)) as i64;
264        let v2 = (self.advance() >> (64 - 53)) as i64;
265        (v1 - v2) as f64 * (-53f64).exp2()
266    }
267    #[inline]
268    /// Draw one unbiased bit as `0` or `1`.
269    pub fn get_bit(&mut self) -> i32 {
270        if self.bits_left == 0 {
271            self.bit_pool = self.advance();
272            let b = (self.bit_pool & 1) as i32;
273            self.bits_left = 63;
274            self.bit_pool >>= 1;
275            return b;
276        }
277        let b = (self.bit_pool & 1) as i32;
278        self.bits_left -= 1;
279        self.bit_pool >>= 1;
280        b
281    }
282    /// Leva's fast normal generator (as in CBiteRnd::getGaussian).
283    fn get_gaussian(&mut self) -> f64 {
284        loop {
285            let mut u = self.get();
286            let mut v = self.get();
287            if u == 0.0 || v == 0.0 {
288                u = 1.0;
289                v = 1.0;
290            }
291            v = 1.7156 * (v - 0.5);
292            let x = u - 0.449871;
293            let y = v.abs() + 0.386595;
294            let q = x * x + y * (0.19600 * y - 0.25472 * x);
295            if q < 0.27597 {
296                return v / u;
297            }
298            if q <= 0.27846 && v * v <= -4.0 * u.ln() * u * u {
299                return v / u;
300            }
301        }
302    }
303}
304
305fn wrap_param(rnd: &mut BiteRnd, v: i64) -> i64 {
306    if v < 0 {
307        if v > -INT_MANT_MULT {
308            (rnd.get() * (-v) as f64) as i64
309        } else {
310            (rnd.get_raw() as i64) & INT_MANT_MASK
311        }
312    } else if v > INT_MANT_MULT {
313        if v < INT_MANT_MULT * 2 {
314            (INT_MANT_MULT as f64 - rnd.get() * (v - INT_MANT_MULT) as f64) as i64
315        } else {
316            (rnd.get_raw() as i64) & INT_MANT_MASK
317        }
318    } else {
319        v
320    }
321}
322
323fn gaussian_int(rnd: &mut BiteRnd, sd: f64, mean: i64) -> i64 {
324    loop {
325        let r = rnd.get_gaussian() * sd;
326        if r > -8.0 && r < 8.0 {
327            return (r * INT_MANT_MULT as f64) as i64 + mean;
328        }
329    }
330}
331
332// ---------------------------------------------------------------------------
333// Adaptive selector (faithful port of CBiteSelBase)
334// ---------------------------------------------------------------------------
335
336const SLOT_COUNT: usize = 5;
337
338/// Selector state captured when a candidate is generated. Delayed batch
339/// feedback must restore this state instead of updating the last selection
340/// subsequently made for another candidate.
341#[derive(Clone, Copy)]
342struct SelUse {
343    index: usize,
344    value: i32,
345    position: usize,
346    slot_id: u8,
347    entry_id: u8,
348}
349
350struct BiteSel {
351    count: usize,
352    count_sp: usize,
353    count_sp1: usize,
354    accum_coeff: f64,
355    slot_accums: [f64; SLOT_COUNT],
356    slot_ids: [u8; SLOT_COUNT],
357    sels: [Vec<i32>; SLOT_COUNT],
358    entry_ids: [Vec<u8>; SLOT_COUNT],
359    sel: i32,
360    sel_id: u8,
361    selp: usize,
362    slot: usize,
363}
364
365impl BiteSel {
366    fn new(count: usize) -> Self {
367        BiteSel {
368            count,
369            count_sp: 0,
370            count_sp1: 0,
371            accum_coeff: 0.0,
372            slot_accums: [0.0; SLOT_COUNT],
373            slot_ids: [0, 1, 2, 3, 4],
374            sels: Default::default(),
375            entry_ids: Default::default(),
376            sel: 0,
377            sel_id: 0,
378            selp: 0,
379            slot: 0,
380        }
381    }
382
383    fn reset(&mut self, rnd: &mut BiteRnd, param_count: usize) {
384        let sparse_mul = 5usize;
385        self.count_sp = self.count * sparse_mul;
386        self.count_sp1 = self.count_sp - 1;
387        self.accum_coeff = 1.0 / (param_count as f64).sqrt();
388        for j in 0..SLOT_COUNT {
389            let mut sp = vec![0i32; self.count_sp];
390            let mut ids: Vec<u8> = (0..self.count_sp as u8).collect();
391            for i in 0..self.count {
392                for k in 0..sparse_mul {
393                    sp[i * sparse_mul + k] = i as i32;
394                }
395            }
396            for _ in 0..self.count_sp * 5 {
397                let i1 = rnd.get_int(self.count_sp as i32) as usize;
398                let i2 = rnd.get_int(self.count_sp as i32) as usize;
399                sp.swap(i1, i2);
400                ids.swap(i1, i2);
401            }
402            self.sels[j] = sp;
403            self.entry_ids[j] = ids;
404            self.slot_accums[j] = 0.0;
405            self.slot_ids[j] = j as u8;
406        }
407        self.slot = 0;
408        self.select(rnd);
409    }
410
411    fn select(&mut self, rnd: &mut BiteRnd) -> i32 {
412        self.slot = rnd.get_pow_int(1.5, SLOT_COUNT as i32) as usize;
413        self.selp = rnd.get_pow_int(1.5, self.count_sp as i32) as usize;
414        self.sel = self.sels[self.slot][self.selp];
415        self.sel_id = self.entry_ids[self.slot][self.selp];
416        self.sel
417    }
418
419    fn incr(&mut self, v: f64) {
420        let dp = (-(self.selp as f64) * v * v) as i64;
421        if dp < 0 {
422            if dp == -1 {
423                self.sels[self.slot].swap(self.selp, self.selp - 1);
424                self.entry_ids[self.slot].swap(self.selp, self.selp - 1);
425            } else {
426                let np = (self.selp as i64 + dp) as usize;
427                self.sels[self.slot].copy_within(np..self.selp, np + 1);
428                self.entry_ids[self.slot].copy_within(np..self.selp, np + 1);
429                self.sels[self.slot][np] = self.sel;
430                self.entry_ids[self.slot][np] = self.sel_id;
431            }
432        }
433        self.slot_accums[self.slot] += self.accum_coeff;
434        if self.slot_accums[self.slot] >= 1.0 {
435            let a = self.slot_accums[self.slot] - 1.0;
436            if self.slot > 0 {
437                self.sels.swap(self.slot, self.slot - 1);
438                self.entry_ids.swap(self.slot, self.slot - 1);
439                self.slot_ids.swap(self.slot, self.slot - 1);
440                self.slot_accums[self.slot] = self.slot_accums[self.slot - 1];
441                self.slot_accums[self.slot - 1] = a;
442            } else {
443                self.slot_accums[self.slot] = a;
444            }
445        }
446    }
447
448    fn decr(&mut self) {
449        if self.selp < self.count_sp1 {
450            self.sels[self.slot].swap(self.selp, self.selp + 1);
451            self.entry_ids[self.slot].swap(self.selp, self.selp + 1);
452        }
453        self.slot_accums[self.slot] -= self.accum_coeff;
454        if self.slot_accums[self.slot] <= -1.0 {
455            let a = self.slot_accums[self.slot] + 1.0;
456            if self.slot < SLOT_COUNT - 1 {
457                self.sels.swap(self.slot, self.slot + 1);
458                self.entry_ids.swap(self.slot, self.slot + 1);
459                self.slot_ids.swap(self.slot, self.slot + 1);
460                self.slot_accums[self.slot] = self.slot_accums[self.slot + 1];
461                self.slot_accums[self.slot + 1] = a;
462            } else {
463                self.slot_accums[self.slot] = a;
464            }
465        }
466    }
467
468    fn captured(&self, index: usize) -> SelUse {
469        SelUse {
470            index,
471            value: self.sel,
472            position: self.selp,
473            slot_id: self.slot_ids[self.slot],
474            entry_id: self.sel_id,
475        }
476    }
477
478    fn restore(&mut self, selection: SelUse) {
479        self.slot = self
480            .slot_ids
481            .iter()
482            .position(|&id| id == selection.slot_id)
483            .unwrap_or(0);
484        self.sel = selection.value;
485        self.sel_id = selection.entry_id;
486        self.selp = self.entry_ids[self.slot]
487            .iter()
488            .position(|&id| id == selection.entry_id)
489            .or_else(|| {
490                self.sels[self.slot]
491                    .iter()
492                    .enumerate()
493                    .filter(|(_, value)| **value == selection.value)
494                    .min_by_key(|(position, _)| position.abs_diff(selection.position))
495                    .map(|(position, _)| position)
496            })
497            .unwrap_or(selection.position.min(self.count_sp1));
498    }
499
500    fn incr_captured(&mut self, selection: SelUse, value: f64) {
501        self.restore(selection);
502        self.incr(value);
503    }
504
505    fn decr_captured(&mut self, selection: SelUse) {
506        self.restore(selection);
507        self.decr();
508    }
509}
510
511// Selector indices (full CBiteOpt roster).
512mod sel {
513    pub const METHOD: usize = 0;
514    pub const M1: usize = 1;
515    pub const M1A: usize = 2;
516    pub const M1B: usize = 3;
517    pub const M1C: usize = 4;
518    pub const M2: usize = 5;
519    pub const M2B: usize = 6;
520    pub const POP_CHANGE_INCR: usize = 7;
521    pub const POP_CHANGE_DECR: usize = 8;
522    pub const PAR_OPT2: usize = 9;
523    pub const PAR_POP_P: usize = 10; // [0..4] = 10..14
524    pub const ALT_POP_P: usize = 14;
525    pub const ALT_POP: usize = 15; // [0..4] = 15..19
526    pub const MIN_SOL_PWR: usize = 19; // [0..4] = 19..23
527    pub const MIN_SOL_MUL: usize = 23; // [0..4] = 23..27
528    pub const GEN1_ALLP: usize = 27;
529    pub const GEN1_MOVE_ASYNC: usize = 28;
530    pub const GEN1_MOVE_SPAN: usize = 29;
531    pub const GEN2_MODE: usize = 30;
532    pub const GEN2B_MODE: usize = 31;
533    pub const GEN2C_MODE: usize = 32;
534    pub const GEN2D_MODE: usize = 33;
535    pub const GEN3_MODE: usize = 34;
536    pub const GEN4_MIX_FAC: usize = 35;
537    pub const GEN5B_MODE: usize = 36;
538    pub const GEN7_POW_FAC: usize = 37;
539    pub const GEN8_MODE: usize = 38;
540    pub const GEN8_NUM: usize = 39;
541    pub const GEN8_SPAN: usize = 40; // [0..2] = 40..42
542    pub const COUNT: usize = 42;
543}
544
545fn build_selectors() -> Vec<BiteSel> {
546    let mut s = Vec::with_capacity(sel::COUNT);
547    s.push(BiteSel::new(4)); // METHOD
548    s.push(BiteSel::new(4)); // M1
549    s.push(BiteSel::new(3)); // M1A
550    s.push(BiteSel::new(2)); // M1B
551    s.push(BiteSel::new(2)); // M1C
552    s.push(BiteSel::new(2)); // M2
553    s.push(BiteSel::new(4)); // M2B
554    s.push(BiteSel::new(2)); // POP_CHANGE_INCR
555    s.push(BiteSel::new(2)); // POP_CHANGE_DECR
556    s.push(BiteSel::new(2)); // PAR_OPT2
557    for _ in 0..4 {
558        s.push(BiteSel::new(2)); // PAR_POP_P[gi]
559    }
560    s.push(BiteSel::new(2)); // ALT_POP_P
561    for _ in 0..4 {
562        s.push(BiteSel::new(2)); // ALT_POP[gi]
563    }
564    for _ in 0..4 {
565        s.push(BiteSel::new(4)); // MIN_SOL_PWR[gi]
566    }
567    for _ in 0..4 {
568        s.push(BiteSel::new(4)); // MIN_SOL_MUL[gi]
569    }
570    s.push(BiteSel::new(2)); // GEN1_ALLP
571    s.push(BiteSel::new(2)); // GEN1_MOVE_ASYNC
572    s.push(BiteSel::new(4)); // GEN1_MOVE_SPAN
573    s.push(BiteSel::new(2)); // GEN2_MODE
574    s.push(BiteSel::new(2)); // GEN2B_MODE
575    s.push(BiteSel::new(2)); // GEN2C_MODE
576    s.push(BiteSel::new(2)); // GEN2D_MODE
577    s.push(BiteSel::new(4)); // GEN3_MODE
578    s.push(BiteSel::new(4)); // GEN4_MIX_FAC
579    s.push(BiteSel::new(2)); // GEN5B_MODE
580    s.push(BiteSel::new(4)); // GEN7_POW_FAC
581    s.push(BiteSel::new(2)); // GEN8_MODE
582    s.push(BiteSel::new(4)); // GEN8_NUM
583    for _ in 0..2 {
584        s.push(BiteSel::new(4)); // GEN8_SPAN[i]
585    }
586    s
587}
588
589// ---------------------------------------------------------------------------
590// Population (faithful port of CBitePop, fixed capacity and dynamic active size)
591// ---------------------------------------------------------------------------
592
593#[derive(Clone)]
594struct BitePop {
595    param_count: usize,
596    pop_size: usize,
597    params: Vec<Vec<i64>>, // ordered by cost ascending
598    costs: Vec<f64>,
599    cent: Vec<i64>,
600    cur_pop_pos: usize,
601    // Dynamic "active window" size (the C++ CurPopSize); <= pop_size.
602    cur_pop_size: usize,
603    cur_pop_size1: usize,
604    cur_pop_size_i: f64,
605    need_cent: bool,
606    cent_lpc: f64,
607}
608
609fn calc_lp1_coeff(count: f64) -> f64 {
610    let theta = 2.8 / count;
611    let costheta2 = 2.0 - theta.cos();
612    1.0 - (costheta2 - (costheta2 * costheta2 - 1.0).sqrt())
613}
614
615impl BitePop {
616    fn new(param_count: usize, pop_size: usize) -> Self {
617        BitePop {
618            param_count,
619            pop_size,
620            params: vec![vec![0i64; param_count]; pop_size],
621            costs: vec![1e300; pop_size],
622            cent: vec![0i64; param_count],
623            cur_pop_pos: 0,
624            cur_pop_size: pop_size,
625            cur_pop_size1: pop_size - 1,
626            cur_pop_size_i: 1.0 / pop_size as f64,
627            need_cent: false,
628            cent_lpc: calc_lp1_coeff(pop_size as f64),
629        }
630    }
631
632    fn reset_cur_pop_pos(&mut self) {
633        self.cur_pop_pos = 0;
634        self.cur_pop_size = self.pop_size;
635        self.cur_pop_size1 = self.pop_size - 1;
636        self.cur_pop_size_i = 1.0 / self.pop_size as f64;
637        self.need_cent = false;
638        self.cent_lpc = calc_lp1_coeff(self.pop_size as f64);
639    }
640
641    fn incr_cur_pop_size(&mut self) {
642        self.cur_pop_size += 1;
643        self.cur_pop_size1 += 1;
644        self.cur_pop_size_i = 1.0 / self.cur_pop_size as f64;
645        self.need_cent = true;
646        self.cent_lpc = calc_lp1_coeff(self.cur_pop_size as f64);
647    }
648
649    fn decr_cur_pop_size(&mut self) {
650        self.cur_pop_size -= 1;
651        self.cur_pop_size1 -= 1;
652        self.cur_pop_size_i = 1.0 / self.cur_pop_size as f64;
653        self.need_cent = true;
654        self.cent_lpc = calc_lp1_coeff(self.cur_pop_size as f64);
655    }
656
657    /// Insert a solution keeping the population cost-sorted; returns the
658    /// insertion index, or `pop_size` if the solution was rejected.
659    fn update_pop(&mut self, mut cost: f64, up: &[i64], do_update_centroid: bool) -> usize {
660        let ri;
661        if self.cur_pop_pos < self.pop_size {
662            ri = self.cur_pop_pos;
663            if cost.is_nan() {
664                cost = 1e300;
665            }
666        } else {
667            ri = self.pop_size - 1;
668            if cost.is_nan() || cost >= self.costs[ri] {
669                return self.pop_size;
670            }
671        }
672        // binary search for insertion position
673        let mut p = 0usize;
674        let mut i = ri;
675        while p < i {
676            let mid = (p + i) >> 1;
677            if self.costs[mid] >= cost {
678                i = mid;
679            } else {
680                p = mid + 1;
681            }
682        }
683        if self.cur_pop_pos < self.pop_size {
684            self.cur_pop_pos += 1;
685        }
686        // shift [p..ri) right by one, insert at p
687        for k in (p + 1..=ri).rev() {
688            self.params.swap(k, k - 1);
689            self.costs[k] = self.costs[k - 1];
690        }
691        self.costs[p] = cost;
692        if self.params[p] != up {
693            if do_update_centroid {
694                for (c, &u) in self.cent.iter_mut().zip(up) {
695                    *c += ((u - *c) as f64 * self.cent_lpc) as i64;
696                }
697                self.params[p].copy_from_slice(up);
698            } else {
699                self.params[p].copy_from_slice(up);
700                self.need_cent = true;
701            }
702        } else {
703            self.need_cent = true;
704        }
705        p
706    }
707
708    fn update_centroid(&mut self) {
709        self.need_cent = false;
710        let cm = 1.0 / self.pop_size as f64;
711        for j in 0..self.param_count {
712            let mut sum = 0i128;
713            for row in &self.params {
714                sum += row[j] as i128;
715            }
716            self.cent[j] = (sum as f64 * cm) as i64;
717        }
718    }
719
720    #[inline]
721    fn ordered(&self, i: usize) -> &[i64] {
722        &self.params[i]
723    }
724
725    #[inline]
726    fn cur_pop_size(&self) -> usize {
727        self.cur_pop_size
728    }
729
730    fn get_centroid(&mut self) -> &[i64] {
731        if self.need_cent {
732            self.update_centroid();
733        }
734        &self.cent
735    }
736
737    /// Copy another (same-size) population wholesale (the C++ `CBitePop::copy`).
738    fn copy_from(&mut self, src: &BitePop) {
739        for (d, s) in self.params.iter_mut().zip(&src.params) {
740            d.copy_from_slice(s);
741        }
742        self.costs.copy_from_slice(&src.costs);
743        self.cent.copy_from_slice(&src.cent);
744        self.cur_pop_pos = src.cur_pop_pos;
745        self.cur_pop_size = src.cur_pop_size;
746        self.cur_pop_size1 = src.cur_pop_size1;
747        self.cur_pop_size_i = src.cur_pop_size_i;
748        self.need_cent = src.need_cent;
749        self.cent_lpc = src.cent_lpc;
750    }
751}
752
753// ---------------------------------------------------------------------------
754// Secondary ("parallel") optimizers (ports of CSpherOpt and CNMSeqOpt)
755// ---------------------------------------------------------------------------
756
757/// Wrap a normalized value into `[0, 1]` (the C++ double-branch `wrapParam`).
758fn wrap01(rnd: &mut BiteRnd, v: f64) -> f64 {
759    if v < 0.0 {
760        if v > -1.0 { rnd.get() * -v } else { rnd.get() }
761    } else if v > 1.0 {
762        if v < 2.0 {
763            1.0 - rnd.get() * (v - 1.0)
764        } else {
765            rnd.get()
766        }
767    } else {
768        v
769    }
770}
771
772/// Reflect a real value back into `[minv, minv+diffv]` (the C++ `wrapParamReal`).
773fn wrap_param_real(rnd: &mut BiteRnd, v: f64, minv: f64, diffv: f64) -> f64 {
774    if v < minv {
775        if v > minv - diffv {
776            minv + rnd.get() * (minv - v)
777        } else {
778            minv + rnd.get() * diffv
779        }
780    } else {
781        let maxv = minv + diffv;
782        if v > maxv {
783            if v < maxv + diffv {
784                maxv - rnd.get() * (v - maxv)
785            } else {
786                maxv - rnd.get() * diffv
787            }
788        } else {
789            v
790        }
791    }
792}
793
794/// One evaluated step from a secondary optimizer.
795struct ParStep {
796    stall: i64,
797    cost: f64,
798    values: Vec<f64>,
799}
800
801/// CSpherOpt — converging hyper-spheroid optimizer (normalized `[0,1]` space).
802struct SpherOpt {
803    dim: usize,
804    pop_size: usize,
805    params: Vec<Vec<f64>>, // sorted ascending by cost
806    costs: Vec<f64>,
807    cur_pop_pos: usize,
808    cent: Vec<f64>,
809    min_values: Vec<f64>,
810    diff_values: Vec<f64>,
811    sels: [BiteSel; 3], // CentPow(4), RadPow(4), EvalFac(3)
812    apply: Vec<usize>,
813    radius: f64,
814    eval_fac: f64,
815    cure: i32,
816    curem: i32,
817    do_cent_eval: bool,
818    jit_mult: f64,
819    jit_offs: f64,
820    avg_cost: f64,
821    hi_bound: f64,
822    stall_count: i64,
823    best_cost: f64,
824    best_values: Vec<f64>,
825}
826
827impl SpherOpt {
828    fn new(dim: usize, min_values: Vec<f64>, diff_values: Vec<f64>, pop_size: usize) -> Self {
829        let dim_i = 1.0 / dim as f64;
830        SpherOpt {
831            dim,
832            pop_size,
833            params: vec![vec![0.0; dim]; pop_size],
834            costs: vec![1e300; pop_size],
835            cur_pop_pos: 0,
836            cent: vec![0.5; dim],
837            min_values,
838            diff_values,
839            sels: [BiteSel::new(4), BiteSel::new(4), BiteSel::new(3)],
840            apply: Vec::new(),
841            radius: 0.5,
842            eval_fac: 2.0,
843            cure: 0,
844            curem: 0,
845            do_cent_eval: false,
846            jit_mult: 5.0 * dim_i,
847            jit_offs: 1.0 - 5.0 * dim_i * 0.5,
848            avg_cost: 0.0,
849            hi_bound: 1e300,
850            stall_count: 0,
851            best_cost: 1e300,
852            best_values: vec![0.0; dim],
853        }
854    }
855
856    fn real_value(&self, norm: &[f64], i: usize) -> f64 {
857        self.min_values[i] + self.diff_values[i] * norm[i]
858    }
859
860    fn init(&mut self, rnd: &mut BiteRnd, init_params: Option<&[f64]>, radius: f64) {
861        self.best_cost = 1e300;
862        self.stall_count = 0;
863        self.hi_bound = 1e300;
864        self.avg_cost = 0.0;
865        for sel in self.sels.iter_mut() {
866            sel.reset(rnd, self.dim);
867        }
868        self.cur_pop_pos = 0;
869        self.radius = 0.5 * radius;
870        self.eval_fac = 2.0;
871        self.cure = 0;
872        self.curem = (self.pop_size as f64 * self.eval_fac).ceil() as i32;
873        match init_params {
874            None => {
875                self.cent = vec![0.5; self.dim];
876                self.do_cent_eval = false;
877            }
878            Some(ip) => {
879                for i in 0..self.dim {
880                    self.cent[i] = wrap01(rnd, (ip[i] - self.min_values[i]) / self.diff_values[i]);
881                }
882                self.do_cent_eval = true;
883            }
884        }
885    }
886
887    fn update_pop(&mut self, cost: f64, params: &[f64]) {
888        let ri;
889        if self.cur_pop_pos < self.pop_size {
890            ri = self.cur_pop_pos;
891        } else {
892            ri = self.pop_size - 1;
893            if cost >= self.costs[ri] {
894                return;
895            }
896        }
897        let mut p = 0usize;
898        let mut i = ri;
899        while p < i {
900            let mid = (p + i) >> 1;
901            if self.costs[mid] >= cost {
902                i = mid;
903            } else {
904                p = mid + 1;
905            }
906        }
907        if self.cur_pop_pos < self.pop_size {
908            self.cur_pop_pos += 1;
909        }
910        for k in (p + 1..=ri).rev() {
911            self.params.swap(k, k - 1);
912            self.costs[k] = self.costs[k - 1];
913        }
914        self.params[p].copy_from_slice(params);
915        self.costs[p] = cost;
916    }
917
918    fn optimize(&mut self, rnd: &mut BiteRnd, obj: &dyn Objective) -> ParStep {
919        let mut params = vec![0.0; self.dim];
920        let mut new_values = vec![0.0; self.dim];
921        if self.do_cent_eval {
922            self.do_cent_eval = false;
923            for i in 0..self.dim {
924                params[i] = self.cent[i];
925                new_values[i] = self.real_value(&self.cent, i);
926            }
927        } else {
928            let mut s2 = 1e-300;
929            for pi in params.iter_mut() {
930                *pi = rnd.get() - 0.5;
931                s2 += *pi * *pi;
932            }
933            let d = self.radius / s2.sqrt();
934            if self.dim > 4 {
935                for i in 0..self.dim {
936                    params[i] = wrap01(rnd, self.cent[i] + params[i] * d);
937                    new_values[i] = self.real_value(&params, i);
938                }
939            } else {
940                for i in 0..self.dim {
941                    let m = self.jit_offs + rnd.get() * self.jit_mult;
942                    params[i] = wrap01(rnd, self.cent[i] + params[i] * d * m);
943                    new_values[i] = self.real_value(&params, i);
944                }
945            }
946        }
947        let cost = objective_cost(obj, &new_values);
948        self.update_pop(cost, &params);
949        if cost <= self.best_cost {
950            self.best_cost = cost;
951            self.best_values.copy_from_slice(&new_values);
952        }
953        self.avg_cost += cost;
954        self.cure += 1;
955        if self.cure >= self.curem {
956            self.avg_cost /= self.cure as f64;
957            if self.avg_cost < self.hi_bound {
958                self.hi_bound = self.avg_cost;
959                self.stall_count = 0;
960                for &s in &self.apply {
961                    self.sels[s].incr(1.0);
962                }
963            } else {
964                self.stall_count += self.cure as i64;
965                for &s in &self.apply {
966                    self.sels[s].decr();
967                }
968            }
969            self.apply.clear();
970            self.cur_pop_pos = 0;
971            self.avg_cost = 0.0;
972            self.cure = 0;
973            self.update(rnd);
974            self.curem = (self.pop_size as f64 * self.eval_fac).ceil() as i32;
975        }
976        ParStep {
977            stall: self.stall_count,
978            cost,
979            values: new_values,
980        }
981    }
982
983    fn sel(&mut self, idx: usize, rnd: &mut BiteRnd) -> i32 {
984        self.apply.push(idx);
985        self.sels[idx].select(rnd)
986    }
987
988    fn update(&mut self, rnd: &mut BiteRnd) {
989        const WCENT: [f64; 4] = [4.5, 6.0, 7.5, 10.0];
990        const WRAD: [f64; 4] = [14.0, 16.0, 18.0, 20.0];
991        const EVAL_FACS: [f64; 3] = [2.1, 2.0, 1.9];
992        let cent_fac = WCENT[self.sel(0, rnd) as usize];
993        let rad_fac = WRAD[self.sel(1, rnd) as usize];
994        self.eval_fac = EVAL_FACS[self.sel(2, rnd) as usize];
995
996        let lm = 1.0 / self.curem as f64;
997        let mut wc = vec![0.0; self.pop_size];
998        let mut wr = vec![0.0; self.pop_size];
999        let mut s1 = 0.0;
1000        let mut s2 = 0.0;
1001        for i in 0..self.pop_size {
1002            let l = 1.0 - i as f64 * lm;
1003            let v1 = l.powf(cent_fac);
1004            wc[i] = v1;
1005            s1 += v1;
1006            let v2 = l.powf(rad_fac);
1007            wr[i] = v2;
1008            s2 += v2;
1009        }
1010        s1 = 1.0 / s1;
1011        s2 = 1.0 / s2;
1012        for j in 0..self.dim {
1013            let mut acc = 0.0;
1014            for i in 0..self.pop_size {
1015                acc += self.params[i][j] * wc[i] * s1;
1016            }
1017            self.cent[j] = acc;
1018        }
1019        let mut radius = 0.0;
1020        for i in 0..self.pop_size {
1021            let mut s = 0.0;
1022            for j in 0..self.dim {
1023                let d = self.params[i][j] - self.cent[j];
1024                s += d * d;
1025            }
1026            radius += s * wr[i];
1027        }
1028        self.radius = (radius * s2).sqrt();
1029    }
1030}
1031
1032/// CNMSeqOpt — sequential Nelder-Mead simplex (real-value space).
1033struct NMSeqOpt {
1034    n: usize,
1035    m: usize,
1036    m1: usize,
1037    m1i: f64,
1038    param_count_i: f64,
1039    x: Vec<Vec<f64>>, // simplex points (real)
1040    y: Vec<f64>,      // costs
1041    x0: Vec<f64>,     // centroid
1042    x1: Vec<f64>,
1043    x2: Vec<f64>,
1044    y1: f64,
1045    xlo: usize,
1046    xhi: usize,
1047    xhi2: usize,
1048    rx: usize, // index of lowest-cost vector during reduction
1049    rj: usize,
1050    do_init_evals: bool,
1051    cur_pop_pos: usize,
1052    state: NmState,
1053    stall_count: i64,
1054    min_values: Vec<f64>,
1055    diff_values: Vec<f64>,
1056    best_cost: f64,
1057    best_values: Vec<f64>,
1058}
1059
1060#[derive(Clone, Copy, PartialEq)]
1061enum NmState {
1062    Reflection,
1063    Expansion,
1064    Contraction,
1065    Reduction,
1066}
1067
1068impl NMSeqOpt {
1069    fn new(dim: usize, min_values: Vec<f64>, diff_values: Vec<f64>) -> Self {
1070        let m = (dim + 1) * 4;
1071        NMSeqOpt {
1072            n: dim,
1073            m,
1074            m1: m - 1,
1075            m1i: 1.0 / (m - 1) as f64,
1076            param_count_i: 1.0 / dim as f64,
1077            x: vec![vec![0.0; dim]; m],
1078            y: vec![1e300; m],
1079            x0: vec![0.0; dim],
1080            x1: vec![0.0; dim],
1081            x2: vec![0.0; dim],
1082            y1: 0.0,
1083            xlo: 0,
1084            xhi: 0,
1085            xhi2: 0,
1086            rx: 0,
1087            rj: 0,
1088            do_init_evals: true,
1089            cur_pop_pos: 0,
1090            state: NmState::Reflection,
1091            stall_count: 0,
1092            min_values,
1093            diff_values,
1094            best_cost: 1e300,
1095            best_values: vec![0.0; dim],
1096        }
1097    }
1098
1099    fn init(&mut self, rnd: &mut BiteRnd, init_params: Option<&[f64]>, radius: f64) {
1100        self.best_cost = 1e300;
1101        self.stall_count = 0;
1102        match init_params {
1103            Some(ip) => self.x[0].copy_from_slice(ip),
1104            None => {
1105                for i in 0..self.n {
1106                    self.x[0][i] = self.min_values[i] + self.diff_values[i] * 0.5;
1107                }
1108            }
1109        }
1110        self.xlo = 0;
1111        let base = self.x[0].clone();
1112        if radius <= 0.0 {
1113            for j in 1..self.m {
1114                for i in 0..self.n {
1115                    self.x[j][i] = self.min_values[i] + self.diff_values[i] * rnd.get();
1116                }
1117            }
1118        } else {
1119            let sd = 0.25 * radius;
1120            for j in 1..self.m {
1121                for i in 0..self.n {
1122                    self.x[j][i] = base[i] + self.diff_values[i] * rnd.get_gaussian() * sd;
1123                }
1124            }
1125        }
1126        self.state = NmState::Reflection;
1127        self.do_init_evals = true;
1128        self.cur_pop_pos = 0;
1129    }
1130
1131    fn eval(&mut self, rnd: &mut BiteRnd, params: &[f64], obj: &dyn Objective) -> (f64, Vec<f64>) {
1132        let mut nv = vec![0.0; self.n];
1133        for i in 0..self.n {
1134            nv[i] = wrap_param_real(rnd, params[i], self.min_values[i], self.diff_values[i]);
1135        }
1136        let cost = objective_cost(obj, &nv);
1137        if cost <= self.best_cost {
1138            self.best_cost = cost;
1139            self.best_values.copy_from_slice(&nv);
1140        }
1141        (cost, nv)
1142    }
1143
1144    fn find_hi(&mut self) {
1145        self.xhi2 = if self.y[0] > self.y[1] { 0 } else { 1 };
1146        self.xhi = 1 - self.xhi2;
1147        for j in 2..self.m {
1148            if self.y[j] > self.y[self.xhi] {
1149                self.xhi2 = self.xhi;
1150                self.xhi = j;
1151            } else if self.y[j] > self.y[self.xhi2] {
1152                self.xhi2 = j;
1153            }
1154        }
1155    }
1156
1157    fn calc_cent(&mut self) {
1158        self.find_hi();
1159        let mut xc = vec![0.0; self.n];
1160        for (j, xj) in self.x.iter().enumerate() {
1161            if j == self.xhi {
1162                continue;
1163            }
1164            for i in 0..self.n {
1165                xc[i] += xj[i];
1166            }
1167        }
1168        for c in xc.iter_mut() {
1169            *c *= self.m1i;
1170        }
1171        self.x0 = xc;
1172    }
1173
1174    fn copy(&mut self, ip: &[f64], cost: f64) {
1175        let replaced_index = self.xhi;
1176        self.y[replaced_index] = cost;
1177        self.x[replaced_index].copy_from_slice(ip);
1178        let replacement = self.x[replaced_index].clone();
1179        self.find_hi();
1180        if replaced_index != self.xhi {
1181            for i in 0..self.n {
1182                self.x0[i] += (replacement[i] - self.x[self.xhi][i]) * self.m1i;
1183            }
1184        }
1185        self.stall_count = 0;
1186    }
1187
1188    fn optimize(&mut self, rnd: &mut BiteRnd, obj: &dyn Objective) -> ParStep {
1189        if self.do_init_evals {
1190            let xp = self.x[self.cur_pop_pos].clone();
1191            let (out_cost, out_values) = self.eval(rnd, &xp, obj);
1192            self.y[self.cur_pop_pos] = out_cost;
1193            if self.y[self.cur_pop_pos] < self.y[self.xlo] {
1194                self.xlo = self.cur_pop_pos;
1195            }
1196            self.cur_pop_pos += 1;
1197            if self.cur_pop_pos == self.m {
1198                self.do_init_evals = false;
1199                self.calc_cent();
1200            }
1201            return ParStep {
1202                stall: 0,
1203                cost: out_cost,
1204                values: out_values,
1205            };
1206        }
1207
1208        let out_cost;
1209        let out_values;
1210        self.stall_count += 1;
1211        let sn = 0.5 * self.param_count_i.sqrt();
1212        let alpha = 1.0;
1213        let gamma = 1.5 + sn;
1214        let rho = -0.75 + sn;
1215        let sigma = 1.0 - sn;
1216        let xh = self.x[self.xhi].clone();
1217
1218        match self.state {
1219            NmState::Reflection => {
1220                for i in 0..self.n {
1221                    self.x1[i] = self.x0[i] + alpha * (self.x0[i] - xh[i]);
1222                }
1223                let x1 = self.x1.clone();
1224                let (c, v) = self.eval(rnd, &x1, obj);
1225                self.y1 = c;
1226                out_cost = c;
1227                out_values = v;
1228                if self.y1 > self.y[self.xlo] && self.y1 < self.y[self.xhi2] {
1229                    let x1c = self.x1.clone();
1230                    self.copy(&x1c, self.y1);
1231                } else if self.y1 < self.y[self.xlo] {
1232                    self.state = NmState::Expansion;
1233                    self.stall_count -= 1;
1234                } else {
1235                    self.state = NmState::Contraction;
1236                }
1237            }
1238            NmState::Expansion => {
1239                for i in 0..self.n {
1240                    self.x2[i] = self.x0[i] + gamma * (self.x0[i] - xh[i]);
1241                }
1242                let x2 = self.x2.clone();
1243                let (y2, v) = self.eval(rnd, &x2, obj);
1244                out_cost = y2;
1245                out_values = v;
1246                self.xlo = self.xhi;
1247                if y2 < self.y1 {
1248                    let x2c = self.x2.clone();
1249                    self.copy(&x2c, y2);
1250                } else {
1251                    let x1c = self.x1.clone();
1252                    self.copy(&x1c, self.y1);
1253                }
1254                self.state = NmState::Reflection;
1255            }
1256            NmState::Contraction => {
1257                for i in 0..self.n {
1258                    self.x2[i] = self.x0[i] + rho * (self.x0[i] - xh[i]);
1259                }
1260                let x2 = self.x2.clone();
1261                let (y2, v) = self.eval(rnd, &x2, obj);
1262                out_cost = y2;
1263                out_values = v;
1264                if y2 < self.y[self.xhi] {
1265                    if y2 < self.y[self.xlo] {
1266                        self.xlo = self.xhi;
1267                    }
1268                    let x2c = self.x2.clone();
1269                    self.copy(&x2c, y2);
1270                    self.state = NmState::Reflection;
1271                } else {
1272                    self.rx = self.xlo;
1273                    self.rj = 0;
1274                    self.state = NmState::Reduction;
1275                }
1276            }
1277            NmState::Reduction => {
1278                if self.rj == self.rx {
1279                    self.rj += 1;
1280                }
1281                let rxv = self.x[self.rx].clone();
1282                for i in 0..self.n {
1283                    self.x[self.rj][i] = rxv[i] + sigma * (self.x[self.rj][i] - rxv[i]);
1284                }
1285                let xx = self.x[self.rj].clone();
1286                let (c, v) = self.eval(rnd, &xx, obj);
1287                self.y[self.rj] = c;
1288                out_cost = c;
1289                out_values = v;
1290                if self.y[self.rj] < self.y[self.xlo] {
1291                    self.xlo = self.rj;
1292                    self.stall_count = 0;
1293                }
1294                self.rj += 1;
1295                if self.rj == self.m || (self.rj == self.m1 && self.rj == self.rx) {
1296                    self.calc_cent();
1297                    self.state = NmState::Reflection;
1298                }
1299            }
1300        }
1301
1302        ParStep {
1303            stall: self.stall_count,
1304            cost: out_cost,
1305            values: out_values,
1306        }
1307    }
1308}
1309
1310// ---------------------------------------------------------------------------
1311// BiteOpt optimizer core (port of CBiteOpt)
1312// ---------------------------------------------------------------------------
1313
1314/// Stateful single-population BiteOpt optimizer.
1315///
1316/// Use [`optimize`](Self::optimize) for an in-process objective or
1317/// [`ask`](Self::ask)/[`tell`](Self::tell) for batched external evaluation.
1318pub struct BiteOpt {
1319    param_count: usize,
1320    param_count_i: f64,
1321    pop_size: usize,
1322    min_values: Vec<f64>,
1323    diff_values: Vec<f64>,
1324    diff_values_i: Vec<f64>,
1325
1326    pop: BitePop,
1327    old_pop: BitePop,
1328    par_pop_count: usize,
1329    par_pops: Vec<BitePop>,
1330    par_opt_pop: BitePop,
1331    par_opt2_pop: BitePop,
1332    spher: SpherOpt,
1333    nmseq: NMSeqOpt,
1334    use_par_opt: i32,
1335
1336    sels: Vec<BiteSel>,
1337    apply_sels: Vec<SelUse>,
1338    deferred_sels: VecDeque<SelUse>,
1339    rnd: BiteRnd,
1340
1341    tmp: Vec<i64>,
1342    real_tmp: Vec<f64>,
1343    best_cost: f64,
1344    best_values: Vec<f64>,
1345    stall_count: i64,
1346
1347    // ask/tell (delayed-feedback batching)
1348    init_queue: VecDeque<Vec<i64>>,
1349    asked: Vec<Candidate>,
1350
1351    max_evaluations: u64,
1352    stopfitness: f64,
1353    stall_criterion: i32,
1354    evaluations: u64,
1355    iterations: i32,
1356    stop: i32,
1357}
1358
1359impl BiteOpt {
1360    /// Construct BiteOpt for finite box bounds and an optional initial point.
1361    ///
1362    /// # Panics
1363    ///
1364    /// Panics on any configuration [`validate_bite_inputs`] rejects. Call that
1365    /// function first to validate user-supplied input without panicking.
1366    pub fn new(lower: &[f64], upper: &[f64], init: Option<&[f64]>, p: &BiteParams) -> Self {
1367        validate_bite_inputs(lower, upper, init, p, 1).expect("invalid BiteOpt configuration");
1368        let param_count = lower.len();
1369        let pop_size = if p.popsize > 0 {
1370            p.popsize as usize
1371        } else {
1372            9 + param_count * 3
1373        };
1374        let min_values = lower.to_vec();
1375        let diff_values: Vec<f64> = upper
1376            .iter()
1377            .zip(lower)
1378            .map(|(u, l)| (u - l) / INT_MANT_MULT as f64)
1379            .collect();
1380        let diff_values_i: Vec<f64> = diff_values.iter().map(|d| 1.0 / d).collect();
1381        let real_diff: Vec<f64> = upper.iter().zip(lower).map(|(u, l)| u - l).collect();
1382        let par_pop_count = 4;
1383        let mut b = BiteOpt {
1384            param_count,
1385            param_count_i: 1.0 / param_count as f64,
1386            pop_size,
1387            min_values: min_values.clone(),
1388            diff_values,
1389            diff_values_i,
1390            pop: BitePop::new(param_count, pop_size),
1391            old_pop: BitePop::new(param_count, pop_size),
1392            par_pop_count,
1393            par_pops: (0..par_pop_count)
1394                .map(|_| BitePop::new(param_count, pop_size))
1395                .collect(),
1396            par_opt_pop: BitePop::new(param_count, pop_size),
1397            par_opt2_pop: BitePop::new(param_count, pop_size),
1398            spher: SpherOpt::new(
1399                param_count,
1400                min_values.clone(),
1401                real_diff.clone(),
1402                14 + param_count,
1403            ),
1404            nmseq: NMSeqOpt::new(param_count, min_values, real_diff),
1405            use_par_opt: 0,
1406            sels: build_selectors(),
1407            apply_sels: Vec::with_capacity(32),
1408            deferred_sels: VecDeque::with_capacity(2),
1409            rnd: BiteRnd::new(p.seed.wrapping_add(p.runid as u64)),
1410            tmp: vec![0; param_count],
1411            real_tmp: vec![0.0; param_count],
1412            best_cost: 1e300,
1413            best_values: vec![0.0; param_count],
1414            stall_count: 0,
1415            init_queue: VecDeque::new(),
1416            asked: Vec::new(),
1417            max_evaluations: if p.max_evaluations > 0 {
1418                p.max_evaluations
1419            } else {
1420                50_000
1421            },
1422            stopfitness: p.stop_fitness,
1423            stall_criterion: p.stall_criterion.max(0),
1424            evaluations: 0,
1425            iterations: 0,
1426            stop: 0,
1427        };
1428        b.init(init);
1429        b
1430    }
1431
1432    fn init(&mut self, init: Option<&[f64]>) {
1433        let seed_reset: Vec<usize> = (0..self.sels.len()).collect();
1434        for i in seed_reset {
1435            self.sels[i].reset(&mut self.rnd, self.param_count);
1436        }
1437        self.pop.reset_cur_pop_pos();
1438        self.old_pop.reset_cur_pop_pos();
1439        self.par_opt_pop.reset_cur_pop_pos();
1440        self.par_opt2_pop.reset_cur_pop_pos();
1441        let init_slice = init.map(|x| x.to_vec());
1442        self.spher.init(&mut self.rnd, init_slice.as_deref(), 1.0);
1443        self.nmseq.init(&mut self.rnd, init_slice.as_deref(), 1.0);
1444        self.use_par_opt = 0;
1445        self.init_queue.clear();
1446        self.asked.clear();
1447        self.deferred_sels.clear();
1448        let sd = 0.25;
1449        let mut members: Vec<Vec<i64>> = vec![vec![0i64; self.param_count]; self.pop_size];
1450        match init {
1451            None => {
1452                for member in members.iter_mut() {
1453                    for slot in member.iter_mut() {
1454                        let g = gaussian_int(&mut self.rnd, sd, INT_MANT_MULT >> 1);
1455                        *slot = wrap_param(&mut self.rnd, g);
1456                    }
1457                }
1458            }
1459            Some(x0) => {
1460                for i in 0..self.param_count {
1461                    let v = ((x0[i] - self.min_values[i]) / self.diff_values[i]) as i64;
1462                    members[0][i] = wrap_param(&mut self.rnd, v);
1463                }
1464                for j in 1..self.pop_size {
1465                    #[allow(clippy::needless_range_loop)]
1466                    for i in 0..self.param_count {
1467                        let mean = members[0][i];
1468                        let g = gaussian_int(&mut self.rnd, sd, mean);
1469                        members[j][i] = wrap_param(&mut self.rnd, g);
1470                    }
1471                }
1472            }
1473        }
1474        self.init_queue = members.into_iter().collect();
1475        self.best_cost = 1e300;
1476        self.stall_count = 0;
1477    }
1478
1479    #[inline]
1480    fn real_value(&self, params: &[i64], i: usize) -> f64 {
1481        self.min_values[i] + self.diff_values[i] * params[i] as f64
1482    }
1483
1484    #[inline]
1485    fn take_tmp(&mut self) -> Vec<i64> {
1486        let mut params = std::mem::take(&mut self.tmp);
1487        params.resize(self.param_count, 0);
1488        params.fill(0);
1489        params
1490    }
1491
1492    fn recycle_candidate(&mut self, mut candidate: Candidate) {
1493        if candidate.enc.capacity() >= self.tmp.capacity() {
1494            candidate.enc.resize(self.param_count, 0);
1495            self.tmp = candidate.enc;
1496        }
1497        if candidate.real.capacity() >= self.real_tmp.capacity() {
1498            candidate.real.resize(self.param_count, 0.0);
1499            self.real_tmp = candidate.real;
1500        }
1501        if candidate.sels.capacity() >= self.apply_sels.capacity() {
1502            candidate.sels.clear();
1503            self.apply_sels = candidate.sels;
1504        }
1505    }
1506
1507    fn select(&mut self, sel_idx: usize) -> i32 {
1508        let value = self.sels[sel_idx].select(&mut self.rnd);
1509        self.apply_sels.push(self.sels[sel_idx].captured(sel_idx));
1510        value
1511    }
1512
1513    fn get_min_sol_index(&mut self, gi: usize, ps: usize) -> usize {
1514        const PP: [f64; 4] = [0.05, 0.125, 0.25, 0.5];
1515        const RM: [f64; 4] = [0.0, 0.125, 0.25, 0.5];
1516        let pwr = self.select(sel::MIN_SOL_PWR + gi) as usize;
1517        let r = ps as f64 * self.rnd.get_pow(ps as f64 * PP[pwr]);
1518        let mul = self.select(sel::MIN_SOL_MUL + gi) as usize;
1519        (r * RM[mul]) as usize
1520    }
1521
1522    fn update_best_cost(&mut self, cost: f64, values: &[f64], p: i64) {
1523        if cost.is_nan() {
1524            return;
1525        }
1526        if p == 0 || (p < 0 && cost <= self.best_cost) {
1527            self.best_cost = cost;
1528            self.best_values.copy_from_slice(values);
1529        }
1530    }
1531
1532    // ---- population selection ----
1533
1534    fn pop_ref(&self, s: PopSel) -> &BitePop {
1535        match s {
1536            PopSel::Main => &self.pop,
1537            PopSel::Par(i) => &self.par_pops[i],
1538            PopSel::ParOpt => &self.par_opt_pop,
1539            PopSel::ParOpt2 => &self.par_opt2_pop,
1540        }
1541    }
1542
1543    fn ordered_of(&self, s: PopSel, i: usize) -> Vec<i64> {
1544        self.pop_ref(s).ordered(i).to_vec()
1545    }
1546
1547    fn cur_pop_size_of(&self, s: PopSel) -> usize {
1548        self.pop_ref(s).cur_pop_size()
1549    }
1550
1551    fn select_par_pop(&mut self, gi: usize) -> PopSel {
1552        if self.select(sel::PAR_POP_P + gi) != 0 {
1553            PopSel::Par(self.rnd.get_int(self.par_pop_count as i32) as usize)
1554        } else {
1555            PopSel::Main
1556        }
1557    }
1558
1559    fn select_alt_pop(&mut self, gi: usize) -> PopSel {
1560        if self.select(sel::ALT_POP_P) != 0 {
1561            if self.select(sel::ALT_POP + gi) != 0 {
1562                if self.par_opt_pop.cur_pop_pos >= self.pop.cur_pop_size() {
1563                    return PopSel::ParOpt;
1564                }
1565            } else if self.par_opt2_pop.cur_pop_pos >= self.pop.cur_pop_size() {
1566                return PopSel::ParOpt2;
1567            }
1568        }
1569        PopSel::Main
1570    }
1571
1572    fn update_par_pop(&mut self, cost: f64, params: &[i64]) {
1573        let p = self.get_min_dist_par_pop(params);
1574        self.par_pops[p].update_pop(cost, params, true);
1575    }
1576
1577    fn get_min_dist_par_pop(&mut self, params: &[i64]) -> usize {
1578        let mut best = 0usize;
1579        let mut best_d = f64::MAX;
1580        for pi in 0..self.par_pop_count {
1581            let c = self.par_pops[pi].get_centroid();
1582            let mut s = 0.0;
1583            for i in 0..self.param_count {
1584                let d = (c[i] - params[i]) as f64;
1585                s += d * d;
1586            }
1587            if s <= best_d {
1588                best_d = s;
1589                best = pi;
1590            }
1591        }
1592        best
1593    }
1594
1595    // ---- solution generators ----
1596
1597    fn generate_sol1(&mut self) {
1598        let par = self.select_par_pop(0);
1599        let par_ps = self.cur_pop_size_of(par);
1600        let si = self.get_min_sol_index(0, par_ps);
1601        let mut params = self.take_tmp();
1602        params.copy_from_slice(self.pop_ref(par).ordered(si));
1603
1604        let mut a;
1605        let mut b;
1606        let mut do_allp = false;
1607        if self.rnd.get() < 1.8 * self.param_count_i && self.select(sel::GEN1_ALLP) != 0 {
1608            do_allp = true;
1609        }
1610        if do_allp {
1611            a = 0;
1612            b = self.param_count;
1613        } else {
1614            a = self.rnd.get_int(self.param_count as i32) as usize;
1615            b = a + 1;
1616        }
1617
1618        let r1 = self.rnd.get();
1619        let r12 = r1 * r1;
1620        let ims = (r12 * r12 * 48.0) as u32;
1621        let imask = INT_MANT_MASK >> ims;
1622        let im2s = self.rnd.get_sqr_int(96);
1623        let imask2 = if im2s > 63 { 0 } else { INT_MANT_MASK >> im2s };
1624        let si1 = (r1 * r12 * par_ps as f64) as usize;
1625        {
1626            let rp1 = self.pop_ref(par).ordered(si1);
1627            for i in a..b {
1628                params[i] = ((params[i] ^ imask) + (rp1[i] ^ imask2)) >> 1;
1629            }
1630        }
1631        if self.rnd.get() < 1.0 - self.param_count_i {
1632            let ri = self.rnd.get_sqr_int(self.pop.cur_pop_size as i32) as usize;
1633            if self.rnd.get() < self.param_count_i.sqrt() && self.select(sel::GEN1_MOVE_ASYNC) != 0
1634            {
1635                a = 0;
1636                b = self.param_count;
1637            }
1638            const SPAN_MULTS: [f64; 4] = [0.5, 1.5, 2.0, 2.5];
1639            let m = SPAN_MULTS[self.select(sel::GEN1_MOVE_SPAN) as usize];
1640            let m1 = self.rnd.get_tpdf() * m;
1641            let m2 = self.rnd.get_tpdf() * m;
1642            let rp2 = self.pop.ordered(ri);
1643            for i in a..b {
1644                params[i] += ((rp2[i] - params[i]) as f64 * m1) as i64;
1645                params[i] += ((rp2[i] - params[i]) as f64 * m2) as i64;
1646            }
1647        }
1648        self.tmp = params;
1649    }
1650
1651    fn generate_sol2(&mut self) {
1652        let ps = self.pop.cur_pop_size;
1653        let ps1 = self.pop.cur_pop_size1;
1654        let si1 = self.get_min_sol_index(1, ps);
1655        let si2 = 1 + self.rnd.get_int(ps1 as i32) as usize;
1656        let si4 = self.rnd.get_sqr_int(ps as i32) as usize;
1657        let mode = self.select(sel::GEN2_MODE);
1658        let si1b = (mode != 0).then(|| self.rnd.get_sqr_int(ps as i32) as usize);
1659        let mut params = self.take_tmp();
1660        let rp1 = self.pop.ordered(si1);
1661        let rp2 = self.pop.ordered(si2);
1662        let rp3 = self.pop.ordered(ps1 - si1);
1663        let rp4 = self.pop.ordered(si4);
1664        let rp5 = self.pop.ordered(ps1 - si4);
1665        if mode == 0 {
1666            for i in 0..self.param_count {
1667                params[i] = rp1[i] + (((rp2[i] - rp3[i]) + (rp4[i] - rp5[i])) >> 1);
1668            }
1669        } else {
1670            let rp1b = self.pop.ordered(si1b.unwrap());
1671            for i in 0..self.param_count {
1672                params[i] = ((rp1[i] + rp1b[i]) + (rp2[i] - rp3[i]) + (rp4[i] - rp5[i])) >> 1;
1673            }
1674        }
1675        self.tmp = params;
1676    }
1677
1678    fn generate_sol2b(&mut self) {
1679        let ps = self.pop.cur_pop_size;
1680        let ps1 = self.pop.cur_pop_size1;
1681        let si1 = self.get_min_sol_index(2, ps);
1682        let si2 = self.rnd.get_int(ps as i32) as usize;
1683        let alt = self.select_alt_pop(0);
1684        let si4 = self.rnd.get_int(ps as i32) as usize;
1685        let mode = self.select(sel::GEN2B_MODE);
1686        let si1b = (mode != 0).then(|| self.rnd.get_sqr_int(ps as i32) as usize);
1687        let mut params = self.take_tmp();
1688        let rp1 = self.pop.ordered(si1);
1689        let rp2 = self.pop.ordered(si2);
1690        let rp3 = self.pop.ordered(ps1 - si2);
1691        let rp4 = self.pop_ref(alt).ordered(si4);
1692        let rp5 = self.pop_ref(alt).ordered(ps1 - si4);
1693        if mode == 0 {
1694            for i in 0..self.param_count {
1695                params[i] = rp1[i] + ((rp2[i] - rp3[i]) + (rp4[i] - rp5[i]));
1696            }
1697        } else {
1698            let rp1b = self.pop.ordered(si1b.unwrap());
1699            for i in 0..self.param_count {
1700                params[i] = ((rp1[i] + rp1b[i]) >> 1) + (rp2[i] - rp3[i]) + (rp4[i] - rp5[i]);
1701            }
1702        }
1703        self.tmp = params;
1704    }
1705
1706    fn generate_sol2c(&mut self) {
1707        let ps = self.pop.cur_pop_size;
1708        let mut params = self.take_tmp();
1709        let si1 = self.rnd.get_pow_int(4.0, (ps / 2) as i32) as usize;
1710        let pc = 7usize; // 1 + 2*PairCount, PairCount=3
1711        let mut pop_idx = [0usize; 7];
1712        pop_idx[0] = si1;
1713        let mut pp = 1;
1714        if self.pop.cur_pop_size1 <= pc {
1715            while pp < pc {
1716                pop_idx[pp] = self.rnd.get_int(ps as i32) as usize;
1717                pp += 1;
1718            }
1719        } else {
1720            while pp < pc {
1721                let sii = self.rnd.get_int(ps as i32) as usize;
1722                if !pop_idx[..pp].contains(&sii) {
1723                    pop_idx[pp] = sii;
1724                    pp += 1;
1725                }
1726            }
1727        }
1728        for i in 0..self.param_count {
1729            params[i] = (self.pop.ordered(pop_idx[1])[i] - self.pop.ordered(pop_idx[2])[i])
1730                + (self.pop.ordered(pop_idx[3])[i] - self.pop.ordered(pop_idx[4])[i])
1731                + (self.pop.ordered(pop_idx[5])[i] - self.pop.ordered(pop_idx[6])[i]);
1732        }
1733        if self.rnd.get_bit() != 0 && self.rnd.get_bit() != 0 {
1734            let k = self.rnd.get_int(self.param_count as i32) as usize;
1735            let v1 = (self.rnd.get_raw()
1736                & self.rnd.get_raw()
1737                & self.rnd.get_raw()
1738                & self.rnd.get_raw()
1739                & self.rnd.get_raw()) as i64
1740                & INT_MANT_MASK;
1741            let v2 = (self.rnd.get_raw()
1742                & self.rnd.get_raw()
1743                & self.rnd.get_raw()
1744                & self.rnd.get_raw()
1745                & self.rnd.get_raw()) as i64
1746                & INT_MANT_MASK;
1747            params[k] += v1 - v2;
1748        }
1749        let mode = self.select(sel::GEN2C_MODE);
1750        if mode == 0 {
1751            let mut si2 = si1 as i64 + self.rnd.get_bit() as i64 * 2 - 1;
1752            if si2 < 0 {
1753                si2 = 1;
1754            }
1755            for i in 0..self.param_count {
1756                params[i] =
1757                    (self.pop.ordered(si1)[i] + self.pop.ordered(si2 as usize)[i] + params[i]) >> 1;
1758            }
1759        } else {
1760            for i in 0..self.param_count {
1761                params[i] = self.pop.ordered(si1)[i] + (params[i] >> 1);
1762            }
1763        }
1764        self.tmp = params;
1765    }
1766
1767    fn generate_sol2d(&mut self) {
1768        if self.old_pop.cur_pop_pos < 3 {
1769            self.generate_sol2c();
1770            return;
1771        }
1772        let ps = self.pop.cur_pop_size;
1773        let i1 = self.rnd.get_sqr_int(ps as i32) as usize;
1774        let i2 = self.rnd.get_int(ps as i32) as usize;
1775        let old_pos = self.old_pop.cur_pop_pos;
1776        let i3 = self.rnd.get_int(old_pos as i32) as usize;
1777        let mode = self.select(sel::GEN2D_MODE);
1778        let i1b = (mode != 0).then(|| self.rnd.get_sqr_int(ps as i32) as usize);
1779        let mut params = self.take_tmp();
1780        let rp1 = self.pop.ordered(i1);
1781        let rp2 = self.pop.ordered(i2);
1782        let rp3 = self.old_pop.ordered(i3);
1783        if mode == 0 {
1784            for i in 0..self.param_count {
1785                params[i] = rp1[i] + ((rp2[i] - rp3[i]) >> 1);
1786            }
1787        } else {
1788            let rp1b = self.pop.ordered(i1b.unwrap());
1789            for i in 0..self.param_count {
1790                params[i] = ((rp1[i] + rp1b[i]) + (rp2[i] - rp3[i])) >> 1;
1791            }
1792        }
1793        self.tmp = params;
1794    }
1795
1796    fn generate_sol4(&mut self) {
1797        let alt = self.select_alt_pop(1);
1798        let par = self.select_par_pop(1);
1799        let use_size = [self.pop.cur_pop_size, self.cur_pop_size_of(par)];
1800        let km = 5 + (self.select(sel::GEN4_MIX_FAC) << 1);
1801        let mut p = self.rnd.get_bit() as usize;
1802        let idx = self.rnd.get_sqr_int(use_size[p] as i32) as usize;
1803        let mut params = self.take_tmp();
1804        params.copy_from_slice(self.pop_ref(if p == 0 { alt } else { par }).ordered(idx));
1805        for _ in 1..km {
1806            p = self.rnd.get_bit() as usize;
1807            let idx = self.rnd.get_sqr_int(use_size[p] as i32) as usize;
1808            let rp = self.pop_ref(if p == 0 { alt } else { par }).ordered(idx);
1809            for i in 0..self.param_count {
1810                params[i] ^= rp[i];
1811            }
1812        }
1813        self.tmp = params;
1814    }
1815
1816    fn generate_sol5(&mut self) {
1817        let par = self.select_par_pop(2);
1818        let par_ps = self.cur_pop_size_of(par);
1819        let si1 = self.rnd.get_sqr_int(par_ps as i32) as usize;
1820        let cp1 = self.ordered_of(par, si1);
1821        let alt = self.select_alt_pop(2);
1822        let si2 = self.rnd.get_sqr_int(self.pop.cur_pop_size as i32) as usize;
1823        let cp2 = self.ordered_of(alt, si2);
1824        let mut params = self.take_tmp();
1825        for i in 0..self.param_count {
1826            let crpl = (self.rnd.get_raw() as i64) & INT_MANT_MASK;
1827            params[i] = (cp1[i] & crpl) | (cp2[i] & !crpl);
1828            let bshift = self.rnd.get_int(INT_MANT_BITS as i32);
1829            params[i] +=
1830                ((self.rnd.get_bit() as i64) << bshift) - ((self.rnd.get_bit() as i64) << bshift);
1831        }
1832        self.tmp = params;
1833    }
1834
1835    fn generate_sol5b(&mut self) {
1836        let par = self.select_par_pop(3);
1837        let par_ps = self.cur_pop_size_of(par);
1838        let i0 = self.rnd.get_sqr_int(par_ps as i32) as usize;
1839        let cp0 = self.ordered_of(par, i0);
1840        let alt = self.select_alt_pop(3);
1841        let ps = self.pop.cur_pop_size;
1842        let ps1 = self.pop.cur_pop_size1;
1843        let cp1 = if self.rnd.get_bit() != 0 {
1844            let i1 = ps1 - self.rnd.get_sqr_int(ps as i32) as usize;
1845            self.ordered_of(alt, i1)
1846        } else {
1847            let i1 = self.rnd.get_sqr_int(ps as i32) as usize;
1848            self.ordered_of(alt, i1)
1849        };
1850        let mode = self.select(sel::GEN5B_MODE);
1851        let mut params = self.take_tmp();
1852        if mode == 0 {
1853            for i in 0..self.param_count {
1854                params[i] = if self.rnd.get_bit() != 0 {
1855                    cp1[i]
1856                } else {
1857                    cp0[i]
1858                };
1859            }
1860        } else {
1861            let i2 = self.rnd.get_sqr_int(par_ps as i32) as usize;
1862            let cp2 = self.ordered_of(par, i2);
1863            let i3 = self.rnd.get_sqr_int(ps as i32) as usize;
1864            let cp3 = self.ordered_of(alt, i3);
1865            let cps = [cp0, cp1, cp2, cp3];
1866            for i in 0..self.param_count {
1867                let sel = ((self.rnd.get_bit() << 1) | self.rnd.get_bit()) as usize;
1868                params[i] = cps[sel][i];
1869            }
1870        }
1871        self.tmp = params;
1872    }
1873
1874    fn generate_sol6(&mut self) {
1875        let ps = self.pop.cur_pop_size;
1876        let r = self.rnd.get_pow(4.0);
1877        let si = (r * ps as f64) as usize;
1878        let mut v = [0.0f64; 2];
1879        let k0 = self.rnd.get_int(self.param_count as i32) as usize;
1880        let use_second = self.rnd.get_bit() != 0;
1881        let k1 = use_second.then(|| self.rnd.get_int(self.param_count as i32) as usize);
1882        let row = self.pop.ordered(si);
1883        v[0] = self.real_value(row, k0);
1884        if let Some(k1) = k1 {
1885            v[1] = self.real_value(row, k1);
1886        } else {
1887            v[1] = v[0];
1888        }
1889        let m = 1.0 - r * r;
1890        v[0] *= m;
1891        v[1] *= m;
1892        let mut params = self.take_tmp();
1893        for i in 0..self.param_count {
1894            let pick = v[self.rnd.get_bit() as usize];
1895            params[i] = ((pick - self.min_values[i]) * self.diff_values_i[i]) as i64;
1896        }
1897        self.tmp = params;
1898    }
1899
1900    fn generate_sol7(&mut self) {
1901        let ps = self.pop.cur_pop_size;
1902        let use_old = self.old_pop.cur_pop_pos > 2;
1903        const P: [f64; 4] = [1.5, 1.75, 2.0, 2.25];
1904        let pwr = P[self.select(sel::GEN7_POW_FAC) as usize];
1905        let mut params = self.take_tmp();
1906        for i in 0..self.param_count {
1907            let rv = self.rnd.get_pow(pwr);
1908            if use_old && self.rnd.get_bit() != 0 && self.rnd.get_bit() != 0 {
1909                let idx = (rv * self.old_pop.cur_pop_pos as f64) as usize;
1910                params[i] = self.old_pop.ordered(idx)[i];
1911            } else {
1912                let idx = (rv * ps as f64) as usize;
1913                params[i] = self.pop.ordered(idx)[i];
1914            }
1915        }
1916        self.tmp = params;
1917    }
1918
1919    fn generate_sol8(&mut self) {
1920        let ps = self.pop.cur_pop_size;
1921        let mode = self.select(sel::GEN8_MODE);
1922        let num_sols = 5 + self.select(sel::GEN8_NUM) as usize;
1923        let mut rp = [0usize; 8];
1924        let first = self.rnd.get_sqr_int(ps as i32) as usize;
1925        rp[0] = first;
1926        let mut params = self.take_tmp();
1927        params.copy_from_slice(self.pop.ordered(first));
1928        for slot in rp.iter_mut().take(num_sols).skip(1) {
1929            let idx = self.rnd.get_sqr_int(ps as i32) as usize;
1930            *slot = idx;
1931            let r0 = self.pop.ordered(idx);
1932            for i in 0..self.param_count {
1933                params[i] = params[i].wrapping_add(r0[i]);
1934            }
1935        }
1936        let m = 1.0 / num_sols as f64;
1937        let mut cent = std::mem::take(&mut self.real_tmp);
1938        cent.resize(self.param_count, 0.0);
1939        for i in 0..self.param_count {
1940            cent[i] = params[i] as f64 * m;
1941            params[i] = cent[i] as i64;
1942        }
1943        if mode == 0 {
1944            const SPANS: [f64; 4] = [1.5, 2.5, 3.5, 4.5];
1945            let gm = SPANS[self.select(sel::GEN8_SPAN) as usize] * m.sqrt();
1946            for &index in &rp[..num_sols] {
1947                let r = self.rnd.get_gaussian() * gm;
1948                let rj = self.pop.ordered(index);
1949                for i in 0..self.param_count {
1950                    params[i] = params[i].wrapping_add(((cent[i] - rj[i] as f64) * r) as i64);
1951                }
1952            }
1953        } else {
1954            const SPANS: [f64; 4] = [0.5, 1.5, 2.5, 3.5];
1955            let gm = SPANS[self.select(sel::GEN8_SPAN + 1) as usize];
1956            for &index in &rp[..num_sols] {
1957                let r = self.rnd.get_gaussian() * gm;
1958                let rj = self.pop.ordered(index);
1959                for i in 0..self.param_count {
1960                    let delta = (params[i].wrapping_sub(rj[i]) as f64 * r) as i64;
1961                    params[i] = params[i].wrapping_add(delta);
1962                }
1963            }
1964        }
1965        self.real_tmp = cent;
1966        self.tmp = params;
1967    }
1968
1969    fn generate_sol9(&mut self) {
1970        let ps = self.pop.cur_pop_size;
1971        let ps1 = self.pop.cur_pop_size1;
1972        let si1 = self.rnd.get_int(ps as i32) as usize;
1973        let si2 = self.rnd.get_sqr_int(ps as i32) as usize;
1974        let subtract = self.rnd.get_bit() != 0;
1975        let mut params = self.take_tmp();
1976        let rp1 = self.pop.ordered(si1);
1977        let rp2 = self.pop.ordered(ps1 - si2);
1978        if subtract {
1979            for i in 0..self.param_count {
1980                params[i] = rp1[i] - ((rp2[i] - rp1[i]) >> 1) * (1 - 2 * self.rnd.get_bit() as i64);
1981            }
1982        } else {
1983            for i in 0..self.param_count {
1984                params[i] = rp1[i] + ((rp2[i] - rp1[i]) >> 1) * (1 - 2 * self.rnd.get_bit() as i64);
1985            }
1986        }
1987        self.tmp = params;
1988    }
1989
1990    fn generate_sol10(&mut self) {
1991        let ps = self.pop.cur_pop_size;
1992        let ps1 = self.pop.cur_pop_size1;
1993        let si1 = self.rnd.get_sqr_int(ps as i32) as usize;
1994        let si2 = self.rnd.get_sqr_int(ps as i32) as usize;
1995        let mut params = self.take_tmp();
1996        {
1997            let rp1 = self.pop.ordered(si1);
1998            let rp2 = self.pop.ordered(ps1 - si2);
1999            for i in 0..self.param_count {
2000                params[i] = (rp1[i] + rp2[i]) >> 1;
2001            }
2002        }
2003        let mut radius = 0.0;
2004        {
2005            let rp1 = self.pop.ordered(si1);
2006            let rp2 = self.pop.ordered(ps1 - si2);
2007            for i in 0..self.param_count {
2008                let v1 = (rp1[i] - params[i]) as f64;
2009                let v2 = (rp2[i] - params[i]) as f64;
2010                radius += v1 * v1 + 0.45 * v2 * v2;
2011            }
2012        }
2013        let mut s2 = 1e-300;
2014        let mut nv = std::mem::take(&mut self.real_tmp);
2015        nv.resize(self.param_count, 0.0);
2016        for n in nv.iter_mut() {
2017            *n = self.rnd.get() - 0.5;
2018            s2 += *n * *n;
2019        }
2020        let d = (radius / s2).sqrt();
2021        for i in 0..self.param_count {
2022            params[i] += (nv[i] * d) as i64;
2023        }
2024        self.real_tmp = nv;
2025        self.tmp = params;
2026    }
2027
2028    fn generate_sol3(&mut self) {
2029        let ps = self.pop.cur_pop_size;
2030        let si1 = self.get_min_sol_index(3, ps);
2031        let si2 = self.rnd.get_sqr_int(ps as i32) as usize;
2032        let mode = self.select(sel::GEN3_MODE);
2033        if mode != 0 && self.pop.need_cent {
2034            self.pop.update_centroid();
2035        }
2036        let mut params = self.take_tmp();
2037        let rp1 = self.pop.ordered(si1);
2038        let rp2 = self.pop.ordered(si2);
2039        if mode == 0 {
2040            for i in 0..self.param_count {
2041                params[i] = rp1[i] + (rp1[i] - rp2[i]);
2042            }
2043        } else {
2044            const CENT_PROB: [f64; 4] = [0.0, 0.25, 0.5, 0.75];
2045            let prob = CENT_PROB[mode as usize];
2046            for i in 0..self.param_count {
2047                params[i] = if self.rnd.get() < prob {
2048                    self.pop.cent[i]
2049                } else {
2050                    rp1[i] + (rp1[i] - rp2[i])
2051                };
2052            }
2053        }
2054        self.tmp = params;
2055    }
2056
2057    /// Generator that draws from an independently-running parallel optimizer.
2058    /// Returns the already-evaluated `(cost, real_values)`.
2059    fn generate_sol_par(&mut self, obj: &dyn Objective) -> (f64, Vec<f64>) {
2060        if self.use_par_opt == 1 {
2061            self.use_par_opt = self.select(sel::PAR_OPT2);
2062        }
2063        let step;
2064        let which_pop;
2065        if self.use_par_opt == 0 {
2066            step = self.spher.optimize(&mut self.rnd, obj);
2067            if step.stall > 0 {
2068                self.use_par_opt = 1;
2069            }
2070            if step.stall > self.param_count as i64 * 64 {
2071                let best = self.best_values.clone();
2072                self.spher.init(&mut self.rnd, Some(&best), 0.5);
2073                self.par_opt_pop.reset_cur_pop_pos();
2074            }
2075            which_pop = 0;
2076        } else {
2077            step = self.nmseq.optimize(&mut self.rnd, obj);
2078            if step.stall > 0 {
2079                self.use_par_opt = 0;
2080            }
2081            if step.stall > self.param_count as i64 * 16 {
2082                let best = self.best_values.clone();
2083                self.nmseq.init(&mut self.rnd, Some(&best), 1.0);
2084                self.par_opt2_pop.reset_cur_pop_pos();
2085            }
2086            which_pop = 1;
2087        }
2088        let mut tmp = self.take_tmp();
2089        for i in 0..self.param_count {
2090            tmp[i] = ((step.values[i] - self.min_values[i]) * self.diff_values_i[i]) as i64;
2091        }
2092        if which_pop == 0 {
2093            self.par_opt_pop.update_pop(step.cost, &tmp, false);
2094        } else {
2095            self.par_opt2_pop.update_pop(step.cost, &tmp, false);
2096        }
2097        self.tmp = tmp;
2098        (step.cost, step.values)
2099    }
2100
2101    /// Route the method-selection tree to a generator. Returns a precomputed
2102    /// cost and exact values when the parallel-optimizer generator (SolPar)
2103    /// was used; `None`
2104    /// otherwise (the caller then evaluates). `obj` is `None` in ask/tell mode,
2105    /// where the method selector is resampled to an ask/tell-compatible
2106    /// generator without registering a second selector use.
2107    fn generate(&mut self, obj: Option<&dyn Objective>) -> Option<(f64, Vec<f64>)> {
2108        let mut method = self.select(sel::METHOD);
2109        while method == 3 && obj.is_none() {
2110            method = self.sels[sel::METHOD].select(&mut self.rnd);
2111        }
2112        if obj.is_none() {
2113            let final_method = self.sels[sel::METHOD].captured(sel::METHOD);
2114            if let Some(selection) = self
2115                .apply_sels
2116                .iter_mut()
2117                .find(|selection| selection.index == sel::METHOD)
2118            {
2119                *selection = final_method;
2120            }
2121        }
2122        match method {
2123            0 => self.generate_sol2(),
2124            1 => {
2125                let m1 = self.select(sel::M1);
2126                match m1 {
2127                    0 => {
2128                        let m1a = self.select(sel::M1A);
2129                        match m1a {
2130                            0 => self.generate_sol2b(),
2131                            1 => self.generate_sol2c(),
2132                            _ => self.generate_sol2d(),
2133                        }
2134                    }
2135                    1 => {
2136                        if self.select(sel::M1B) != 0 {
2137                            self.generate_sol4();
2138                        } else {
2139                            self.generate_sol5b();
2140                        }
2141                    }
2142                    2 => {
2143                        if self.select(sel::M1C) != 0 {
2144                            self.generate_sol5();
2145                        } else {
2146                            self.generate_sol10();
2147                        }
2148                    }
2149                    _ => self.generate_sol6(),
2150                }
2151            }
2152            2 => {
2153                if self.select(sel::M2) != 0 {
2154                    self.generate_sol1();
2155                } else {
2156                    let m2b = self.select(sel::M2B);
2157                    match m2b {
2158                        0 => self.generate_sol3(),
2159                        1 => self.generate_sol7(),
2160                        2 => self.generate_sol8(),
2161                        _ => self.generate_sol9(),
2162                    }
2163                }
2164            }
2165            _ => {
2166                let (cost, real) =
2167                    self.generate_sol_par(obj.expect("parallel generator requires an objective"));
2168                return Some((cost, real));
2169            }
2170        }
2171        None
2172    }
2173
2174    fn in_init(&self) -> bool {
2175        !self.init_queue.is_empty()
2176    }
2177
2178    /// Generate one candidate from the current (frozen) population state.
2179    /// `obj` enables the SolPar generator (parallel optimizers); `None` in
2180    /// ask/tell mode.
2181    fn gen_one(&mut self, obj: Option<&dyn Objective>) -> Candidate {
2182        if let Some(enc) = self.init_queue.pop_front() {
2183            let mut real = std::mem::take(&mut self.real_tmp);
2184            real.resize(self.param_count, 0.0);
2185            for i in 0..self.param_count {
2186                real[i] = self.real_value(&enc, i);
2187            }
2188            return Candidate {
2189                enc,
2190                real,
2191                sels: vec![],
2192                is_init: true,
2193                precomputed_cost: None,
2194            };
2195        }
2196        self.apply_sels.clear();
2197        if let Some(selection) = self.deferred_sels.pop_front() {
2198            self.apply_sels.push(selection);
2199        }
2200        let precomputed = self.generate(obj);
2201        let mut enc = std::mem::take(&mut self.tmp);
2202        for e in enc.iter_mut() {
2203            *e = wrap_param(&mut self.rnd, *e);
2204        }
2205        let (precomputed_cost, real) = match precomputed {
2206            Some((cost, values)) => (Some(cost), values),
2207            None => {
2208                let mut values = std::mem::take(&mut self.real_tmp);
2209                values.resize(self.param_count, 0.0);
2210                for i in 0..self.param_count {
2211                    values[i] = self.real_value(&enc, i);
2212                }
2213                (None, values)
2214            }
2215        };
2216        let sels = std::mem::take(&mut self.apply_sels);
2217        Candidate {
2218            enc,
2219            real,
2220            sels,
2221            is_init: false,
2222            precomputed_cost,
2223        }
2224    }
2225
2226    fn select_deferred(&mut self, index: usize) -> i32 {
2227        let value = self.sels[index].select(&mut self.rnd);
2228        self.deferred_sels
2229            .push_back(self.sels[index].captured(index));
2230        value
2231    }
2232
2233    /// Apply a candidate's evaluated cost, updating population and selectors.
2234    /// Returns whether the solution is push-worthy (inserted at
2235    /// `0 < p <= cur_pop_size1`) for the deep layer.
2236    fn apply_one(&mut self, cand: &Candidate, cost: f64, collect_push: bool) -> bool {
2237        let cost = sanitize_cost(cost);
2238        if cand.is_init {
2239            let p = self.pop.update_pop(cost, &cand.enc, false) as i64;
2240            self.update_best_cost(cost, &cand.real, p);
2241            if self.init_queue.is_empty() && self.pop.cur_pop_pos == self.pop_size {
2242                self.pop.update_centroid();
2243                // Seed the diverging parallel populations from the main one.
2244                for parallel in &mut self.par_pops {
2245                    parallel.copy_from(&self.pop);
2246                }
2247            }
2248            return false;
2249        }
2250        let do_eval = cand.precomputed_cost.is_none();
2251        let p = self.pop.update_pop(cost, &cand.enc, true);
2252        let mut push = false;
2253        if p > self.pop.cur_pop_size1 {
2254            for &selection in &cand.sels {
2255                self.sels[selection.index].decr_captured(selection);
2256            }
2257            self.stall_count += 1;
2258            // Dynamic population sizing: grow on failure.
2259            if do_eval
2260                && self.pop.cur_pop_size < self.pop_size
2261                && self.select_deferred(sel::POP_CHANGE_INCR) != 0
2262            {
2263                self.pop.incr_cur_pop_size();
2264            }
2265        } else {
2266            self.update_best_cost(cost, &cand.real, p as i64);
2267            let v = 1.0 - p as f64 * self.pop.cur_pop_size_i;
2268            for &selection in &cand.sels {
2269                self.sels[selection.index].incr_captured(selection, v);
2270            }
2271            self.stall_count = 0;
2272            if collect_push && p > 0 {
2273                push = true;
2274            }
2275            // Probabilistically push the current worst into OldPop.
2276            if self.rnd.get() < self.param_count_i {
2277                let w = self.pop.cur_pop_size1;
2278                let worst_cost = self.pop.costs[w];
2279                self.old_pop
2280                    .update_pop(worst_cost, self.pop.ordered(w), false);
2281            }
2282            // Dynamic population sizing: shrink on success.
2283            if do_eval
2284                && self.pop.cur_pop_size > self.pop_size / 2
2285                && self.select_deferred(sel::POP_CHANGE_DECR) != 0
2286            {
2287                self.pop.decr_cur_pop_size();
2288            }
2289        }
2290        // "Diverging populations" technique: feed the nearest parallel pop.
2291        self.update_par_pop(cost, &cand.enc);
2292        push
2293    }
2294
2295    /// Push an improving solution into this optimizer (deep-mode PushOpt).
2296    fn push_solution(&mut self, cost: f64, enc: &[i64]) {
2297        if self.in_init() {
2298            return;
2299        }
2300        self.pop.update_pop(cost, enc, true);
2301        self.update_par_pop(cost, enc);
2302    }
2303
2304    /// One optimization iteration (1 objective evaluation).
2305    fn optimize_step(&mut self, obj: &impl Objective) {
2306        let cand = self.gen_one(Some(obj));
2307        let cost = cand
2308            .precomputed_cost
2309            .unwrap_or_else(|| objective_cost(obj, &cand.real));
2310        self.apply_one(&cand, cost, false);
2311        self.recycle_candidate(cand);
2312    }
2313
2314    /// Deep-mode step: generate + evaluate + apply, returning the stall count
2315    /// and any push-worthy `(cost, enc)` for the PushOpt.
2316    fn step_collect(
2317        &mut self,
2318        obj: &impl Objective,
2319        collect_push: bool,
2320    ) -> (i64, Option<(f64, Vec<i64>)>) {
2321        let mut cand = self.gen_one(Some(obj));
2322        let cost = cand
2323            .precomputed_cost
2324            .unwrap_or_else(|| objective_cost(obj, &cand.real));
2325        let should_push = self.apply_one(&cand, cost, collect_push);
2326        let push = should_push.then(|| (cost, std::mem::take(&mut cand.enc)));
2327        self.iterations += 1;
2328        self.evaluations += 1;
2329        self.recycle_candidate(cand);
2330        (self.stall_count, push)
2331    }
2332
2333    fn best_cost(&self) -> f64 {
2334        self.best_cost
2335    }
2336
2337    fn in_init_phase(&self) -> bool {
2338        self.in_init()
2339    }
2340
2341    fn init_remaining(&self) -> usize {
2342        self.init_queue.len()
2343    }
2344
2345    // ---- ask/tell interface (delayed-feedback batching) ----
2346
2347    /// Ask for up to `batch` candidate rows (real-valued). During the initial
2348    /// population fill the batch is capped to the remaining init members.
2349    pub fn ask(&mut self, batch: usize) -> Vec<Vec<f64>> {
2350        if !self.asked.is_empty() {
2351            return self
2352                .asked
2353                .iter()
2354                .map(|candidate| candidate.real.clone())
2355                .collect();
2356        }
2357        if batch == 0 || self.stop != 0 || self.evaluations >= self.max_evaluations {
2358            return Vec::new();
2359        }
2360        let remaining_budget = (self.max_evaluations - self.evaluations) as usize;
2361        let requested = batch.min(remaining_budget);
2362        let n = if self.in_init() {
2363            requested.min(self.init_queue.len())
2364        } else {
2365            requested
2366        };
2367        for _ in 0..n {
2368            let cand = self.gen_one(None);
2369            self.asked.push(cand);
2370        }
2371        self.asked.iter().map(|c| c.real.clone()).collect()
2372    }
2373
2374    /// Tell the costs for the batch returned by [`ask`](BiteOpt::ask).
2375    pub fn tell(&mut self, costs: &[f64]) -> i32 {
2376        if self.asked.is_empty() || costs.len() != self.asked.len() {
2377            return -1;
2378        }
2379        let asked = std::mem::take(&mut self.asked);
2380        for (cand, &cost) in asked.into_iter().zip(costs) {
2381            self.apply_one(&cand, cost, false);
2382            self.iterations += 1;
2383            self.evaluations += 1;
2384            self.recycle_candidate(cand);
2385        }
2386        self.update_stop();
2387        self.stop
2388    }
2389
2390    /// Number of candidates awaiting a matching [`tell`](Self::tell).
2391    pub fn current_batch_size(&self) -> usize {
2392        self.asked.len()
2393    }
2394    /// Number of decision variables.
2395    pub fn dim(&self) -> usize {
2396        self.param_count
2397    }
2398    /// Size of the principal adaptive population.
2399    pub fn population_size(&self) -> usize {
2400        self.pop_size
2401    }
2402    /// Current termination code, or zero while optimization can continue.
2403    pub fn stop_code(&self) -> i32 {
2404        self.stop
2405    }
2406    /// Return a snapshot of the best result found so far.
2407    pub fn result_public(&self) -> BiteResult {
2408        self.result()
2409    }
2410
2411    fn update_stop(&mut self) {
2412        if self.best_cost < self.stopfitness {
2413            self.stop = 1;
2414        } else if self.stall_criterion > 0
2415            && self.stall_count > self.stall_criterion as i64 * 128 * self.param_count as i64
2416        {
2417            self.stop = 2;
2418        }
2419    }
2420
2421    /// Run until the evaluation budget or a stop condition is reached.
2422    pub fn optimize(&mut self, obj: &impl Objective) -> BiteResult {
2423        while self.evaluations < self.max_evaluations && self.stop == 0 {
2424            self.optimize_step(obj);
2425            self.iterations += 1;
2426            self.evaluations += 1;
2427            self.update_stop();
2428        }
2429        self.result()
2430    }
2431
2432    fn result(&self) -> BiteResult {
2433        BiteResult {
2434            x: self.best_values.clone(),
2435            y: self.best_cost,
2436            evaluations: self.evaluations,
2437            iterations: self.iterations,
2438            stop: self.stop,
2439        }
2440    }
2441}
2442
2443// ---------------------------------------------------------------------------
2444// Deep multi-population layer (port of CBiteOptDeep / CBiteOptDeepAT)
2445// ---------------------------------------------------------------------------
2446
2447/// `M` BiteOpt instances with solution "pushing" between them. `M == 1` is a
2448/// plain single-population BiteOpt.
2449pub struct DeepBiteOpt {
2450    opts: Vec<BiteOpt>,
2451    m: usize,
2452    cur_opt: usize,
2453    push_opt: usize,
2454    best_opt: usize,
2455    at_stall_count: i64,
2456    batch_cur_opt: usize,
2457    rnd: BiteRnd,
2458    max_evaluations: u64,
2459    stopfitness: f64,
2460    stall_criterion: i32,
2461    param_count: usize,
2462    evaluations: u64,
2463    iterations: i32,
2464    stop: i32,
2465}
2466
2467impl DeepBiteOpt {
2468    /// Construct `m` communicating BiteOpt populations.
2469    ///
2470    /// Values of `m` below one select one population; the maximum is 36.
2471    ///
2472    /// # Panics
2473    ///
2474    /// Panics on any configuration [`validate_bite_inputs`] rejects, including
2475    /// `m` above 36.
2476    pub fn new(lower: &[f64], upper: &[f64], init: Option<&[f64]>, p: &BiteParams, m: i32) -> Self {
2477        validate_bite_inputs(lower, upper, init, p, m).expect("invalid deep BiteOpt configuration");
2478        let m = m.max(1) as usize;
2479        let opts: Vec<BiteOpt> = (0..m)
2480            .map(|i| {
2481                let mut pi = p.clone();
2482                // Distinct per-optimizer RNG streams for diversity.
2483                pi.runid = p.runid.wrapping_add((i as i64).wrapping_mul(0x9E3779B1));
2484                BiteOpt::new(lower, upper, init, &pi)
2485            })
2486            .collect();
2487        let mut d = DeepBiteOpt {
2488            opts,
2489            m,
2490            cur_opt: 0,
2491            push_opt: 0,
2492            best_opt: 0,
2493            at_stall_count: 0,
2494            batch_cur_opt: 0,
2495            rnd: BiteRnd::new(p.seed.wrapping_add(p.runid as u64).wrapping_add(0xB17E)),
2496            max_evaluations: if p.max_evaluations > 0 {
2497                p.max_evaluations
2498            } else {
2499                50_000
2500            },
2501            stopfitness: p.stop_fitness,
2502            stall_criterion: p.stall_criterion.max(0),
2503            param_count: lower.len(),
2504            evaluations: 0,
2505            iterations: 0,
2506            stop: 0,
2507        };
2508        d.pick_push();
2509        d
2510    }
2511
2512    fn pick_push(&mut self) {
2513        if self.m == 1 {
2514            self.push_opt = self.cur_opt;
2515        } else if self.m == 2 {
2516            self.push_opt = 1 - self.cur_opt;
2517        } else {
2518            loop {
2519                let p = self.rnd.get_int(self.m as i32) as usize;
2520                if p != self.cur_opt {
2521                    self.push_opt = p;
2522                    break;
2523                }
2524            }
2525        }
2526    }
2527
2528    fn total_evaluations(&self) -> u64 {
2529        self.evaluations
2530    }
2531
2532    fn best(&self) -> &BiteOpt {
2533        &self.opts[self.best_opt]
2534    }
2535
2536    fn update_stop(&mut self) {
2537        if self.best().best_cost < self.stopfitness {
2538            self.stop = 1;
2539        } else if self.stall_criterion > 0
2540            && self.at_stall_count > self.stall_criterion as i64 * 128 * self.param_count as i64
2541        {
2542            self.stop = 2;
2543        }
2544    }
2545
2546    fn track_best(&mut self, idx: usize) {
2547        if self.opts[idx].best_cost() <= self.opts[self.best_opt].best_cost() {
2548            self.best_opt = idx;
2549        }
2550    }
2551
2552    /// One-shot optimization until the (total) evaluation budget or a stop.
2553    pub fn optimize(&mut self, obj: &impl Objective) -> BiteResult {
2554        while self.total_evaluations() < self.max_evaluations && self.stop == 0 {
2555            self.pick_push();
2556            let opt_idx = self.cur_opt;
2557            let (sc, push) = self.opts[opt_idx].step_collect(obj, self.m > 1);
2558            self.evaluations += 1;
2559            self.iterations += 1;
2560            if let Some((cost, enc)) = push {
2561                self.opts[self.push_opt].push_solution(cost, &enc);
2562                self.opts[opt_idx].tmp = enc;
2563            }
2564            self.track_best(opt_idx);
2565            if sc == 0 {
2566                self.at_stall_count = 0;
2567            } else {
2568                self.cur_opt = self.push_opt;
2569                self.at_stall_count += 1;
2570            }
2571            self.update_stop();
2572        }
2573        self.result()
2574    }
2575
2576    /// Ask for up to `batch` candidate rows from the current optimizer.
2577    pub fn ask(&mut self, batch: usize) -> Vec<Vec<f64>> {
2578        if self.current_batch_size() != 0 {
2579            return self.opts[self.batch_cur_opt]
2580                .asked
2581                .iter()
2582                .map(|candidate| candidate.real.clone())
2583                .collect();
2584        }
2585        let evaluations = self.total_evaluations();
2586        if batch == 0 || self.stop != 0 || evaluations >= self.max_evaluations {
2587            return Vec::new();
2588        }
2589        let mut b = batch.min((self.max_evaluations - evaluations) as usize);
2590        if self.opts[self.cur_opt].in_init_phase() {
2591            let rem = self.opts[self.cur_opt].init_remaining();
2592            if b > rem {
2593                b = rem.max(1);
2594            }
2595        }
2596        self.batch_cur_opt = self.cur_opt;
2597        self.opts[self.cur_opt].ask(b)
2598    }
2599
2600    /// Tell the costs for the last [`ask`](DeepBiteOpt::ask). Candidates are
2601    /// applied best-first, with solution pushing and optimizer switching.
2602    pub fn tell(&mut self, costs: &[f64]) -> i32 {
2603        let opt_idx = self.batch_cur_opt;
2604        if costs.len() != self.opts[opt_idx].asked.len() || costs.is_empty() {
2605            return -1;
2606        }
2607        let mut asked = std::mem::take(&mut self.opts[opt_idx].asked);
2608        let mut order: Vec<usize> = (0..asked.len()).collect();
2609        order.sort_by(|&a, &b| {
2610            let ca = sanitize_cost(costs[a]);
2611            let cb = sanitize_cost(costs[b]);
2612            ca.partial_cmp(&cb)
2613                .unwrap_or(std::cmp::Ordering::Equal)
2614                .then(a.cmp(&b))
2615        });
2616        for &i in &order {
2617            let push = self.opts[opt_idx].apply_one(&asked[i], costs[i], self.m > 1);
2618            self.opts[opt_idx].iterations += 1;
2619            self.opts[opt_idx].evaluations += 1;
2620            self.iterations += 1;
2621            self.evaluations += 1;
2622            let sc = self.opts[opt_idx].stall_count;
2623            if push {
2624                self.opts[self.push_opt].push_solution(sanitize_cost(costs[i]), &asked[i].enc);
2625            }
2626            self.track_best(opt_idx);
2627            if self.m > 1 {
2628                if sc == 0 {
2629                    self.at_stall_count = 0;
2630                } else {
2631                    self.at_stall_count += 1;
2632                    self.cur_opt = self.push_opt;
2633                    self.pick_push();
2634                }
2635            } else {
2636                self.at_stall_count = sc;
2637            }
2638        }
2639        if let Some(candidate) = asked.pop() {
2640            self.opts[opt_idx].recycle_candidate(candidate);
2641        }
2642        self.update_stop();
2643        self.stop
2644    }
2645
2646    /// Number of decision variables.
2647    pub fn dim(&self) -> usize {
2648        self.param_count
2649    }
2650    /// Size of each constituent BiteOpt population.
2651    pub fn population_size(&self) -> usize {
2652        self.opts[0].population_size()
2653    }
2654    /// Number of candidates awaiting a matching [`tell`](Self::tell).
2655    pub fn current_batch_size(&self) -> usize {
2656        self.opts[self.batch_cur_opt].current_batch_size()
2657    }
2658    /// Current termination code, or zero while optimization can continue.
2659    pub fn stop_code(&self) -> i32 {
2660        self.stop
2661    }
2662
2663    /// Return a snapshot of the best result across all populations.
2664    pub fn result(&self) -> BiteResult {
2665        let b = self.best();
2666        BiteResult {
2667            x: b.best_values.clone(),
2668            y: b.best_cost,
2669            evaluations: self.evaluations,
2670            iterations: self.iterations,
2671            stop: self.stop,
2672        }
2673    }
2674
2675    /// Compatibility alias for [`result`](Self::result).
2676    pub fn result_public(&self) -> BiteResult {
2677        self.result()
2678    }
2679}
2680
2681/// Run BiteOpt on a bounded problem. `m` is the "deep" depth (number of
2682/// populations); `m <= 1` is a plain single-population run.
2683pub fn optimize_bite(
2684    obj: &impl Objective,
2685    lower: &[f64],
2686    upper: &[f64],
2687    init: Option<&[f64]>,
2688    p: &BiteParams,
2689    m: i32,
2690) -> BiteResult {
2691    let mut opt = DeepBiteOpt::new(lower, upper, init, p, m);
2692    opt.optimize(obj)
2693}
2694
2695#[cfg(test)]
2696mod tests {
2697    use super::*;
2698
2699    fn sphere(x: &[f64]) -> f64 {
2700        x.iter().map(|v| v * v).sum()
2701    }
2702    fn rosen(x: &[f64]) -> f64 {
2703        (0..x.len() - 1)
2704            .map(|i| 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2))
2705            .sum()
2706    }
2707    fn rastrigin(x: &[f64]) -> f64 {
2708        let n = x.len() as f64;
2709        10.0 * n
2710            + x.iter()
2711                .map(|v| v * v - 10.0 * (2.0 * std::f64::consts::PI * v).cos())
2712                .sum::<f64>()
2713    }
2714
2715    fn run(obj: impl Objective, dim: usize, seed: u64, evals: u64) -> f64 {
2716        let params = BiteParams {
2717            max_evaluations: evals,
2718            seed,
2719            ..Default::default()
2720        };
2721        optimize_bite(&obj, &vec![-5.0; dim], &vec![5.0; dim], None, &params, 1).y
2722    }
2723
2724    #[test]
2725    fn deep_minimizes_rosenbrock() {
2726        let params = BiteParams {
2727            max_evaluations: 30000,
2728            seed: 4,
2729            ..Default::default()
2730        };
2731        // depth M = 3
2732        let r = optimize_bite(
2733            &(rosen as fn(&[f64]) -> f64),
2734            &[-5.0; 6],
2735            &[5.0; 6],
2736            None,
2737            &params,
2738            3,
2739        );
2740        assert!(r.y < 1e-2, "deep rosen: {}", r.y);
2741    }
2742
2743    #[test]
2744    fn minimizes_sphere() {
2745        assert!(run(sphere as fn(&[f64]) -> f64, 5, 1, 15000) < 1e-6);
2746    }
2747
2748    #[test]
2749    fn minimizes_rosenbrock() {
2750        let mut v: Vec<f64> = (0..5)
2751            .map(|s| run(rosen as fn(&[f64]) -> f64, 5, s, 30000))
2752            .collect();
2753        v.sort_by(|a, b| a.partial_cmp(b).unwrap());
2754        assert!(v[2] < 1e-2, "rosen median too large: {v:?}");
2755    }
2756
2757    #[test]
2758    fn minimizes_rastrigin() {
2759        let mut v: Vec<f64> = (0..5)
2760            .map(|s| run(rastrigin as fn(&[f64]) -> f64, 5, s, 30000))
2761            .collect();
2762        v.sort_by(|a, b| a.partial_cmp(b).unwrap());
2763        assert!(v[2] < 5.0, "rastrigin median too large: {v:?}");
2764    }
2765
2766    #[test]
2767    fn ask_tell_converges() {
2768        let params = BiteParams {
2769            max_evaluations: 15000,
2770            seed: 7,
2771            ..Default::default()
2772        };
2773        let mut opt = BiteOpt::new(&[-5.0; 5], &[5.0; 5], None, &params);
2774        while opt.evaluations < 15000 && opt.stop == 0 {
2775            let xs = opt.ask(8);
2776            let ys: Vec<f64> = xs.iter().map(|x| sphere(x)).collect();
2777            opt.tell(&ys);
2778        }
2779        assert!(
2780            opt.result_public().y < 1e-4,
2781            "ask/tell: {}",
2782            opt.result_public().y
2783        );
2784    }
2785
2786    #[test]
2787    fn validates_configuration() {
2788        let params = BiteParams::default();
2789        assert!(validate_bite_inputs(&[], &[], None, &params, 1).is_err());
2790        assert!(validate_bite_inputs(&[0.0], &[1.0, 2.0], None, &params, 1).is_err());
2791        assert!(validate_bite_inputs(&[1.0], &[1.0], None, &params, 1).is_err());
2792        assert!(validate_bite_inputs(&[2.0], &[1.0], None, &params, 1).is_err());
2793        assert!(validate_bite_inputs(&[f64::NAN], &[1.0], None, &params, 1).is_err());
2794        assert!(validate_bite_inputs(&[0.0], &[f64::INFINITY], None, &params, 1).is_err());
2795        assert!(validate_bite_inputs(&[-f64::MAX], &[f64::MAX], None, &params, 1).is_err());
2796        assert!(validate_bite_inputs(&[0.0], &[1.0], Some(&[0.5, 0.5]), &params, 1).is_err());
2797        assert!(validate_bite_inputs(&[0.0], &[1.0], Some(&[f64::NAN]), &params, 1).is_err());
2798        assert!(validate_bite_inputs(&[0.0], &[1.0], None, &params, 37).is_err());
2799
2800        let invalid_pop = BiteParams {
2801            popsize: 3,
2802            ..Default::default()
2803        };
2804        assert!(validate_bite_inputs(&[0.0], &[1.0], None, &invalid_pop, 1).is_err());
2805        let invalid_stop = BiteParams {
2806            stop_fitness: f64::NAN,
2807            ..Default::default()
2808        };
2809        assert!(validate_bite_inputs(&[0.0], &[1.0], None, &invalid_stop, 1).is_err());
2810
2811        // The upstream API treats non-positive depth and population size as
2812        // requests for their defaults.
2813        let defaults = BiteParams {
2814            popsize: -1,
2815            ..Default::default()
2816        };
2817        assert!(validate_bite_inputs(&[0.0], &[1.0], Some(&[0.5]), &defaults, -1).is_ok());
2818    }
2819
2820    #[test]
2821    fn helper_edge_paths_are_bounded_and_stable() {
2822        let mut rnd = BiteRnd::new(101);
2823        for value in [
2824            wrap_param(&mut rnd, -INT_MANT_MULT * 2),
2825            wrap_param(&mut rnd, INT_MANT_MULT * 3),
2826        ] {
2827            assert!((0..=INT_MANT_MULT).contains(&value));
2828        }
2829        for value in [wrap01(&mut rnd, -2.0), wrap01(&mut rnd, 3.0)] {
2830            assert!((0.0..=1.0).contains(&value));
2831        }
2832        for value in [
2833            wrap_param_real(&mut rnd, -30.0, -5.0, 10.0),
2834            wrap_param_real(&mut rnd, 30.0, -5.0, 10.0),
2835        ] {
2836            assert!((-5.0..=5.0).contains(&value));
2837        }
2838
2839        let mut population = BitePop::new(2, 4);
2840        population.update_pop(f64::NAN, &[1, 2], false);
2841        assert_eq!(population.costs[0], BAD_COST);
2842        population.need_cent = true;
2843        assert_eq!(population.get_centroid().len(), 2);
2844
2845        let mut selector = BiteSel::new(2);
2846        selector.reset(&mut rnd, 2);
2847        let fallback = SelUse {
2848            index: 0,
2849            value: selector.sels[0][0],
2850            position: 0,
2851            slot_id: selector.slot_ids[0],
2852            entry_id: u8::MAX,
2853        };
2854        selector.restore(fallback);
2855        assert_eq!(selector.sel, fallback.value);
2856    }
2857
2858    #[test]
2859    fn direct_driver_initial_guess_defaults_and_getters() {
2860        let params = BiteParams {
2861            max_evaluations: 20,
2862            stop_fitness: f64::INFINITY,
2863            seed: 102,
2864            ..Default::default()
2865        };
2866        let mut opt = BiteOpt::new(&[-1.0; 2], &[1.0; 2], Some(&[0.2, -0.2]), &params);
2867        assert_eq!(opt.dim(), 2);
2868        assert_eq!(opt.population_size(), 15);
2869        assert_eq!(opt.stop_code(), 0);
2870        let result = opt.optimize(&(sphere as fn(&[f64]) -> f64));
2871        assert_eq!(result.evaluations, 1);
2872        assert_eq!(result.stop, 1);
2873
2874        let default_budget = BiteParams {
2875            max_evaluations: 0,
2876            ..Default::default()
2877        };
2878        let opt = BiteOpt::new(&[-1.0], &[1.0], None, &default_budget);
2879        assert_eq!(opt.max_evaluations, 50_000);
2880        let mut deep = DeepBiteOpt::new(&[-1.0], &[1.0], None, &default_budget, 1);
2881        assert_eq!(deep.max_evaluations, 50_000);
2882        assert_eq!(deep.dim(), 1);
2883        assert_eq!(deep.population_size(), 12);
2884        assert_eq!(deep.stop_code(), 0);
2885        let asked = deep.ask(2);
2886        assert_eq!(deep.ask(5), asked);
2887    }
2888
2889    #[test]
2890    fn ask_tell_enforces_batches_and_budget() {
2891        let params = BiteParams {
2892            popsize: 4,
2893            max_evaluations: 5,
2894            seed: 11,
2895            ..Default::default()
2896        };
2897        let mut opt = BiteOpt::new(&[-1.0; 2], &[1.0; 2], None, &params);
2898
2899        assert!(opt.ask(0).is_empty());
2900        assert_eq!(opt.tell(&[0.0]), -1);
2901
2902        let first = opt.ask(3);
2903        assert_eq!(first.len(), 3);
2904        assert_eq!(opt.ask(99), first);
2905        assert_eq!(opt.tell(&[1.0, 2.0]), -1);
2906        assert_eq!(opt.current_batch_size(), 3);
2907        assert_eq!(opt.tell(&[1.0, 2.0, 3.0]), 0);
2908
2909        // Initial population fill is not mixed with generated candidates.
2910        let second = opt.ask(8);
2911        assert_eq!(second.len(), 1);
2912        assert_eq!(opt.tell(&[4.0]), 0);
2913        let last = opt.ask(8);
2914        assert_eq!(last.len(), 1);
2915        opt.tell(&[5.0]);
2916
2917        assert!(opt.ask(8).is_empty());
2918        let result = opt.result_public();
2919        assert_eq!(result.evaluations, 5);
2920        assert_eq!(result.iterations, 5);
2921    }
2922
2923    #[test]
2924    fn ask_tell_records_the_resampled_compatible_method() {
2925        let params = BiteParams {
2926            popsize: 4,
2927            max_evaluations: 300,
2928            seed: 111,
2929            ..Default::default()
2930        };
2931        let mut opt = BiteOpt::new(&[-1.0; 2], &[1.0; 2], None, &params);
2932        let initial = opt.ask(4);
2933        opt.tell(&vec![1.0; initial.len()]);
2934
2935        for cost in 2..250 {
2936            let cand = opt.gen_one(None);
2937            let method = cand
2938                .sels
2939                .iter()
2940                .find(|selection| selection.index == sel::METHOD)
2941                .unwrap();
2942            assert_ne!(method.value, 3);
2943            opt.apply_one(&cand, cost as f64, false);
2944            opt.recycle_candidate(cand);
2945        }
2946    }
2947
2948    #[test]
2949    fn deep_ask_tell_enforces_budget_and_sanitizes_costs() {
2950        let params = BiteParams {
2951            popsize: 4,
2952            max_evaluations: 7,
2953            seed: 12,
2954            ..Default::default()
2955        };
2956        let mut opt = DeepBiteOpt::new(&[-1.0; 2], &[1.0; 2], None, &params, 3);
2957
2958        assert_eq!(opt.tell(&[0.0]), -1);
2959        let first = opt.ask(9);
2960        assert_eq!(first.len(), 4);
2961        assert_eq!(opt.tell(&[f64::NAN, f64::INFINITY, f64::NEG_INFINITY]), -1);
2962        assert_eq!(opt.current_batch_size(), 4);
2963        assert_eq!(
2964            opt.tell(&[f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::NAN]),
2965            0
2966        );
2967
2968        let second = opt.ask(9);
2969        assert_eq!(second.len(), 3);
2970        opt.tell(&[3.0, 2.0, 1.0]);
2971        assert!(opt.ask(1).is_empty());
2972
2973        let result = opt.result_public();
2974        assert_eq!(result.evaluations, 7);
2975        assert_eq!(result.iterations, 7);
2976        assert!(result.y.is_finite());
2977        assert_eq!(result.y, 1.0);
2978    }
2979
2980    #[test]
2981    fn non_finite_objective_values_are_rejected() {
2982        let params = BiteParams {
2983            max_evaluations: 25,
2984            seed: 13,
2985            ..Default::default()
2986        };
2987        let result = optimize_bite(
2988            &(|_: &[f64]| f64::NAN),
2989            &[-1.0; 2],
2990            &[1.0; 2],
2991            None,
2992            &params,
2993            2,
2994        );
2995        assert_eq!(result.y, BAD_COST);
2996        assert_eq!(result.evaluations, 25);
2997        assert!(result.x.iter().all(|value| value.is_finite()));
2998    }
2999
3000    #[test]
3001    fn stop_fitness_terminates_immediately() {
3002        let params = BiteParams {
3003            max_evaluations: 100,
3004            stop_fitness: f64::INFINITY,
3005            seed: 14,
3006            ..Default::default()
3007        };
3008        let result = optimize_bite(
3009            &(sphere as fn(&[f64]) -> f64),
3010            &[-1.0; 2],
3011            &[1.0; 2],
3012            None,
3013            &params,
3014            1,
3015        );
3016        assert_eq!(result.evaluations, 1);
3017        assert_eq!(result.stop, 1);
3018    }
3019
3020    #[test]
3021    fn delayed_selector_feedback_restores_the_exact_slot() {
3022        let mut rnd = BiteRnd::new(15);
3023        let mut selector = BiteSel::new(3);
3024        selector.reset(&mut rnd, 4);
3025
3026        selector.slot = 2;
3027        selector.selp = 3;
3028        selector.sel = selector.sels[2][3];
3029        selector.sel_id = selector.entry_ids[2][3];
3030        let captured = selector.captured(7);
3031        let captured_id = captured.slot_id;
3032
3033        // Simulate later candidates overwriting the selector's current state.
3034        selector.slot = 4;
3035        selector.selp = 0;
3036        selector.sel = selector.sels[4][0];
3037        selector.sel_id = selector.entry_ids[4][0];
3038        let later_id = selector.slot_ids[4];
3039        selector.incr_captured(captured, 1.0);
3040
3041        let captured_slot = selector
3042            .slot_ids
3043            .iter()
3044            .position(|&id| id == captured_id)
3045            .unwrap();
3046        let later_slot = selector
3047            .slot_ids
3048            .iter()
3049            .position(|&id| id == later_id)
3050            .unwrap();
3051        assert_eq!(selector.slot_accums[captured_slot], 0.5);
3052        assert_eq!(selector.slot_accums[later_slot], 0.0);
3053        selector.decr_captured(captured);
3054        assert_eq!(selector.slot_accums[captured_slot], 0.0);
3055        assert_eq!(
3056            selector.entry_ids[captured_slot]
3057                .iter()
3058                .position(|&id| id == captured.entry_id),
3059            Some(1)
3060        );
3061    }
3062
3063    #[test]
3064    fn nelder_mead_copy_keeps_centroid_consistent() {
3065        let mut nm = NMSeqOpt::new(1, vec![-10.0], vec![20.0]);
3066        for i in 0..nm.m {
3067            nm.x[i][0] = i as f64;
3068            nm.y[i] = i as f64;
3069        }
3070        nm.calc_cent();
3071        assert_eq!(nm.xhi, nm.m - 1);
3072        nm.copy(&[-1.0], -1.0);
3073
3074        let expected =
3075            nm.x.iter()
3076                .enumerate()
3077                .filter(|(i, _)| *i != nm.xhi)
3078                .map(|(_, x)| x[0])
3079                .sum::<f64>()
3080                * nm.m1i;
3081        assert!((nm.x0[0] - expected).abs() < 1e-14);
3082    }
3083
3084    #[test]
3085    fn dynamic_population_size_stays_within_upstream_limits() {
3086        let params = BiteParams {
3087            popsize: 12,
3088            max_evaluations: 600,
3089            seed: 16,
3090            ..Default::default()
3091        };
3092        let mut opt = BiteOpt::new(&[-2.0; 3], &[2.0; 3], None, &params);
3093        let mut next_good = -1.0;
3094        while opt.evaluations < params.max_evaluations {
3095            let xs = opt.ask(1);
3096            let cost = if opt.evaluations.is_multiple_of(2) {
3097                next_good -= 1.0;
3098                next_good
3099            } else {
3100                BAD_COST
3101            };
3102            opt.tell(&vec![cost; xs.len()]);
3103            assert!(opt.pop.cur_pop_size >= opt.pop_size / 2);
3104            assert!(opt.pop.cur_pop_size <= opt.pop_size);
3105        }
3106    }
3107
3108    #[test]
3109    fn secondary_optimizers_make_progress() {
3110        let objective = sphere as fn(&[f64]) -> f64;
3111        let mut rnd = BiteRnd::new(17);
3112        let mut spher = SpherOpt::new(3, vec![-5.0; 3], vec![10.0; 3], 17);
3113        spher.init(&mut rnd, None, 1.0);
3114        for _ in 0..2_000 {
3115            spher.optimize(&mut rnd, &objective);
3116        }
3117        assert!(spher.best_cost < 1e-5, "spher: {}", spher.best_cost);
3118
3119        let mut nm = NMSeqOpt::new(3, vec![-5.0; 3], vec![10.0; 3]);
3120        nm.init(&mut rnd, None, 1.0);
3121        for _ in 0..2_000 {
3122            nm.optimize(&mut rnd, &objective);
3123        }
3124        assert!(nm.best_cost < 1e-8, "nelder-mead: {}", nm.best_cost);
3125    }
3126}