Skip to main content

rlevo_evolution/algorithms/gep/
operators.rs

1//! GEP genetic operators, all valid by construction (no repair pass).
2//!
3//! Every operator preserves the head/tail position-class invariant and the
4//! fixed chromosome length, so any offspring still decodes to a complete
5//! expression tree. Operators act on host-side `&mut [Symbol]`
6//! chromosomes: the population tensor is pulled to host once per generation
7//! (alongside the host-side roulette selection it shares the round-trip with),
8//! the operators run, and the result is re-uploaded.
9//!
10//! - **Point mutation** is locus-class aware — the single operator that needs a
11//!   sampling constraint. Head loci draw any symbol; tail loci draw terminals
12//!   only, keeping the tail invariant intact.
13//! - **IS / RIS transposition** rewrite only the head (fixed length, tail
14//!   untouched); any symbol is legal in the head, so they cannot break validity.
15//! - **One-/two-point crossover** swap class-aligned segments between two
16//!   equal-layout parents, so a tail locus always exchanges with a tail locus.
17
18use rand::{Rng, RngExt};
19
20use crate::function_set::{FunctionSet, Symbol};
21
22use super::alphabet::Alphabet;
23
24/// Maximum transposed-sequence length (Ferreira uses short IS/RIS elements).
25const MAX_TRANSPOSON_LEN: usize = 3;
26
27/// Applies per-gene point mutation in place, respecting locus classes.
28///
29/// Each locus mutates independently with probability `rate`. A head locus
30/// (index `< head_len`) is replaced by any symbol; a tail locus is replaced by
31/// a terminal drawn from [`Alphabet::terminal_range`]. The tail invariant is
32/// therefore preserved without any repair.
33pub fn point_mutation<F: FunctionSet>(
34    chromosome: &mut [Symbol],
35    head_len: usize,
36    alphabet: &Alphabet<F>,
37    rate: f32,
38    rng: &mut dyn Rng,
39) {
40    for (i, locus) in chromosome.iter_mut().enumerate() {
41        // `< rate` reads as "mutate with probability `rate`" and is robust to a
42        // stray non-finite `rate` (`x < NaN` is `false` ⇒ no mutation). Callers
43        // pass a validated `Probability::get()`, so this is defense-in-depth.
44        if rng.random::<f32>() < rate {
45            *locus = if i < head_len {
46                alphabet.sample_head_symbol(rng)
47            } else {
48                alphabet.sample_tail_symbol(rng)
49            };
50        }
51    }
52}
53
54/// Insertion-Sequence (IS) transposition: copies a short sequence and inserts
55/// it at a non-root head locus, shifting the rest of the head right and dropping
56/// the overflow off the head end. The tail is untouched.
57pub fn is_transposition(chromosome: &mut [Symbol], head_len: usize, rng: &mut dyn Rng) {
58    let genome_len = chromosome.len();
59    if head_len < 2 || genome_len == 0 {
60        return; // no valid (non-root) insertion site
61    }
62    let max_len = MAX_TRANSPOSON_LEN.min(genome_len);
63    let len = rng.random_range(1..=max_len);
64    let source_start = rng.random_range(0..=(genome_len - len));
65    let insert = rng.random_range(1..head_len); // != 0: keep the root
66
67    let seq: Vec<Symbol> = chromosome[source_start..source_start + len].to_vec();
68    let mut new_head: Vec<Symbol> = Vec::with_capacity(head_len + len);
69    new_head.extend_from_slice(&chromosome[0..insert]);
70    new_head.extend_from_slice(&seq);
71    new_head.extend_from_slice(&chromosome[insert..head_len]);
72    new_head.truncate(head_len);
73    chromosome[0..head_len].copy_from_slice(&new_head);
74}
75
76/// Root-Insertion-Sequence (RIS) transposition: finds a function symbol in the
77/// head, copies the sequence starting there to head position 0, shifts right,
78/// and drops the overflow. Guarantees the root becomes a function. The tail is
79/// untouched.
80pub fn ris_transposition<F: FunctionSet>(
81    chromosome: &mut [Symbol],
82    head_len: usize,
83    alphabet: &Alphabet<F>,
84    rng: &mut dyn Rng,
85) {
86    if head_len == 0 {
87        return;
88    }
89    // Scan the head from a random offset for the first function (arity >= 1).
90    let offset = rng.random_range(0..head_len);
91    let func_pos = (0..head_len)
92        .map(|k| (offset + k) % head_len)
93        .find(|&i| alphabet.arity(chromosome[i]) >= 1);
94    let Some(func_pos) = func_pos else {
95        return; // no function in the head; nothing to root
96    };
97
98    let max_len = MAX_TRANSPOSON_LEN.min(head_len - func_pos);
99    let len = rng.random_range(1..=max_len);
100    let seq: Vec<Symbol> = chromosome[func_pos..func_pos + len].to_vec();
101
102    let mut new_head: Vec<Symbol> = Vec::with_capacity(head_len + len);
103    new_head.extend_from_slice(&seq);
104    new_head.extend_from_slice(&chromosome[0..head_len]);
105    new_head.truncate(head_len);
106    chromosome[0..head_len].copy_from_slice(&new_head);
107}
108
109/// One-point crossover: swaps the suffixes of two equal-length parents at a
110/// random cut. Class-aligned, so the tail stays terminal-only.
111///
112/// # Panics
113///
114/// Panics if `a.len() != b.len()` — crossover parents must share the genome
115/// layout. This is a programming error, not a recoverable condition.
116pub fn one_point_crossover(a: &mut [Symbol], b: &mut [Symbol], rng: &mut dyn Rng) {
117    let n = a.len();
118    assert_eq!(n, b.len(), "crossover parents must share genome length");
119    if n < 2 {
120        return;
121    }
122    let cut = rng.random_range(1..n);
123    a[cut..].swap_with_slice(&mut b[cut..]);
124}
125
126/// Two-point crossover: swaps the middle segment between two random cuts.
127/// Class-aligned, so the tail stays terminal-only.
128///
129/// # Panics
130///
131/// Panics if `a.len() != b.len()` — crossover parents must share the genome
132/// layout. This is a programming error, not a recoverable condition.
133pub fn two_point_crossover(a: &mut [Symbol], b: &mut [Symbol], rng: &mut dyn Rng) {
134    let n = a.len();
135    assert_eq!(n, b.len(), "crossover parents must share genome length");
136    if n < 2 {
137        return;
138    }
139    let mut c1 = rng.random_range(0..n);
140    let mut c2 = rng.random_range(1..=n);
141    if c1 > c2 {
142        std::mem::swap(&mut c1, &mut c2);
143    }
144    if c1 == c2 {
145        return;
146    }
147    a[c1..c2].swap_with_slice(&mut b[c1..c2]);
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::algorithms::gep::decode::GenotypePhenotypeMap;
154    use crate::algorithms::gep::{GepConfig, GepDecoder};
155    use crate::function_set::ArithmeticFunctionSet;
156    use crate::rng::{SeedPurpose, seed_stream};
157    // Explicit list (not the glob prelude): the prelude re-exports a `Rng`
158    // that would collide with `rand::Rng` pulled in via `use super::*`.
159    use proptest::prelude::{ProptestConfig, any, prop_assert, proptest};
160
161    type Fs = ArithmeticFunctionSet;
162
163    fn alphabet() -> Alphabet<Fs> {
164        Alphabet::new(ArithmeticFunctionSet, 1, vec![])
165    }
166
167    /// Samples a fresh valid chromosome (head = any, tail = terminals).
168    fn sample_valid(alphabet: &Alphabet<Fs>, cfg: &GepConfig, rng: &mut dyn Rng) -> Vec<Symbol> {
169        let mut g = Vec::with_capacity(cfg.genome_len());
170        for _ in 0..cfg.head_len {
171            g.push(alphabet.sample_head_symbol(rng));
172        }
173        for _ in 0..cfg.tail_len {
174            g.push(alphabet.sample_tail_symbol(rng));
175        }
176        g
177    }
178
179    /// True iff every tail locus holds a terminal (arity 0).
180    fn tail_all_terminals(g: &[Symbol], head_len: usize, a: &Alphabet<Fs>) -> bool {
181        g[head_len..].iter().all(|&s| a.arity(s) == 0)
182    }
183
184    /// True iff the decoded tree is a complete expression tree (the ORF closed
185    /// inside the chromosome — the "no repair needed" guarantee).
186    fn decodes_complete(g: &[Symbol], a: &Alphabet<Fs>) -> bool {
187        let tree = GepDecoder.decode(a, g);
188        let mut needed: i64 = 1;
189        for &s in tree.nodes() {
190            needed += i64::try_from(a.arity(s)).unwrap() - 1;
191        }
192        needed == 0 && tree.node_count() >= 1
193    }
194
195    /// 1000 point mutations never violate the tail invariant.
196    #[test]
197    fn point_mutation_preserves_tail_invariant() {
198        let a = alphabet();
199        let cfg = GepConfig::new(7, 2, 1, 100).unwrap();
200        let mut rng = seed_stream(1, 0, SeedPurpose::Mutation);
201        for _ in 0..1000 {
202            let mut g = sample_valid(&a, &cfg, &mut rng);
203            // Force a high rate to exercise many loci.
204            point_mutation(&mut g, cfg.head_len, &a, 0.5, &mut rng);
205            assert!(
206                tail_all_terminals(&g, cfg.head_len, &a),
207                "tail invariant violated: {g:?}"
208            );
209            assert!(decodes_complete(&g, &a));
210        }
211    }
212
213    /// `rate == 0.0` never mutates; `rate == 1.0` mutates every locus. Pins the
214    /// `< rate` gate semantics (finding operators.rs §1.1).
215    #[test]
216    fn test_point_mutation_rate_bounds_are_none_and_all() {
217        let a = alphabet();
218        let cfg = GepConfig::new(7, 2, 1, 100).unwrap();
219        let mut rng = seed_stream(9, 0, SeedPurpose::Mutation);
220
221        // rate 0 -> identity on every locus.
222        let original = sample_valid(&a, &cfg, &mut rng);
223        let mut g = original.clone();
224        point_mutation(&mut g, cfg.head_len, &a, 0.0, &mut rng);
225        assert_eq!(g, original, "rate 0.0 must leave the chromosome unchanged");
226
227        // rate 1 -> every locus is resampled (still a valid symbol for its class).
228        let mut g = sample_valid(&a, &cfg, &mut rng);
229        point_mutation(&mut g, cfg.head_len, &a, 1.0, &mut rng);
230        assert!(
231            tail_all_terminals(&g, cfg.head_len, &a),
232            "rate 1.0 must preserve the tail invariant: {g:?}"
233        );
234        assert!(
235            decodes_complete(&g, &a),
236            "rate 1.0 offspring must still decode completely"
237        );
238    }
239
240    /// 500 trials of each operator yield offspring that decode completely.
241    #[test]
242    fn all_operators_yield_decodable_offspring() {
243        let a = alphabet();
244        let cfg = GepConfig::new(7, 2, 1, 100).unwrap();
245        let mut rng = seed_stream(2, 0, SeedPurpose::Crossover);
246
247        for _ in 0..500 {
248            // IS
249            let mut g = sample_valid(&a, &cfg, &mut rng);
250            is_transposition(&mut g, cfg.head_len, &mut rng);
251            assert!(tail_all_terminals(&g, cfg.head_len, &a));
252            assert!(decodes_complete(&g, &a));
253
254            // RIS
255            let mut g = sample_valid(&a, &cfg, &mut rng);
256            ris_transposition(&mut g, cfg.head_len, &a, &mut rng);
257            assert!(tail_all_terminals(&g, cfg.head_len, &a));
258            assert!(decodes_complete(&g, &a));
259
260            // 1-point crossover
261            let mut p1 = sample_valid(&a, &cfg, &mut rng);
262            let mut p2 = sample_valid(&a, &cfg, &mut rng);
263            one_point_crossover(&mut p1, &mut p2, &mut rng);
264            assert!(tail_all_terminals(&p1, cfg.head_len, &a));
265            assert!(tail_all_terminals(&p2, cfg.head_len, &a));
266            assert!(decodes_complete(&p1, &a));
267            assert!(decodes_complete(&p2, &a));
268
269            // 2-point crossover
270            let mut p1 = sample_valid(&a, &cfg, &mut rng);
271            let mut p2 = sample_valid(&a, &cfg, &mut rng);
272            two_point_crossover(&mut p1, &mut p2, &mut rng);
273            assert!(tail_all_terminals(&p1, cfg.head_len, &a));
274            assert!(tail_all_terminals(&p2, cfg.head_len, &a));
275            assert!(decodes_complete(&p1, &a));
276            assert!(decodes_complete(&p2, &a));
277        }
278    }
279
280    /// RIS makes the root a function (when the head holds at least one).
281    #[test]
282    fn ris_roots_a_function() {
283        let a = alphabet();
284        let cfg = GepConfig::new(7, 2, 1, 10).unwrap();
285        let mut rng = seed_stream(3, 0, SeedPurpose::Crossover);
286        let mut rooted = 0;
287        for _ in 0..200 {
288            let mut g = sample_valid(&a, &cfg, &mut rng);
289            // Guarantee at least one function in the head.
290            g[0] = Symbol::from_raw(0);
291            ris_transposition(&mut g, cfg.head_len, &a, &mut rng);
292            if a.arity(g[0]) >= 1 {
293                rooted += 1;
294            }
295        }
296        assert_eq!(rooted, 200, "RIS should always root a function");
297    }
298
299    /// Transposition leaves the tail bytes untouched.
300    #[test]
301    #[allow(clippy::similar_names)]
302    fn transposition_does_not_touch_tail() {
303        let a = alphabet();
304        let cfg = GepConfig::new(7, 2, 1, 10).unwrap();
305        let mut rng = seed_stream(4, 0, SeedPurpose::Crossover);
306        let g = sample_valid(&a, &cfg, &mut rng);
307        let tail_before = g[cfg.head_len..].to_vec();
308        let mut g_is = g.clone();
309        is_transposition(&mut g_is, cfg.head_len, &mut rng);
310        assert_eq!(&g_is[cfg.head_len..], &tail_before[..]);
311        let mut g_ris = g.clone();
312        ris_transposition(&mut g_ris, cfg.head_len, &a, &mut rng);
313        assert_eq!(&g_ris[cfg.head_len..], &tail_before[..]);
314    }
315
316    /// A `NaN` rate is a no-op: `x < NaN` is `false`, so no locus mutates
317    /// (defense-in-depth against a stray non-finite rate, operators.rs §1).
318    #[test]
319    fn point_mutation_nan_rate_is_no_op() {
320        let a = alphabet();
321        let cfg = GepConfig::new(7, 2, 1, 10).unwrap();
322        let mut rng = seed_stream(21, 0, SeedPurpose::Mutation);
323        let original = sample_valid(&a, &cfg, &mut rng);
324        let mut g = original.clone();
325        point_mutation(&mut g, cfg.head_len, &a, f32::NAN, &mut rng);
326        assert_eq!(g, original, "NaN rate must leave the chromosome unchanged");
327    }
328
329    /// An out-of-range rate (`> 1`) mutates every locus — `x < 2.0` is always
330    /// true — yet the tail invariant and the decode guarantee still hold.
331    #[test]
332    fn point_mutation_out_of_range_rate_resamples_all_but_stays_valid() {
333        let a = alphabet();
334        let cfg = GepConfig::new(7, 2, 1, 10).unwrap();
335        let mut rng = seed_stream(22, 0, SeedPurpose::Mutation);
336        let mut g = sample_valid(&a, &cfg, &mut rng);
337        point_mutation(&mut g, cfg.head_len, &a, 2.0, &mut rng);
338        assert!(tail_all_terminals(&g, cfg.head_len, &a));
339        assert!(decodes_complete(&g, &a));
340    }
341
342    /// No operator panics on an empty chromosome / empty parent pair.
343    #[test]
344    fn operators_do_not_panic_on_empty_slices() {
345        let a = alphabet();
346        let mut rng = seed_stream(23, 0, SeedPurpose::Crossover);
347
348        let mut empty: Vec<Symbol> = Vec::new();
349        point_mutation(&mut empty, 0, &a, 1.0, &mut rng);
350        is_transposition(&mut empty, 0, &mut rng);
351        ris_transposition(&mut empty, 0, &a, &mut rng);
352        assert!(empty.is_empty());
353
354        let mut lhs: Vec<Symbol> = Vec::new();
355        let mut rhs: Vec<Symbol> = Vec::new();
356        one_point_crossover(&mut lhs, &mut rhs, &mut rng);
357        two_point_crossover(&mut lhs, &mut rhs, &mut rng);
358        assert!(lhs.is_empty() && rhs.is_empty());
359    }
360
361    /// With `head_len == 1` both transpositions are safe: IS has no non-root
362    /// insertion site (no-op) and RIS keeps the single root, preserving length,
363    /// the tail invariant, and a complete decode.
364    #[test]
365    fn transposition_with_head_len_one_is_safe() {
366        let a = alphabet();
367        let cfg = GepConfig::new(1, 2, 1, 10).unwrap();
368        assert_eq!(cfg.head_len, 1);
369        let mut rng = seed_stream(24, 0, SeedPurpose::Transposition);
370        for _ in 0..200 {
371            let mut g = sample_valid(&a, &cfg, &mut rng);
372            // Guarantee a function at the single head locus for RIS.
373            g[0] = Symbol::from_raw(0);
374            let len_before = g.len();
375            is_transposition(&mut g, cfg.head_len, &mut rng);
376            ris_transposition(&mut g, cfg.head_len, &a, &mut rng);
377            assert_eq!(g.len(), len_before);
378            assert!(tail_all_terminals(&g, cfg.head_len, &a));
379            assert!(decodes_complete(&g, &a));
380        }
381    }
382
383    /// Single-locus parents (`n == 1`) leave both crossovers as no-ops.
384    #[test]
385    fn crossover_with_single_locus_is_no_op() {
386        let mut rng = seed_stream(25, 0, SeedPurpose::Crossover);
387        let a0: Vec<Symbol> = vec![Symbol::from_raw(8)];
388        let b0: Vec<Symbol> = vec![Symbol::from_raw(0)];
389
390        let mut a1 = a0.clone();
391        let mut b1 = b0.clone();
392        one_point_crossover(&mut a1, &mut b1, &mut rng);
393        two_point_crossover(&mut a1, &mut b1, &mut rng);
394        assert_eq!(a1, a0);
395        assert_eq!(b1, b0);
396    }
397
398    /// One-point crossover documents equal parent lengths as a precondition
399    /// (`assert_eq!`); a mismatch panics in all builds with the named message.
400    #[test]
401    #[should_panic(expected = "crossover parents must share genome length")]
402    fn one_point_crossover_panics_on_mismatched_lengths() {
403        let mut rng = seed_stream(26, 0, SeedPurpose::Crossover);
404        let mut a = vec![Symbol::from_raw(8); 4];
405        let mut b = vec![Symbol::from_raw(8); 5];
406        one_point_crossover(&mut a, &mut b, &mut rng);
407    }
408
409    /// Two-point crossover shares the equal-length precondition.
410    #[test]
411    #[should_panic(expected = "crossover parents must share genome length")]
412    fn two_point_crossover_panics_on_mismatched_lengths() {
413        let mut rng = seed_stream(27, 0, SeedPurpose::Crossover);
414        let mut a = vec![Symbol::from_raw(8); 4];
415        let mut b = vec![Symbol::from_raw(8); 5];
416        two_point_crossover(&mut a, &mut b, &mut rng);
417    }
418
419    /// RIS is a no-op when the head holds no function symbol — there is nothing
420    /// to root, so the chromosome is left untouched (§7.4).
421    #[test]
422    fn ris_no_op_when_head_has_no_functions() {
423        let a = alphabet();
424        let cfg = GepConfig::new(7, 2, 1, 10).unwrap();
425        let mut rng = seed_stream(28, 0, SeedPurpose::Transposition);
426        for _ in 0..200 {
427            let mut g = sample_valid(&a, &cfg, &mut rng);
428            // Force the whole head to a terminal (variable id 8 = n_func).
429            for locus in &mut g[..cfg.head_len] {
430                *locus = Symbol::from_raw(8);
431            }
432            let before = g.clone();
433            ris_transposition(&mut g, cfg.head_len, &a, &mut rng);
434            assert_eq!(g, before, "RIS must not alter an all-terminal head");
435        }
436    }
437
438    proptest! {
439        // Structural / backend-free invariants: no tensor or backend init, so a
440        // generous case count is cheap. Default shrinking is fine for the
441        // small integer / f32 inputs.
442        #![proptest_config(ProptestConfig { cases: 128, ..ProptestConfig::default() })]
443
444        /// Point mutation over a generated `(config, rate, seed)` space always
445        /// preserves the tail invariant and the complete-decode guarantee.
446        /// Property-test generalization of the fixed 1000-trial
447        /// `point_mutation_preserves_tail_invariant`.
448        ///
449        /// The RNG is built directly from the proptest-generated `u64` seed via
450        /// `seed_stream` (ADR 0029; GEP is backend-free — no `B::seed`).
451        #[test]
452        fn prop_point_mutation_preserves_invariants(
453            head_len in 1usize..=16,
454            // `tail_len = head_len * (max_arity - 1) + 1` must cover the
455            // function set's true max arity, else an arity-2 function that
456            // lands in the head cannot close and the genome fails to decode.
457            // `ArithmeticFunctionSet::max_arity() == 2`, so the config's
458            // `max_arity` floors at 2 (a `max_arity == 1` config is invalid
459            // for this alphabet, not merely `>= 1`).
460            max_arity in 2usize..=3,
461            n_vars in 1usize..=8,
462            rate in 0.0f32..=1.0,
463            seed in any::<u64>(),
464        ) {
465            let cfg = GepConfig::new(head_len, max_arity, n_vars, 100).unwrap();
466            let a = Alphabet::new(ArithmeticFunctionSet, n_vars, vec![]);
467            let mut rng = seed_stream(seed, 0, SeedPurpose::Mutation);
468
469            let mut g = sample_valid(&a, &cfg, &mut rng);
470            point_mutation(&mut g, cfg.head_len, &a, rate, &mut rng);
471            prop_assert!(
472                tail_all_terminals(&g, cfg.head_len, &a),
473                "tail invariant violated: {g:?}"
474            );
475            prop_assert!(decodes_complete(&g, &a), "offspring failed to decode: {g:?}");
476        }
477
478        /// IS/RIS transposition and one-/two-point crossover over a generated
479        /// `(config, seed)` space always yield offspring that keep the tail
480        /// invariant and decode completely (crossover: BOTH parents, built at
481        /// equal length from the same config). Property-test generalization of
482        /// the fixed 500-trial `all_operators_yield_decodable_offspring`.
483        #[test]
484        fn prop_operators_yield_decodable_offspring(
485            head_len in 1usize..=16,
486            // Floor at the alphabet's true max arity (2); see the mutation
487            // property for why `max_arity == 1` is an invalid config here.
488            max_arity in 2usize..=3,
489            n_vars in 1usize..=8,
490            seed in any::<u64>(),
491        ) {
492            let cfg = GepConfig::new(head_len, max_arity, n_vars, 100).unwrap();
493            let a = Alphabet::new(ArithmeticFunctionSet, n_vars, vec![]);
494
495            // IS / RIS transposition share a transposition-purpose stream.
496            let mut rng = seed_stream(seed, 0, SeedPurpose::Transposition);
497
498            let mut g = sample_valid(&a, &cfg, &mut rng);
499            is_transposition(&mut g, cfg.head_len, &mut rng);
500            prop_assert!(tail_all_terminals(&g, cfg.head_len, &a));
501            prop_assert!(decodes_complete(&g, &a));
502
503            let mut g = sample_valid(&a, &cfg, &mut rng);
504            ris_transposition(&mut g, cfg.head_len, &a, &mut rng);
505            prop_assert!(tail_all_terminals(&g, cfg.head_len, &a));
506            prop_assert!(decodes_complete(&g, &a));
507
508            // Crossover draws from a crossover-purpose stream; parents share a
509            // genome length (same config), satisfying the equal-length pre.
510            let mut rng = seed_stream(seed, 0, SeedPurpose::Crossover);
511
512            let mut p1 = sample_valid(&a, &cfg, &mut rng);
513            let mut p2 = sample_valid(&a, &cfg, &mut rng);
514            one_point_crossover(&mut p1, &mut p2, &mut rng);
515            prop_assert!(tail_all_terminals(&p1, cfg.head_len, &a));
516            prop_assert!(tail_all_terminals(&p2, cfg.head_len, &a));
517            prop_assert!(decodes_complete(&p1, &a));
518            prop_assert!(decodes_complete(&p2, &a));
519
520            let mut p1 = sample_valid(&a, &cfg, &mut rng);
521            let mut p2 = sample_valid(&a, &cfg, &mut rng);
522            two_point_crossover(&mut p1, &mut p2, &mut rng);
523            prop_assert!(tail_all_terminals(&p1, cfg.head_len, &a));
524            prop_assert!(tail_all_terminals(&p2, cfg.head_len, &a));
525            prop_assert!(decodes_complete(&p1, &a));
526            prop_assert!(decodes_complete(&p2, &a));
527        }
528    }
529}