1use std::fmt;
25use std::hash::Hash;
26use std::str::FromStr;
27
28use sicada::arc::ArcLabel;
29use sicada::fst_type::WeightType;
30use sicada::utils::io::{FstScalar, read_scalar, write_scalar};
31use sicada::weight::{
32 Divide, DivideType, IDEMPOTENT, IdempotentWeight, LEFT_SEMIRING, LeftSemiring, PATH,
33 PathWeight, RIGHT_SEMIRING, RightSemiring, Weight, WeightIo,
34};
35use smallvec::SmallVec;
36
37use crate::lattice_weight::LatticeWeight;
38
39pub type Alignment<L> = SmallVec<[L; 8]>;
46
47#[derive(Debug, Clone, Default)]
49pub struct CompactLatticeWeight<L: ArcLabel> {
50 weight: LatticeWeight,
51 alignment: Alignment<L>,
52}
53
54impl<L: ArcLabel> CompactLatticeWeight<L> {
55 #[inline]
63 pub fn new(weight: LatticeWeight, alignment: Alignment<L>) -> Self {
64 if weight == LatticeWeight::zero() {
65 return Self::zero();
66 }
67 Self { weight, alignment }
68 }
69
70 #[inline]
72 pub fn from_weight(weight: LatticeWeight) -> Self {
73 Self::new(weight, Alignment::new())
74 }
75
76 #[inline(always)]
78 pub fn weight(&self) -> &LatticeWeight {
79 &self.weight
80 }
81
82 #[inline(always)]
84 pub fn alignment(&self) -> &[L] {
85 &self.alignment
86 }
87
88 #[inline]
96 fn compare(&self, other: &Self) -> std::cmp::Ordering {
97 use std::cmp::Ordering::*;
98 match compare_lattice_weights(&self.weight, &other.weight) {
99 Equal => {}
100 ordering => return ordering,
101 }
102 match other.alignment.len().cmp(&self.alignment.len()) {
103 Equal => {}
104 ordering => return ordering,
105 }
106 other.alignment.cmp(&self.alignment)
110 }
111}
112
113#[inline]
115fn compare_lattice_weights(lhs: &LatticeWeight, rhs: &LatticeWeight) -> std::cmp::Ordering {
116 use std::cmp::Ordering::*;
117 let (mine, theirs) = (lhs.total(), rhs.total());
118 if mine < theirs {
119 Greater
120 } else if mine > theirs {
121 Less
122 } else if lhs.graph < rhs.graph {
123 Greater
124 } else if lhs.graph > rhs.graph {
125 Less
126 } else {
127 Equal
128 }
129}
130
131impl<L: ArcLabel> PartialEq for CompactLatticeWeight<L> {
132 #[inline]
133 fn eq(&self, other: &Self) -> bool {
134 self.weight == other.weight && self.alignment == other.alignment
135 }
136}
137
138impl<L: ArcLabel> Eq for CompactLatticeWeight<L> {}
139
140impl<L: ArcLabel> Hash for CompactLatticeWeight<L> {
141 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
142 self.weight.hash(state);
143 self.alignment.as_slice().hash(state);
144 }
145}
146
147impl<L: ArcLabel> fmt::Display for CompactLatticeWeight<L> {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{},", self.weight)?;
151 for (index, label) in self.alignment.iter().enumerate() {
152 if index > 0 {
153 write!(f, "_")?;
154 }
155 write!(f, "{label}")?;
156 }
157 Ok(())
158 }
159}
160
161impl<L: ArcLabel> FromStr for CompactLatticeWeight<L> {
162 type Err = String;
163
164 fn from_str(s: &str) -> Result<Self, Self::Err> {
165 let (graph, rest) = s.split_once(',').ok_or_else(|| {
166 format!(
167 "{}: expected `graph,acoustic,alignment`, got {s:?}",
168 Self::type_name()
169 )
170 })?;
171 let (acoustic, labels) = rest.split_once(',').ok_or_else(|| {
172 format!(
173 "{}: expected `graph,acoustic,alignment`, got {s:?}",
174 Self::type_name()
175 )
176 })?;
177 let weight: LatticeWeight = format!("{graph},{acoustic}").parse()?;
178
179 let mut alignment = Alignment::new();
180 for label in labels.split('_').filter(|piece| !piece.is_empty()) {
181 alignment.push(
182 label
183 .trim()
184 .parse()
185 .map_err(|_| format!("{}: {label:?} is not a label", Self::type_name()))?,
186 );
187 }
188 Ok(Self::new(weight, alignment))
189 }
190}
191
192impl<L: ArcLabel> Weight for CompactLatticeWeight<L> {
193 type ReverseWeight = Self;
194
195 #[inline]
196 fn zero() -> Self {
197 Self {
198 weight: LatticeWeight::zero(),
199 alignment: Alignment::new(),
200 }
201 }
202
203 #[inline]
204 fn one() -> Self {
205 Self {
206 weight: LatticeWeight::one(),
207 alignment: Alignment::new(),
208 }
209 }
210
211 #[inline]
212 fn no_weight() -> Self {
213 Self {
214 weight: LatticeWeight::no_weight(),
215 alignment: Alignment::new(),
216 }
217 }
218
219 #[inline]
226 fn type_name() -> WeightType {
227 WeightType::new_dynamic(format!(
228 "compact{}{}",
229 LatticeWeight::type_name(),
230 std::mem::size_of::<L>()
231 ))
232 }
233
234 #[inline(always)]
235 fn properties() -> u64 {
236 LEFT_SEMIRING | RIGHT_SEMIRING | PATH | IDEMPOTENT
238 }
239
240 #[inline]
241 fn plus(&self, rhs: &Self) -> Self {
242 if !self.is_member() || !rhs.is_member() {
243 return Self::no_weight();
244 }
245 if self.compare(rhs).is_ge() {
246 self.clone()
247 } else {
248 rhs.clone()
249 }
250 }
251
252 #[inline]
253 fn times(&self, rhs: &Self) -> Self {
254 if !self.is_member() || !rhs.is_member() {
255 return Self::no_weight();
256 }
257 let weight = self.weight.times(&rhs.weight);
258 if weight == LatticeWeight::zero() {
259 return Self::zero();
260 }
261 let mut alignment = Alignment::with_capacity(self.alignment.len() + rhs.alignment.len());
262 alignment.extend_from_slice(&self.alignment);
263 alignment.extend_from_slice(&rhs.alignment);
264 Self { weight, alignment }
265 }
266
267 #[inline]
268 fn reverse(&self) -> Self::ReverseWeight {
269 let mut alignment = self.alignment.clone();
270 alignment.reverse();
271 Self {
272 weight: self.weight.reverse(),
273 alignment,
274 }
275 }
276
277 #[inline]
278 fn is_member(&self) -> bool {
279 self.weight.is_member()
281 && (self.weight != LatticeWeight::zero() || self.alignment.is_empty())
282 }
283
284 #[inline]
285 fn approx_equal(&self, other: &Self, delta: f32) -> bool {
286 self.weight.approx_equal(&other.weight, delta) && self.alignment == other.alignment
287 }
288
289 #[inline]
290 fn quantize(&self, delta: f32) -> Self {
291 Self {
292 weight: self.weight.quantize(delta),
293 alignment: self.alignment.clone(),
294 }
295 }
296}
297
298impl<L: ArcLabel + FstScalar> WeightIo for CompactLatticeWeight<L> {
301 fn read<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
302 let weight = LatticeWeight::read(reader)?;
303 let size: i32 = read_scalar(reader)?;
304 if size < 0 {
305 return Err(std::io::Error::new(
306 std::io::ErrorKind::InvalidData,
307 format!("{}: an alignment of {size} labels", Self::type_name()),
308 ));
309 }
310 let mut alignment = Alignment::with_capacity(size as usize);
311 for _ in 0..size {
312 alignment.push(read_scalar(reader)?);
313 }
314 Ok(Self::new(weight, alignment))
315 }
316
317 fn write<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
318 self.weight.write(writer)?;
319 write_scalar(writer, self.alignment.len() as i32)?;
320 for &label in &self.alignment {
321 write_scalar(writer, label)?;
322 }
323 Ok(())
324 }
325}
326
327impl<L: ArcLabel> Divide for CompactLatticeWeight<L> {
328 fn divide(&self, rhs: &Self, side: DivideType) -> Self {
338 if !self.is_member() || !rhs.is_member() {
339 return Self::no_weight();
340 }
341 if rhs.weight == LatticeWeight::zero() {
342 return Self::no_weight();
343 }
344 if self.weight == LatticeWeight::zero() {
345 return Self::zero();
346 }
347 if rhs.alignment.len() > self.alignment.len() {
348 return Self::no_weight();
349 }
350
351 let weight = self.weight.divide(&rhs.weight, side);
352 if !weight.is_member() {
353 return Self::no_weight();
354 }
355 let split = self.alignment.len() - rhs.alignment.len();
356 let alignment = match side {
357 DivideType::Left => {
358 if self.alignment[..rhs.alignment.len()] != rhs.alignment[..] {
359 return Self::no_weight();
360 }
361 Alignment::from_slice(&self.alignment[rhs.alignment.len()..])
362 }
363 DivideType::Right => {
364 if self.alignment[split..] != rhs.alignment[..] {
365 return Self::no_weight();
366 }
367 Alignment::from_slice(&self.alignment[..split])
368 }
369 DivideType::Any => return Self::no_weight(),
372 };
373 Self::new(weight, alignment)
374 }
375}
376
377impl<L: ArcLabel> LeftSemiring for CompactLatticeWeight<L> {}
378impl<L: ArcLabel> RightSemiring for CompactLatticeWeight<L> {}
379impl<L: ArcLabel> IdempotentWeight for CompactLatticeWeight<L> {}
380impl<L: ArcLabel> PathWeight for CompactLatticeWeight<L> {}
381
382pub type CompactLatticeArc<A> = sicada::arc::ArcTpl<
385 CompactLatticeWeight<<A as sicada::arc::Arc>::Label>,
386 <A as sicada::arc::Arc>::Label,
387 <A as sicada::arc::Arc>::StateId,
388>;
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use sicada::weight::axioms;
394
395 type W = CompactLatticeWeight<i32>;
396
397 fn aligned(graph: f32, acoustic: f32, labels: &[i32]) -> W {
398 W::new(
399 LatticeWeight::new(graph, acoustic),
400 Alignment::from_slice(labels),
401 )
402 }
403
404 fn samples() -> Vec<W> {
405 vec![
406 aligned(0.0, 0.0, &[]),
407 aligned(1.0, 0.5, &[7]),
408 aligned(0.25, 2.0, &[7, 8]),
409 aligned(2.0, -1.0, &[9]),
410 aligned(1.0, 0.5, &[8]),
411 W::zero(),
412 ]
413 }
414
415 #[test]
416 fn it_is_the_semiring_it_says_it_is() {
417 axioms::check(&samples());
418 axioms::check_divide(&samples());
419 }
420
421 #[test]
425 fn it_does_not_claim_to_commute() {
426 assert_eq!(W::properties() & sicada::weight::COMMUTATIVE, 0);
427 let a = aligned(0.0, 0.0, &[1]);
428 let b = aligned(0.0, 0.0, &[2]);
429 assert_ne!(a.times(&b), b.times(&a));
430 assert_eq!(a.times(&b).alignment(), &[1, 2]);
431 }
432
433 #[test]
435 fn plus_keeps_the_better_alignment_whole() {
436 let cheap = aligned(1.0, 1.0, &[5, 5, 6]);
437 let dear = aligned(1.0, 3.0, &[5, 6, 6]);
438 assert_eq!(cheap.plus(&dear), cheap);
439 assert_eq!(dear.plus(&cheap), cheap);
440 assert_eq!(cheap.plus(&dear).alignment(), &[5, 5, 6]);
441 }
442
443 #[test]
446 fn a_tie_prefers_the_shorter_alignment() {
447 let short = aligned(1.0, 1.0, &[5]);
448 let long = aligned(1.0, 1.0, &[5, 5]);
449 assert_eq!(short.plus(&long), short);
450 assert_eq!(long.plus(&short), short);
451
452 let low = aligned(1.0, 1.0, &[4, 9]);
453 let high = aligned(1.0, 1.0, &[5, 5]);
454 assert_eq!(low.plus(&high), low);
455 assert_eq!(high.plus(&low), low);
456 }
457
458 #[test]
461 fn the_zero_is_unique() {
462 assert_eq!(
463 W::new(LatticeWeight::zero(), Alignment::from_slice(&[5])),
464 W::zero()
465 );
466 assert!(W::zero().is_member());
467 assert!(
468 !W {
469 weight: LatticeWeight::zero(),
470 alignment: Alignment::from_slice(&[5]),
471 }
472 .is_member(),
473 "one built behind `new`'s back is not a weight"
474 );
475 assert_eq!(aligned(1.0, 1.0, &[3]).times(&W::zero()), W::zero());
476 }
477
478 #[test]
479 fn dividing_takes_the_alignment_off_the_named_end() {
480 let whole = aligned(3.0, 3.0, &[1, 2, 3]);
481 let head = aligned(1.0, 1.0, &[1]);
482 let tail = aligned(1.0, 1.0, &[3]);
483
484 let rest = whole.divide(&head, DivideType::Left);
485 assert_eq!(rest.alignment(), &[2, 3]);
486 assert_eq!(head.times(&rest), whole);
487
488 let start = whole.divide(&tail, DivideType::Right);
489 assert_eq!(start.alignment(), &[1, 2]);
490 assert_eq!(start.times(&tail), whole);
491
492 assert!(!whole.divide(&tail, DivideType::Left).is_member());
494 assert!(!whole.divide(&head, DivideType::Right).is_member());
495 assert!(!whole.divide(&head, DivideType::Any).is_member());
497 }
498
499 #[test]
500 fn reversing_reverses_the_alignment() {
501 let w = aligned(1.0, 2.0, &[1, 2, 3]);
502 assert_eq!(w.reverse().alignment(), &[3, 2, 1]);
503 assert_eq!(w.reverse().reverse(), w);
504 }
505
506 #[test]
507 fn it_reads_back_what_it_prints() {
508 for weight in samples() {
509 let text = weight.to_string();
510 let parsed: W = text.parse().expect(&text);
511 assert_eq!(parsed, weight, "{text}");
512 }
513 assert_eq!(aligned(1.0, 2.0, &[3, 4]).to_string(), "1,2,3_4");
514 assert!("1,2".parse::<W>().is_err());
515 }
516
517 #[test]
520 fn its_type_name_is_kaldis() {
521 assert_eq!(W::type_name().as_str(), "compactlattice44");
522 assert_eq!(
523 CompactLatticeWeight::<i64>::type_name().as_str(),
524 "compactlattice48"
525 );
526 }
527
528 #[test]
532 fn it_writes_the_bytes_upstream_writes() {
533 let mut bytes = Vec::new();
534 aligned(1.0, 2.0, &[7, 8]).write(&mut bytes).unwrap();
535
536 let mut expected = Vec::new();
537 expected.extend_from_slice(&1.0f32.to_le_bytes());
538 expected.extend_from_slice(&2.0f32.to_le_bytes());
539 expected.extend_from_slice(&2i32.to_le_bytes());
540 expected.extend_from_slice(&7i32.to_le_bytes());
541 expected.extend_from_slice(&8i32.to_le_bytes());
542 assert_eq!(bytes, expected);
543
544 for weight in samples() {
545 let mut bytes = Vec::new();
546 weight.write(&mut bytes).unwrap();
547 let read = W::read(&mut bytes.as_slice()).unwrap();
548 assert_eq!(read, weight);
549 }
550 }
551
552 #[test]
553 fn it_can_be_a_key() {
554 use std::collections::HashSet;
555 let mut seen = HashSet::new();
556 assert!(seen.insert(aligned(1.0, 2.0, &[3])));
557 assert!(!seen.insert(aligned(1.0, 2.0, &[3])));
558 assert!(seen.insert(aligned(1.0, 2.0, &[4])));
559 }
560}