1use std::collections::HashMap;
23use std::sync::{Arc, Mutex};
24
25use fugue::runtime::handler::Handler;
26#[cfg(test)]
27use fugue::{addr, sample, Normal};
28use fugue::{Address, ChoiceValue, Distribution, Trace};
29use rand::Rng;
30
31use super::trace_operators::{CrossoverMask, MutationSelector};
32use crate::error::GenomeError;
33use crate::genome::trace_genome::TraceGenome;
34
35pub struct TraceScoringHandler {
53 pub trace: Trace,
55}
56
57impl TraceScoringHandler {
58 pub fn new(base: Trace) -> Self {
63 Self {
64 trace: Trace {
65 choices: base.choices,
66 log_prior: 0.0,
67 log_likelihood: 0.0,
68 log_factors: 0.0,
69 },
70 }
71 }
72
73 fn score_sample<T: Copy + Default>(
74 &mut self,
75 addr: &Address,
76 dist: &dyn Distribution<T>,
77 extract: impl Fn(&ChoiceValue) -> Option<T>,
78 ) -> T {
79 match self.trace.choices.get(addr).and_then(|c| extract(&c.value)) {
80 Some(v) => {
81 self.trace.log_prior += dist.log_prob(&v);
82 v
83 }
84 None => {
85 self.trace.log_prior += f64::NEG_INFINITY;
88 T::default()
89 }
90 }
91 }
92}
93
94impl Handler for TraceScoringHandler {
95 fn on_sample_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>) -> f64 {
96 self.score_sample(addr, dist, ChoiceValue::as_f64)
97 }
98
99 fn on_sample_bool(&mut self, addr: &Address, dist: &dyn Distribution<bool>) -> bool {
100 self.score_sample(addr, dist, ChoiceValue::as_bool)
101 }
102
103 fn on_sample_u64(&mut self, addr: &Address, dist: &dyn Distribution<u64>) -> u64 {
104 self.score_sample(addr, dist, ChoiceValue::as_u64)
105 }
106
107 fn on_sample_usize(&mut self, addr: &Address, dist: &dyn Distribution<usize>) -> usize {
108 self.score_sample(addr, dist, ChoiceValue::as_usize)
109 }
110
111 fn on_observe_f64(&mut self, _: &Address, dist: &dyn Distribution<f64>, value: f64) {
112 self.trace.log_likelihood += dist.log_prob(&value);
113 }
114
115 fn on_observe_bool(&mut self, _: &Address, dist: &dyn Distribution<bool>, value: bool) {
116 self.trace.log_likelihood += dist.log_prob(&value);
117 }
118
119 fn on_observe_u64(&mut self, _: &Address, dist: &dyn Distribution<u64>, value: u64) {
120 self.trace.log_likelihood += dist.log_prob(&value);
121 }
122
123 fn on_observe_usize(&mut self, _: &Address, dist: &dyn Distribution<usize>, value: usize) {
124 self.trace.log_likelihood += dist.log_prob(&value);
125 }
126
127 fn on_factor(&mut self, logw: f64) {
128 self.trace.log_factors += logw;
129 }
130
131 fn finish(self) -> Trace {
132 self.trace
133 }
134}
135
136pub struct RecordingHandler<H: Handler> {
147 inner: H,
148 events: Arc<Mutex<Vec<(Address, ChoiceValue)>>>,
149}
150
151impl<H: Handler> RecordingHandler<H> {
152 pub fn new(inner: H, sink: Arc<Mutex<Vec<(Address, ChoiceValue)>>>) -> Self {
154 Self {
155 inner,
156 events: sink,
157 }
158 }
159
160 fn record(&self, addr: &Address, value: ChoiceValue) {
161 self.events.lock().unwrap().push((addr.clone(), value));
162 }
163}
164
165impl<H: Handler> Handler for RecordingHandler<H> {
166 fn on_sample_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>) -> f64 {
167 let v = self.inner.on_sample_f64(addr, dist);
168 self.record(addr, ChoiceValue::F64(v));
169 v
170 }
171
172 fn on_sample_bool(&mut self, addr: &Address, dist: &dyn Distribution<bool>) -> bool {
173 let v = self.inner.on_sample_bool(addr, dist);
174 self.record(addr, ChoiceValue::Bool(v));
175 v
176 }
177
178 fn on_sample_u64(&mut self, addr: &Address, dist: &dyn Distribution<u64>) -> u64 {
179 let v = self.inner.on_sample_u64(addr, dist);
180 self.record(addr, ChoiceValue::U64(v));
181 v
182 }
183
184 fn on_sample_usize(&mut self, addr: &Address, dist: &dyn Distribution<usize>) -> usize {
185 let v = self.inner.on_sample_usize(addr, dist);
186 self.record(addr, ChoiceValue::Usize(v));
187 v
188 }
189
190 fn on_observe_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>, value: f64) {
191 self.inner.on_observe_f64(addr, dist, value);
192 }
193
194 fn on_observe_bool(&mut self, addr: &Address, dist: &dyn Distribution<bool>, value: bool) {
195 self.inner.on_observe_bool(addr, dist, value);
196 }
197
198 fn on_observe_u64(&mut self, addr: &Address, dist: &dyn Distribution<u64>, value: u64) {
199 self.inner.on_observe_u64(addr, dist, value);
200 }
201
202 fn on_observe_usize(&mut self, addr: &Address, dist: &dyn Distribution<usize>, value: usize) {
203 self.inner.on_observe_usize(addr, dist, value);
204 }
205
206 fn on_factor(&mut self, logw: f64) {
207 self.inner.on_factor(logw);
208 }
209
210 fn finish(self) -> Trace {
211 self.inner.finish()
212 }
213}
214
215#[derive(Clone, Debug)]
217pub struct MutationRecord {
218 pub generation: usize,
220 pub mutated_addresses: Vec<Address>,
222 pub original_values: HashMap<Address, ChoiceValue>,
224 pub new_values: HashMap<Address, ChoiceValue>,
226}
227
228#[derive(Clone, Debug)]
230pub struct CrossoverRecord {
231 pub generation: usize,
233 pub from_parent1: Vec<Address>,
235 pub from_parent2: Vec<Address>,
237}
238
239#[derive(Clone, Debug)]
241pub struct SelectionRecord {
242 pub generation: usize,
244 pub selected_indices: Vec<usize>,
246 pub selected_fitness: Vec<f64>,
248}
249
250pub trait MutationHook: Send + Sync {
256 fn before_mutation(&self, trace: &Trace, generation: usize) -> bool;
259
260 fn after_mutation(&self, original: &Trace, mutated: &Trace, record: &MutationRecord);
262
263 fn modify_sites(
265 &self,
266 sites: std::collections::HashSet<Address>,
267 _trace: &Trace,
268 ) -> std::collections::HashSet<Address> {
269 sites }
271}
272
273pub trait CrossoverHook: Send + Sync {
277 fn before_crossover(&self, parent1: &Trace, parent2: &Trace, generation: usize) -> bool;
280
281 fn after_crossover(
283 &self,
284 parent1: &Trace,
285 parent2: &Trace,
286 child1: &Trace,
287 child2: &Trace,
288 record: &CrossoverRecord,
289 );
290}
291
292pub trait SelectionHook: Send + Sync {
296 fn before_selection(&self, population_size: usize, generation: usize);
298
299 fn after_selection(&self, record: &SelectionRecord);
301
302 fn modify_probabilities(&self, probabilities: Vec<f64>) -> Vec<f64> {
304 probabilities }
306}
307
308#[derive(Clone, Debug, Default)]
310pub struct LoggingHook {
311 pub mutations: Arc<Mutex<Vec<MutationRecord>>>,
313 pub crossovers: Arc<Mutex<Vec<CrossoverRecord>>>,
315 pub selections: Arc<Mutex<Vec<SelectionRecord>>>,
317}
318
319impl LoggingHook {
320 pub fn new() -> Self {
322 Self::default()
323 }
324
325 pub fn get_mutations(&self) -> Vec<MutationRecord> {
327 self.mutations.lock().unwrap().clone()
328 }
329
330 pub fn get_crossovers(&self) -> Vec<CrossoverRecord> {
332 self.crossovers.lock().unwrap().clone()
333 }
334
335 pub fn get_selections(&self) -> Vec<SelectionRecord> {
337 self.selections.lock().unwrap().clone()
338 }
339
340 pub fn clear(&self) {
342 self.mutations.lock().unwrap().clear();
343 self.crossovers.lock().unwrap().clear();
344 self.selections.lock().unwrap().clear();
345 }
346}
347
348impl MutationHook for LoggingHook {
349 fn before_mutation(&self, _trace: &Trace, _generation: usize) -> bool {
350 true }
352
353 fn after_mutation(&self, _original: &Trace, _mutated: &Trace, record: &MutationRecord) {
354 self.mutations.lock().unwrap().push(record.clone());
355 }
356}
357
358impl CrossoverHook for LoggingHook {
359 fn before_crossover(&self, _parent1: &Trace, _parent2: &Trace, _generation: usize) -> bool {
360 true }
362
363 fn after_crossover(
364 &self,
365 _parent1: &Trace,
366 _parent2: &Trace,
367 _child1: &Trace,
368 _child2: &Trace,
369 record: &CrossoverRecord,
370 ) {
371 self.crossovers.lock().unwrap().push(record.clone());
372 }
373}
374
375impl SelectionHook for LoggingHook {
376 fn before_selection(&self, _population_size: usize, _generation: usize) {}
377
378 fn after_selection(&self, record: &SelectionRecord) {
379 self.selections.lock().unwrap().push(record.clone());
380 }
381}
382
383#[derive(Clone, Debug)]
385pub struct RateLimitingHook {
386 pub max_mutations: usize,
388 pub max_crossovers: usize,
390 mutation_count: Arc<Mutex<(usize, usize)>>, crossover_count: Arc<Mutex<(usize, usize)>>,
394}
395
396impl RateLimitingHook {
397 pub fn new(max_mutations: usize, max_crossovers: usize) -> Self {
399 Self {
400 max_mutations,
401 max_crossovers,
402 mutation_count: Arc::new(Mutex::new((0, 0))),
403 crossover_count: Arc::new(Mutex::new((0, 0))),
404 }
405 }
406
407 pub fn reset(&self, generation: usize) {
409 *self.mutation_count.lock().unwrap() = (generation, 0);
410 *self.crossover_count.lock().unwrap() = (generation, 0);
411 }
412}
413
414impl MutationHook for RateLimitingHook {
415 fn before_mutation(&self, _trace: &Trace, generation: usize) -> bool {
416 let mut count = self.mutation_count.lock().unwrap();
417 if count.0 != generation {
418 *count = (generation, 0);
419 }
420 if count.1 < self.max_mutations {
421 count.1 += 1;
422 true
423 } else {
424 false
425 }
426 }
427
428 fn after_mutation(&self, _original: &Trace, _mutated: &Trace, _record: &MutationRecord) {}
429}
430
431impl CrossoverHook for RateLimitingHook {
432 fn before_crossover(&self, _parent1: &Trace, _parent2: &Trace, generation: usize) -> bool {
433 let mut count = self.crossover_count.lock().unwrap();
434 if count.0 != generation {
435 *count = (generation, 0);
436 }
437 if count.1 < self.max_crossovers {
438 count.1 += 1;
439 true
440 } else {
441 false
442 }
443 }
444
445 fn after_crossover(
446 &self,
447 _parent1: &Trace,
448 _parent2: &Trace,
449 _child1: &Trace,
450 _child2: &Trace,
451 _record: &CrossoverRecord,
452 ) {
453 }
454}
455
456pub struct ConditionalHook<F>
458where
459 F: Fn(usize) -> bool + Send + Sync,
460{
461 pub predicate: F,
463}
464
465impl<F> ConditionalHook<F>
466where
467 F: Fn(usize) -> bool + Send + Sync,
468{
469 pub fn new(predicate: F) -> Self {
471 Self { predicate }
472 }
473}
474
475impl<F> MutationHook for ConditionalHook<F>
476where
477 F: Fn(usize) -> bool + Send + Sync,
478{
479 fn before_mutation(&self, _trace: &Trace, generation: usize) -> bool {
480 (self.predicate)(generation)
481 }
482
483 fn after_mutation(&self, _original: &Trace, _mutated: &Trace, _record: &MutationRecord) {}
484}
485
486impl<F> CrossoverHook for ConditionalHook<F>
487where
488 F: Fn(usize) -> bool + Send + Sync,
489{
490 fn before_crossover(&self, _parent1: &Trace, _parent2: &Trace, generation: usize) -> bool {
491 (self.predicate)(generation)
492 }
493
494 fn after_crossover(
495 &self,
496 _parent1: &Trace,
497 _parent2: &Trace,
498 _child1: &Trace,
499 _child2: &Trace,
500 _record: &CrossoverRecord,
501 ) {
502 }
503}
504
505pub struct ComposedMutationHook {
507 hooks: Vec<Box<dyn MutationHook>>,
508}
509
510impl ComposedMutationHook {
511 pub fn new() -> Self {
513 Self { hooks: Vec::new() }
514 }
515
516 pub fn add<H: MutationHook + 'static>(mut self, hook: H) -> Self {
518 self.hooks.push(Box::new(hook));
519 self
520 }
521}
522
523impl Default for ComposedMutationHook {
524 fn default() -> Self {
525 Self::new()
526 }
527}
528
529impl MutationHook for ComposedMutationHook {
530 fn before_mutation(&self, trace: &Trace, generation: usize) -> bool {
531 self.hooks
533 .iter()
534 .all(|h| h.before_mutation(trace, generation))
535 }
536
537 fn after_mutation(&self, original: &Trace, mutated: &Trace, record: &MutationRecord) {
538 for hook in &self.hooks {
539 hook.after_mutation(original, mutated, record);
540 }
541 }
542
543 fn modify_sites(
544 &self,
545 mut sites: std::collections::HashSet<Address>,
546 trace: &Trace,
547 ) -> std::collections::HashSet<Address> {
548 for hook in &self.hooks {
549 sites = hook.modify_sites(sites, trace);
550 }
551 sites
552 }
553}
554
555pub struct ComposedCrossoverHook {
557 hooks: Vec<Box<dyn CrossoverHook>>,
558}
559
560impl ComposedCrossoverHook {
561 pub fn new() -> Self {
563 Self { hooks: Vec::new() }
564 }
565
566 pub fn add<H: CrossoverHook + 'static>(mut self, hook: H) -> Self {
568 self.hooks.push(Box::new(hook));
569 self
570 }
571}
572
573impl Default for ComposedCrossoverHook {
574 fn default() -> Self {
575 Self::new()
576 }
577}
578
579impl CrossoverHook for ComposedCrossoverHook {
580 fn before_crossover(&self, parent1: &Trace, parent2: &Trace, generation: usize) -> bool {
581 self.hooks
582 .iter()
583 .all(|h| h.before_crossover(parent1, parent2, generation))
584 }
585
586 fn after_crossover(
587 &self,
588 parent1: &Trace,
589 parent2: &Trace,
590 child1: &Trace,
591 child2: &Trace,
592 record: &CrossoverRecord,
593 ) {
594 for hook in &self.hooks {
595 hook.after_crossover(parent1, parent2, child1, child2, record);
596 }
597 }
598}
599
600pub fn hooked_mutate_trace<G, S, H, R>(
610 genome: &G,
611 selector: &S,
612 mutation_fn: impl Fn(&Address, &ChoiceValue, &mut R) -> ChoiceValue,
613 hook: &H,
614 generation: usize,
615 rng: &mut R,
616) -> Result<G, GenomeError>
617where
618 G: TraceGenome,
619 S: MutationSelector,
620 H: MutationHook,
621 R: Rng,
622{
623 let trace = genome.to_trace();
624
625 if !hook.before_mutation(&trace, generation) {
627 return G::from_trace(&trace);
628 }
629
630 let mut mutation_sites = selector.select_sites(&trace, rng);
632 mutation_sites = hook.modify_sites(mutation_sites, &trace);
633
634 let mut new_trace = Trace::default();
635 let mut original_values = HashMap::new();
636 let mut new_values = HashMap::new();
637
638 for (addr, choice) in &trace.choices {
639 if mutation_sites.contains(addr) {
640 original_values.insert(addr.clone(), choice.value.clone());
641 let mutated = mutation_fn(addr, &choice.value, rng);
642 new_values.insert(addr.clone(), mutated.clone());
643 new_trace.insert_choice(addr.clone(), mutated, 0.0);
645 } else {
646 new_trace.insert_choice(addr.clone(), choice.value.clone(), choice.logp);
648 }
649 }
650
651 let record = MutationRecord {
653 generation,
654 mutated_addresses: mutation_sites.into_iter().collect(),
655 original_values,
656 new_values,
657 };
658
659 hook.after_mutation(&trace, &new_trace, &record);
660
661 G::from_trace(&new_trace)
662}
663
664pub fn hooked_crossover_traces<G, M, H, R>(
670 parent1: &G,
671 parent2: &G,
672 mask: &M,
673 hook: &H,
674 generation: usize,
675 _rng: &mut R,
676) -> Result<(G, G), GenomeError>
677where
678 G: TraceGenome,
679 M: CrossoverMask,
680 H: CrossoverHook,
681 R: Rng,
682{
683 let trace1 = parent1.to_trace();
684 let trace2 = parent2.to_trace();
685
686 if !hook.before_crossover(&trace1, &trace2, generation) {
688 return Ok((G::from_trace(&trace1)?, G::from_trace(&trace2)?));
689 }
690
691 let mut child1_trace = Trace::default();
692 let mut child2_trace = Trace::default();
693 let mut from_parent1 = Vec::new();
694 let mut from_parent2 = Vec::new();
695
696 let all_addresses: std::collections::HashSet<Address> = trace1
698 .choices
699 .keys()
700 .chain(trace2.choices.keys())
701 .cloned()
702 .collect();
703
704 for addr in all_addresses {
705 let (val_for_child1, val_for_child2) = if mask.from_parent1(&addr) {
706 from_parent1.push(addr.clone());
707 (
708 trace1
709 .choices
710 .get(&addr)
711 .map(|c| c.value.clone())
712 .unwrap_or(ChoiceValue::F64(0.0)),
713 trace2
714 .choices
715 .get(&addr)
716 .map(|c| c.value.clone())
717 .unwrap_or(ChoiceValue::F64(0.0)),
718 )
719 } else {
720 from_parent2.push(addr.clone());
721 (
722 trace2
723 .choices
724 .get(&addr)
725 .map(|c| c.value.clone())
726 .unwrap_or(ChoiceValue::F64(0.0)),
727 trace1
728 .choices
729 .get(&addr)
730 .map(|c| c.value.clone())
731 .unwrap_or(ChoiceValue::F64(0.0)),
732 )
733 };
734
735 child1_trace.insert_choice(addr.clone(), val_for_child1, 0.0);
736 child2_trace.insert_choice(addr, val_for_child2, 0.0);
737 }
738
739 let record = CrossoverRecord {
741 generation,
742 from_parent1,
743 from_parent2,
744 };
745
746 hook.after_crossover(&trace1, &trace2, &child1_trace, &child2_trace, &record);
747
748 let child1 = G::from_trace(&child1_trace)?;
749 let child2 = G::from_trace(&child2_trace)?;
750
751 Ok((child1, child2))
752}
753
754#[derive(Clone, Debug, Default)]
756pub struct OperationStatistics {
757 pub total_mutations: usize,
759 pub total_crossovers: usize,
761 pub avg_mutation_sites: f64,
763 pub avg_parent1_contribution: f64,
765}
766
767impl OperationStatistics {
768 pub fn from_hook(hook: &LoggingHook) -> Self {
770 let mutations = hook.get_mutations();
771 let crossovers = hook.get_crossovers();
772
773 let total_mutations = mutations.len();
774 let total_crossovers = crossovers.len();
775
776 let avg_mutation_sites = if total_mutations > 0 {
777 mutations
778 .iter()
779 .map(|r| r.mutated_addresses.len())
780 .sum::<usize>() as f64
781 / total_mutations as f64
782 } else {
783 0.0
784 };
785
786 let avg_parent1_contribution = if total_crossovers > 0 {
787 let total: f64 = crossovers
788 .iter()
789 .map(|r| {
790 let total = r.from_parent1.len() + r.from_parent2.len();
791 if total > 0 {
792 r.from_parent1.len() as f64 / total as f64
793 } else {
794 0.5
795 }
796 })
797 .sum();
798 total / total_crossovers as f64
799 } else {
800 0.0
801 };
802
803 Self {
804 total_mutations,
805 total_crossovers,
806 avg_mutation_sites,
807 avg_parent1_contribution,
808 }
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815 use crate::genome::real_vector::RealVector;
816 use crate::genome::trace_genome::TraceGenome;
817 use crate::inference::trace_operators::{
818 gaussian_mutation, UniformCrossoverMask, UniformMutationSelector,
819 };
820 use fugue::runtime::handler::run;
821 use fugue::{factor, ModelExt};
822 use rand::SeedableRng;
823
824 #[test]
825 fn test_trace_scoring_handler_injects_factor() {
826 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
828 let base = genome.to_trace();
829 let (_r, trace) = run(TraceScoringHandler::new(base), factor(-4.5));
830 assert!((trace.log_factors - (-4.5)).abs() < 1e-12);
831 assert!((trace.total_log_weight() - (-4.5)).abs() < 1e-12);
832 assert_eq!(trace.get_f64(&addr!("gene", 0)), Some(1.0));
834 assert_eq!(trace.get_f64(&addr!("gene", 2)), Some(3.0));
835 }
836
837 #[test]
838 fn test_trace_scoring_handler_scores_prior() {
839 let mut base = Trace::default();
841 base.insert_choice(addr!("x"), ChoiceValue::F64(0.0), 0.0);
842 let (_v, trace) = run(
843 TraceScoringHandler::new(base),
844 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
845 );
846 let expected = -0.5 * (2.0 * std::f64::consts::PI).ln();
848 assert!((trace.log_prior - expected).abs() < 1e-9);
849 }
850
851 #[test]
852 fn test_recording_handler_preserves_bookkeeping() {
853 let model = || {
856 sample(addr!("a"), Normal::new(0.0, 1.0).unwrap())
857 .and_then(|_| sample(addr!("b"), Normal::new(1.0, 2.0).unwrap()))
858 .and_then(|_| factor(-1.25))
859 };
860
861 let mut rng1 = rand::rngs::StdRng::from_seed([7u8; 32]);
862 let plain = fugue::runtime::interpreters::PriorHandler {
863 rng: &mut rng1,
864 trace: Trace::default(),
865 };
866 let (_a, plain_trace) = run(plain, model());
867
868 let mut rng2 = rand::rngs::StdRng::from_seed([7u8; 32]);
869 let sink = Arc::new(Mutex::new(Vec::new()));
870 let recording = RecordingHandler::new(
871 fugue::runtime::interpreters::PriorHandler {
872 rng: &mut rng2,
873 trace: Trace::default(),
874 },
875 sink.clone(),
876 );
877 let (_b, rec_trace) = run(recording, model());
878
879 assert!((plain_trace.log_prior - rec_trace.log_prior).abs() < 1e-12);
881 assert!((plain_trace.log_factors - rec_trace.log_factors).abs() < 1e-12);
882 let events = sink.lock().unwrap();
884 assert_eq!(events.len(), 2);
885 assert_eq!(events[0].0, addr!("a"));
886 assert_eq!(events[1].0, addr!("b"));
887 }
888
889 #[test]
890 fn test_hooked_mutate_does_not_copy_stale_logp() {
891 let mut rng = rand::rngs::StdRng::from_seed([3u8; 32]);
893 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
895 let mut seeded = genome.to_trace();
896 for c in seeded.choices.values_mut() {
897 c.logp = -12.34; }
899 let hook = LoggingHook::new();
903 let selector = UniformMutationSelector::new(1.0);
904 let mutation_fn = gaussian_mutation(0.5);
905 let mutated =
906 hooked_mutate_trace(&genome, &selector, mutation_fn, &hook, 0, &mut rng).unwrap();
907 let child_trace = mutated.to_trace();
908 for choice in child_trace.choices.values() {
909 assert_eq!(choice.logp, 0.0, "child trace logp must be neutral (0.0)");
910 }
911 }
912
913 #[test]
914 fn test_logging_hook_mutation() {
915 let mut rng = rand::thread_rng();
916 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
917
918 let hook = LoggingHook::new();
919 let selector = UniformMutationSelector::new(1.0); let mutation_fn = gaussian_mutation(0.1);
921
922 let _mutated =
923 hooked_mutate_trace(&genome, &selector, mutation_fn, &hook, 0, &mut rng).unwrap();
924
925 let records = hook.get_mutations();
926 assert_eq!(records.len(), 1);
927 assert_eq!(records[0].generation, 0);
928 assert!(!records[0].mutated_addresses.is_empty());
929 }
930
931 #[test]
932 fn test_logging_hook_crossover() {
933 let mut rng = rand::thread_rng();
934 let parent1 = RealVector::new(vec![1.0, 2.0, 3.0]);
935 let parent2 = RealVector::new(vec![4.0, 5.0, 6.0]);
936
937 let hook = LoggingHook::new();
938 let trace1 = parent1.to_trace();
939 let mask = UniformCrossoverMask::balanced(&trace1, &mut rng);
940
941 let (_child1, _child2) =
942 hooked_crossover_traces(&parent1, &parent2, &mask, &hook, 0, &mut rng).unwrap();
943
944 let records = hook.get_crossovers();
945 assert_eq!(records.len(), 1);
946 assert_eq!(records[0].generation, 0);
947 }
948
949 #[test]
950 fn test_rate_limiting_hook() {
951 let mut rng = rand::thread_rng();
952 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
953
954 let hook = RateLimitingHook::new(2, 2);
955 let selector = UniformMutationSelector::new(1.0);
956 let mutation_fn = gaussian_mutation(0.1);
957
958 for _ in 0..2 {
960 let result = hooked_mutate_trace(&genome, &selector, &mutation_fn, &hook, 0, &mut rng);
961 assert!(result.is_ok());
962 }
963
964 let result = hooked_mutate_trace(&genome, &selector, &mutation_fn, &hook, 0, &mut rng);
966 assert!(result.is_ok());
967 }
968
969 #[test]
970 fn test_composed_hook() {
971 let mut rng = rand::thread_rng();
972 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
973
974 let logging = LoggingHook::new();
975 let rate_limit = RateLimitingHook::new(5, 5);
976
977 let composed = ComposedMutationHook::new()
978 .add(logging.clone())
979 .add(rate_limit);
980
981 let selector = UniformMutationSelector::new(1.0);
982 let mutation_fn = gaussian_mutation(0.1);
983
984 let _ = hooked_mutate_trace(&genome, &selector, mutation_fn, &composed, 0, &mut rng);
985
986 assert_eq!(logging.get_mutations().len(), 1);
988 }
989
990 #[test]
991 fn test_operation_statistics() {
992 let hook = LoggingHook::new();
993
994 hook.mutations.lock().unwrap().push(MutationRecord {
996 generation: 0,
997 mutated_addresses: vec![addr!("test", 0), addr!("test", 1)],
998 original_values: HashMap::new(),
999 new_values: HashMap::new(),
1000 });
1001
1002 let stats = OperationStatistics::from_hook(&hook);
1003 assert_eq!(stats.total_mutations, 1);
1004 assert_eq!(stats.avg_mutation_sites, 2.0);
1005 }
1006}