use std::ops::Range;
use sicada::error::OpenFstError;
use sicada::weight::Weight;
use sicada::weights::float_weight::LogWeight;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Step {
pub advance: u8,
pub cost: f32,
}
impl Step {
pub const ABSENT: Self = Self {
advance: 0,
cost: f32::INFINITY,
};
#[inline(always)]
pub const fn new(advance: u8, cost: f32) -> Self {
Self { advance, cost }
}
}
pub trait Trellis<const DEGREE: usize> {
const REACH: u8 = 1;
type Frame<'a>: Copy
where
Self: 'a;
fn num_frames(&self) -> usize;
fn num_positions(&self) -> usize;
fn frame(&self, frame: usize) -> Self::Frame<'_>;
fn steps_into(&self, frame: Self::Frame<'_>, position: usize) -> [Step; DEGREE];
}
pub trait ReversibleTrellis<const DEGREE: usize>: Trellis<DEGREE> {
fn steps_out_of(&self, frame: Self::Frame<'_>, position: usize) -> [Step; DEGREE] {
derive_steps_out_of(self, frame, position)
}
}
pub fn derive_steps_out_of<const DEGREE: usize, T>(
trellis: &T,
frame: T::Frame<'_>,
position: usize,
) -> [Step; DEGREE]
where
T: Trellis<DEGREE> + ?Sized,
{
let mut out = [Step::ABSENT; DEGREE];
let last = trellis.num_positions();
for advance in 0..=usize::from(T::REACH) {
let to = position + advance;
if to > last {
break;
}
for (code, step) in trellis.steps_into(frame, to).iter().enumerate() {
if usize::from(step.advance) == advance && step.cost.is_finite() {
debug_assert!(
!out[code].cost.is_finite(),
"transition {code} leaves position {position} by two different advances"
);
out[code] = *step;
}
}
}
out
}
#[inline(always)]
pub fn band(frame: usize, num_frames: usize, num_positions: usize, reach: usize) -> Range<usize> {
let left = num_frames - frame.min(num_frames);
let lo = num_positions.saturating_sub(reach.saturating_mul(left));
let hi = reach.saturating_mul(frame).min(num_positions);
lo..hi + 1
}
#[derive(Debug, Clone, PartialEq)]
pub struct Path {
codes: Vec<u8>,
positions: Vec<u32>,
cost: f32,
}
impl Path {
#[inline(always)]
pub fn num_frames(&self) -> usize {
self.codes.len()
}
#[inline(always)]
pub fn codes(&self) -> &[u8] {
&self.codes
}
#[inline(always)]
pub fn positions(&self) -> &[u32] {
&self.positions
}
#[inline(always)]
pub fn cost(&self) -> f32 {
self.cost
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Transition {
pub frame: usize,
pub position: usize,
pub code: u8,
}
pub fn best_path<const DEGREE: usize, T>(trellis: &T) -> Result<Option<Path>, OpenFstError>
where
T: Trellis<DEGREE> + ?Sized,
{
let num_frames = trellis.num_frames();
let num_positions = trellis.num_positions();
let reach = usize::from(T::REACH).max(1);
check_degree(DEGREE)?;
if band(0, num_frames, num_positions, reach).is_empty() {
return Ok(None);
}
let mut trace = Traceback::new(num_frames, num_positions, DEGREE)?;
let mut cur = vec![f32::INFINITY; num_positions + 1 + reach];
let mut next = cur.clone();
cur[reach] = 0.0;
for t in 0..num_frames {
let frame = trellis.frame(t);
let cells = band(t + 1, num_frames, num_positions, reach);
let mut row = trace.row(t);
for i in cells {
let steps = trellis.steps_into(frame, i);
let (mut best, mut code) = (f32::INFINITY, 0u8);
for (candidate, step) in steps.iter().enumerate() {
debug_assert!(
!step.cost.is_finite() || usize::from(step.advance) <= i.min(reach),
"a step advancing {} into position {i} is outside REACH or before the start",
step.advance
);
let total = cur[reach + i - usize::from(step.advance)] + step.cost;
if total < best {
(best, code) = (total, candidate as u8);
}
}
next[reach + i] = best;
row.put(i, code);
}
row.finish();
std::mem::swap(&mut cur, &mut next);
}
let cost = cur[reach + num_positions];
if !cost.is_finite() {
return Ok(None);
}
let mut codes = vec![0u8; num_frames];
let mut positions = vec![0u32; num_frames];
let mut at = num_positions;
for t in (0..num_frames).rev() {
let code = trace.get(t, at);
codes[t] = code;
positions[t] = at as u32;
let advance = trellis.steps_into(trellis.frame(t), at)[usize::from(code)].advance;
at -= usize::from(advance);
}
debug_assert_eq!(at, 0, "the traceback left the trellis");
Ok(Some(Path {
codes,
positions,
cost,
}))
}
pub fn posteriors<const DEGREE: usize, T>(
trellis: &T,
mut visit: impl FnMut(Transition, f64),
) -> Result<Option<f32>, OpenFstError>
where
T: ReversibleTrellis<DEGREE> + ?Sized,
{
let num_frames = trellis.num_frames();
let num_positions = trellis.num_positions();
let reach = usize::from(T::REACH).max(1);
check_degree(DEGREE)?;
if band(0, num_frames, num_positions, reach).is_empty() {
return Ok(None);
}
let width = num_positions + 1 + reach;
let plane = (num_frames + 1).checked_mul(width).ok_or_else(|| {
OpenFstError::InvalidOperation(format!(
"posteriors: a forward plane for {num_frames} frames of {num_positions} positions \
does not fit"
))
})?;
let mut alpha = vec![f32::INFINITY; plane];
alpha[reach] = LogWeight::one().0;
for t in 0..num_frames {
let frame = trellis.frame(t);
let (done, rest) = alpha.split_at_mut((t + 1) * width);
let prev = &done[t * width..];
for i in band(t + 1, num_frames, num_positions, reach) {
let steps = trellis.steps_into(frame, i);
let mut terms = [f32::INFINITY; DEGREE];
for (term, step) in terms.iter_mut().zip(steps.iter()) {
*term = prev[reach + i - usize::from(step.advance)] + step.cost;
}
rest[reach + i] = log_sum(&terms).sum;
}
}
let total = alpha[num_frames * width + reach + num_positions];
if !total.is_finite() {
return Ok(None);
}
let mut beta_next = vec![f32::INFINITY; num_positions + 1];
let mut beta_cur = beta_next.clone();
beta_next[num_positions] = LogWeight::one().0;
for t in (0..num_frames).rev() {
let frame = trellis.frame(t);
let alpha_row = &alpha[t * width..(t + 1) * width];
let reachable = band(t + 1, num_frames, num_positions, reach);
for i in band(t, num_frames, num_positions, reach) {
let steps = trellis.steps_out_of(frame, i);
let mut terms = [f32::INFINITY; DEGREE];
for (term, step) in terms.iter_mut().zip(steps.iter()) {
let to = i + usize::from(step.advance);
if reachable.contains(&to) {
*term = step.cost + beta_next[to];
}
}
let folded = log_sum(&terms);
beta_cur[i] = folded.sum;
let cell = alpha_row[reach + i] + folded.pivot - total;
if cell > NEGLIGIBLE {
continue;
}
let scale = f64::from(-cell).exp();
for (code, &share) in folded.shares.iter().enumerate() {
if share == 0.0 {
continue;
}
visit(
Transition {
frame: t,
position: i + usize::from(steps[code].advance),
code: code as u8,
},
scale * share,
);
}
}
std::mem::swap(&mut beta_cur, &mut beta_next);
}
debug_assert!(
(beta_next[0] - total).abs() < 1e-2 * total.abs().max(1.0),
"the backward pass ended at {} where the forward one ended at {total}",
beta_next[0]
);
Ok(Some(total))
}
pub mod axioms {
use super::*;
pub fn check<const DEGREE: usize, T>(trellis: &T)
where
T: ReversibleTrellis<DEGREE> + ?Sized,
{
let reach = usize::from(T::REACH);
assert!(reach >= 1, "a trellis whose REACH is zero can never finish");
let last = trellis.num_positions();
for f in 0..trellis.num_frames() {
let frame = trellis.frame(f);
for position in 0..=last {
for (code, step) in trellis.steps_into(frame, position).iter().enumerate() {
if !step.cost.is_finite() {
continue;
}
let advance = usize::from(step.advance);
assert!(
advance <= reach,
"frame {f}: transition {code} into position {position} advances \
{advance}, past a REACH of {reach}"
);
assert!(
advance <= position,
"frame {f}: transition {code} into position {position} advances \
{advance}, from before the start, so it has to be Step::ABSENT there"
);
}
for code in 0..DEGREE {
let leaving: Vec<usize> = (0..=reach)
.filter(|advance| position + advance <= last)
.filter(|&advance| {
let step = trellis.steps_into(frame, position + advance)[code];
usize::from(step.advance) == advance && step.cost.is_finite()
})
.collect();
assert!(
leaving.len() <= 1,
"frame {f}: transition {code} leaves position {position} by advances \
{leaving:?}, so it names more than one transition"
);
}
let written = trellis.steps_out_of(frame, position);
let derived = derive_steps_out_of(trellis, frame, position);
for code in 0..DEGREE {
let (written, derived) = (written[code], derived[code]);
let agree = written == derived
|| (!written.cost.is_finite() && !derived.cost.is_finite());
assert!(
agree,
"frame {f}: transition {code} out of position {position} reads \
{written:?} backwards but {derived:?} forwards"
);
}
}
}
}
}
fn check_degree(degree: usize) -> Result<(), OpenFstError> {
if degree == 0 || degree > 256 {
return Err(OpenFstError::InvalidOperation(format!(
"trellis: a degree of {degree} cannot be named by a code"
)));
}
Ok(())
}
pub const NEGLIGIBLE: f32 = 40.0;
#[derive(Debug, Clone, Copy)]
struct Folded<const DEGREE: usize> {
sum: f32,
pivot: f32,
shares: [f64; DEGREE],
}
#[inline(always)]
fn log_sum<const DEGREE: usize>(terms: &[f32; DEGREE]) -> Folded<DEGREE> {
let mut pivot = f32::INFINITY;
for &term in terms {
if term < pivot {
pivot = term;
}
}
if !pivot.is_finite() {
return Folded {
sum: f32::INFINITY,
pivot: f32::INFINITY,
shares: [0.0; DEGREE],
};
}
let mut shares = [0f64; DEGREE];
let mut rest = 0f64;
let mut claimed = false;
for (share, &term) in shares.iter_mut().zip(terms) {
let above = term - pivot;
if above == 0.0 && !claimed {
claimed = true;
*share = 1.0;
} else if above <= NEGLIGIBLE {
*share = f64::from(-above).exp();
rest += *share;
}
}
Folded {
sum: pivot - rest.ln_1p() as f32,
pivot,
shares,
}
}
struct Traceback {
plane: Vec<u8>,
stride: usize,
bits: u32,
per_byte: usize,
}
const fn code_bits(degree: usize) -> u32 {
match degree {
0..=2 => 1,
3..=4 => 2,
5..=16 => 4,
_ => 8,
}
}
impl Traceback {
fn new(num_frames: usize, num_positions: usize, degree: usize) -> Result<Self, OpenFstError> {
let bits = code_bits(degree);
let per_byte = 8 / bits as usize;
let stride = num_positions / per_byte + 1;
let cells = num_frames.checked_mul(stride).ok_or_else(|| {
OpenFstError::InvalidOperation(format!(
"trellis: a traceback for {num_frames} frames of {num_positions} positions does \
not fit"
))
})?;
Ok(Self {
plane: vec![0u8; cells],
stride,
bits,
per_byte,
})
}
#[inline(always)]
fn row(&mut self, frame: usize) -> RowWriter<'_> {
RowWriter {
row: &mut self.plane[frame * self.stride..(frame + 1) * self.stride],
bits: self.bits,
per_byte: self.per_byte,
packed: 0,
at: 0,
pending: false,
}
}
#[inline(always)]
fn get(&self, frame: usize, position: usize) -> u8 {
let byte = self.plane[frame * self.stride + position / self.per_byte];
let mask = (1u16 << self.bits) - 1;
(byte >> (self.bits * (position % self.per_byte) as u32)) & mask as u8
}
}
struct RowWriter<'a> {
row: &'a mut [u8],
bits: u32,
per_byte: usize,
packed: u8,
at: usize,
pending: bool,
}
impl RowWriter<'_> {
#[inline(always)]
fn put(&mut self, position: usize, code: u8) {
let within = position % self.per_byte;
self.packed |= code << (self.bits * within as u32);
self.at = position / self.per_byte;
self.pending = true;
if within == self.per_byte - 1 {
self.row[self.at] = self.packed;
self.packed = 0;
self.pending = false;
}
}
#[inline(always)]
fn finish(self) {
if self.pending {
self.row[self.at] = self.packed;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Chain<'a> {
scores: &'a [f32],
symbols: usize,
phones: &'a [u32],
skip: f32,
}
const HOLD_BLANK: u8 = 0;
const HOLD_PHONE: u8 = 1;
const COMMIT: u8 = 2;
const SKIP: u8 = 3;
impl Trellis<4> for Chain<'_> {
type Frame<'a>
= &'a [f32]
where
Self: 'a;
fn num_frames(&self) -> usize {
self.scores.len() / self.symbols
}
fn num_positions(&self) -> usize {
self.phones.len()
}
fn frame(&self, frame: usize) -> &[f32] {
&self.scores[frame * self.symbols..(frame + 1) * self.symbols]
}
fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 4] {
let blank = Step::new(0, frame[0]);
if position == 0 {
return [blank, Step::ABSENT, Step::ABSENT, Step::ABSENT];
}
let phone = frame[self.phones[position - 1] as usize];
[
blank,
Step::new(0, phone),
Step::new(1, phone),
Step::new(1, self.skip + frame[0]),
]
}
}
impl ReversibleTrellis<4> for Chain<'_> {
fn steps_out_of(&self, frame: &[f32], position: usize) -> [Step; 4] {
let blank = Step::new(0, frame[0]);
let hold = if position > 0 {
Step::new(0, frame[self.phones[position - 1] as usize])
} else {
Step::ABSENT
};
let (commit, skip) = if position < self.phones.len() {
(
Step::new(1, frame[self.phones[position] as usize]),
Step::new(1, self.skip + frame[0]),
)
} else {
(Step::ABSENT, Step::ABSENT)
};
[blank, hold, commit, skip]
}
}
fn chain<'a>(scores: &'a [f32], phones: &'a [u32]) -> Chain<'a> {
Chain {
scores,
symbols: 4,
phones,
skip: f32::INFINITY,
}
}
#[test]
fn it_finds_the_path_the_scores_ask_for() {
let scores = [
9.0, 0.0, 9.0, 9.0, 9.0, 0.0, 9.0, 9.0, 0.0, 9.0, 9.0, 9.0, 9.0, 9.0, 0.0, 9.0,
];
let path = best_path(&chain(&scores, &[1, 2]))
.unwrap()
.expect("a path");
assert_eq!(path.positions(), [1, 1, 1, 2]);
assert_eq!(path.codes(), [COMMIT, HOLD_PHONE, HOLD_BLANK, COMMIT]);
assert!(path.cost().abs() < 1e-6);
assert_eq!(path.num_frames(), 4);
}
#[test]
fn a_reference_the_frames_cannot_carry_has_no_path() {
let scores = [0.0; 8];
assert_eq!(best_path(&chain(&scores, &[1, 2, 3])).unwrap(), None);
assert_eq!(
posteriors(&chain(&scores, &[1, 2, 3]), |_, _| {}).unwrap(),
None
);
}
#[test]
fn the_order_transitions_are_listed_in_is_the_tie_break() {
let scores = [1.0; 12];
let mut chain = chain(&scores, &[1]);
chain.skip = 0.0;
let path = best_path(&chain).unwrap().expect("a path");
assert!(
!path.codes().contains(&SKIP),
"a tie must not give up a phone"
);
assert_eq!(path.codes(), [COMMIT, HOLD_BLANK, HOLD_BLANK]);
assert_eq!(path.positions(), [1, 1, 1]);
assert!(!path.codes().contains(&HOLD_PHONE));
}
#[test]
fn a_trellis_that_advances_more_than_one_position() {
struct Words<'a> {
scores: &'a [f32],
phones: &'a [u32],
word: usize,
give_up: f32,
}
impl Trellis<3> for Words<'_> {
const REACH: u8 = 3;
type Frame<'a>
= &'a [f32]
where
Self: 'a;
fn num_frames(&self) -> usize {
self.scores.len() / 4
}
fn num_positions(&self) -> usize {
self.phones.len()
}
fn frame(&self, frame: usize) -> &[f32] {
&self.scores[frame * 4..(frame + 1) * 4]
}
fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 3] {
let blank = Step::new(0, frame[0]);
if position == 0 {
return [blank, Step::ABSENT, Step::ABSENT];
}
let phone = Step::new(1, frame[self.phones[position - 1] as usize]);
let word = if position >= self.word && position.is_multiple_of(self.word) {
Step::new(self.word as u8, self.give_up + frame[0])
} else {
Step::ABSENT
};
[blank, phone, word]
}
}
let mut scores = vec![9.0f32; 6 * 4];
for (frame, column) in [1usize, 2, 3, 0, 0, 0].into_iter().enumerate() {
scores[frame * 4 + column] = 0.0;
}
let words = Words {
scores: &scores,
phones: &[1, 2, 3, 1, 2, 3],
word: 3,
give_up: 1.0,
};
let path = best_path(&words).unwrap().expect("a path");
assert_eq!(path.num_frames(), 6);
assert_eq!(path.positions(), [1, 2, 3, 6, 6, 6]);
assert_eq!(path.codes(), [1, 1, 1, 2, 0, 0]);
assert!((path.cost() - 1.0).abs() < 1e-6, "{}", path.cost());
}
#[test]
fn a_degree_no_code_could_name_is_reported() {
struct Nothing;
impl Trellis<0> for Nothing {
type Frame<'a> = ();
fn num_frames(&self) -> usize {
1
}
fn num_positions(&self) -> usize {
0
}
fn frame(&self, _: usize) {}
fn steps_into(&self, _: (), _: usize) -> [Step; 0] {
[]
}
}
let err = best_path(&Nothing).unwrap_err();
assert!(format!("{err}").contains("cannot be named"), "{err}");
}
#[test]
fn the_band_is_the_cells_a_complete_path_can_stand_in() {
assert_eq!(band(0, 4, 2, 1), 0..1);
assert_eq!(band(1, 4, 2, 1), 0..2);
assert_eq!(band(3, 4, 2, 1), 1..3);
assert_eq!(band(4, 4, 2, 1), 2..3);
assert_eq!(band(2, 4, 4, 1), 2..3);
assert!(band(0, 2, 3, 1).is_empty());
assert_eq!(band(1, 4, 6, 2), 0..3);
assert_eq!(band(0, 2, 3, 2), 0..1);
}
#[test]
fn a_code_packs_into_the_bits_it_needs() {
assert_eq!(
(code_bits(2), code_bits(4), code_bits(16), code_bits(17)),
(1, 2, 4, 8)
);
for degree in [2usize, 4, 16, 256] {
let mut trace = Traceback::new(3, 20, degree).unwrap();
let codes: Vec<u8> = (0..21).map(|i| (i % degree) as u8).collect();
for frame in 0..3 {
let mut row = trace.row(frame);
for (position, &code) in codes.iter().enumerate() {
row.put(position, code);
}
row.finish();
}
for frame in 0..3 {
for (position, &code) in codes.iter().enumerate() {
assert_eq!(trace.get(frame, position), code, "degree {degree}");
}
}
}
}
#[test]
fn a_partial_row_reads_back() {
let mut trace = Traceback::new(1, 20, 4).unwrap();
let mut row = trace.row(0);
for position in 6..=13 {
row.put(position, (position % 4) as u8);
}
row.finish();
for position in 6..=13 {
assert_eq!(trace.get(0, position), (position % 4) as u8);
}
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn cost(&mut self) -> f32 {
self.below(1 << 14) as f32 / 4096.0
}
}
#[test]
fn both_solvers_agree_with_enumerating_every_path() {
fn walk(chain: &Chain<'_>, frame: usize, position: usize, cost: f32, paths: &mut Vec<f32>) {
if frame == chain.num_frames() {
if position == chain.num_positions() {
paths.push(cost);
}
return;
}
let scores = chain.frame(frame);
for step in chain.steps_out_of(scores, position) {
if step.cost.is_finite() {
walk(
chain,
frame + 1,
position + usize::from(step.advance),
cost + step.cost,
paths,
);
}
}
}
let mut rng = Rng(0x7A17_1CE0_1234_5678);
let mut compared = 0;
for round in 0..200 {
let num_frames = 1 + rng.below(6);
let num_positions = rng.below(num_frames.min(3) + 1);
let phones: Vec<u32> = (0..num_positions)
.map(|_| 1 + rng.below(3) as u32)
.collect();
let scores: Vec<f32> = (0..num_frames * 4).map(|_| rng.cost()).collect();
let mut under_test = chain(&scores, &phones);
if rng.below(2) == 0 {
under_test.skip = rng.cost();
}
let mut paths = Vec::new();
walk(&under_test, 0, 0, 0.0, &mut paths);
let best = best_path(&under_test).unwrap();
let total = posteriors(&under_test, |_, _| {}).unwrap();
if paths.is_empty() {
assert_eq!(best, None, "round {round}");
assert_eq!(total, None, "round {round}");
continue;
}
compared += 1;
let cheapest = paths.iter().copied().fold(f32::INFINITY, f32::min);
let best = best.expect("a path");
assert!(
(best.cost() - cheapest).abs() < 1e-3,
"round {round}: best_path {} against {cheapest}",
best.cost()
);
let mass: f64 = paths.iter().map(|&cost| (-cost as f64).exp()).sum();
let total = total.expect("a total");
assert!(
(total - -(mass.ln() as f32)).abs() < 1e-3,
"round {round}: posteriors {total} against {}",
-(mass.ln() as f32)
);
}
assert!(compared > 150, "only {compared} rounds had a path");
}
#[test]
fn every_frames_visits_come_to_one() {
let mut rng = Rng(0x1DEA_5EED_9876_4321);
for _ in 0..40 {
let num_frames = 2 + rng.below(10);
let num_positions = rng.below(num_frames.min(4) + 1);
let phones: Vec<u32> = (0..num_positions)
.map(|_| 1 + rng.below(3) as u32)
.collect();
let scores: Vec<f32> = (0..num_frames * 4).map(|_| rng.cost()).collect();
let mut under_test = chain(&scores, &phones);
under_test.skip = 2.0;
let mut per_frame = vec![0f64; num_frames];
let Some(_) = posteriors(&under_test, |seen, mass| {
assert!(seen.position <= num_positions);
assert!(seen.code < 4);
per_frame[seen.frame] += mass;
})
.unwrap() else {
continue;
};
for (frame, mass) in per_frame.iter().enumerate() {
assert!((mass - 1.0).abs() < 1e-4, "frame {frame} carries {mass}");
}
}
}
#[test]
fn the_chain_obeys_the_contract() {
let scores: Vec<f32> = (0..5 * 4).map(|i| i as f32 / 3.0).collect();
let mut under_test = chain(&scores, &[1, 2, 3]);
under_test.skip = 1.5;
axioms::check(&under_test);
axioms::check(&chain(&scores, &[1, 2, 3]));
axioms::check(&chain(&scores, &[]));
}
#[test]
#[should_panic(expected = "backwards but")]
fn it_catches_a_backward_reading_that_disagrees() {
struct Crooked<'a>(Chain<'a>);
impl Trellis<4> for Crooked<'_> {
type Frame<'f>
= &'f [f32]
where
Self: 'f;
fn num_frames(&self) -> usize {
self.0.num_frames()
}
fn num_positions(&self) -> usize {
self.0.num_positions()
}
fn frame(&self, frame: usize) -> &[f32] {
self.0.frame(frame)
}
fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 4] {
self.0.steps_into(frame, position)
}
}
impl ReversibleTrellis<4> for Crooked<'_> {
fn steps_out_of(&self, frame: &[f32], position: usize) -> [Step; 4] {
let mut out = derive_steps_out_of(&self.0, frame, position);
if position > 0 && position < self.0.num_positions() {
out[COMMIT as usize] =
Step::new(1, frame[self.0.phones[position - 1] as usize]);
}
out
}
}
let scores: Vec<f32> = (0..5 * 4).map(|i| i as f32 / 3.0).collect();
axioms::check(&Crooked(chain(&scores, &[1, 2, 3])));
}
#[test]
#[should_panic(expected = "past a REACH")]
fn it_catches_a_transition_that_outruns_its_reach() {
struct TooFar;
impl Trellis<2> for TooFar {
type Frame<'a> = ();
fn num_frames(&self) -> usize {
4
}
fn num_positions(&self) -> usize {
3
}
fn frame(&self, _: usize) {}
fn steps_into(&self, _: (), position: usize) -> [Step; 2] {
if position >= 2 {
[Step::new(0, 1.0), Step::new(2, 1.0)]
} else {
[Step::new(0, 1.0), Step::ABSENT]
}
}
}
impl ReversibleTrellis<2> for TooFar {}
axioms::check(&TooFar);
}
#[test]
#[should_panic(expected = "from before the start")]
fn it_catches_a_transition_that_reaches_back_past_the_start() {
struct OffTheFront;
impl Trellis<2> for OffTheFront {
type Frame<'a> = ();
fn num_frames(&self) -> usize {
3
}
fn num_positions(&self) -> usize {
2
}
fn frame(&self, _: usize) {}
fn steps_into(&self, _: (), _: usize) -> [Step; 2] {
[Step::new(0, 1.0), Step::new(1, 1.0)]
}
}
impl ReversibleTrellis<2> for OffTheFront {}
axioms::check(&OffTheFront);
}
#[test]
fn the_derived_backward_reading_is_the_written_one() {
let scores: Vec<f32> = (0..6 * 4).map(|i| (i % 7) as f32 / 2.0).collect();
let mut under_test = chain(&scores, &[1, 2, 3, 1]);
under_test.skip = 0.75;
for frame in 0..under_test.num_frames() {
let scores = under_test.frame(frame);
for position in 0..=under_test.num_positions() {
assert_eq!(
under_test.steps_out_of(scores, position),
derive_steps_out_of(&under_test, scores, position),
"frame {frame}, position {position}"
);
}
}
}
}