Skip to main content

fugue_evo/genome/
permutation.rs

1//! Permutation genome
2//!
3//! This module provides a permutation genome type for ordering problems
4//! (e.g., TSP, scheduling) with Fugue trace integration.
5
6#[cfg(feature = "ppl")]
7use fugue::{addr, ChoiceValue, Trace};
8use rand::seq::SliceRandom;
9use rand::Rng;
10use serde::{Deserialize, Serialize};
11
12use crate::error::GenomeError;
13use crate::genome::bounds::MultiBounds;
14use crate::genome::traits::{EvolutionaryGenome, PermutationGenome};
15
16/// Permutation genome for ordering problems
17///
18/// Represents a permutation of indices 0..n, commonly used for:
19/// - Traveling Salesman Problem (TSP)
20/// - Job Shop Scheduling
21/// - Vehicle Routing Problems
22/// - Any problem where the solution is an ordering of elements
23#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct Permutation {
25    /// The permutation (indices 0..n in some order)
26    perm: Vec<usize>,
27}
28
29impl Permutation {
30    /// Create a new permutation from a vector of indices
31    ///
32    /// # Panics
33    /// Panics if the input is not a valid permutation of 0..n
34    pub fn new(perm: Vec<usize>) -> Self {
35        let result = Self { perm };
36        assert!(
37            result.is_valid_permutation(),
38            "Input must be a valid permutation of 0..n"
39        );
40        result
41    }
42
43    /// Create a permutation from a vector **without** validating it.
44    ///
45    /// # Invariants
46    /// The caller must guarantee that `perm` is a valid permutation of
47    /// `0..perm.len()` — every index in that range appears exactly once.
48    /// Violating this invariant does not fail here, but corrupts downstream
49    /// operations that assume it:
50    /// - [`inverse`](Self::inverse) panics with an out-of-bounds index for an
51    ///   out-of-range value, or silently produces a meaningless result for
52    ///   in-range duplicates.
53    /// - [`compose`](Self::compose) panics on out-of-range indices.
54    ///
55    /// Prefer [`try_new`](Self::try_new) (or [`new`](Self::new)) unless validity
56    /// has already been established elsewhere and you need to skip the O(n)
57    /// re-check. In debug builds this constructor still asserts validity to
58    /// catch contract violations early.
59    pub fn from_vec_unchecked(perm: Vec<usize>) -> Self {
60        let result = Self { perm };
61        debug_assert!(
62            result.is_valid_permutation(),
63            "from_vec_unchecked called with a vector that is not a valid permutation of 0..n"
64        );
65        result
66    }
67
68    /// Try to create a permutation, returning an error if invalid
69    pub fn try_new(perm: Vec<usize>) -> Result<Self, GenomeError> {
70        let result = Self { perm };
71        if result.is_valid_permutation() {
72            Ok(result)
73        } else {
74            Err(GenomeError::InvalidStructure(
75                "Input is not a valid permutation of 0..n".to_string(),
76            ))
77        }
78    }
79
80    /// Create the identity permutation [0, 1, 2, ..., n-1]
81    pub fn identity(n: usize) -> Self {
82        Self {
83            perm: (0..n).collect(),
84        }
85    }
86
87    /// Create a random permutation of size n
88    pub fn random<R: Rng>(n: usize, rng: &mut R) -> Self {
89        let mut perm: Vec<usize> = (0..n).collect();
90        perm.shuffle(rng);
91        Self { perm }
92    }
93
94    /// Generate a random permutation of an explicit length.
95    ///
96    /// This is the honest constructor for random generation: unlike
97    /// [`EvolutionaryGenome::generate`],
98    /// which overloads `MultiBounds` and only reads its dimension count, this
99    /// takes the permutation length directly. Equivalent to [`random`](Self::random).
100    pub fn generate_with_len<R: Rng>(rng: &mut R, len: usize) -> Self {
101        Self::random(len, rng)
102    }
103
104    /// Get the length of the permutation
105    pub fn len(&self) -> usize {
106        self.perm.len()
107    }
108
109    /// Check if the permutation is empty
110    pub fn is_empty(&self) -> bool {
111        self.perm.is_empty()
112    }
113
114    /// Get the element at index i
115    pub fn get(&self, i: usize) -> Option<usize> {
116        self.perm.get(i).copied()
117    }
118
119    /// Get the inverse permutation
120    ///
121    /// If `perm[i] = j`, then `inverse[j] = i`
122    pub fn inverse(&self) -> Self {
123        let n = self.perm.len();
124        let mut inv = vec![0; n];
125        for (i, &j) in self.perm.iter().enumerate() {
126            inv[j] = i;
127        }
128        Self { perm: inv }
129    }
130
131    /// Compose this permutation with another
132    ///
133    /// Returns a permutation where `result[i] = other[self[i]]`
134    pub fn compose(&self, other: &Self) -> Result<Self, GenomeError> {
135        if self.perm.len() != other.perm.len() {
136            return Err(GenomeError::DimensionMismatch {
137                expected: self.perm.len(),
138                actual: other.perm.len(),
139            });
140        }
141        let composed: Vec<usize> = self.perm.iter().map(|&i| other.perm[i]).collect();
142        Ok(Self { perm: composed })
143    }
144
145    /// Swap two elements at positions i and j
146    pub fn swap(&mut self, i: usize, j: usize) {
147        self.perm.swap(i, j);
148    }
149
150    /// Reverse a segment from start to end (inclusive)
151    pub fn reverse_segment(&mut self, start: usize, end: usize) {
152        if start < end && end < self.perm.len() {
153            self.perm[start..=end].reverse();
154        }
155    }
156
157    /// Insert element at position `from` to position `to`
158    pub fn insert(&mut self, from: usize, to: usize) {
159        if from == to || from >= self.perm.len() || to >= self.perm.len() {
160            return;
161        }
162        let elem = self.perm.remove(from);
163        self.perm.insert(to, elem);
164    }
165
166    /// Calculate the number of inversions (disorder measure)
167    ///
168    /// An inversion is a pair (i, j) where i < j but `perm[i] > perm[j]`.
169    /// Returns a value in `[0, n*(n-1)/2]` where 0 means sorted.
170    pub fn inversions(&self) -> usize {
171        let n = self.perm.len();
172        let mut count = 0;
173        for i in 0..n {
174            for j in (i + 1)..n {
175                if self.perm[i] > self.perm[j] {
176                    count += 1;
177                }
178            }
179        }
180        count
181    }
182
183    /// Calculate Kendall tau distance to another permutation
184    ///
185    /// Counts the number of pairwise disagreements (i.e., pairs that are
186    /// in different order in the two permutations).
187    pub fn kendall_tau_distance(&self, other: &Self) -> Result<usize, GenomeError> {
188        if self.perm.len() != other.perm.len() {
189            return Err(GenomeError::DimensionMismatch {
190                expected: self.perm.len(),
191                actual: other.perm.len(),
192            });
193        }
194
195        // Compose with inverse of other to get relative order
196        let other_inv = other.inverse();
197        let composed = self.compose(&other_inv)?;
198
199        // Count inversions in the composed permutation
200        Ok(composed.inversions())
201    }
202
203    /// Check if this is a cyclic permutation (single cycle)
204    pub fn is_cyclic(&self) -> bool {
205        if self.perm.is_empty() {
206            return true;
207        }
208
209        let n = self.perm.len();
210        let mut visited = vec![false; n];
211        let mut current = 0;
212        let mut cycle_len = 0;
213
214        while !visited[current] {
215            visited[current] = true;
216            current = self.perm[current];
217            cycle_len += 1;
218        }
219
220        cycle_len == n && current == 0
221    }
222
223    /// Get the underlying vector
224    pub fn into_inner(self) -> Vec<usize> {
225        self.perm
226    }
227
228    /// Get a reference to the underlying slice
229    pub fn as_slice(&self) -> &[usize] {
230        &self.perm
231    }
232}
233
234impl EvolutionaryGenome for Permutation {
235    type Allele = usize;
236    type Phenotype = Vec<usize>;
237
238    fn decode(&self) -> Self::Phenotype {
239        self.perm.clone()
240    }
241
242    fn dimension(&self) -> usize {
243        self.perm.len()
244    }
245
246    /// Generate a random permutation.
247    ///
248    /// Only `bounds.dimension()` is consulted — it is the permutation length —
249    /// and the per-dimension `min`/`max` values are ignored. Prefer
250    /// [`Permutation::generate_with_len`] to make the length explicit.
251    fn generate<R: Rng>(rng: &mut R, bounds: &MultiBounds) -> Self {
252        Self::generate_with_len(rng, bounds.dimension())
253    }
254
255    fn distance(&self, other: &Self) -> f64 {
256        self.try_distance(other).unwrap_or_else(|e| {
257            panic!("Permutation::distance: {e}; use try_distance for a fallible comparison")
258        })
259    }
260
261    fn try_distance(&self, other: &Self) -> Result<f64, GenomeError> {
262        // kendall_tau_distance already errors on a length mismatch; propagate it
263        // instead of collapsing it to 0.0 (which meant "identical").
264        self.kendall_tau_distance(other).map(|d| d as f64)
265    }
266}
267
268#[cfg(feature = "ppl")]
269impl crate::genome::trace_genome::TraceGenome for Permutation {
270    /// Convert Permutation to Fugue trace using the **Lehmer-code (rank)**
271    /// encoding: position `i` stores the rank of `perm[i]` among the values
272    /// not yet used at positions `< i` (a `Usize` in `0..n-i`).
273    ///
274    /// This encoding (rather than storing raw values) is what makes the trace
275    /// *generative*: any in-range assignment of ranks decodes to a valid
276    /// permutation, so a single-site change of one rank is a valid move — the
277    /// value encoding would make every single-site change a duplicate. It
278    /// coincides site-for-site with the sequential categorical prior model in
279    /// [`crate::inference::prior::PermutationPrior`].
280    fn to_trace(&self) -> Trace {
281        let n = self.perm.len();
282        let mut available: Vec<usize> = (0..n).collect();
283        let mut trace = Trace::default();
284        for (i, &val) in self.perm.iter().enumerate() {
285            let rank = available
286                .iter()
287                .position(|&v| v == val)
288                .expect("Permutation invariant guarantees the value is available");
289            available.remove(rank);
290            trace.insert_choice(addr!("perm", i), ChoiceValue::Usize(rank), 0.0);
291        }
292        trace
293    }
294
295    /// Reconstruct Permutation from Fugue trace.
296    ///
297    /// Reads values from addresses "perm#0", "perm#1", ... until no more are
298    /// found. A *missing* address terminates the scan (normal end of the
299    /// sequence), but an address that is *present with the wrong value type* is
300    /// a corrupt trace and yields [`GenomeError::TypeMismatch`] rather than
301    /// silently truncating — which for a permutation is especially dangerous,
302    /// since a truncated prefix can itself pass the validity check.
303    fn from_trace(trace: &Trace) -> Result<Self, GenomeError> {
304        // First pass: read the rank sequence (Lehmer code).
305        let mut ranks = Vec::new();
306        let mut i = 0;
307        loop {
308            match trace.choices.get(&addr!("perm", i)) {
309                None => break,
310                Some(choice) => match choice.value.as_usize() {
311                    Some(rank) => {
312                        ranks.push(rank);
313                        i += 1;
314                    }
315                    None => {
316                        return Err(GenomeError::TypeMismatch {
317                            address: format!("perm#{i}"),
318                            expected: "usize".to_string(),
319                            actual: choice.value.type_name().to_string(),
320                        });
321                    }
322                },
323            }
324        }
325        if ranks.is_empty() {
326            return Err(GenomeError::InvalidStructure(
327                "No permutation found in trace".to_string(),
328            ));
329        }
330        // Second pass: decode ranks against the shrinking available list.
331        let n = ranks.len();
332        let mut available: Vec<usize> = (0..n).collect();
333        let mut perm = Vec::with_capacity(n);
334        for (i, &rank) in ranks.iter().enumerate() {
335            if rank >= available.len() {
336                return Err(GenomeError::InvalidStructure(format!(
337                    "Lehmer rank {rank} at perm#{i} out of range 0..{}",
338                    available.len()
339                )));
340            }
341            perm.push(available.remove(rank));
342        }
343        Self::try_new(perm)
344    }
345
346    fn trace_prefix() -> &'static str {
347        "perm"
348    }
349}
350
351impl PermutationGenome for Permutation {
352    fn permutation(&self) -> &[usize] {
353        &self.perm
354    }
355
356    fn permutation_mut(&mut self) -> &mut [usize] {
357        &mut self.perm
358    }
359
360    fn from_permutation(perm: Vec<usize>) -> Result<Self, GenomeError> {
361        Self::try_new(perm)
362    }
363}
364
365impl std::ops::Index<usize> for Permutation {
366    type Output = usize;
367
368    fn index(&self, index: usize) -> &Self::Output {
369        &self.perm[index]
370    }
371}
372
373impl From<Vec<usize>> for Permutation {
374    fn from(perm: Vec<usize>) -> Self {
375        Self::new(perm)
376    }
377}
378
379impl From<Permutation> for Vec<usize> {
380    fn from(p: Permutation) -> Self {
381        p.perm
382    }
383}
384
385impl IntoIterator for Permutation {
386    type Item = usize;
387    type IntoIter = std::vec::IntoIter<usize>;
388
389    fn into_iter(self) -> Self::IntoIter {
390        self.perm.into_iter()
391    }
392}
393
394impl<'a> IntoIterator for &'a Permutation {
395    type Item = &'a usize;
396    type IntoIter = std::slice::Iter<'a, usize>;
397
398    fn into_iter(self) -> Self::IntoIter {
399        self.perm.iter()
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::genome::traits::PermutationGenome;
407
408    #[test]
409    fn test_permutation_new() {
410        let p = Permutation::new(vec![2, 0, 1, 3]);
411        assert_eq!(p.len(), 4);
412        assert_eq!(p[0], 2);
413        assert_eq!(p[1], 0);
414    }
415
416    #[test]
417    #[should_panic(expected = "valid permutation")]
418    fn test_permutation_new_invalid_duplicate() {
419        Permutation::new(vec![0, 1, 1, 3]);
420    }
421
422    #[test]
423    #[should_panic(expected = "valid permutation")]
424    fn test_permutation_new_invalid_out_of_range() {
425        Permutation::new(vec![0, 1, 5, 3]);
426    }
427
428    #[test]
429    fn test_permutation_try_new() {
430        assert!(Permutation::try_new(vec![2, 0, 1, 3]).is_ok());
431        assert!(Permutation::try_new(vec![0, 1, 1, 3]).is_err());
432    }
433
434    #[test]
435    fn test_permutation_identity() {
436        let p = Permutation::identity(5);
437        assert_eq!(p.as_slice(), &[0, 1, 2, 3, 4]);
438    }
439
440    #[test]
441    fn test_permutation_random() {
442        let mut rng = rand::thread_rng();
443        let p = Permutation::random(10, &mut rng);
444        assert!(p.is_valid_permutation());
445        assert_eq!(p.len(), 10);
446    }
447
448    #[test]
449    fn test_permutation_inverse() {
450        let p = Permutation::new(vec![2, 0, 3, 1]);
451        let inv = p.inverse();
452        // If p[i] = j, then inv[j] = i
453        // p[0]=2 -> inv[2]=0, p[1]=0 -> inv[0]=1, p[2]=3 -> inv[3]=2, p[3]=1 -> inv[1]=3
454        assert_eq!(inv.as_slice(), &[1, 3, 0, 2]);
455
456        // Composing with inverse should give identity
457        let composed = p.compose(&inv).unwrap();
458        assert_eq!(composed.as_slice(), &[0, 1, 2, 3]);
459    }
460
461    #[test]
462    fn test_permutation_compose() {
463        let p1 = Permutation::new(vec![1, 2, 0]);
464        let p2 = Permutation::new(vec![2, 0, 1]);
465        let composed = p1.compose(&p2).unwrap();
466        // composed[i] = p2[p1[i]]
467        // composed[0] = p2[1] = 0, composed[1] = p2[2] = 1, composed[2] = p2[0] = 2
468        assert_eq!(composed.as_slice(), &[0, 1, 2]);
469    }
470
471    #[test]
472    fn test_permutation_swap() {
473        let mut p = Permutation::new(vec![0, 1, 2, 3]);
474        p.swap(0, 3);
475        assert_eq!(p.as_slice(), &[3, 1, 2, 0]);
476    }
477
478    #[test]
479    fn test_permutation_reverse_segment() {
480        let mut p = Permutation::new(vec![0, 1, 2, 3, 4]);
481        p.reverse_segment(1, 3);
482        assert_eq!(p.as_slice(), &[0, 3, 2, 1, 4]);
483    }
484
485    #[test]
486    fn test_permutation_insert() {
487        let mut p = Permutation::new(vec![0, 1, 2, 3, 4]);
488        p.insert(1, 4);
489        assert_eq!(p.as_slice(), &[0, 2, 3, 4, 1]);
490    }
491
492    #[test]
493    fn test_permutation_inversions() {
494        // Sorted: 0 inversions
495        let p1 = Permutation::identity(5);
496        assert_eq!(p1.inversions(), 0);
497
498        // Reversed: n*(n-1)/2 inversions
499        let p2 = Permutation::new(vec![4, 3, 2, 1, 0]);
500        assert_eq!(p2.inversions(), 10); // 5*4/2 = 10
501
502        // Single swap: 1 inversion
503        let p3 = Permutation::new(vec![1, 0, 2, 3, 4]);
504        assert_eq!(p3.inversions(), 1);
505    }
506
507    #[test]
508    fn test_permutation_kendall_tau() {
509        let p1 = Permutation::new(vec![0, 1, 2, 3]);
510        let p2 = Permutation::new(vec![0, 1, 2, 3]);
511        assert_eq!(p1.kendall_tau_distance(&p2).unwrap(), 0);
512
513        let p3 = Permutation::new(vec![0, 1, 3, 2]);
514        assert_eq!(p1.kendall_tau_distance(&p3).unwrap(), 1);
515
516        let p4 = Permutation::new(vec![3, 2, 1, 0]);
517        assert_eq!(p1.kendall_tau_distance(&p4).unwrap(), 6);
518    }
519
520    #[test]
521    fn test_permutation_is_cyclic() {
522        // Single cycle (3 -> 1 -> 2 -> 0 -> 3)
523        let cyclic = Permutation::new(vec![3, 2, 0, 1]);
524        // Let's trace: 0 -> 3 -> 1 -> 2 -> 0, that's 4 elements in one cycle
525        assert!(cyclic.is_cyclic());
526
527        // Not a single cycle: identity has n fixed points (1-cycles)
528        let identity = Permutation::identity(4);
529        assert!(!identity.is_cyclic()); // Each element maps to itself
530
531        // Empty is trivially cyclic
532        let empty = Permutation::identity(0);
533        assert!(empty.is_cyclic());
534    }
535
536    #[test]
537    fn test_permutation_decode() {
538        let p = Permutation::new(vec![2, 0, 1]);
539        assert_eq!(p.decode(), vec![2, 0, 1]);
540    }
541
542    #[test]
543    fn test_permutation_dimension() {
544        let p = Permutation::new(vec![2, 0, 1, 3, 4]);
545        assert_eq!(p.dimension(), 5);
546    }
547
548    #[test]
549    fn test_permutation_generate() {
550        let mut rng = rand::thread_rng();
551        let bounds = MultiBounds::symmetric(1.0, 10);
552        let p = Permutation::generate(&mut rng, &bounds);
553        assert_eq!(p.dimension(), 10);
554        assert!(p.is_valid_permutation());
555    }
556
557    #[test]
558    fn test_permutation_distance() {
559        let p1 = Permutation::new(vec![0, 1, 2, 3]);
560        let p2 = Permutation::new(vec![3, 2, 1, 0]);
561        assert_eq!(p1.distance(&p2), 6.0);
562    }
563
564    #[test]
565    #[cfg(feature = "ppl")]
566    fn test_permutation_to_trace() {
567        use crate::genome::trace_genome::TraceGenome;
568        let p = Permutation::new(vec![2, 0, 1]);
569        let trace = p.to_trace();
570
571        // Lehmer-code (rank) encoding: [2,0,1] -> ranks (2, 0, 0).
572        assert_eq!(trace.get_usize(&addr!("perm", 0)), Some(2));
573        assert_eq!(trace.get_usize(&addr!("perm", 1)), Some(0));
574        assert_eq!(trace.get_usize(&addr!("perm", 2)), Some(0));
575        assert_eq!(trace.get_usize(&addr!("perm", 3)), None);
576    }
577
578    #[test]
579    #[cfg(feature = "ppl")]
580    fn test_permutation_from_trace() {
581        use crate::genome::trace_genome::TraceGenome;
582        let mut trace = Trace::default();
583        // Lehmer ranks (1, 1, 0): available [0,1,2] -> 1; [0,2] -> 2; [0] -> 0.
584        trace.insert_choice(addr!("perm", 0), ChoiceValue::Usize(1), 0.0);
585        trace.insert_choice(addr!("perm", 1), ChoiceValue::Usize(1), 0.0);
586        trace.insert_choice(addr!("perm", 2), ChoiceValue::Usize(0), 0.0);
587
588        let p = Permutation::from_trace(&trace).unwrap();
589        assert_eq!(p.as_slice(), &[1, 2, 0]);
590    }
591
592    #[test]
593    #[cfg(feature = "ppl")]
594    fn test_permutation_trace_roundtrip() {
595        use crate::genome::trace_genome::TraceGenome;
596        let original = Permutation::new(vec![4, 2, 0, 3, 1]);
597        let trace = original.to_trace();
598        let recovered = Permutation::from_trace(&trace).unwrap();
599        assert_eq!(original, recovered);
600    }
601
602    #[test]
603    #[cfg(feature = "ppl")]
604    fn test_permutation_from_trace_invalid() {
605        use crate::genome::trace_genome::TraceGenome;
606        let mut trace = Trace::default();
607        trace.insert_choice(addr!("perm", 0), ChoiceValue::Usize(0), 0.0);
608        trace.insert_choice(addr!("perm", 1), ChoiceValue::Usize(5), 0.0); // rank out of range
609
610        let result = Permutation::from_trace(&trace);
611        assert!(result.is_err());
612    }
613
614    #[test]
615    #[cfg(feature = "ppl")]
616    fn test_permutation_from_trace_empty() {
617        use crate::genome::trace_genome::TraceGenome;
618        let trace = Trace::default();
619        let result = Permutation::from_trace(&trace);
620        assert!(result.is_err());
621    }
622
623    #[test]
624    fn test_permutation_serialization() {
625        let p = Permutation::new(vec![2, 0, 1, 3]);
626        let serialized = serde_json::to_string(&p).unwrap();
627        let deserialized: Permutation = serde_json::from_str(&serialized).unwrap();
628        assert_eq!(p, deserialized);
629    }
630
631    #[test]
632    fn test_permutation_iteration() {
633        let p = Permutation::new(vec![2, 0, 1]);
634        let collected: Vec<usize> = p.into_iter().collect();
635        assert_eq!(collected, vec![2, 0, 1]);
636    }
637
638    #[test]
639    fn test_permutation_ref_iteration() {
640        let p = Permutation::new(vec![2, 0, 1]);
641        let sum: usize = p.into_iter().sum();
642        assert_eq!(sum, 3);
643    }
644
645    #[test]
646    fn test_permutation_into_inner() {
647        let p = Permutation::new(vec![2, 0, 1]);
648        let v: Vec<usize> = p.into_inner();
649        assert_eq!(v, vec![2, 0, 1]);
650    }
651
652    #[test]
653    fn test_permutation_from_vec() {
654        let p: Permutation = vec![1, 0, 2].into();
655        assert_eq!(p.as_slice(), &[1, 0, 2]);
656    }
657
658    #[test]
659    fn test_permutation_try_distance_length_mismatch() {
660        // regression: EV-19 — distance previously swallowed the length-mismatch
661        // Err via unwrap_or(0) and reported 0.0 ("identical") for different sizes.
662        let p1 = Permutation::identity(3);
663        let p2 = Permutation::identity(5);
664        assert!(matches!(
665            p1.try_distance(&p2),
666            Err(GenomeError::DimensionMismatch {
667                expected: 3,
668                actual: 5
669            })
670        ));
671    }
672
673    #[test]
674    #[should_panic(expected = "Dimension mismatch")]
675    fn test_permutation_distance_length_mismatch_panics() {
676        // regression: EV-19 — distance() must loudly reject a length mismatch
677        // rather than returning 0.0 as if the permutations were identical.
678        let p1 = Permutation::identity(3);
679        let p2 = Permutation::identity(5);
680        let _ = p1.distance(&p2);
681    }
682
683    #[test]
684    #[cfg(feature = "ppl")]
685    fn test_permutation_from_trace_type_mismatch() {
686        // regression: EV-59 — a present-but-wrong-typed choice must raise
687        // TypeMismatch. This is especially important for permutations, since a
688        // silently truncated prefix can itself be a "valid" shorter permutation.
689        use crate::genome::trace_genome::TraceGenome;
690        let mut trace = Trace::default();
691        trace.insert_choice(addr!("perm", 0), ChoiceValue::Usize(2), 0.0);
692        trace.insert_choice(addr!("perm", 1), ChoiceValue::Bool(true), 0.0); // wrong type
693        trace.insert_choice(addr!("perm", 2), ChoiceValue::Usize(0), 0.0);
694
695        match Permutation::from_trace(&trace) {
696            Err(GenomeError::TypeMismatch {
697                address,
698                expected,
699                actual,
700            }) => {
701                assert_eq!(address, "perm#1");
702                assert_eq!(expected, "usize");
703                assert_eq!(actual, "bool");
704            }
705            other => panic!("expected TypeMismatch, got {other:?}"),
706        }
707    }
708
709    #[test]
710    fn test_permutation_from_vec_unchecked_debug_asserts() {
711        // regression: EV-92 — the unchecked constructor now debug-asserts the
712        // invariant. In debug builds an invalid vector triggers the assertion.
713        let p = Permutation::from_vec_unchecked(vec![2, 0, 1]);
714        assert!(p.is_valid_permutation());
715    }
716
717    #[test]
718    #[cfg(debug_assertions)]
719    #[should_panic(expected = "not a valid permutation")]
720    fn test_permutation_from_vec_unchecked_rejects_invalid_in_debug() {
721        // regression: EV-92 — in-range duplicates are a contract violation.
722        let _ = Permutation::from_vec_unchecked(vec![0, 0, 2]);
723    }
724
725    #[test]
726    fn test_permutation_generate_with_len() {
727        // EV-94: honest constructor takes an explicit length.
728        let mut rng = rand::thread_rng();
729        let p = Permutation::generate_with_len(&mut rng, 8);
730        assert_eq!(p.len(), 8);
731        assert!(p.is_valid_permutation());
732    }
733}