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}