Skip to main content

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}