fx_durable_ga/models/mutagen.rs
1use crate::models::{Genotype, Morphology};
2use rand::Rng;
3use serde::{Deserialize, Serialize};
4use tracing::instrument;
5
6/// Applies linear decay from upper to lower bounds based on progress.
7fn decay_linear(upper: f64, lower: f64, progress: f64, multiplier: f64) -> f64 {
8 lower + (upper - lower) * (1.0 - progress * multiplier).max(0.0)
9}
10
11/// Applies exponential decay from upper to lower bounds based on progress.
12fn decay_exponential(upper: f64, lower: f64, progress: f64, multiplier: f64, exponent: i32) -> f64 {
13 lower + (upper - lower) * (1.0 - progress * multiplier).max(0.0).powi(exponent)
14}
15
16// ============================================================
17// Decay
18// ============================================================
19
20/// Strategy for decaying parameter values over optimization progress.
21///
22/// Decay strategies control how parameters like temperature and mutation rate change
23/// as the genetic algorithm progresses. This allows the algorithm to start with
24/// exploratory behavior (high values) and gradually shift to exploitative behavior
25/// (low values) as it converges on optimal solutions.
26///
27/// # Examples
28///
29/// ```rust
30/// use fx_durable_ga::models::{Temperature, MutationRate};
31///
32/// // Start aggressive, end conservative
33/// let temp = Temperature::linear(0.8, 0.1, 1.0)?;
34///
35/// // Exponential cooling for faster convergence
36/// let rate = MutationRate::exponential(0.5, 0.05, 1.0, 3)?;
37/// # Ok::<(), Box<dyn std::error::Error>>(())
38/// ```
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub enum Decay {
41 /// Parameter remains unchanged throughout optimization.
42 ///
43 /// Use when you want consistent behavior or when you're unsure
44 /// about the optimal decay strategy for your problem.
45 Constant,
46
47 /// Parameter decreases linearly from upper to lower bound.
48 ///
49 /// Formula: `lower + (upper - lower) * (1.0 - progress * multiplier)`
50 ///
51 /// - `lower`: Final value when fully converged (progress = 1.0)
52 /// - `multiplier`: Controls decay speed (1.0 = normal, >1.0 = faster, <1.0 = slower)
53 Linear { lower: f64, multiplier: f64 },
54
55 /// Parameter decreases exponentially, providing rapid early decay that slows over time.
56 ///
57 /// Formula: `lower + (upper - lower) * (1.0 - progress * multiplier)^exponent`
58 ///
59 /// - `lower`: Final value when fully converged
60 /// - `multiplier`: Controls decay timing (higher = faster initial decay)
61 /// - `exponent`: Controls curve steepness (higher = more aggressive early decay)
62 Exponential {
63 lower: f64,
64 multiplier: f64,
65 exponent: i32,
66 },
67}
68
69impl Decay {
70 /// Applies the decay strategy to get the current parameter value.
71 fn apply(&self, upper: f64, progress: f64) -> f64 {
72 match self {
73 Decay::Constant => upper,
74 Decay::Linear { lower, multiplier } => {
75 decay_linear(upper, *lower, progress, *multiplier)
76 }
77 Decay::Exponential {
78 lower,
79 multiplier,
80 exponent,
81 } => decay_exponential(upper, *lower, progress, *multiplier, *exponent),
82 }
83 }
84}
85
86// ============================================================
87// Temperature
88// ============================================================
89
90/// Controls mutation step size in the genetic algorithm.
91///
92/// Temperature determines how large the mutations can be when modifying genes.
93/// Higher temperatures allow larger jumps in the search space (exploration),
94/// while lower temperatures make smaller, more refined changes (exploitation).
95///
96/// # When to Use Different Values
97///
98/// - **High (0.7-1.0)**: Early exploration, large search spaces, getting unstuck from local optima
99/// - **Medium (0.3-0.7)**: Balanced exploration/exploitation, most general-purpose scenarios
100/// - **Low (0.0-0.3)**: Fine-tuning, convergence phase, small search spaces
101///
102/// # Decay Strategies
103///
104/// - **Constant**: Consistent behavior throughout optimization
105/// - **Linear**: Gradual transition from exploration to exploitation
106/// - **Exponential**: Rapid early exploration followed by fine-tuning
107///
108/// # Examples
109///
110/// ```rust
111/// use fx_durable_ga::models::Temperature;
112///
113/// // Simple constant temperature
114/// let temp = Temperature::constant(0.5)?;
115///
116/// // Start exploratory, end conservative
117/// let temp = Temperature::linear(0.9, 0.1, 1.0)?;
118///
119/// // Aggressive early exploration with rapid cooling
120/// let temp = Temperature::exponential(0.8, 0.05, 1.2, 3)?;
121/// # Ok::<(), Box<dyn std::error::Error>>(())
122/// ```
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct Temperature {
125 value: f64,
126 decay: Decay,
127}
128
129#[derive(Debug, thiserror::Error)]
130pub enum TemperatureError {
131 #[error("temperature value must be between 0.0 and 1.0, got: {0}")]
132 ValueOutOfRange(f64),
133 #[error("temperature lower bound ({lower}) must be <= upper bound ({upper})")]
134 InvalidBounds { lower: f64, upper: f64 },
135}
136
137impl Temperature {
138 /// Creates a constant temperature that remains unchanged throughout optimization.
139 ///
140 /// # Parameters
141 /// - `value`: Temperature value in [0.0, 1.0]. Higher values enable larger mutation steps.
142 ///
143 /// # When to Use
144 /// - Simple optimization problems
145 /// - When you want predictable, consistent mutation behavior
146 /// - Testing and debugging genetic algorithms
147 ///
148 /// # Examples
149 /// ```rust
150 /// use fx_durable_ga::models::Temperature;
151 ///
152 /// let low_temp = Temperature::constant(0.2)?; // Conservative mutations
153 /// let high_temp = Temperature::constant(0.8)?; // Aggressive mutations
154 /// # Ok::<(), Box<dyn std::error::Error>>(())
155 /// ```
156 pub fn constant(value: f64) -> Result<Self, TemperatureError> {
157 let value = Self::validate(value)?;
158
159 Ok(Self {
160 value,
161 decay: Decay::Constant,
162 })
163 }
164
165 /// Validates that temperature value is within [0.0, 1.0].
166 fn validate(value: f64) -> Result<f64, TemperatureError> {
167 if !(0.0..=1.0).contains(&value) {
168 return Err(TemperatureError::ValueOutOfRange(value));
169 }
170
171 Ok(value)
172 }
173
174 /// Creates a temperature that decays linearly from high to low values.
175 ///
176 /// Linear decay provides a smooth, predictable transition from exploration
177 /// to exploitation as the algorithm progresses.
178 ///
179 /// # Parameters
180 /// - `upper`: Starting temperature (0.0-1.0) for early exploration
181 /// - `lower`: Ending temperature (0.0-1.0) for final convergence
182 /// - `multiplier`: Decay speed (1.0 = normal, >1.0 = faster, <1.0 = slower)
183 ///
184 /// # When to Use
185 /// - Most general-purpose optimization problems
186 /// - When you want predictable exploration-to-exploitation transition
187 /// - Medium to large search spaces where gradual refinement helps
188 ///
189 /// # Examples
190 /// ```rust
191 /// use fx_durable_ga::models::Temperature;
192 ///
193 /// // Standard linear cooling
194 /// let temp = Temperature::linear(0.8, 0.1, 1.0)?;
195 ///
196 /// // Slower cooling for complex problems
197 /// let temp = Temperature::linear(0.7, 0.2, 0.5)?;
198 ///
199 /// // Faster cooling for quick convergence
200 /// let temp = Temperature::linear(0.9, 0.05, 1.5)?;
201 /// # Ok::<(), Box<dyn std::error::Error>>(())
202 /// ```
203 pub fn linear(upper: f64, lower: f64, multiplier: f64) -> Result<Self, TemperatureError> {
204 let upper = Self::validate(upper)?;
205 let lower = Self::validate(lower)?;
206
207 if lower > upper {
208 return Err(TemperatureError::InvalidBounds { lower, upper });
209 }
210
211 Ok(Self {
212 value: upper,
213 decay: Decay::Linear { lower, multiplier },
214 })
215 }
216
217 /// Creates a temperature that decays exponentially for rapid early cooling.
218 ///
219 /// Exponential decay provides aggressive early exploration followed by
220 /// fine-tuned exploitation. The algorithm quickly transitions from broad
221 /// search to focused refinement.
222 ///
223 /// # Parameters
224 /// - `upper`: Starting temperature (0.0-1.0) for initial exploration
225 /// - `lower`: Ending temperature (0.0-1.0) for final fine-tuning
226 /// - `multiplier`: Controls when decay begins (higher = earlier/faster)
227 /// - `exponent`: Controls decay curve steepness (higher = more aggressive)
228 ///
229 /// # When to Use
230 /// - Complex landscapes with many local optima
231 /// - When you need rapid convergence after initial exploration
232 /// - Problems where early diversity is crucial
233 /// - Time-constrained optimization
234 ///
235 /// # Examples
236 /// ```rust
237 /// use fx_durable_ga::models::Temperature;
238 ///
239 /// // Moderate exponential cooling
240 /// let temp = Temperature::exponential(0.8, 0.1, 1.0, 2)?;
241 ///
242 /// // Aggressive early exploration
243 /// let temp = Temperature::exponential(0.9, 0.05, 1.2, 4)?;
244 ///
245 /// // Gentle exponential curve
246 /// let temp = Temperature::exponential(0.6, 0.2, 0.8, 2)?;
247 /// # Ok::<(), Box<dyn std::error::Error>>(())
248 /// ```
249 pub fn exponential(
250 upper: f64,
251 lower: f64,
252 multiplier: f64,
253 exponent: i32,
254 ) -> Result<Self, TemperatureError> {
255 let upper = Self::validate(upper)?;
256 let lower = Self::validate(lower)?;
257
258 if lower > upper {
259 return Err(TemperatureError::InvalidBounds { lower, upper });
260 }
261
262 Ok(Self {
263 value: upper,
264 decay: Decay::Exponential {
265 lower,
266 multiplier,
267 exponent,
268 },
269 })
270 }
271
272 /// Gets the current temperature value based on optimization progress.
273 fn get(&self, progress: f64) -> f64 {
274 self.decay.apply(self.value, progress)
275 }
276}
277
278// ============================================================
279// MutationRate
280// ============================================================
281
282/// Controls the probability that each gene will be mutated during reproduction.
283///
284/// Mutation rate determines how frequently genes are changed when creating offspring.
285/// Higher rates provide more genetic diversity but may disrupt good solutions,
286/// while lower rates preserve good solutions but may limit exploration.
287///
288/// # When to Use Different Values
289///
290/// - **High (0.7-1.0)**: Dense problems, escaping local optima, early exploration
291/// - **Medium (0.3-0.7)**: Balanced search, most general-purpose applications
292/// - **Low (0.0-0.3)**: Preserving good solutions, fine-tuning, convergence phase
293///
294/// # Interaction with Population Size
295///
296/// - **Small populations**: Use higher mutation rates to maintain diversity
297/// - **Large populations**: Can use lower mutation rates as diversity emerges naturally
298///
299/// # Examples
300///
301/// ```rust
302/// use fx_durable_ga::models::MutationRate;
303///
304/// // Conservative mutation for stable problems
305/// let rate = MutationRate::constant(0.1)?;
306///
307/// // Start diverse, converge to precision
308/// let rate = MutationRate::linear(0.6, 0.05, 1.0)?;
309///
310/// // Rapid diversity reduction
311/// let rate = MutationRate::exponential(0.8, 0.02, 1.5, 3)?;
312/// # Ok::<(), Box<dyn std::error::Error>>(())
313/// ```
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct MutationRate {
316 value: f64,
317 decay: Decay,
318}
319
320#[derive(Debug, thiserror::Error)]
321pub enum MutationRateError {
322 #[error("mutation rate value must be between 0.0 and 1.0, got: {0}")]
323 ValueOutOfRange(f64),
324 #[error("mutation rate lower bound ({lower}) must be <= upper bound ({upper})")]
325 InvalidBounds { lower: f64, upper: f64 },
326}
327
328impl MutationRate {
329 /// Creates a constant mutation rate that remains unchanged throughout optimization.
330 ///
331 /// # Parameters
332 /// - `value`: Mutation probability per gene in [0.0, 1.0]. Higher values increase genetic diversity.
333 ///
334 /// # When to Use
335 /// - Simple problems with stable optima
336 /// - Baseline testing and comparison
337 /// - When optimal mutation rate is already known
338 /// - Problems requiring consistent exploration level
339 ///
340 /// # Examples
341 /// ```rust
342 /// use fx_durable_ga::models::MutationRate;
343 ///
344 /// let conservative = MutationRate::constant(0.05)?; // 5% mutation chance
345 /// let balanced = MutationRate::constant(0.3)?; // 30% mutation chance
346 /// let aggressive = MutationRate::constant(0.7)?; // 70% mutation chance
347 /// # Ok::<(), Box<dyn std::error::Error>>(())
348 /// ```
349 pub fn constant(value: f64) -> Result<Self, MutationRateError> {
350 let value = Self::validate(value)?;
351
352 Ok(Self {
353 value,
354 decay: Decay::Constant,
355 })
356 }
357
358 /// Validates that mutation rate value is within [0.0, 1.0].
359 fn validate(value: f64) -> Result<f64, MutationRateError> {
360 if !(0.0..=1.0).contains(&value) {
361 return Err(MutationRateError::ValueOutOfRange(value));
362 }
363
364 Ok(value)
365 }
366
367 /// Creates a mutation rate that decays linearly to reduce disruption over time.
368 ///
369 /// Linear decay starts with high genetic diversity and gradually reduces
370 /// mutations to preserve promising solutions as they emerge.
371 ///
372 /// # Parameters
373 /// - `upper`: Starting mutation rate (0.0-1.0) for early diversity
374 /// - `lower`: Ending mutation rate (0.0-1.0) for solution preservation
375 /// - `multiplier`: Decay speed (1.0 = normal, >1.0 = faster, <1.0 = slower)
376 ///
377 /// # When to Use
378 /// - Most optimization problems benefit from this approach
379 /// - When good solutions emerge gradually over time
380 /// - Problems where early diversity and later stability both matter
381 /// - Balanced exploration-exploitation scenarios
382 ///
383 /// # Examples
384 /// ```rust
385 /// use fx_durable_ga::models::MutationRate;
386 ///
387 /// // Standard diversity-to-stability transition
388 /// let rate = MutationRate::linear(0.5, 0.1, 1.0)?;
389 ///
390 /// // Slower transition for complex problems
391 /// let rate = MutationRate::linear(0.6, 0.2, 0.7)?;
392 ///
393 /// // Rapid stabilization
394 /// let rate = MutationRate::linear(0.8, 0.05, 1.4)?;
395 /// # Ok::<(), Box<dyn std::error::Error>>(())
396 /// ```
397 pub fn linear(upper: f64, lower: f64, multiplier: f64) -> Result<Self, MutationRateError> {
398 let upper = Self::validate(upper)?;
399 let lower = Self::validate(lower)?;
400
401 if lower > upper {
402 return Err(MutationRateError::InvalidBounds { lower, upper });
403 }
404
405 Ok(Self {
406 value: upper,
407 decay: Decay::Linear { lower, multiplier },
408 })
409 }
410
411 /// Creates a mutation rate that decays exponentially for rapid convergence.
412 ///
413 /// Exponential decay maintains high genetic diversity initially, then rapidly
414 /// reduces mutations to lock in promising solutions. This provides an intense
415 /// early search followed by strong solution preservation.
416 ///
417 /// # Parameters
418 /// - `upper`: Starting mutation rate (0.0-1.0) for initial diversity
419 /// - `lower`: Ending mutation rate (0.0-1.0) for final stability
420 /// - `multiplier`: Controls timing of decay (higher = earlier/faster reduction)
421 /// - `exponent`: Controls decay aggressiveness (higher = more dramatic curve)
422 ///
423 /// # When to Use
424 /// - Time-constrained optimization requiring fast convergence
425 /// - Problems with clear early vs. late phases
426 /// - When disruption of good solutions is particularly costly
427 /// - Search spaces where early exploration is critical
428 ///
429 /// # Examples
430 /// ```rust
431 /// use fx_durable_ga::models::MutationRate;
432 ///
433 /// // Rapid diversity reduction
434 /// let rate = MutationRate::exponential(0.7, 0.05, 1.0, 3)?;
435 ///
436 /// // Extreme early diversity, quick stabilization
437 /// let rate = MutationRate::exponential(0.9, 0.01, 1.3, 4)?;
438 ///
439 /// // Gentler exponential curve
440 /// let rate = MutationRate::exponential(0.5, 0.1, 0.8, 2)?;
441 /// # Ok::<(), Box<dyn std::error::Error>>(())
442 /// ```
443 pub fn exponential(
444 upper: f64,
445 lower: f64,
446 multiplier: f64,
447 exponent: i32,
448 ) -> Result<Self, MutationRateError> {
449 let upper = Self::validate(upper)?;
450 let lower = Self::validate(lower)?;
451
452 if lower > upper {
453 return Err(MutationRateError::InvalidBounds { lower, upper });
454 }
455
456 Ok(Self {
457 value: upper,
458 decay: Decay::Exponential {
459 lower,
460 multiplier,
461 exponent,
462 },
463 })
464 }
465
466 /// Gets the current mutation rate value based on optimization progress.
467 fn get(&self, progress: f64) -> f64 {
468 self.decay.apply(self.value, progress)
469 }
470}
471
472// ============================================================
473// Mutagen
474// ============================================================
475
476/// Orchestrates genetic mutation by combining temperature and mutation rate strategies.
477///
478/// The `Mutagen` is the core component that controls how genetic algorithms modify
479/// candidate solutions. It combines two key parameters:
480///
481/// - **Temperature**: Controls mutation step size (how big changes can be)
482/// - **Mutation Rate**: Controls mutation frequency (how often changes occur)
483///
484/// Together, these parameters determine the exploration/exploitation balance
485/// throughout the optimization process.
486///
487/// # Key Concepts
488///
489/// ## Exploration vs. Exploitation
490/// - **High temperature + High mutation rate**: Aggressive exploration
491/// - **Low temperature + Low mutation rate**: Focused exploitation
492/// - **Mixed strategies**: Balanced search behavior
493///
494/// ## Common Patterns
495/// - **Simulated Annealing**: Start hot and diverse, end cool and stable
496/// - **Constant Search**: Maintain consistent behavior throughout
497/// - **Rapid Convergence**: Quickly transition from exploration to exploitation
498///
499/// # Configuration Examples
500///
501/// ```rust
502/// use fx_durable_ga::models::{Mutagen, Temperature, MutationRate};
503///
504/// // Simple constant behavior - good for testing
505/// let simple = Mutagen::constant(0.3, 0.1)?;
506///
507/// // Classic simulated annealing approach
508/// let annealing = Mutagen::new(
509/// Temperature::exponential(0.8, 0.1, 1.0, 2)?,
510/// MutationRate::linear(0.5, 0.05, 1.0)?
511/// );
512///
513/// // Balanced linear decay for most problems
514/// let balanced = Mutagen::new(
515/// Temperature::linear(0.6, 0.2, 1.0)?,
516/// MutationRate::linear(0.4, 0.1, 1.0)?
517/// );
518///
519/// // Rapid early exploration, then fine-tuning
520/// let rapid = Mutagen::new(
521/// Temperature::exponential(0.9, 0.05, 1.2, 3)?,
522/// MutationRate::exponential(0.7, 0.02, 1.1, 3)?
523/// );
524/// # Ok::<(), Box<dyn std::error::Error>>(())
525/// ```
526///
527/// # Choosing the Right Strategy
528///
529/// | Problem Type | Temperature | Mutation Rate | Rationale |
530/// |--------------|-------------|---------------|----------|
531/// | Simple/Known | Constant | Constant | Predictable behavior |
532/// | General Purpose | Linear decay | Linear decay | Balanced approach |
533/// | Complex/Time-constrained | Exponential | Exponential | Rapid convergence |
534/// | Large search space | High initial | High initial | Need exploration |
535/// | Fine-tuning | Low constant | Low constant | Preserve solutions |
536///
537#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct Mutagen {
539 mutation_rate: MutationRate,
540 temperature: Temperature,
541}
542
543/// Errors that can occur when configuring genetic algorithm mutation parameters.
544///
545/// These errors provide specific feedback about invalid configuration values,
546/// helping you quickly identify and fix parameter setup issues.
547///
548/// # Common Scenarios
549///
550/// - **Out of Range**: Parameter values must be within [0.0, 1.0]
551/// - **Invalid Bounds**: Lower bounds must be ≤ upper bounds for decay strategies
552/// - **Configuration Conflicts**: Incompatible parameter combinations
553///
554/// # Examples
555///
556/// ```rust
557/// use fx_durable_ga::models::{Mutagen, Temperature, MutationRate};
558///
559/// // This will return MutagenError::Temperature
560/// let result = Mutagen::constant(1.5, 0.3); // temperature > 1.0
561/// assert!(result.is_err());
562///
563/// // This will return MutagenError::MutationRate
564/// let result = Mutagen::constant(0.5, -0.1); // negative mutation rate
565/// assert!(result.is_err());
566///
567/// // Invalid bounds in decay strategies
568/// let result = Temperature::linear(0.2, 0.8, 1.0); // lower > upper
569/// assert!(result.is_err());
570/// # Ok::<(), Box<dyn std::error::Error>>(())
571/// ```
572///
573/// # Error Handling
574///
575/// ```rust
576/// use fx_durable_ga::models::{Mutagen, MutagenError};
577///
578/// match Mutagen::constant(1.5, 0.3) {
579/// Ok(mutagen) => {
580/// // Use the configured mutagen
581/// }
582/// Err(MutagenError::Temperature(e)) => {
583/// eprintln!("Temperature configuration error: {e}");
584/// // Handle temperature-specific error
585/// }
586/// Err(MutagenError::MutationRate(e)) => {
587/// eprintln!("Mutation rate configuration error: {e}");
588/// // Handle mutation rate-specific error
589/// }
590/// }
591/// # Ok::<(), Box<dyn std::error::Error>>(())
592/// ```
593#[derive(Debug, thiserror::Error)]
594pub enum MutagenError {
595 /// Mutation rate parameter configuration is invalid.
596 ///
597 /// This occurs when mutation rate values are outside [0.0, 1.0] or when
598 /// decay strategy bounds are inconsistent (lower > upper).
599 #[error("Mutation rate error: {0}")]
600 MutationRate(#[from] MutationRateError),
601
602 /// Temperature parameter configuration is invalid.
603 ///
604 /// This occurs when temperature values are outside [0.0, 1.0] or when
605 /// decay strategy bounds are inconsistent (lower > upper).
606 #[error("Temperature error: {0}")]
607 Temperature(#[from] TemperatureError),
608}
609
610impl Mutagen {
611 /// Creates a new mutagen by combining temperature and mutation rate strategies.
612 ///
613 /// This is the primary constructor for creating sophisticated mutation behaviors
614 /// by combining different decay strategies for temperature and mutation rate.
615 ///
616 /// # Parameters
617 /// - `temperature`: Controls mutation step size over time
618 /// - `mutation_rate`: Controls mutation frequency over time
619 ///
620 /// # Examples
621 /// ```rust
622 /// use fx_durable_ga::models::{Mutagen, Temperature, MutationRate};
623 ///
624 /// // Mixed strategies: exponential temperature, linear mutation rate
625 /// let mutagen = Mutagen::new(
626 /// Temperature::exponential(0.8, 0.1, 1.0, 2)?,
627 /// MutationRate::linear(0.4, 0.05, 1.0)?
628 /// );
629 /// # Ok::<(), Box<dyn std::error::Error>>(())
630 /// ```
631 pub fn new(temperature: Temperature, mutation_rate: MutationRate) -> Self {
632 Self {
633 temperature,
634 mutation_rate,
635 }
636 }
637
638 /// Creates a mutagen with constant temperature and mutation rate values.
639 ///
640 /// This convenience method creates the simplest possible mutagen configuration
641 /// with unchanging parameters. Use this for baseline testing, simple problems,
642 /// or when you want predictable mutation behavior.
643 ///
644 /// # Parameters
645 /// - `temperature_value`: Fixed temperature in [0.0, 1.0] for mutation step size
646 /// - `mutation_rate_value`: Fixed mutation rate in [0.0, 1.0] for gene change frequency
647 ///
648 /// # When to Use
649 /// - Testing and debugging genetic algorithms
650 /// - Simple optimization problems with known characteristics
651 /// - Baseline comparisons against adaptive strategies
652 /// - When optimal parameters are already determined
653 ///
654 /// # Examples
655 /// ```rust
656 /// use fx_durable_ga::models::Mutagen;
657 ///
658 /// let conservative = Mutagen::constant(0.2, 0.05)?; // Small, rare mutations
659 /// let balanced = Mutagen::constant(0.5, 0.3)?; // Medium mutations
660 /// let aggressive = Mutagen::constant(0.8, 0.7)?; // Large, frequent mutations
661 /// # Ok::<(), Box<dyn std::error::Error>>(())
662 /// ```
663 pub fn constant(
664 temperature_value: f64,
665 mutation_rate_value: f64,
666 ) -> Result<Self, MutagenError> {
667 Ok(Self::new(
668 Temperature::constant(temperature_value)?,
669 MutationRate::constant(mutation_rate_value)?,
670 ))
671 }
672
673 /// Mutates a genotype in place based on current temperature and mutation rate.
674 #[instrument(level = "debug", skip(self, rng, genotype, morphology), fields(progress = progress, genotype_id = %genotype.id()))]
675 pub(crate) fn mutate<R: Rng>(
676 &self,
677 rng: &mut R,
678 genotype: &mut Genotype,
679 morphology: &Morphology,
680 progress: f64,
681 ) {
682 let temperature = self.temperature.get(progress);
683 let mutation_rate = self.mutation_rate.get(progress);
684
685 for (gene, bounds) in genotype
686 .genome_mut()
687 .iter_mut()
688 .zip(morphology.gene_bounds.iter())
689 {
690 // Apply mutation based on current mutation rate
691 if rng.random_range(0.0..1.0) < mutation_rate {
692 // Temperature controls mutation step size: higher = larger jumps
693 let max_step = (1.0 + (bounds.steps() as f64 * temperature)) as i64;
694
695 // Choose random direction and step size
696 let direction = if rng.random_bool(0.5) { 1 } else { -1 };
697 let step = rng.random_range(1..=max_step);
698
699 // Apply mutation and clamp to valid range
700 *gene = (*gene + direction * step).clamp(0, bounds.steps() as i64 - 1);
701 }
702 }
703
704 // Recompute hash after mutation since the genome may have changed
705 let new_hash = Genotype::compute_genome_hash(&genotype.genome());
706 genotype.set_genome_hash(new_hash);
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use crate::models::GeneBounds;
714 use rand::{SeedableRng, rngs::StdRng};
715 use uuid::Uuid;
716
717 fn get_test_genotype() -> Genotype {
718 Genotype::new("TestType", 123, vec![5, 2], Uuid::now_v7(), 1)
719 }
720
721 fn get_test_morphology() -> Morphology {
722 Morphology::new(
723 "TestType",
724 123,
725 vec![
726 GeneBounds::integer(0, 9, 10).unwrap(), // Gene can be 0-9 (steps=10)
727 GeneBounds::integer(0, 4, 5).unwrap(), // Gene can be 0-4 (steps=5)
728 ],
729 )
730 }
731
732 #[test]
733 fn it_validates_temperature_and_mutation_rate_bounds() {
734 // Temperature validation
735 assert!(Temperature::constant(-0.1).is_err());
736 assert!(Temperature::constant(1.5).is_err());
737 assert!(Temperature::linear(-0.1, 0.0, 1.0).is_err());
738 assert!(Temperature::exponential(1.5, 0.0, 1.0, 2).is_err());
739
740 // MutationRate validation
741 assert!(MutationRate::constant(-0.1).is_err());
742 assert!(MutationRate::constant(1.5).is_err());
743 assert!(MutationRate::linear(-0.1, 0.0, 1.0).is_err());
744 assert!(MutationRate::exponential(1.5, 0.0, 1.0, 2).is_err());
745
746 // Lower > upper validation for MutationRate
747 assert!(MutationRate::linear(0.3, 0.5, 1.0).is_err());
748 assert!(MutationRate::exponential(0.2, 0.4, 1.0, 2).is_err());
749
750 // Lower > upper validation for Temperature
751 assert!(Temperature::linear(0.3, 0.5, 1.0).is_err());
752 assert!(Temperature::exponential(0.2, 0.4, 1.0, 2).is_err());
753 }
754
755 #[test]
756 fn it_applies_constant_decay() {
757 let temp = Temperature::constant(0.7).unwrap();
758 let rate = MutationRate::constant(0.3).unwrap();
759
760 // Constant values shouldn't change with progress
761 assert_eq!(temp.get(0.0), 0.7);
762 assert_eq!(temp.get(0.5), 0.7);
763 assert_eq!(temp.get(1.0), 0.7);
764
765 assert_eq!(rate.get(0.0), 0.3);
766 assert_eq!(rate.get(0.5), 0.3);
767 assert_eq!(rate.get(1.0), 0.3);
768 }
769
770 #[test]
771 fn it_applies_linear_decay() {
772 let temp = Temperature::linear(1.0, 0.0, 1.0).unwrap();
773 let rate = MutationRate::linear(1.0, 0.0, 1.0).unwrap();
774
775 // Linear decay: value * (1.0 - progress * multiplier)
776 assert_eq!(temp.get(0.0), 1.0); // No progress
777 assert_eq!(temp.get(0.5), 0.5); // Half progress
778 assert_eq!(temp.get(1.0), 0.0); // Full progress
779 assert_eq!(temp.get(1.5), 0.0); // Over-progress (clamped)
780
781 assert_eq!(rate.get(0.0), 1.0);
782 assert_eq!(rate.get(0.5), 0.5);
783 assert_eq!(rate.get(1.0), 0.0);
784 assert_eq!(rate.get(1.5), 0.0);
785 }
786
787 #[test]
788 fn it_applies_exponential_decay() {
789 let temp = Temperature::exponential(1.0, 0.0, 1.0, 2).unwrap();
790 let rate = MutationRate::exponential(1.0, 0.0, 1.0, 2).unwrap();
791
792 // Exponential decay: value * (1.0 - progress * multiplier)^exponent
793 assert_eq!(temp.get(0.0), 1.0); // No progress
794 assert_eq!(temp.get(0.5), 0.25); // Quadratic: (1.0 - 0.5)^2 = 0.25
795 assert_eq!(temp.get(1.0), 0.0); // Full progress
796
797 assert_eq!(rate.get(0.0), 1.0);
798 assert_eq!(rate.get(0.5), 0.25);
799 assert_eq!(rate.get(1.0), 0.0);
800 }
801
802 #[test]
803 fn it_mutates() {
804 let mut rng = StdRng::seed_from_u64(42);
805 let morphology = get_test_morphology();
806 let mut genotype = get_test_genotype();
807
808 // Create test data
809 let mutagen = Mutagen::new(
810 Temperature::constant(0.1).expect("temperature is in range"), // Low temp = small steps
811 MutationRate::constant(1.0).expect("mutation_rate is in range"), // 100% mutation rate
812 );
813
814 let original_genome = genotype.genome.clone();
815
816 // Mutate with 100% rate - should change something
817 mutagen.mutate(&mut rng, &mut genotype, &morphology, 0.0);
818
819 // With 100% mutation rate and seeded RNG, genome should change
820 assert_ne!(genotype.genome, original_genome);
821
822 // Verify genes stay within bounds
823 assert!(genotype.genome[0] >= 0 && genotype.genome[0] < 10);
824 assert!(genotype.genome[1] >= 0 && genotype.genome[1] < 5);
825 }
826
827 #[test]
828 fn it_respects_zero_mutation_rate() {
829 let mut rng = StdRng::seed_from_u64(42);
830 let morphology = get_test_morphology();
831 let mut genotype = get_test_genotype();
832
833 let mutagen = Mutagen::new(
834 Temperature::constant(1.0).expect("temperature is in range"),
835 MutationRate::constant(0.0).expect("mutation_rate is in range"), // 0% mutation rate
836 );
837
838 let original_genome = genotype.genome.clone();
839
840 mutagen.mutate(&mut rng, &mut genotype, &morphology, 0.0);
841
842 // Should be unchanged with 0% mutation rate
843 assert_eq!(genotype.genome, original_genome);
844 }
845
846 #[test]
847 fn it_composes_different_decay_strategies() {
848 // Test that we can mix different decay types
849 let mutagen = Mutagen::new(
850 Temperature::linear(0.8, 0.1, 0.9).unwrap(), // Linear temperature decay
851 MutationRate::exponential(0.5, 0.0, 1.0, 2).unwrap(), // Exponential mutation rate decay
852 );
853
854 // Verify the composition works correctly
855 assert_eq!(mutagen.temperature.get(0.0), 0.8);
856 assert_eq!(mutagen.mutation_rate.get(0.0), 0.5);
857 // Exponential decay: lower + (upper - lower) * (1.0 - progress * multiplier)^exponent
858 // 0.0 + (0.5 - 0.0) * (1.0 - 0.5 * 1.0)^2 = 0.5 * (0.5)^2 = 0.5 * 0.25 = 0.125
859 assert_eq!(mutagen.mutation_rate.get(0.5), 0.125);
860
861 // Linear temperature decay: lower + (upper - lower) * (1.0 - progress * multiplier)
862 // 0.1 + (0.8 - 0.1) * (1.0 - 0.5 * 0.9) = 0.1 + 0.7 * 0.55 = 0.1 + 0.385 = 0.485
863 assert!((mutagen.temperature.get(0.5) - 0.485).abs() < 1e-10);
864 }
865
866 #[test]
867 fn it_validates_mutagen_constant_parameters() {
868 // Test temperature validation error
869 assert!(Mutagen::constant(-0.1, 0.5).is_err());
870 assert!(Mutagen::constant(1.5, 0.5).is_err());
871
872 // Test mutation rate validation error
873 assert!(Mutagen::constant(0.5, -0.1).is_err());
874 assert!(Mutagen::constant(0.5, 1.5).is_err());
875
876 // Test successful case
877 assert!(Mutagen::constant(0.5, 0.3).is_ok());
878 }
879}