fugue_evo/hyperparameter/
self_adaptive.rs1use rand::Rng;
7use rand_distr::StandardNormal;
8use serde::{Deserialize, Serialize};
9
10use crate::genome::traits::EvolutionaryGenome;
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
14pub enum StrategyParams {
15 Isotropic(f64),
17
18 NonIsotropic(Vec<f64>),
20
21 Correlated {
24 sigmas: Vec<f64>,
25 rotations: Vec<f64>,
26 },
27}
28
29impl StrategyParams {
30 pub fn isotropic(sigma: f64) -> Self {
32 Self::Isotropic(sigma)
33 }
34
35 pub fn non_isotropic(sigmas: Vec<f64>) -> Self {
37 Self::NonIsotropic(sigmas)
38 }
39
40 pub fn correlated(sigmas: Vec<f64>) -> Self {
42 let n = sigmas.len();
43 let num_rotations = n * (n - 1) / 2;
44 Self::Correlated {
45 sigmas,
46 rotations: vec![0.0; num_rotations],
47 }
48 }
49
50 pub fn dimension(&self) -> usize {
52 match self {
53 Self::Isotropic(_) => 1,
54 Self::NonIsotropic(sigmas) => sigmas.len(),
55 Self::Correlated { sigmas, rotations } => sigmas.len() + rotations.len(),
56 }
57 }
58
59 const MIN_SIGMA: f64 = 1e-10;
61
62 pub fn mutate<R: Rng>(&mut self, n: usize, rng: &mut R) {
67 let tau = 1.0 / (2.0 * n as f64).sqrt();
68 let tau_prime = 1.0 / (2.0 * (n as f64).sqrt()).sqrt();
69 let n0: f64 = rng.sample(StandardNormal);
70
71 match self {
72 Self::Isotropic(sigma) => {
73 *sigma *= (tau_prime * n0).exp();
74 *sigma = sigma.max(Self::MIN_SIGMA);
75 }
76 Self::NonIsotropic(sigmas) => {
77 for sigma in sigmas.iter_mut() {
78 let ni: f64 = rng.sample(StandardNormal);
79 *sigma *= (tau_prime * n0 + tau * ni).exp();
80 *sigma = sigma.max(Self::MIN_SIGMA);
81 }
82 }
83 Self::Correlated { sigmas, rotations } => {
84 for sigma in sigmas.iter_mut() {
86 let ni: f64 = rng.sample(StandardNormal);
87 *sigma *= (tau_prime * n0 + tau * ni).exp();
88 *sigma = sigma.max(Self::MIN_SIGMA);
89 }
90 let beta = 0.0873;
92 for alpha in rotations.iter_mut() {
93 *alpha += beta * rng.sample::<f64, _>(StandardNormal);
94 }
95 }
96 }
97 }
98
99 pub fn get_sigma(&self, gene_idx: usize) -> f64 {
101 match self {
102 Self::Isotropic(sigma) => *sigma,
103 Self::NonIsotropic(sigmas) => sigmas.get(gene_idx).copied().unwrap_or(sigmas[0]),
104 Self::Correlated { sigmas, .. } => sigmas.get(gene_idx).copied().unwrap_or(sigmas[0]),
105 }
106 }
107
108 pub fn sigmas(&self) -> Vec<f64> {
110 match self {
111 Self::Isotropic(sigma) => vec![*sigma],
112 Self::NonIsotropic(sigmas) => sigmas.clone(),
113 Self::Correlated { sigmas, .. } => sigmas.clone(),
114 }
115 }
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize)]
120pub struct AdaptiveGenome<G> {
121 pub genome: G,
123 pub strategy: StrategyParams,
125}
126
127impl<G: EvolutionaryGenome> AdaptiveGenome<G> {
128 pub fn new_isotropic(genome: G, initial_sigma: f64) -> Self {
130 Self {
131 genome,
132 strategy: StrategyParams::Isotropic(initial_sigma),
133 }
134 }
135
136 pub fn new_non_isotropic(genome: G, initial_sigmas: Vec<f64>) -> Self {
138 Self {
139 genome,
140 strategy: StrategyParams::NonIsotropic(initial_sigmas),
141 }
142 }
143
144 pub fn new_correlated(genome: G, initial_sigmas: Vec<f64>) -> Self {
146 Self {
147 genome,
148 strategy: StrategyParams::correlated(initial_sigmas),
149 }
150 }
151
152 pub fn inner(&self) -> &G {
154 &self.genome
155 }
156
157 pub fn inner_mut(&mut self) -> &mut G {
159 &mut self.genome
160 }
161
162 pub fn into_inner(self) -> G {
164 self.genome
165 }
166}
167
168pub fn adaptive_crossover<G: Clone, R: Rng>(
172 parent1: &AdaptiveGenome<G>,
173 parent2: &AdaptiveGenome<G>,
174 child_genome: G,
175 rng: &mut R,
176) -> AdaptiveGenome<G> {
177 let strategy = match (&parent1.strategy, &parent2.strategy) {
178 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
179 StrategyParams::Isotropic((s1 * s2).sqrt())
181 }
182 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
183 let sigmas: Vec<f64> = s1
184 .iter()
185 .zip(s2.iter())
186 .map(|(a, b)| (a * b).sqrt())
187 .collect();
188 StrategyParams::NonIsotropic(sigmas)
189 }
190 (
191 StrategyParams::Correlated {
192 sigmas: s1,
193 rotations: r1,
194 },
195 StrategyParams::Correlated {
196 sigmas: s2,
197 rotations: r2,
198 },
199 ) => {
200 let sigmas: Vec<f64> = s1
201 .iter()
202 .zip(s2.iter())
203 .map(|(a, b)| (a * b).sqrt())
204 .collect();
205 let rotations: Vec<f64> = r1
206 .iter()
207 .zip(r2.iter())
208 .map(|(a, b)| (a + b) / 2.0)
209 .collect();
210 StrategyParams::Correlated { sigmas, rotations }
211 }
212 (s1, s2) => {
214 if rng.gen_bool(0.5) {
215 s1.clone()
216 } else {
217 s2.clone()
218 }
219 }
220 };
221
222 AdaptiveGenome {
223 genome: child_genome,
224 strategy,
225 }
226}
227
228#[derive(Clone, Debug)]
230pub struct LearningRates {
231 pub tau_prime: f64,
233 pub tau: f64,
235 pub beta: f64,
237}
238
239impl LearningRates {
240 pub fn for_dimension(n: usize) -> Self {
242 Self {
243 tau_prime: 1.0 / (2.0 * (n as f64).sqrt()).sqrt(),
244 tau: 1.0 / (2.0 * n as f64).sqrt(),
245 beta: 0.0873, }
247 }
248
249 pub fn custom(tau_prime: f64, tau: f64, beta: f64) -> Self {
251 Self {
252 tau_prime,
253 tau,
254 beta,
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::genome::real_vector::RealVector;
263 use crate::genome::traits::RealValuedGenome;
264
265 #[test]
266 fn test_strategy_params_isotropic() {
267 let mut params = StrategyParams::isotropic(1.0);
268 assert_eq!(params.dimension(), 1);
269 assert!((params.get_sigma(0) - 1.0).abs() < 1e-10);
270
271 let mut rng = rand::thread_rng();
272 params.mutate(10, &mut rng);
273
274 assert!(params.get_sigma(0) > 0.0);
276 }
277
278 #[test]
279 fn test_strategy_params_non_isotropic() {
280 let params = StrategyParams::non_isotropic(vec![0.1, 0.2, 0.3]);
281 assert_eq!(params.dimension(), 3);
282 assert!((params.get_sigma(0) - 0.1).abs() < 1e-10);
283 assert!((params.get_sigma(1) - 0.2).abs() < 1e-10);
284 assert!((params.get_sigma(2) - 0.3).abs() < 1e-10);
285 }
286
287 #[test]
288 fn test_strategy_params_correlated() {
289 let params = StrategyParams::correlated(vec![0.1, 0.2, 0.3]);
290 assert_eq!(params.dimension(), 6);
292 }
293
294 #[test]
295 fn test_strategy_params_min_sigma() {
296 let mut params = StrategyParams::isotropic(1e-20);
297 let mut rng = rand::thread_rng();
298
299 for _ in 0..100 {
301 params.mutate(10, &mut rng);
302 }
303
304 assert!(params.get_sigma(0) >= StrategyParams::MIN_SIGMA);
305 }
306
307 #[test]
308 fn test_adaptive_genome_creation() {
309 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
310 let adaptive = AdaptiveGenome::new_isotropic(genome.clone(), 0.5);
311
312 assert_eq!(adaptive.inner().genes(), genome.genes());
313 assert!((adaptive.strategy.get_sigma(0) - 0.5).abs() < 1e-10);
314 }
315
316 #[test]
317 fn test_adaptive_crossover() {
318 let mut rng = rand::thread_rng();
319
320 let g1 = RealVector::new(vec![1.0, 2.0]);
321 let g2 = RealVector::new(vec![3.0, 4.0]);
322 let child_genome = RealVector::new(vec![2.0, 3.0]);
323
324 let p1 = AdaptiveGenome::new_isotropic(g1, 0.1);
325 let p2 = AdaptiveGenome::new_isotropic(g2, 0.4);
326
327 let child = adaptive_crossover(&p1, &p2, child_genome, &mut rng);
328
329 assert!((child.strategy.get_sigma(0) - 0.2).abs() < 1e-10);
331 }
332
333 #[test]
334 fn test_learning_rates() {
335 let rates = LearningRates::for_dimension(10);
336
337 assert!((rates.tau - 0.2236).abs() < 0.01);
339
340 assert!((rates.tau_prime - 0.3976).abs() < 0.01);
342 }
343}