1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
//! Host-side local-search refinement for memetic algorithms.
//!
//! A *memetic algorithm* (MA) interleaves a population-level evolutionary
//! strategy with a per-individual **local search** that polishes promising
//! genomes between generations. This module defines the local-search seam —
//! the [`LocalSearch`] trait — together with the four reference searchers
//! ([`HillClimbing`], [`NelderMead`], [`SimulatedAnnealing`],
//! [`RandomRestart`]) that a `MemeticWrapper` can compose with any existing
//! [`Strategy`](crate::strategy::Strategy).
//!
//! # Design seam
//!
//! Local search here is **host-side and gradient-free**. Searchers operate on
//! a flat `Vec<f32>` genome and a single-member
//! [`FitnessFn`], never touching device tensors or
//! computing gradients. This keeps refinement trivially reproducible and
//! independent of the Burn backend: it is pure host arithmetic plus calls to
//! the supplied fitness function.
//!
//! # Stochasticity convention
//!
//! Every searcher takes a `&mut dyn Rng` and **all** randomness flows through
//! it. Searchers never seed the process-wide backend RNG (`B::seed` +
//! `Tensor::random`) and never reach for `rand::rng()` / thread-local RNGs —
//! that would race the global Flex mutex under the parallel test runner and
//! destroy reproducibility (see [`crate::rng`] for the host-RNG convention).
//! A memetic wrapper derives a dedicated stream via
//! `seed_stream(base, generation, SeedPurpose::LocalSearch)` and threads it in
//! here.
//!
//! # Evaluation budget
//!
//! Each `refine` call is bounded by `Params::max_iters` **total**
//! [`FitnessFn::evaluate_one`] calls
//! (including the mandatory first re-evaluation of the input genome). The
//! shared `BudgetedEval` helper enforces this so the bound holds structurally
//! even on flat landscapes where no probe ever improves.
//!
//! [`refine_with_known_fitness`](LocalSearch::refine_with_known_fitness) skips
//! that seeding eval when the caller already knows the input's fitness, so the
//! same `max_iters` then buys one extra *probe* evaluation. The seeding eval
//! consumes no rng, so skipping it never shifts a searcher's random stream.
use Backend;
use Rng;
use Bounds;
use crateFitnessFn;
// `sanitize_fitness` is the crate-wide fitness-hygiene primitive; it now lives in
// [`crate::fitness`]. Re-exported here so the searchers below (and existing
// `crate::fitness::sanitize_fitness` callers) keep resolving it unchanged.
pub use cratesanitize_fitness;
pub use ;
pub use ;
pub use ;
pub use ;
/// A gradient-free, host-side local search over real-valued genomes.
///
/// Implementors refine a single genome by repeatedly probing the supplied
/// [`FitnessFn`] and returning the best point found, subject to a strict
/// evaluation budget. They are the *meme* in a memetic algorithm: a
/// `MemeticWrapper` invokes `refine` on selected population members between an
/// inner strategy's `ask` and `tell`.
///
/// # Contract
///
/// For an input `genome` of length `D`, `refine` **must**:
///
/// 1. **Preserve dimensionality** — the returned `Vec<f32>` has length `D`.
/// 2. **Return a fresh, honest fitness** — the returned `f32` is the actual
/// value the supplied `fitness_fn` assigns to the returned genome (never a
/// stale or estimated value). For a deterministic `fitness_fn` this is
/// exact; for a stochastic one it is the value observed on the evaluation
/// that produced the returned genome.
/// 3. **Never worsen the input** (maximise, monotone non-worsening) — the
/// returned fitness is `>=` the fitness of the *input* `genome` under the
/// same `fitness_fn`. Implementors guarantee this structurally by
/// evaluating the input genome first and tracking a best-so-far pair that is
/// updated on *every* evaluation; the returned pair is always that tracked
/// best.
/// 4. **Terminate within budget** — make at most `Params::max_iters` total
/// `evaluate_one` calls, even on a perfectly flat landscape where no probe
/// ever improves.
/// 5. **Respect bounds** — every coordinate of the returned genome lies within
/// the `bounds` carried by `Params`.
///
/// Because the input genome is always evaluated once (contract item 3), a
/// `max_iters` of `0` cannot be honored honestly. Reference searchers treat
/// `max_iters == 0` as an invalid configuration and **panic**; implementors
/// should do the same rather than fabricate a fitness value. This holds on the
/// [`refine_with_known_fitness`](Self::refine_with_known_fitness) path too: the
/// reference searchers keep the `max_iters >= 1` panic even though that path
/// performs no seeding eval, so the two entry points share one budget contract.
///
/// All reference searchers route every evaluation — including the seeding eval
/// of the input — through a shared budget helper that
/// maps a `NaN` fitness to [`f32::NEG_INFINITY`], so a `NaN` probe can never seed or
/// displace a finite best-so-far and thus never propagates to the returned
/// fitness. The same rule applies to the `known_fitness` hint, which arrives
/// from a path that does *not* flow through the budget helper: every reference
/// override sanitizes the hint before seeding. Custom implementors that probe a
/// `fitness_fn` directly — or seed from a hint — should apply the same
/// sanitization rather than let a `NaN` reach their best-so-far tracker.
///
/// # Type parameters
///
/// - `B`: Burn backend. **Currently unused** by every reference searcher (they
/// are pure host code) and present only to reserve the seam for future
/// on-device searchers — e.g. a batched line search that materializes probe
/// tensors directly. Keeping `B` on the trait now avoids a breaking signature
/// change when such a searcher lands.
///
/// # Example
///
/// A one-line searcher that simply re-evaluates the input (the trivial,
/// always-valid refinement) illustrates the contract:
///
/// ```
/// use burn::backend::Flex;
/// use rand::{rngs::StdRng, Rng, SeedableRng};
/// use rlevo_evolution::fitness::FitnessFn;
/// use rlevo_evolution::local_search::LocalSearch;
///
/// struct Identity;
/// impl<B: burn::tensor::backend::Backend> LocalSearch<B> for Identity {
/// type Params = ();
/// fn refine(
/// &self,
/// _params: &(),
/// genome: Vec<f32>,
/// fitness_fn: &mut dyn FitnessFn<Vec<f32>>,
/// _rng: &mut dyn Rng,
/// ) -> (Vec<f32>, f32) {
/// let f = fitness_fn.evaluate_one(&genome); // fresh fitness
/// (genome, f) // same length, no worsening
/// }
/// }
///
/// struct Sphere;
/// impl FitnessFn<Vec<f32>> for Sphere {
/// fn evaluate_one(&mut self, x: &Vec<f32>) -> f32 {
/// x.iter().map(|v| v * v).sum()
/// }
/// }
///
/// let searcher = Identity;
/// let mut fitness = Sphere;
/// let mut rng = StdRng::seed_from_u64(0);
/// let (refined, fit) = LocalSearch::<Flex>::refine(
/// &searcher,
/// &(),
/// vec![3.0, 4.0],
/// &mut fitness,
/// &mut rng,
/// );
/// assert_eq!(refined.len(), 2);
/// assert_eq!(fit, 25.0);
/// ```
/// A fitness function wrapped with a hard evaluation budget.
///
/// `BudgetedEval` decrements its `remaining` counter on every successful
/// [`eval`](Self::eval) call and refuses further evaluations once the budget
/// is exhausted. Searchers route *all* their `evaluate_one` calls through it so
/// the [`LocalSearch`] budget invariant holds structurally rather than relying
/// on each searcher's loop bound being correct.
pub
/// Clamps every coordinate of `genome` into the inclusive range `bounds`.
///
/// `bounds` is `(lo, hi)`; coordinates below `lo` are raised to `lo` and those
/// above `hi` are lowered to `hi`. Uses `x.max(lo).min(hi)`, so a degenerate
/// range where `lo > hi` collapses every coordinate to `hi` — the `min` is
/// applied last and wins. Callers are expected to supply valid bounds.
pub