Skip to main content

sicada_decode/
trellis.rs

1//! The exact solver forced alignment is built on, over any trellis of the same
2//! shape.
3//!
4//! [`align`](crate::align::align) is one instance of something more general.
5//! Strip the phones out of it and what is left is `T` frames against `N + 1`
6//! positions, where every transition consumes exactly one frame and advances the
7//! position by a bounded number of places, and the path starts at position 0 and
8//! ends at position `N`. Nothing else about the chain matters to the search.
9//!
10//! That shape permits exact rather than pruned search. This module provides the
11//! reachable-cell band, packed traceback, reusable score rows and a numerically
12//! careful ⊕ in the log semiring. A [`Trellis`] implementation supplies the
13//! transition topology and costs.
14//!
15//! | fixed | free |
16//! |---|---|
17//! | one frame per transition | how many transitions there are |
18//! | position never goes backwards | what each of them costs |
19//! | an advance of at most [`Trellis::REACH`] | what it reads, and what it means |
20//! | starts at 0, ends at `N` | whether the cost depends on the frame |
21//!
22//! # Writing one
23//!
24//! [`Trellis`] answers "what enters this cell, and what does it cost". The cost
25//! is the *whole* cost, meaning a structural penalty and whatever the frame
26//! charges for what the transition reads, already multiplied together, so a
27//! penalty that varies by position, by frame, or by both needs no extra
28//! machinery.
29//!
30//! ```
31//! use sicada_decode::trellis::{Step, Trellis, best_path};
32//!
33//! /// A reference that must be sounded in order, one frame at a time, with no
34//! /// silence and nothing skippable: the smallest trellis there is.
35//! struct Rigid<'a> {
36//!     scores: &'a [f32],
37//!     num_symbols: usize,
38//!     phones: &'a [u32],
39//! }
40//!
41//! impl Trellis<2> for Rigid<'_> {
42//!     type Frame<'a> = &'a [f32] where Self: 'a;
43//!
44//!     fn num_frames(&self) -> usize {
45//!         self.scores.len() / self.num_symbols
46//!     }
47//!     fn num_positions(&self) -> usize {
48//!         self.phones.len()
49//!     }
50//!     fn frame(&self, frame: usize) -> &[f32] {
51//!         &self.scores[frame * self.num_symbols..(frame + 1) * self.num_symbols]
52//!     }
53//!
54//!     fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 2] {
55//!         if position == 0 {
56//!             // Nothing reaches `s_0` after the start: the reference has to
57//!             // begin in the first frame.
58//!             return [Step::ABSENT; 2];
59//!         }
60//!         let sounding = frame[self.phones[position - 1] as usize];
61//!         // Hold this phone, or arrive at it. Listed best-first, which is the
62//!         // tie-break: a phone is held rather than started again.
63//!         [Step::new(0, sounding), Step::new(1, sounding)]
64//!     }
65//! }
66//!
67//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
68//! let scores = [
69//!     9.0, 0.0, 9.0, // phone 1
70//!     9.0, 0.0, 9.0, // still phone 1
71//!     9.0, 9.0, 0.0, // phone 2
72//! ];
73//! let path = best_path(&Rigid { scores: &scores, num_symbols: 3, phones: &[1, 2] })?
74//!     .expect("the reference fits");
75//! assert_eq!(path.positions(), [1, 1, 2]);
76//! assert_eq!(path.codes(), [1, 0, 1]); // arrive, hold, arrive
77//! # Ok(())
78//! # }
79//! ```
80//!
81//! [`ReversibleTrellis`] adds the same transitions read backwards, which is all
82//! [`posteriors`] needs. It has nothing to implement, because the backward
83//! reading is derived from the forward one. Write it out only to make the
84//! backward pass faster, and then put [`axioms::check`] in a test: a
85//! forward-backward over two graphs that differ does not fail, it returns
86//! numbers that look entirely reasonable.
87//!
88//! # What it will not do
89//!
90//! There is no beam here and no place to put one. The band is the set of cells
91//! a complete path can stand in at all, so leaving out the rest costs nothing;
92//! narrowing it further would make the result approximate.
93
94use std::ops::Range;
95
96use sicada::error::OpenFstError;
97use sicada::weight::Weight;
98use sicada::weights::float_weight::LogWeight;
99
100/// One transition of a [`Trellis`]: how far it moves, and what it costs.
101#[derive(Debug, Clone, Copy, PartialEq)]
102pub struct Step {
103    /// How many positions it advances. Read into a cell it comes from
104    /// `position - advance`; read out of one it goes to `position + advance`.
105    /// Must not exceed [`Trellis::REACH`].
106    pub advance: u8,
107    /// The whole cost of taking it in this frame: a structural penalty and
108    /// whatever the frame charges for what it reads, already multiplied
109    /// together. Costs are negative log probabilities, so smaller is better.
110    pub cost: f32,
111}
112
113impl Step {
114    /// A transition that is not available at this cell.
115    ///
116    /// A trellis has the same number of transitions everywhere, which is the
117    /// meaning of [`Trellis`]'s `DEGREE`, so an edge case is expressed by a
118    /// transition costing infinity rather than by returning fewer of them. It
119    /// is also the only way to say "this one would run off the start", which
120    /// the solver relies on: a step whose `advance` exceeds its cell's position
121    /// **must** be absent.
122    pub const ABSENT: Self = Self {
123        advance: 0,
124        cost: f32::INFINITY,
125    };
126
127    /// A transition advancing `advance` positions at cost `cost`.
128    #[inline(always)]
129    pub const fn new(advance: u8, cost: f32) -> Self {
130        Self { advance, cost }
131    }
132}
133
134/// `T` frames against `N + 1` positions, one frame consumed per transition.
135///
136/// `DEGREE` is how many transitions enter every cell, four for the chain
137/// [`align`](mod@crate::align) uses. **They are listed best-first**: the solver
138/// keeps the first of them that is strictly better than what it has, so the
139/// order is the tie-break, and a trellis states its own by the order it lists
140/// them in.
141///
142/// See [the module docs](self) for a worked implementation.
143pub trait Trellis<const DEGREE: usize> {
144    /// The most positions any one transition advances. One for a chain, where a
145    /// frame either holds its position or moves to the next; more for a trellis
146    /// that can pass over several positions at once, such as one that gives up
147    /// a whole word.
148    ///
149    /// It sets how wide the band has to be, so an overstated `REACH` costs
150    /// work while an understated one is a contract violation.
151    const REACH: u8 = 1;
152
153    /// What one frame's transitions are read from, usually a row of acoustic
154    /// scores.
155    ///
156    /// Taken once per frame rather than once per cell, which for a matrix means
157    /// the row is sliced `T` times rather than `T × N` times.
158    type Frame<'a>: Copy
159    where
160        Self: 'a;
161
162    /// The number of frames to be accounted for.
163    fn num_frames(&self) -> usize;
164
165    /// The last position. There are `num_positions() + 1` cells in a frame,
166    /// `s_0` through `s_N`; a path starts at `s_0` and has to finish at `s_N`.
167    fn num_positions(&self) -> usize;
168
169    /// One frame's scores.
170    fn frame(&self, frame: usize) -> Self::Frame<'_>;
171
172    /// The `DEGREE` transitions entering `position`, best-first.
173    ///
174    /// A transition that does not exist at this cell is [`Step::ABSENT`], and so
175    /// is one that would come from before `s_0`, since the solver indexes
176    /// `position - advance` without checking it.
177    fn steps_into(&self, frame: Self::Frame<'_>, position: usize) -> [Step; DEGREE];
178}
179
180/// A [`Trellis`] that can also be read backwards, as a forward-backward
181/// requires.
182///
183/// **There is nothing to implement.** The backward reading is derived from the
184/// forward one, so the two cannot disagree:
185///
186/// ```
187/// # use sicada_decode::trellis::{ReversibleTrellis, Step, Trellis};
188/// # struct Chain;
189/// # impl Trellis<2> for Chain {
190/// #     type Frame<'a> = ();
191/// #     fn num_frames(&self) -> usize { 1 }
192/// #     fn num_positions(&self) -> usize { 1 }
193/// #     fn frame(&self, _: usize) {}
194/// #     fn steps_into(&self, _: (), p: usize) -> [Step; 2] {
195/// #         if p == 0 { [Step::new(0, 1.0), Step::ABSENT] }
196/// #         else { [Step::new(0, 1.0), Step::new(1, 1.0)] }
197/// #     }
198/// # }
199/// impl ReversibleTrellis<2> for Chain {}
200/// ```
201///
202/// Overriding it buys speed, since the derived reading asks
203/// [`steps_into`](Trellis::steps_into) once per advance where a written one
204/// answers in a single call. It is also the only way the two readings can come
205/// apart, and that matters more than it looks: [`posteriors`] over two different
206/// graphs does not fail, it returns numbers that look entirely reasonable and
207/// are wrong. An override is therefore a claim, and [`axioms::check`] is how
208/// that claim is checked.
209pub trait ReversibleTrellis<const DEGREE: usize>: Trellis<DEGREE> {
210    /// The `DEGREE` transitions leaving `position`, in the same order
211    /// [`steps_into`](Trellis::steps_into) lists them.
212    ///
213    /// A transition running past `s_N` is [`Step::ABSENT`]; unlike the forward
214    /// direction the solver does bound the target, because it has to anyway.
215    ///
216    /// The default is [`derive_steps_out_of`]. Override it only to make the
217    /// backward pass faster, and put [`axioms::check`] in a test when you do.
218    fn steps_out_of(&self, frame: Self::Frame<'_>, position: usize) -> [Step; DEGREE] {
219        derive_steps_out_of(self, frame, position)
220    }
221}
222
223/// The transitions leaving `position`, read off the ones entering the cells
224/// they could reach.
225///
226/// A transition coded `c` leaves `position` for `position + a` exactly when the
227/// cell `a` along says code `c` arrived from `a` back. So asking each cell
228/// within [`REACH`](Trellis::REACH) recovers the backward reading from the
229/// forward one, which makes [`ReversibleTrellis`]'s default correct rather than
230/// merely plausible.
231///
232/// # Panics
233///
234/// In debug builds, if one code leaves `position` by two different advances. The
235/// trellis is then ambiguous, because the code names two transitions out of one
236/// cell, and no backward reading of it exists. [`axioms::check`] reports it in
237/// any build.
238pub fn derive_steps_out_of<const DEGREE: usize, T>(
239    trellis: &T,
240    frame: T::Frame<'_>,
241    position: usize,
242) -> [Step; DEGREE]
243where
244    T: Trellis<DEGREE> + ?Sized,
245{
246    let mut out = [Step::ABSENT; DEGREE];
247    let last = trellis.num_positions();
248    for advance in 0..=usize::from(T::REACH) {
249        let to = position + advance;
250        if to > last {
251            break;
252        }
253        for (code, step) in trellis.steps_into(frame, to).iter().enumerate() {
254            // `Step::ABSENT` also advances zero, so a finite cost is what tells
255            // a real transition from a missing one.
256            if usize::from(step.advance) == advance && step.cost.is_finite() {
257                debug_assert!(
258                    !out[code].cost.is_finite(),
259                    "transition {code} leaves position {position} by two different advances"
260                );
261                out[code] = *step;
262            }
263        }
264    }
265    out
266}
267
268/// The cells a complete path can stand in after `frame` frames.
269///
270/// Every transition consumes one frame and advances at most `reach` positions,
271/// so reaching `s_N` from `s_0` in `T` frames pins the position after `t` of
272/// them to `max(0, N - reach(T - t)) ..= min(reach · t, N)`.
273///
274/// This is not a beam. Outside it there is no complete path at all, so the
275/// cells left out could not have contributed. It is exact when a trellis uses
276/// every advance from 0 to `reach`; one that uses only some of them leaves some
277/// cells in the band unreachable, and they carry their infinities harmlessly.
278#[inline(always)]
279pub fn band(frame: usize, num_frames: usize, num_positions: usize, reach: usize) -> Range<usize> {
280    let left = num_frames - frame.min(num_frames);
281    let lo = num_positions.saturating_sub(reach.saturating_mul(left));
282    let hi = reach.saturating_mul(frame).min(num_positions);
283    lo..hi + 1
284}
285
286/// The best path through a trellis: which transition each frame took.
287#[derive(Debug, Clone, PartialEq)]
288pub struct Path {
289    codes: Vec<u8>,
290    positions: Vec<u32>,
291    cost: f32,
292}
293
294impl Path {
295    /// The number of frames, which is the number of transitions taken.
296    #[inline(always)]
297    pub fn num_frames(&self) -> usize {
298        self.codes.len()
299    }
300
301    /// Which transition each frame took, as an index into what
302    /// [`Trellis::steps_into`] returns.
303    #[inline(always)]
304    pub fn codes(&self) -> &[u8] {
305        &self.codes
306    }
307
308    /// The position each frame landed in. The last is `N`.
309    #[inline(always)]
310    pub fn positions(&self) -> &[u32] {
311        &self.positions
312    }
313
314    /// The path's total cost: every transition's, multiplied together.
315    #[inline(always)]
316    pub fn cost(&self) -> f32 {
317        self.cost
318    }
319}
320
321/// One transition of a trellis, as [`posteriors`] visits it.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct Transition {
324    /// The frame it consumed.
325    pub frame: usize,
326    /// The position it landed in. This is the target, so that a caller can ask
327    /// what was read there without also tracking the advance.
328    pub position: usize,
329    /// Which transition it was, as an index into what
330    /// [`Trellis::steps_into`] returns.
331    pub code: u8,
332}
333
334/// The best path through `trellis`, exactly.
335///
336/// Returns `None` when no path completes: a reference longer than the frames
337/// can carry, or one every route through which costs infinity.
338///
339/// # Errors
340///
341/// A degree of zero or above 256, which no code could name; or a trellis so
342/// large that its traceback does not fit in memory, reported rather than
343/// attempted.
344pub fn best_path<const DEGREE: usize, T>(trellis: &T) -> Result<Option<Path>, OpenFstError>
345where
346    T: Trellis<DEGREE> + ?Sized,
347{
348    let num_frames = trellis.num_frames();
349    let num_positions = trellis.num_positions();
350    let reach = usize::from(T::REACH).max(1);
351    check_degree(DEGREE)?;
352    if band(0, num_frames, num_positions, reach).is_empty() {
353        // Not even the start is on a complete path: there are more positions
354        // than the frames can advance through.
355        return Ok(None);
356    }
357
358    let mut trace = Traceback::new(num_frames, num_positions, DEGREE)?;
359    // Padded in front by `reach`, so that `position - advance` is in range for
360    // every step a trellis may legally return and the inner loop needs no
361    // guard. The padding holds infinity for the whole run.
362    let mut cur = vec![f32::INFINITY; num_positions + 1 + reach];
363    let mut next = cur.clone();
364    cur[reach] = 0.0;
365
366    for t in 0..num_frames {
367        let frame = trellis.frame(t);
368        let cells = band(t + 1, num_frames, num_positions, reach);
369        let mut row = trace.row(t);
370
371        for i in cells {
372            let steps = trellis.steps_into(frame, i);
373            let (mut best, mut code) = (f32::INFINITY, 0u8);
374            for (candidate, step) in steps.iter().enumerate() {
375                debug_assert!(
376                    !step.cost.is_finite() || usize::from(step.advance) <= i.min(reach),
377                    "a step advancing {} into position {i} is outside REACH or before the start",
378                    step.advance
379                );
380                let total = cur[reach + i - usize::from(step.advance)] + step.cost;
381                if total < best {
382                    (best, code) = (total, candidate as u8);
383                }
384            }
385            next[reach + i] = best;
386            row.put(i, code);
387        }
388
389        row.finish();
390        // Cells outside the band keep the infinity they were built with. The
391        // band never writes them, and the next frame's never reads below its
392        // own predecessor's or above a cell no frame has reached yet.
393        std::mem::swap(&mut cur, &mut next);
394    }
395
396    let cost = cur[reach + num_positions];
397    if !cost.is_finite() {
398        return Ok(None);
399    }
400
401    let mut codes = vec![0u8; num_frames];
402    let mut positions = vec![0u32; num_frames];
403    let mut at = num_positions;
404    for t in (0..num_frames).rev() {
405        let code = trace.get(t, at);
406        codes[t] = code;
407        positions[t] = at as u32;
408        let advance = trellis.steps_into(trellis.frame(t), at)[usize::from(code)].advance;
409        at -= usize::from(advance);
410    }
411    debug_assert_eq!(at, 0, "the traceback left the trellis");
412
413    Ok(Some(Path {
414        codes,
415        positions,
416        cost,
417    }))
418}
419
420/// Forward-backward over `trellis` in the log semiring: the posterior of every
421/// transition, and the total.
422///
423/// `visit` is called once per transition carrying appreciable probability, with
424/// that probability. What to do with it is the caller's: sum it by column for a
425/// label prior, by position for an expected duration, by code for how often
426/// each transition is taken. Transitions below [`NEGLIGIBLE`] are not visited,
427/// so a caller must not count visits, only weigh them.
428///
429/// Returns the total cost, meaning `-log` of the probability of *all* paths,
430/// which is at most [`Path::cost`] and equal to it only when there is one path.
431/// Returns `None` in the same cases [`best_path`] does.
432///
433/// # Errors
434///
435/// As [`best_path`], with the forward plane in place of the traceback. It is
436/// the larger of the two: `(T + 1) × (N + 1)` floats, against two bits a cell.
437pub fn posteriors<const DEGREE: usize, T>(
438    trellis: &T,
439    mut visit: impl FnMut(Transition, f64),
440) -> Result<Option<f32>, OpenFstError>
441where
442    T: ReversibleTrellis<DEGREE> + ?Sized,
443{
444    let num_frames = trellis.num_frames();
445    let num_positions = trellis.num_positions();
446    let reach = usize::from(T::REACH).max(1);
447    check_degree(DEGREE)?;
448    if band(0, num_frames, num_positions, reach).is_empty() {
449        return Ok(None);
450    }
451
452    // Rows are padded in front exactly as `best_path`'s are.
453    let width = num_positions + 1 + reach;
454    let plane = (num_frames + 1).checked_mul(width).ok_or_else(|| {
455        OpenFstError::InvalidOperation(format!(
456            "posteriors: a forward plane for {num_frames} frames of {num_positions} positions \
457             does not fit"
458        ))
459    })?;
460    let mut alpha = vec![f32::INFINITY; plane];
461    alpha[reach] = LogWeight::one().0;
462
463    // Forward. The aligner's recurrence but for the ⊕: every transition into a
464    // cell contributes, rather than the best one winning.
465    for t in 0..num_frames {
466        let frame = trellis.frame(t);
467        let (done, rest) = alpha.split_at_mut((t + 1) * width);
468        let prev = &done[t * width..];
469        for i in band(t + 1, num_frames, num_positions, reach) {
470            let steps = trellis.steps_into(frame, i);
471            let mut terms = [f32::INFINITY; DEGREE];
472            for (term, step) in terms.iter_mut().zip(steps.iter()) {
473                *term = prev[reach + i - usize::from(step.advance)] + step.cost;
474            }
475            rest[reach + i] = log_sum(&terms).sum;
476        }
477    }
478
479    let total = alpha[num_frames * width + reach + num_positions];
480    if !total.is_finite() {
481        return Ok(None);
482    }
483
484    // Backward, visiting as it goes. A transition's posterior is
485    // `alpha[t][from] ⊗ weight ⊗ beta[t + 1][to] ⊘ total`, and those are the
486    // terms the backward recurrence forms anyway, so the two share a loop.
487    let mut beta_next = vec![f32::INFINITY; num_positions + 1];
488    let mut beta_cur = beta_next.clone();
489    beta_next[num_positions] = LogWeight::one().0;
490
491    for t in (0..num_frames).rev() {
492        let frame = trellis.frame(t);
493        let alpha_row = &alpha[t * width..(t + 1) * width];
494        // A cell outside the next frame's band is on no complete path, so
495        // `beta_next` holds nothing meaningful there. Bounding the targets is
496        // what lets the two rows be reused without being cleared.
497        let reachable = band(t + 1, num_frames, num_positions, reach);
498
499        for i in band(t, num_frames, num_positions, reach) {
500            let steps = trellis.steps_out_of(frame, i);
501            let mut terms = [f32::INFINITY; DEGREE];
502            for (term, step) in terms.iter_mut().zip(steps.iter()) {
503                let to = i + usize::from(step.advance);
504                if reachable.contains(&to) {
505                    *term = step.cost + beta_next[to];
506                }
507            }
508            let folded = log_sum(&terms);
509            beta_cur[i] = folded.sum;
510
511            // Everything through this cell, over everything at all. The
512            // transitions share the factor that does not depend on which one is
513            // taken, so it is exponentiated once and the shares, which the ⊕ has
514            // already formed, carry the rest. When the whole cell is too far
515            // down to move an `f32`, none of its transitions can be.
516            let cell = alpha_row[reach + i] + folded.pivot - total;
517            if cell > NEGLIGIBLE {
518                continue;
519            }
520            let scale = f64::from(-cell).exp();
521            for (code, &share) in folded.shares.iter().enumerate() {
522                if share == 0.0 {
523                    continue;
524                }
525                visit(
526                    Transition {
527                        frame: t,
528                        position: i + usize::from(steps[code].advance),
529                        code: code as u8,
530                    },
531                    scale * share,
532                );
533            }
534        }
535
536        std::mem::swap(&mut beta_cur, &mut beta_next);
537    }
538
539    debug_assert!(
540        (beta_next[0] - total).abs() < 1e-2 * total.abs().max(1.0),
541        "the backward pass ended at {} where the forward one ended at {total}",
542        beta_next[0]
543    );
544
545    Ok(Some(total))
546}
547
548/// Checks that a trellis obeys the contract the solvers rely on.
549///
550/// This is `sicada::weight::axioms` for [`Trellis`]: the laws
551/// an implementation asserts by existing, written as something that can be run.
552/// Put `check` in a test for every trellis you write. The conditions it checks
553/// are ones the solvers *assume*, so breaking one does not produce an error, it
554/// produces an answer.
555///
556/// It is not behind a feature. A contract you have to switch on is not one, and
557/// the cost of leaving it available is nothing: it is generic, so a program
558/// that never calls it never compiles it.
559pub mod axioms {
560    use super::*;
561
562    /// Checks `trellis` over every frame, position and code.
563    ///
564    /// Walks the whole trellis, so give it a small one; a few frames against a
565    /// few positions exercises every branch an implementation has.
566    ///
567    /// What it checks:
568    ///
569    /// 1. **[`REACH`](Trellis::REACH) is honest.** No transition advances
570    ///    further than it says. The band is built from `REACH`, so a transition
571    ///    that outruns it lands in a cell the search never considered.
572    /// 2. **Nothing reaches back past the start.** A transition into position
573    ///    `j` advancing more than `j` must be [`Step::ABSENT`]; [`best_path`]
574    ///    subtracts without checking.
575    /// 3. **No code is ambiguous.** One code names at most one transition out
576    ///    of a cell, or there is no backward reading to have.
577    /// 4. **The two readings are one graph.**
578    ///    [`steps_out_of`](ReversibleTrellis::steps_out_of) agrees with
579    ///    [`derive_steps_out_of`] everywhere. This is the one that matters and
580    ///    the one that cannot be seen from the outside: [`posteriors`] over a
581    ///    forward and a backward graph that differ returns numbers rather than
582    ///    an error.
583    ///
584    /// # Panics
585    ///
586    /// On the first violation, naming the frame, position and code.
587    pub fn check<const DEGREE: usize, T>(trellis: &T)
588    where
589        T: ReversibleTrellis<DEGREE> + ?Sized,
590    {
591        let reach = usize::from(T::REACH);
592        assert!(reach >= 1, "a trellis whose REACH is zero can never finish");
593        let last = trellis.num_positions();
594
595        for f in 0..trellis.num_frames() {
596            let frame = trellis.frame(f);
597
598            for position in 0..=last {
599                for (code, step) in trellis.steps_into(frame, position).iter().enumerate() {
600                    if !step.cost.is_finite() {
601                        continue;
602                    }
603                    let advance = usize::from(step.advance);
604                    assert!(
605                        advance <= reach,
606                        "frame {f}: transition {code} into position {position} advances \
607                         {advance}, past a REACH of {reach}"
608                    );
609                    assert!(
610                        advance <= position,
611                        "frame {f}: transition {code} into position {position} advances \
612                         {advance}, from before the start, so it has to be Step::ABSENT there"
613                    );
614                }
615
616                // One code, one transition out of a cell.
617                for code in 0..DEGREE {
618                    let leaving: Vec<usize> = (0..=reach)
619                        .filter(|advance| position + advance <= last)
620                        .filter(|&advance| {
621                            let step = trellis.steps_into(frame, position + advance)[code];
622                            usize::from(step.advance) == advance && step.cost.is_finite()
623                        })
624                        .collect();
625                    assert!(
626                        leaving.len() <= 1,
627                        "frame {f}: transition {code} leaves position {position} by advances \
628                         {leaving:?}, so it names more than one transition"
629                    );
630                }
631
632                // And the reading a caller wrote is the one the forward
633                // direction implies.
634                let written = trellis.steps_out_of(frame, position);
635                let derived = derive_steps_out_of(trellis, frame, position);
636                for code in 0..DEGREE {
637                    let (written, derived) = (written[code], derived[code]);
638                    let agree = written == derived
639                        || (!written.cost.is_finite() && !derived.cost.is_finite());
640                    assert!(
641                        agree,
642                        "frame {f}: transition {code} out of position {position} reads \
643                         {written:?} backwards but {derived:?} forwards"
644                    );
645                }
646            }
647        }
648    }
649}
650
651fn check_degree(degree: usize) -> Result<(), OpenFstError> {
652    if degree == 0 || degree > 256 {
653        return Err(OpenFstError::InvalidOperation(format!(
654            "trellis: a degree of {degree} cannot be named by a code"
655        )));
656    }
657    Ok(())
658}
659
660/// Where a term stops being able to change an `f32`.
661///
662/// `e^-40` is 4e-18. Added to a total of one it moves nothing an `f32` holds,
663/// since the type resolves 6e-8, and even the `DEGREE · T · N` such terms a
664/// ten-minute utterance can produce come to 3e-9 between them. In the log
665/// domain the same gap makes ⊕ its own smaller argument to within 4e-18 nats,
666/// against an `f32` step of 8e-6 at the magnitudes these costs reach.
667///
668/// This is a numerical cutoff, not a search beam: terms within this distance of
669/// the pivot are summed, while more distant terms cannot affect an `f32`
670/// result.
671pub const NEGLIGIBLE: f32 = 40.0;
672
673/// The log semiring's ⊕ over the transitions into one cell, and the share each
674/// of them takes of the result.
675#[derive(Debug, Clone, Copy)]
676struct Folded<const DEGREE: usize> {
677    /// `-log Σ e^-term`: ⊕ over all of them.
678    sum: f32,
679    /// The smallest term, which the shares are measured against. Keeping it is
680    /// what lets a caller rebuild any one term's contribution as
681    /// `e^-pivot × share` rather than exponentiating a second time.
682    pivot: f32,
683    /// `e^-(term - pivot)` for each term, or zero for one that was dropped.
684    shares: [f64; DEGREE],
685}
686
687/// ⊕ over `terms`, folded on the smallest rather than in pairs.
688///
689/// This is [`LogWeight::plus`] across them, the same semiring and the same
690/// value, but not folded pairwise. Pairwise costs a logarithm per pair to get
691/// back into the log domain, only to leave it again for the next one; pivoting
692/// on the smallest instead exponentiates each term once and takes a single
693/// logarithm at the end. Both passes do this per cell of a `T × N` plane, so it is most
694/// of the arithmetic there.
695///
696/// SICADA-OPT: a term more than [`NEGLIGIBLE`] above the pivot is dropped
697/// rather than exponentiated, and so is the pivot's own `e^0`. Upstream's
698/// `LogWeight::plus`, which sees two arguments and knows nothing of the fold it
699/// is part of, can do neither.
700#[inline(always)]
701fn log_sum<const DEGREE: usize>(terms: &[f32; DEGREE]) -> Folded<DEGREE> {
702    let mut pivot = f32::INFINITY;
703    for &term in terms {
704        if term < pivot {
705            pivot = term;
706        }
707    }
708    if !pivot.is_finite() {
709        return Folded {
710            sum: f32::INFINITY,
711            pivot: f32::INFINITY,
712            shares: [0.0; DEGREE],
713        };
714    }
715
716    let mut shares = [0f64; DEGREE];
717    // The pivot contributes exactly one, so the remainder is the argument the
718    // logarithm needs, and `ln_1p` of it stays accurate when there is no
719    // remainder at all.
720    // Only one term may claim that one: a second sitting at the same cost is an
721    // ordinary term whose share happens to be `e^0`.
722    let mut rest = 0f64;
723    let mut claimed = false;
724    for (share, &term) in shares.iter_mut().zip(terms) {
725        let above = term - pivot;
726        if above == 0.0 && !claimed {
727            claimed = true;
728            *share = 1.0;
729        } else if above <= NEGLIGIBLE {
730            *share = f64::from(-above).exp();
731            rest += *share;
732        }
733    }
734    Folded {
735        sum: pivot - rest.ln_1p() as f32,
736        pivot,
737        shares,
738    }
739}
740
741// Which transition each cell took, packed to the bits a code needs.
742//
743// SICADA-OPT: k2 stores one byte per cell. Packing each code to the minimum
744// divisor of eight reduces both traceback storage and memory traffic.
745struct Traceback {
746    plane: Vec<u8>,
747    stride: usize,
748    bits: u32,
749    per_byte: usize,
750}
751
752/// The widths a code packs into. Only divisors of 8, so a cell never straddles
753/// two bytes.
754const fn code_bits(degree: usize) -> u32 {
755    match degree {
756        0..=2 => 1,
757        3..=4 => 2,
758        5..=16 => 4,
759        _ => 8,
760    }
761}
762
763impl Traceback {
764    fn new(num_frames: usize, num_positions: usize, degree: usize) -> Result<Self, OpenFstError> {
765        let bits = code_bits(degree);
766        let per_byte = 8 / bits as usize;
767        let stride = num_positions / per_byte + 1;
768        let cells = num_frames.checked_mul(stride).ok_or_else(|| {
769            OpenFstError::InvalidOperation(format!(
770                "trellis: a traceback for {num_frames} frames of {num_positions} positions does \
771                 not fit"
772            ))
773        })?;
774        Ok(Self {
775            plane: vec![0u8; cells],
776            stride,
777            bits,
778            per_byte,
779        })
780    }
781
782    /// Writes one frame's codes, which the solver produces in position order.
783    #[inline(always)]
784    fn row(&mut self, frame: usize) -> RowWriter<'_> {
785        RowWriter {
786            row: &mut self.plane[frame * self.stride..(frame + 1) * self.stride],
787            bits: self.bits,
788            per_byte: self.per_byte,
789            packed: 0,
790            at: 0,
791            pending: false,
792        }
793    }
794
795    #[inline(always)]
796    fn get(&self, frame: usize, position: usize) -> u8 {
797        let byte = self.plane[frame * self.stride + position / self.per_byte];
798        let mask = (1u16 << self.bits) - 1;
799        (byte >> (self.bits * (position % self.per_byte) as u32)) & mask as u8
800    }
801}
802
803/// Accumulates a byte of codes before storing it.
804///
805/// Cells arrive in order, so a byte is filled and written once rather than read
806/// back out of the plane to have one cell updated. Codes below the band's start
807/// are left zero, which the traceback never reads.
808struct RowWriter<'a> {
809    row: &'a mut [u8],
810    bits: u32,
811    per_byte: usize,
812    packed: u8,
813    at: usize,
814    pending: bool,
815}
816
817impl RowWriter<'_> {
818    #[inline(always)]
819    fn put(&mut self, position: usize, code: u8) {
820        let within = position % self.per_byte;
821        self.packed |= code << (self.bits * within as u32);
822        self.at = position / self.per_byte;
823        self.pending = true;
824        if within == self.per_byte - 1 {
825            self.row[self.at] = self.packed;
826            self.packed = 0;
827            self.pending = false;
828        }
829    }
830
831    #[inline(always)]
832    fn finish(self) {
833        if self.pending {
834            self.row[self.at] = self.packed;
835        }
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842
843    // The chain of `align`, written out again against a plain matrix, so that
844    // this module's tests do not depend on that one's.
845    //
846    // Codes, best-first: hold the blank, hold the phone, commit, skip.
847    struct Chain<'a> {
848        scores: &'a [f32],
849        symbols: usize,
850        phones: &'a [u32],
851        skip: f32,
852    }
853
854    const HOLD_BLANK: u8 = 0;
855    const HOLD_PHONE: u8 = 1;
856    const COMMIT: u8 = 2;
857    const SKIP: u8 = 3;
858
859    impl Trellis<4> for Chain<'_> {
860        type Frame<'a>
861            = &'a [f32]
862        where
863            Self: 'a;
864
865        fn num_frames(&self) -> usize {
866            self.scores.len() / self.symbols
867        }
868        fn num_positions(&self) -> usize {
869            self.phones.len()
870        }
871        fn frame(&self, frame: usize) -> &[f32] {
872            &self.scores[frame * self.symbols..(frame + 1) * self.symbols]
873        }
874
875        fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 4] {
876            let blank = Step::new(0, frame[0]);
877            if position == 0 {
878                return [blank, Step::ABSENT, Step::ABSENT, Step::ABSENT];
879            }
880            let phone = frame[self.phones[position - 1] as usize];
881            [
882                blank,
883                Step::new(0, phone),
884                Step::new(1, phone),
885                Step::new(1, self.skip + frame[0]),
886            ]
887        }
888    }
889
890    impl ReversibleTrellis<4> for Chain<'_> {
891        fn steps_out_of(&self, frame: &[f32], position: usize) -> [Step; 4] {
892            let blank = Step::new(0, frame[0]);
893            let hold = if position > 0 {
894                Step::new(0, frame[self.phones[position - 1] as usize])
895            } else {
896                Step::ABSENT
897            };
898            let (commit, skip) = if position < self.phones.len() {
899                (
900                    Step::new(1, frame[self.phones[position] as usize]),
901                    Step::new(1, self.skip + frame[0]),
902                )
903            } else {
904                (Step::ABSENT, Step::ABSENT)
905            };
906            [blank, hold, commit, skip]
907        }
908    }
909
910    fn chain<'a>(scores: &'a [f32], phones: &'a [u32]) -> Chain<'a> {
911        Chain {
912            scores,
913            symbols: 4,
914            phones,
915            skip: f32::INFINITY,
916        }
917    }
918
919    #[test]
920    fn it_finds_the_path_the_scores_ask_for() {
921        // phone 1, phone 1, blank, phone 2.
922        let scores = [
923            9.0, 0.0, 9.0, 9.0, //
924            9.0, 0.0, 9.0, 9.0, //
925            0.0, 9.0, 9.0, 9.0, //
926            9.0, 9.0, 0.0, 9.0,
927        ];
928        let path = best_path(&chain(&scores, &[1, 2]))
929            .unwrap()
930            .expect("a path");
931        assert_eq!(path.positions(), [1, 1, 1, 2]);
932        assert_eq!(path.codes(), [COMMIT, HOLD_PHONE, HOLD_BLANK, COMMIT]);
933        assert!(path.cost().abs() < 1e-6);
934        assert_eq!(path.num_frames(), 4);
935    }
936
937    #[test]
938    fn a_reference_the_frames_cannot_carry_has_no_path() {
939        let scores = [0.0; 8];
940        assert_eq!(best_path(&chain(&scores, &[1, 2, 3])).unwrap(), None);
941        assert_eq!(
942            posteriors(&chain(&scores, &[1, 2, 3]), |_, _| {}).unwrap(),
943            None
944        );
945    }
946
947    // The order the transitions are listed in is the tie-break, and callers
948    // depend on it: a skip listed last never wins one.
949    #[test]
950    fn the_order_transitions_are_listed_in_is_the_tie_break() {
951        // Every column costs the same, so all four transitions tie wherever
952        // they are all available, including the skip, which is free here.
953        let scores = [1.0; 12];
954        let mut chain = chain(&scores, &[1]);
955        chain.skip = 0.0;
956        let path = best_path(&chain).unwrap().expect("a path");
957
958        assert!(
959            !path.codes().contains(&SKIP),
960            "a tie must not give up a phone"
961        );
962        // The preference is read at the cell being computed, so what it prefers
963        // is the path that was *already there*. On a tie that resolves to
964        // arriving as early as the band allows and waiting, not to putting the
965        // arrival off.
966        assert_eq!(path.codes(), [COMMIT, HOLD_BLANK, HOLD_BLANK]);
967        assert_eq!(path.positions(), [1, 1, 1]);
968        // And among ways of standing still, silence beats sounding, which is
969        // what keeps a phone's span down to the frames that argue for it.
970        assert!(!path.codes().contains(&HOLD_PHONE));
971    }
972
973    // Exercise a custom topology that skips a three-position word.
974    #[test]
975    fn a_trellis_that_advances_more_than_one_position() {
976        struct Words<'a> {
977            scores: &'a [f32],
978            phones: &'a [u32],
979            word: usize,
980            give_up: f32,
981        }
982
983        impl Trellis<3> for Words<'_> {
984            const REACH: u8 = 3;
985            type Frame<'a>
986                = &'a [f32]
987            where
988                Self: 'a;
989
990            fn num_frames(&self) -> usize {
991                self.scores.len() / 4
992            }
993            fn num_positions(&self) -> usize {
994                self.phones.len()
995            }
996            fn frame(&self, frame: usize) -> &[f32] {
997                &self.scores[frame * 4..(frame + 1) * 4]
998            }
999
1000            fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 3] {
1001                let blank = Step::new(0, frame[0]);
1002                if position == 0 {
1003                    return [blank, Step::ABSENT, Step::ABSENT];
1004                }
1005                let phone = Step::new(1, frame[self.phones[position - 1] as usize]);
1006                // A whole word given up at once, landing on a word boundary.
1007                let word = if position >= self.word && position.is_multiple_of(self.word) {
1008                    Step::new(self.word as u8, self.give_up + frame[0])
1009                } else {
1010                    Step::ABSENT
1011                };
1012                [blank, phone, word]
1013            }
1014        }
1015
1016        // Six frames, two words of three phones. The audio says only the first
1017        // word; the second has no evidence anywhere.
1018        let mut scores = vec![9.0f32; 6 * 4];
1019        for (frame, column) in [1usize, 2, 3, 0, 0, 0].into_iter().enumerate() {
1020            scores[frame * 4 + column] = 0.0;
1021        }
1022        let words = Words {
1023            scores: &scores,
1024            phones: &[1, 2, 3, 1, 2, 3],
1025            word: 3,
1026            give_up: 1.0,
1027        };
1028
1029        let path = best_path(&words).unwrap().expect("a path");
1030        assert_eq!(path.num_frames(), 6);
1031        // The first word is sounded, then the second is given up in one frame
1032        // and the rest is silence.
1033        assert_eq!(path.positions(), [1, 2, 3, 6, 6, 6]);
1034        assert_eq!(path.codes(), [1, 1, 1, 2, 0, 0]);
1035        assert!((path.cost() - 1.0).abs() < 1e-6, "{}", path.cost());
1036    }
1037
1038    #[test]
1039    fn a_degree_no_code_could_name_is_reported() {
1040        struct Nothing;
1041        impl Trellis<0> for Nothing {
1042            type Frame<'a> = ();
1043            fn num_frames(&self) -> usize {
1044                1
1045            }
1046            fn num_positions(&self) -> usize {
1047                0
1048            }
1049            fn frame(&self, _: usize) {}
1050            fn steps_into(&self, _: (), _: usize) -> [Step; 0] {
1051                []
1052            }
1053        }
1054        let err = best_path(&Nothing).unwrap_err();
1055        assert!(format!("{err}").contains("cannot be named"), "{err}");
1056    }
1057
1058    #[test]
1059    fn the_band_is_the_cells_a_complete_path_can_stand_in() {
1060        // Four frames, two positions, reach one.
1061        assert_eq!(band(0, 4, 2, 1), 0..1);
1062        assert_eq!(band(1, 4, 2, 1), 0..2);
1063        assert_eq!(band(3, 4, 2, 1), 1..3);
1064        assert_eq!(band(4, 4, 2, 1), 2..3);
1065        // A reference as long as the frames leaves no slack anywhere.
1066        assert_eq!(band(2, 4, 4, 1), 2..3);
1067        // More positions than the frames can advance through: nothing at all.
1068        assert!(band(0, 2, 3, 1).is_empty());
1069        // A wider reach opens the band at both ends.
1070        assert_eq!(band(1, 4, 6, 2), 0..3);
1071        assert_eq!(band(0, 2, 3, 2), 0..1);
1072    }
1073
1074    #[test]
1075    fn a_code_packs_into_the_bits_it_needs() {
1076        assert_eq!(
1077            (code_bits(2), code_bits(4), code_bits(16), code_bits(17)),
1078            (1, 2, 4, 8)
1079        );
1080
1081        for degree in [2usize, 4, 16, 256] {
1082            let mut trace = Traceback::new(3, 20, degree).unwrap();
1083            let codes: Vec<u8> = (0..21).map(|i| (i % degree) as u8).collect();
1084            for frame in 0..3 {
1085                let mut row = trace.row(frame);
1086                for (position, &code) in codes.iter().enumerate() {
1087                    row.put(position, code);
1088                }
1089                row.finish();
1090            }
1091            for frame in 0..3 {
1092                for (position, &code) in codes.iter().enumerate() {
1093                    assert_eq!(trace.get(frame, position), code, "degree {degree}");
1094                }
1095            }
1096        }
1097    }
1098
1099    // A row written only over part of its width, as a band writes it, still
1100    // reads back where it was written.
1101    #[test]
1102    fn a_partial_row_reads_back() {
1103        let mut trace = Traceback::new(1, 20, 4).unwrap();
1104        let mut row = trace.row(0);
1105        for position in 6..=13 {
1106            row.put(position, (position % 4) as u8);
1107        }
1108        row.finish();
1109        for position in 6..=13 {
1110            assert_eq!(trace.get(0, position), (position % 4) as u8);
1111        }
1112    }
1113
1114    // A small xorshift, so the random cases below are the same every run.
1115    struct Rng(u64);
1116
1117    impl Rng {
1118        fn next(&mut self) -> u64 {
1119            self.0 ^= self.0 << 13;
1120            self.0 ^= self.0 >> 7;
1121            self.0 ^= self.0 << 17;
1122            self.0
1123        }
1124        fn below(&mut self, n: usize) -> usize {
1125            (self.next() % n as u64) as usize
1126        }
1127        fn cost(&mut self) -> f32 {
1128            self.below(1 << 14) as f32 / 4096.0
1129        }
1130    }
1131
1132    // Every path of the chain, enumerated and weighed, against both solvers.
1133    #[test]
1134    fn both_solvers_agree_with_enumerating_every_path() {
1135        fn walk(chain: &Chain<'_>, frame: usize, position: usize, cost: f32, paths: &mut Vec<f32>) {
1136            if frame == chain.num_frames() {
1137                if position == chain.num_positions() {
1138                    paths.push(cost);
1139                }
1140                return;
1141            }
1142            // Walked forwards, so the transitions out are what is wanted.
1143            let scores = chain.frame(frame);
1144            for step in chain.steps_out_of(scores, position) {
1145                if step.cost.is_finite() {
1146                    walk(
1147                        chain,
1148                        frame + 1,
1149                        position + usize::from(step.advance),
1150                        cost + step.cost,
1151                        paths,
1152                    );
1153                }
1154            }
1155        }
1156
1157        let mut rng = Rng(0x7A17_1CE0_1234_5678);
1158        let mut compared = 0;
1159
1160        for round in 0..200 {
1161            let num_frames = 1 + rng.below(6);
1162            let num_positions = rng.below(num_frames.min(3) + 1);
1163            let phones: Vec<u32> = (0..num_positions)
1164                .map(|_| 1 + rng.below(3) as u32)
1165                .collect();
1166            let scores: Vec<f32> = (0..num_frames * 4).map(|_| rng.cost()).collect();
1167            let mut under_test = chain(&scores, &phones);
1168            if rng.below(2) == 0 {
1169                under_test.skip = rng.cost();
1170            }
1171
1172            let mut paths = Vec::new();
1173            walk(&under_test, 0, 0, 0.0, &mut paths);
1174
1175            let best = best_path(&under_test).unwrap();
1176            let total = posteriors(&under_test, |_, _| {}).unwrap();
1177
1178            if paths.is_empty() {
1179                assert_eq!(best, None, "round {round}");
1180                assert_eq!(total, None, "round {round}");
1181                continue;
1182            }
1183            compared += 1;
1184
1185            let cheapest = paths.iter().copied().fold(f32::INFINITY, f32::min);
1186            let best = best.expect("a path");
1187            assert!(
1188                (best.cost() - cheapest).abs() < 1e-3,
1189                "round {round}: best_path {} against {cheapest}",
1190                best.cost()
1191            );
1192
1193            let mass: f64 = paths.iter().map(|&cost| (-cost as f64).exp()).sum();
1194            let total = total.expect("a total");
1195            assert!(
1196                (total - -(mass.ln() as f32)).abs() < 1e-3,
1197                "round {round}: posteriors {total} against {}",
1198                -(mass.ln() as f32)
1199            );
1200        }
1201
1202        assert!(compared > 150, "only {compared} rounds had a path");
1203    }
1204
1205    // What a visitor is handed has to name a transition that is actually
1206    // there, and the mass of one frame has to come to one.
1207    #[test]
1208    fn every_frames_visits_come_to_one() {
1209        let mut rng = Rng(0x1DEA_5EED_9876_4321);
1210        for _ in 0..40 {
1211            let num_frames = 2 + rng.below(10);
1212            let num_positions = rng.below(num_frames.min(4) + 1);
1213            let phones: Vec<u32> = (0..num_positions)
1214                .map(|_| 1 + rng.below(3) as u32)
1215                .collect();
1216            let scores: Vec<f32> = (0..num_frames * 4).map(|_| rng.cost()).collect();
1217            let mut under_test = chain(&scores, &phones);
1218            under_test.skip = 2.0;
1219
1220            let mut per_frame = vec![0f64; num_frames];
1221            let Some(_) = posteriors(&under_test, |seen, mass| {
1222                assert!(seen.position <= num_positions);
1223                assert!(seen.code < 4);
1224                per_frame[seen.frame] += mass;
1225            })
1226            .unwrap() else {
1227                continue;
1228            };
1229            for (frame, mass) in per_frame.iter().enumerate() {
1230                assert!((mass - 1.0).abs() < 1e-4, "frame {frame} carries {mass}");
1231            }
1232        }
1233    }
1234
1235    // The contract, run as the checker a caller is told to run.
1236    #[test]
1237    fn the_chain_obeys_the_contract() {
1238        let scores: Vec<f32> = (0..5 * 4).map(|i| i as f32 / 3.0).collect();
1239        let mut under_test = chain(&scores, &[1, 2, 3]);
1240        under_test.skip = 1.5;
1241        axioms::check(&under_test);
1242
1243        // Including with the skips forbidden, which is a different set of
1244        // absent transitions.
1245        axioms::check(&chain(&scores, &[1, 2, 3]));
1246        axioms::check(&chain(&scores, &[]));
1247    }
1248
1249    #[test]
1250    #[should_panic(expected = "backwards but")]
1251    fn it_catches_a_backward_reading_that_disagrees() {
1252        struct Crooked<'a>(Chain<'a>);
1253
1254        impl Trellis<4> for Crooked<'_> {
1255            type Frame<'f>
1256                = &'f [f32]
1257            where
1258                Self: 'f;
1259            fn num_frames(&self) -> usize {
1260                self.0.num_frames()
1261            }
1262            fn num_positions(&self) -> usize {
1263                self.0.num_positions()
1264            }
1265            fn frame(&self, frame: usize) -> &[f32] {
1266                self.0.frame(frame)
1267            }
1268            fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 4] {
1269                self.0.steps_into(frame, position)
1270            }
1271        }
1272
1273        impl ReversibleTrellis<4> for Crooked<'_> {
1274            fn steps_out_of(&self, frame: &[f32], position: usize) -> [Step; 4] {
1275                // The mistake that costs nothing to make: the commit is priced
1276                // by the phone being left rather than the one being reached.
1277                let mut out = derive_steps_out_of(&self.0, frame, position);
1278                if position > 0 && position < self.0.num_positions() {
1279                    out[COMMIT as usize] =
1280                        Step::new(1, frame[self.0.phones[position - 1] as usize]);
1281                }
1282                out
1283            }
1284        }
1285
1286        let scores: Vec<f32> = (0..5 * 4).map(|i| i as f32 / 3.0).collect();
1287        axioms::check(&Crooked(chain(&scores, &[1, 2, 3])));
1288    }
1289
1290    #[test]
1291    #[should_panic(expected = "past a REACH")]
1292    fn it_catches_a_transition_that_outruns_its_reach() {
1293        struct TooFar;
1294        impl Trellis<2> for TooFar {
1295            type Frame<'a> = ();
1296            fn num_frames(&self) -> usize {
1297                4
1298            }
1299            fn num_positions(&self) -> usize {
1300                3
1301            }
1302            fn frame(&self, _: usize) {}
1303            fn steps_into(&self, _: (), position: usize) -> [Step; 2] {
1304                if position >= 2 {
1305                    // REACH is the default 1, and this advances 2.
1306                    [Step::new(0, 1.0), Step::new(2, 1.0)]
1307                } else {
1308                    [Step::new(0, 1.0), Step::ABSENT]
1309                }
1310            }
1311        }
1312        impl ReversibleTrellis<2> for TooFar {}
1313        axioms::check(&TooFar);
1314    }
1315
1316    #[test]
1317    #[should_panic(expected = "from before the start")]
1318    fn it_catches_a_transition_that_reaches_back_past_the_start() {
1319        struct OffTheFront;
1320        impl Trellis<2> for OffTheFront {
1321            type Frame<'a> = ();
1322            fn num_frames(&self) -> usize {
1323                3
1324            }
1325            fn num_positions(&self) -> usize {
1326                2
1327            }
1328            fn frame(&self, _: usize) {}
1329            fn steps_into(&self, _: (), _: usize) -> [Step; 2] {
1330                // Position 0 has no cell before it, so the advancing one has to
1331                // be absent there and is not.
1332                [Step::new(0, 1.0), Step::new(1, 1.0)]
1333            }
1334        }
1335        impl ReversibleTrellis<2> for OffTheFront {}
1336        axioms::check(&OffTheFront);
1337    }
1338
1339    // The derived reading is the point of the default, so it has to be the one
1340    // a careful implementation would have written.
1341    #[test]
1342    fn the_derived_backward_reading_is_the_written_one() {
1343        let scores: Vec<f32> = (0..6 * 4).map(|i| (i % 7) as f32 / 2.0).collect();
1344        let mut under_test = chain(&scores, &[1, 2, 3, 1]);
1345        under_test.skip = 0.75;
1346
1347        // `Chain` writes `steps_out_of` out; deriving it must give the same
1348        // thing, and solving with either must give the same answer.
1349        for frame in 0..under_test.num_frames() {
1350            let scores = under_test.frame(frame);
1351            for position in 0..=under_test.num_positions() {
1352                assert_eq!(
1353                    under_test.steps_out_of(scores, position),
1354                    derive_steps_out_of(&under_test, scores, position),
1355                    "frame {frame}, position {position}"
1356                );
1357            }
1358        }
1359    }
1360}