1use sicada::arc::Arc;
37use sicada::error::OpenFstError;
38
39use crate::align::{AlignChain, column_read};
40use crate::dense::{DenseFst, FromScore};
41use crate::trellis::posteriors;
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct Occupancy {
46 posteriors: Vec<f32>,
48 durations: Vec<f32>,
50 skips: Vec<f32>,
52 num_frames: usize,
53 num_symbols: usize,
54 cost: f32,
55}
56
57impl Occupancy {
58 #[inline(always)]
60 pub fn num_frames(&self) -> usize {
61 self.num_frames
62 }
63
64 #[inline(always)]
66 pub fn num_symbols(&self) -> usize {
67 self.num_symbols
68 }
69
70 #[inline(always)]
76 pub fn frame(&self, frame: usize) -> &[f32] {
77 &self.posteriors[frame * self.num_symbols..(frame + 1) * self.num_symbols]
78 }
79
80 #[inline(always)]
87 pub fn cost(&self) -> f32 {
88 self.cost
89 }
90
91 pub fn label_prior(&self) -> Vec<f32> {
98 let mut prior = vec![0f64; self.num_symbols];
99 for frame in self.posteriors.chunks_exact(self.num_symbols.max(1)) {
100 for (total, &value) in prior.iter_mut().zip(frame) {
101 *total += value as f64;
102 }
103 }
104 let frames = self.num_frames.max(1) as f64;
105 prior
106 .into_iter()
107 .map(|total| (total / frames) as f32)
108 .collect()
109 }
110
111 #[inline(always)]
118 pub fn expected_durations(&self) -> &[f32] {
119 &self.durations
120 }
121
122 #[inline(always)]
140 pub fn skip_posteriors(&self) -> &[f32] {
141 &self.skips
142 }
143}
144
145pub fn occupancy<A>(
156 chain: &AlignChain,
157 dense: &DenseFst<'_, A>,
158) -> Result<Option<Occupancy>, OpenFstError>
159where
160 A: Arc,
161 A::Weight: FromScore,
162{
163 let num_symbols = dense.num_symbols();
164 let num_frames = dense.num_frames();
165 let num_phones = chain.num_phones();
166 let trellis = chain.against(dense)?;
167
168 let cells = num_frames.checked_mul(num_symbols).ok_or_else(|| {
169 OpenFstError::InvalidOperation(format!(
170 "occupancy: posteriors for {num_frames} frames of {num_symbols} symbols do not fit"
171 ))
172 })?;
173 let mut posterior = vec![0f64; cells];
176 let mut durations = vec![0f64; num_phones];
177 let mut skips = vec![0f64; num_phones];
178
179 let total = posteriors(&trellis, |taken, mass| {
182 let column = column_read(chain, taken.code, taken.position);
183 posterior[taken.frame * num_symbols + column as usize] += mass;
184 if AlignChain::sounds(taken.code) {
185 durations[taken.position - 1] += mass;
186 } else if taken.code == AlignChain::SKIP {
187 skips[taken.position - 1] += mass;
188 }
189 })?;
190 let Some(cost) = total else {
191 return Ok(None);
192 };
193
194 Ok(Some(Occupancy {
195 posteriors: posterior.into_iter().map(|mass| mass as f32).collect(),
196 durations: durations.into_iter().map(|value| value as f32).collect(),
197 skips: skips.into_iter().map(|value| value as f32).collect(),
198 num_frames,
199 num_symbols,
200 cost,
201 }))
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use sicada::arc::StdArc;
208
209 use crate::align::{Alignment, align};
210
211 const SYMBOLS: usize = 4;
213
214 #[derive(Clone, Copy)]
217 struct Step {
218 column: usize,
219 sounded: Option<usize>,
220 skipped: Option<usize>,
221 }
222
223 #[derive(Debug)]
230 struct Enumerated {
231 cost: f32,
232 posteriors: Vec<f64>,
233 durations: Vec<f64>,
234 skips: Vec<f64>,
235 }
236
237 fn by_enumeration(
238 chain: &AlignChain,
239 dense: &DenseFst<'_, StdArc>,
240 num_frames: usize,
241 ) -> Option<Enumerated> {
242 #[allow(clippy::too_many_arguments)]
243 fn walk(
244 chain: &AlignChain,
245 dense: &DenseFst<'_, StdArc>,
246 num_frames: usize,
247 frame: usize,
248 position: usize,
249 cost: f32,
250 path: &mut Vec<Step>,
251 paths: &mut Vec<(f64, Vec<Step>)>,
252 ) {
253 if frame == num_frames {
254 if position == chain.num_phones() {
255 paths.push(((-cost as f64).exp(), path.clone()));
256 }
257 return;
258 }
259 let scores = dense.frame(frame);
260 let blank = chain.blank() as usize;
261 let mut take = |step: Step, extra: f32, to: usize| {
262 path.push(step);
263 walk(
264 chain,
265 dense,
266 num_frames,
267 frame + 1,
268 to,
269 cost + extra,
270 path,
271 paths,
272 );
273 path.pop();
274 };
275
276 let silent = Step {
277 column: blank,
278 sounded: None,
279 skipped: None,
280 };
281 take(silent, scores[blank], position);
282 if position > 0 {
283 let column = chain.phones()[position - 1] as usize;
284 let step = Step {
285 column,
286 sounded: Some(position - 1),
287 skipped: None,
288 };
289 take(step, scores[column], position);
290 }
291 if position < chain.num_phones() {
292 let column = chain.phones()[position] as usize;
293 let step = Step {
294 column,
295 sounded: Some(position),
296 skipped: None,
297 };
298 take(step, scores[column], position + 1);
299
300 let skip = chain.skip_costs()[position];
301 if skip.is_finite() {
302 let step = Step {
303 skipped: Some(position),
304 ..silent
305 };
306 take(step, skip + scores[blank], position + 1);
307 }
308 }
309 }
310
311 let mut paths = Vec::new();
312 walk(
313 chain,
314 dense,
315 num_frames,
316 0,
317 0,
318 0.0,
319 &mut Vec::new(),
320 &mut paths,
321 );
322 if paths.is_empty() {
323 return None;
324 }
325
326 let total: f64 = paths.iter().map(|(weight, _)| weight).sum();
327 let num_phones = chain.num_phones();
328 let mut posteriors = vec![0f64; num_frames * SYMBOLS];
329 let mut durations = vec![0f64; num_phones];
330 let mut skips = vec![0f64; num_phones];
331 for (weight, path) in &paths {
332 let share = weight / total;
333 for (frame, step) in path.iter().enumerate() {
334 posteriors[frame * SYMBOLS + step.column] += share;
335 if let Some(position) = step.sounded {
336 durations[position] += share;
337 }
338 if let Some(position) = step.skipped {
339 skips[position] += share;
340 }
341 }
342 }
343 Some(Enumerated {
344 cost: -(total.ln() as f32),
345 posteriors,
346 durations,
347 skips,
348 })
349 }
350
351 struct Rng(u64);
353
354 impl Rng {
355 fn next(&mut self) -> u64 {
356 self.0 ^= self.0 << 13;
357 self.0 ^= self.0 >> 7;
358 self.0 ^= self.0 << 17;
359 self.0
360 }
361
362 fn below(&mut self, n: usize) -> usize {
363 (self.next() % n as u64) as usize
364 }
365
366 fn cost(&mut self) -> f32 {
369 self.below(1 << 14) as f32 / 4096.0
370 }
371 }
372
373 #[test]
374 fn it_agrees_with_enumerating_every_alignment() {
375 let mut rng = Rng(0x0CC0_9E37_79B9_7C15);
376 let mut compared = 0;
377
378 for round in 0..200 {
379 let num_frames = 1 + rng.below(6);
380 let num_phones = rng.below(num_frames.min(3) + 1);
381 let phones: Vec<u32> = (0..num_phones)
382 .map(|_| 1 + rng.below(SYMBOLS - 1) as u32)
383 .collect();
384 let chain = AlignChain::new(phones);
385 let chain = if rng.below(2) == 0 {
386 chain.with_uniform_skip_cost(rng.cost()).unwrap()
387 } else {
388 chain
389 };
390
391 let scores: Vec<f32> = (0..num_frames * SYMBOLS).map(|_| rng.cost()).collect();
392 let dense = DenseFst::<StdArc>::new(&scores, num_frames, SYMBOLS).unwrap();
393
394 let expected = by_enumeration(&chain, &dense, num_frames);
395 let measured = occupancy(&chain, &dense).unwrap();
396
397 match (expected, measured) {
398 (None, None) => {}
399 (Some(expected), Some(measured)) => {
400 compared += 1;
401 assert!(
402 (measured.cost() - expected.cost).abs() < 1e-3,
403 "round {round}: total {} against every path's {}",
404 measured.cost(),
405 expected.cost
406 );
407 for frame in 0..num_frames {
408 for column in 0..SYMBOLS {
409 let want = expected.posteriors[frame * SYMBOLS + column];
410 let got = measured.frame(frame)[column] as f64;
411 assert!(
412 (got - want).abs() < 1e-4,
413 "round {round}: frame {frame} column {column}, {got} against {want}"
414 );
415 }
416 }
417 for position in 0..chain.num_phones() {
418 assert!(
419 (measured.expected_durations()[position] as f64
420 - expected.durations[position])
421 .abs()
422 < 1e-4,
423 "round {round}: duration of position {position}"
424 );
425 assert!(
426 (measured.skip_posteriors()[position] as f64
427 - expected.skips[position])
428 .abs()
429 < 1e-4,
430 "round {round}: skip of position {position}"
431 );
432 }
433 }
434 (expected, measured) => {
435 panic!("round {round}: enumeration {expected:?}, occupancy {measured:?}")
436 }
437 }
438 }
439
440 assert!(compared > 150, "only {compared} rounds had an alignment");
441 }
442
443 #[test]
444 fn every_frame_is_a_distribution() {
445 let mut rng = Rng(0xABCD_1234_5678_9EF1);
446 for _ in 0..50 {
447 let num_frames = 2 + rng.below(20);
448 let num_phones = rng.below(num_frames.min(8) + 1);
449 let phones: Vec<u32> = (0..num_phones)
450 .map(|_| 1 + rng.below(SYMBOLS - 1) as u32)
451 .collect();
452 let chain = AlignChain::new(phones).with_uniform_skip_cost(2.0).unwrap();
453 let scores: Vec<f32> = (0..num_frames * SYMBOLS).map(|_| rng.cost()).collect();
454 let dense = DenseFst::<StdArc>::new(&scores, num_frames, SYMBOLS).unwrap();
455
456 let measured = occupancy(&chain, &dense).unwrap().expect("an occupancy");
457 for frame in 0..num_frames {
458 let mass: f32 = measured.frame(frame).iter().sum();
459 assert!((mass - 1.0).abs() < 1e-4, "frame {frame} carries {mass}");
460 }
461 let sounded: f32 = measured.expected_durations().iter().sum();
464 let silent: f32 = (0..num_frames)
465 .map(|frame| measured.frame(frame)[chain.blank() as usize])
466 .sum();
467 assert!(
468 (sounded + silent - num_frames as f32).abs() < 1e-2,
469 "{sounded} sounding and {silent} silent, of {num_frames}"
470 );
471 }
472 }
473
474 #[test]
477 fn the_total_is_over_every_alignment_not_the_best_one() {
478 let scores = [
481 1.0, 0.5, 9.0, 9.0, 1.0, 0.5, 9.0, 9.0, 1.0, 0.5, 9.0, 9.0,
484 ];
485 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
486 let chain = AlignChain::new(vec![1]);
487
488 let best = align(&chain, &dense).unwrap().expect("an alignment");
489 let all = occupancy(&chain, &dense).unwrap().expect("an occupancy");
490 assert!(
491 all.cost() < best.cost() - 0.1,
492 "sum {} against best {}",
493 all.cost(),
494 best.cost()
495 );
496
497 let chain = AlignChain::new(vec![1, 1, 1]);
500 let best = align(&chain, &dense).unwrap().expect("an alignment");
501 let all = occupancy(&chain, &dense).unwrap().expect("an occupancy");
502 assert!(
503 (all.cost() - best.cost()).abs() < 1e-5,
504 "sum {} against best {}",
505 all.cost(),
506 best.cost()
507 );
508 }
509
510 #[test]
511 fn a_confident_model_puts_the_mass_on_the_alignment() {
512 let mut scores = vec![20.0; 5 * SYMBOLS];
513 for (frame, column) in [1usize, 1, 0, 2, 0].into_iter().enumerate() {
514 scores[frame * SYMBOLS + column] = 0.0;
515 }
516 let dense = DenseFst::<StdArc>::new(&scores, 5, SYMBOLS).unwrap();
517 let chain = AlignChain::new(vec![1, 2]);
518
519 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
520 let measured = occupancy(&chain, &dense).unwrap().expect("an occupancy");
521
522 for frame in 0..5 {
523 let column = match alignment.sounding(frame) {
524 Some(position) => chain.phones()[position] as usize,
525 None => chain.blank() as usize,
526 };
527 assert!(
528 measured.frame(frame)[column] > 0.99,
529 "frame {frame}: {:?}",
530 measured.frame(frame)
531 );
532 }
533 assert!((measured.expected_durations()[0] - 2.0).abs() < 0.01);
534 assert!((measured.expected_durations()[1] - 1.0).abs() < 0.01);
535 assert!(measured.skip_posteriors().iter().all(|&mass| mass == 0.0));
536 }
537
538 #[test]
540 fn a_skip_that_is_a_coin_toss_shows_as_one() {
541 let scores = [
543 10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 4.0, 10.0,
545 ];
546 let dense = DenseFst::<StdArc>::new(&scores, 2, SYMBOLS).unwrap();
547 let chain = AlignChain::new(vec![1, 2])
548 .with_skip_costs(&[9.0, 4.0])
549 .unwrap();
550
551 let measured = occupancy(&chain, &dense).unwrap().expect("an occupancy");
552 assert!(
553 (measured.skip_posteriors()[1] - 0.5).abs() < 1e-3,
554 "{:?}",
555 measured.skip_posteriors()
556 );
557 assert!(measured.skip_posteriors()[0] < 1e-6, "no reason to skip it");
558
559 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
562 assert!(alignment.skipped().is_empty());
563 }
564
565 #[test]
566 fn the_label_prior_is_the_posterior_averaged_over_the_frames() {
567 let scores = [
568 1.0, 0.5, 9.0, 9.0, 1.0, 0.5, 9.0, 9.0, 1.0, 0.5, 9.0, 9.0,
571 ];
572 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
573 let chain = AlignChain::new(vec![1]);
574 let measured = occupancy(&chain, &dense).unwrap().expect("an occupancy");
575
576 let prior = measured.label_prior();
577 assert_eq!(prior.len(), SYMBOLS);
578 assert!((prior.iter().sum::<f32>() - 1.0).abs() < 1e-5);
579 for (column, &averaged) in prior.iter().enumerate() {
580 let by_hand: f32 = (0..3)
581 .map(|frame| measured.frame(frame)[column])
582 .sum::<f32>()
583 / 3.0;
584 assert!((averaged - by_hand).abs() < 1e-6);
585 }
586 assert_eq!(prior[2], 0.0);
588 assert_eq!(prior[3], 0.0);
589 }
590
591 #[test]
592 fn a_reference_longer_than_the_audio_has_no_occupancy() {
593 let scores = vec![1.0; 2 * SYMBOLS];
594 let dense = DenseFst::<StdArc>::new(&scores, 2, SYMBOLS).unwrap();
595 assert_eq!(
596 occupancy(&AlignChain::new(vec![1, 2, 3]), &dense).unwrap(),
597 None
598 );
599
600 let err = occupancy(&AlignChain::new(vec![9]), &dense).unwrap_err();
601 assert!(format!("{err}").contains("does not have"), "{err}");
602 }
603
604 #[test]
605 fn an_empty_reference_is_all_blank() {
606 let scores = [0.25, 9.0, 9.0, 9.0].repeat(3);
607 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
608 let measured = occupancy(&AlignChain::new(vec![]), &dense)
609 .unwrap()
610 .expect("an occupancy");
611
612 assert!((measured.cost() - 0.75).abs() < 1e-5, "{}", measured.cost());
613 assert!(measured.expected_durations().is_empty());
614 for frame in 0..3 {
615 assert!((measured.frame(frame)[0] - 1.0).abs() < 1e-6);
616 }
617 }
618
619 #[test]
622 fn it_lines_up_with_the_alignment_frame_for_frame() {
623 let mut rng = Rng(0x5151_2727_3939_4B4B);
624 for _ in 0..40 {
625 let num_frames = 2 + rng.below(12);
626 let num_phones = rng.below(num_frames.min(5) + 1);
627 let phones: Vec<u32> = (0..num_phones)
628 .map(|_| 1 + rng.below(SYMBOLS - 1) as u32)
629 .collect();
630 let chain = AlignChain::new(phones);
631 let scores: Vec<f32> = (0..num_frames * SYMBOLS).map(|_| rng.cost()).collect();
632 let dense = DenseFst::<StdArc>::new(&scores, num_frames, SYMBOLS).unwrap();
633
634 let alignment: Alignment = align(&chain, &dense).unwrap().expect("an alignment");
635 let measured = occupancy(&chain, &dense).unwrap().expect("an occupancy");
636 assert_eq!(measured.num_frames(), alignment.num_frames());
637 assert_eq!(measured.num_symbols(), SYMBOLS);
638 assert_eq!(measured.expected_durations().len(), alignment.num_phones());
639 assert!(measured.cost() <= alignment.cost() + 1e-4);
642 }
643 }
644}