arcweight/semiring/log.rs
1//! Log semiring implementation.
2//!
3//! The log semiring provides numerically stable computation over probabilities by
4//! working in the logarithmic domain, avoiding underflow issues with very small
5//! probabilities.
6//!
7//! # References
8//!
9//! - Mohri, M. (2002). Semiring frameworks and algorithms for shortest-distance problems.
10//! *Journal of Automata, Languages and Combinatorics*, 7(3), 321–350.
11//!
12//! - Mohri, M., Pereira, F., & Riley, M. (2002). Weighted finite-state transducers in
13//! speech recognition. *Computer Speech & Language*, 16(1), 69–88.
14
15use super::traits::*;
16use core::fmt;
17use core::ops::{Add, Mul};
18use num_traits::{One, Zero};
19use ordered_float::OrderedFloat;
20
21/// Log weight for numerically stable probability computation.
22///
23/// The **log semiring** addresses numerical stability issues inherent in probability
24/// computation by working in the negative log domain. This enables handling of very
25/// small probabilities (e.g., $`10^{-100}`$) that would underflow in linear probability
26/// space, while maintaining mathematically equivalent probabilistic semantics.
27///
28/// # Mathematical Definition
29///
30/// The log semiring is defined as $`(\mathbb{R} \cup \{+\infty\}, \oplus_{\log}, +, +\infty, 0)`$:
31///
32/// | Operation | Definition | Interpretation |
33/// |-----------|------------|----------------|
34/// | $`a \oplus b`$ | $`-\log(e^{-a} + e^{-b})`$ | Combine probabilities |
35/// | $`a \otimes b`$ | $`a + b`$ | Multiply probabilities |
36/// | $`\bar{0}`$ | $`+\infty`$ | Impossible event ($`p = 0`$) |
37/// | $`\bar{1}`$ | $`0`$ | Certain event ($`p = 1`$) |
38///
39/// # Relationship to Probability Semiring
40///
41/// The log semiring is isomorphic to the probability semiring through the transformation
42/// $`w = -\log p`$ where $`p`$ is a probability:
43///
44/// - If $`p, q`$ are probabilities: $`-\log p \oplus -\log q = -\log(p + q)`$
45/// - If $`p, q`$ are probabilities: $`-\log p \otimes -\log q = -\log(p \times q)`$
46///
47/// This preserves probabilistic semantics while providing numerical stability.
48///
49/// # Use Cases
50///
51/// ## Large-Vocabulary Speech Recognition
52/// ```rust
53/// use arcweight::prelude::*;
54///
55/// // Acoustic model probabilities (very small values)
56/// let acoustic_prob = LogWeight::from_probability(1e-50); // Converts to log space
57/// let language_prob = LogWeight::from_probability(0.001);
58///
59/// // Combine probabilities safely
60/// let combined = acoustic_prob.times(&language_prob); // Multiplication in log space
61///
62/// // Convert back to probability if needed
63/// let final_prob = combined.to_probability();
64/// println!("Final probability: {:.2e}", final_prob); // ~1e-53
65/// ```
66///
67/// ## Machine Translation with Large Models
68/// ```rust
69/// use arcweight::prelude::*;
70///
71/// // Translation model scores (often very small probabilities)
72/// let phrase_prob = LogWeight::from_probability(1e-20);
73/// let alignment_prob = LogWeight::from_probability(1e-15);
74/// let reordering_prob = LogWeight::from_probability(0.1);
75///
76/// // Combine all model scores
77/// let translation_score = phrase_prob
78/// .times(&alignment_prob)
79/// .times(&reordering_prob);
80///
81/// // Alternative translations (add probabilities)
82/// let alternative1 = LogWeight::from_probability(1e-35);
83/// let alternative2 = LogWeight::from_probability(2e-35);
84/// let combined_alternatives = alternative1.plus(&alternative2); // LogSumExp
85/// ```
86///
87/// ## Neural Language Model Integration
88/// ```rust
89/// use arcweight::prelude::*;
90///
91/// // Softmax probabilities from neural networks
92/// let word_probs = vec![
93/// LogWeight::from_probability(0.4), // Most likely word
94/// LogWeight::from_probability(0.3), // Second choice
95/// LogWeight::from_probability(0.2), // Third choice
96/// LogWeight::from_probability(0.1), // Least likely
97/// ];
98///
99/// // Compute probability of any of these words
100/// let any_word_prob = word_probs.into_iter()
101/// .fold(LogWeight::zero(), |acc, prob| acc.plus(&prob));
102///
103/// // Should be close to 1.0 (sum of probabilities)
104/// assert!((any_word_prob.to_probability() - 1.0).abs() < 1e-10);
105/// ```
106///
107/// ## Sequence Analysis in Bioinformatics
108/// ```rust
109/// use arcweight::prelude::*;
110///
111/// // DNA sequence alignment with very long sequences
112/// let base_prob = LogWeight::from_probability(0.25); // Each base equally likely
113/// let sequence_length = 1000;
114///
115/// // Probability of specific sequence (would underflow in linear space)
116/// let mut sequence_prob = LogWeight::one();
117/// for _ in 0..sequence_length {
118/// sequence_prob = sequence_prob.times(&base_prob);
119/// }
120///
121/// // Convert to scientific notation for display
122/// println!("Sequence probability: {:.2e}", sequence_prob.to_probability());
123/// ```
124///
125/// # Working with FSTs
126///
127/// ```rust
128/// use arcweight::prelude::*;
129///
130/// let log_p1 = LogWeight::from_probability(0.5); // Convert from probability
131/// let log_p2 = LogWeight::from_probability(0.25); // Convert from probability
132///
133/// // Addition performs log-sum-exp (combines probabilities)
134/// let sum = log_p1 + log_p2; // -log(0.5 + 0.25) = -log(0.75)
135/// assert!((sum.to_probability() - 0.75).abs() < 1e-6);
136///
137/// // Multiplication is addition in log space (multiplies probabilities)
138/// let product = log_p1 * log_p2; // -log(0.5 × 0.25) = -log(0.125)
139/// assert!((product.to_probability() - 0.125).abs() < 1e-10);
140///
141/// // Identity elements
142/// assert_eq!(LogWeight::zero(), LogWeight::INFINITY); // Impossible event
143/// assert_eq!(LogWeight::one(), LogWeight::new(0.0)); // Certain event
144/// ```
145///
146/// # Numerical Stability Implementation
147///
148/// The log semiring implements numerically stable log-sum-exp operation:
149/// ```text
150/// -log(e^(-a) + e^(-b)) = -max(a,b) - log(1 + e^(-|a-b|))
151/// ```
152///
153/// This formulation prevents overflow/underflow by:
154/// - Working with the larger magnitude value first
155/// - Computing the difference in a stable manner
156/// - Using the identity: `log(1 + x) ≈ x` for small `x`
157///
158/// # Performance Characteristics
159///
160/// - **Arithmetic:** Addition is expensive (log-sum-exp), multiplication is O(1)
161/// - **Memory:** 8 bytes per weight (single f64)
162/// - **Precision:** Double precision for high-accuracy probability computation
163/// - **Conversion:** Efficient probability ↔ log conversions available
164/// - **Range:** Handles probabilities from ~10^-308 to 1.0
165///
166/// # Conversion Utilities
167///
168/// ```rust
169/// use arcweight::prelude::*;
170///
171/// // Convert from probability to log weight
172/// let prob = 0.001;
173/// let log_weight = LogWeight::from_probability(prob);
174/// assert_eq!(log_weight.value(), &(-prob.ln()));
175///
176/// // Convert back to probability
177/// let recovered_prob = log_weight.to_probability();
178/// assert!((recovered_prob - prob).abs() < 1e-15);
179///
180/// // Handle edge cases
181/// let zero_prob = LogWeight::from_probability(0.0);
182/// assert!(<LogWeight as num_traits::Zero>::is_zero(&zero_prob));
183/// assert_eq!(zero_prob.to_probability(), 0.0);
184/// ```
185///
186/// # Advanced Usage
187///
188/// ## Normalization in Log Space
189/// ```rust
190/// use arcweight::prelude::*;
191///
192/// // Normalize a probability distribution in log space
193/// let log_probs = vec![
194/// LogWeight::new(1.0), // Unnormalized log probabilities
195/// LogWeight::new(2.0),
196/// LogWeight::new(0.5),
197/// ];
198///
199/// // Compute log partition function (log of sum of probabilities)
200/// let log_z = log_probs.iter()
201/// .fold(LogWeight::zero(), |acc, &p| acc.plus(&p));
202///
203/// // Normalize each probability
204/// let normalized: Vec<_> = log_probs.iter()
205/// .map(|&p| p.divide(&log_z).unwrap())
206/// .collect();
207///
208/// // Verify normalization (sum should be 1.0)
209/// let sum = normalized.iter()
210/// .fold(LogWeight::zero(), |acc, &p| acc.plus(&p));
211/// assert!((sum.to_probability() - 1.0).abs() < 1e-10);
212/// ```
213///
214/// # Integration with FST Algorithms
215///
216/// Log weights work with all FST algorithms while providing numerical stability:
217/// - **Shortest Path:** Finds maximum probability paths
218/// - **Forward-Backward:** Stable computation of path probabilities
219/// - **Composition:** Combines probabilistic models
220/// - **Determinization:** Maintains probability distributions
221///
222/// # Algebraic Properties
223///
224/// - **Commutative:** Both $`\oplus`$ and $`\otimes`$ are commutative
225/// - **Not idempotent:** $`a \oplus a \neq a`$ in general
226/// - **No path property:** $`a \oplus b \notin \{a, b\}`$ in general
227/// - **Divisible:** Division is subtraction: $`a \oslash b = a - b`$
228///
229/// # See Also
230///
231/// - [`ProbabilityWeight`](crate::semiring::ProbabilityWeight) for simple probability computation
232/// - [`TropicalWeight`](crate::semiring::TropicalWeight) for optimization problems
233///
234/// # References
235///
236/// - Mohri, M. (2002). Semiring frameworks and algorithms for shortest-distance problems.
237/// *Journal of Automata, Languages and Combinatorics*, 7(3), 321–350.
238///
239/// - Mohri, M., Pereira, F., & Riley, M. (2002). Weighted finite-state transducers in
240/// speech recognition. *Computer Speech & Language*, 16(1), 69–88.
241#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
242#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
243pub struct LogWeight(OrderedFloat<f64>);
244
245impl LogWeight {
246 /// Positive infinity (zero element)
247 pub const INFINITY: Self = Self(OrderedFloat(f64::INFINITY));
248
249 /// Create a new log weight
250 pub fn new(value: f64) -> Self {
251 Self(OrderedFloat(value))
252 }
253
254 /// Convert from probability
255 pub fn from_probability(p: f64) -> Self {
256 if p == 0.0 {
257 Self::INFINITY
258 } else {
259 Self::new(-p.ln())
260 }
261 }
262
263 /// Convert to probability
264 pub fn to_probability(&self) -> f64 {
265 if self.0.is_infinite() {
266 0.0
267 } else {
268 (-*self.0).exp()
269 }
270 }
271}
272
273impl fmt::Display for LogWeight {
274 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275 if self.0.is_infinite() {
276 write!(f, "∞")
277 } else {
278 let value = self.0;
279 write!(f, "{value}")
280 }
281 }
282}
283
284impl Zero for LogWeight {
285 fn zero() -> Self {
286 Self::INFINITY
287 }
288
289 fn is_zero(&self) -> bool {
290 self.0.is_infinite()
291 }
292}
293
294impl One for LogWeight {
295 fn one() -> Self {
296 Self::new(0.0)
297 }
298}
299
300impl Add for LogWeight {
301 type Output = Self;
302
303 fn add(self, rhs: Self) -> Self::Output {
304 if <Self as num_traits::Zero>::is_zero(&self) {
305 rhs
306 } else if <Self as num_traits::Zero>::is_zero(&rhs) {
307 self
308 } else {
309 let a = -*self.0;
310 let b = -*rhs.0;
311 Self::new(-(a.max(b) + (1.0 + (-(a - b).abs()).exp()).ln()))
312 }
313 }
314}
315
316impl Mul for LogWeight {
317 type Output = Self;
318
319 fn mul(self, rhs: Self) -> Self::Output {
320 if <Self as num_traits::Zero>::is_zero(&self) || <Self as num_traits::Zero>::is_zero(&rhs) {
321 Self::zero()
322 } else {
323 Self(self.0 + rhs.0)
324 }
325 }
326}
327
328impl Semiring for LogWeight {
329 type Value = f64;
330
331 fn new(value: Self::Value) -> Self {
332 Self::new(value)
333 }
334
335 fn value(&self) -> &Self::Value {
336 &self.0
337 }
338
339 fn properties() -> SemiringProperties {
340 SemiringProperties {
341 left_semiring: true,
342 right_semiring: true,
343 commutative: true,
344 idempotent: false,
345 path: false,
346 }
347 }
348
349 fn approx_eq(&self, other: &Self, epsilon: f64) -> bool {
350 if <Self as num_traits::Zero>::is_zero(self) && <Self as num_traits::Zero>::is_zero(other) {
351 true
352 } else {
353 (self.0 - other.0).abs() < epsilon
354 }
355 }
356}
357
358impl DivisibleSemiring for LogWeight {
359 fn divide(&self, other: &Self) -> Option<Self> {
360 if <Self as num_traits::Zero>::is_zero(other) {
361 None
362 } else if <Self as num_traits::Zero>::is_zero(self) {
363 Some(Self::zero())
364 } else {
365 Some(Self(self.0 - other.0))
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use num_traits::{One, Zero};
374
375 #[test]
376 fn test_log_weight_creation() {
377 let w = LogWeight::new(2.0);
378 assert_eq!(*w.value(), 2.0);
379 }
380
381 #[test]
382 fn test_log_zero_one() {
383 let zero = LogWeight::zero();
384 let one = LogWeight::one();
385
386 assert!(Semiring::is_zero(&zero));
387 assert!(Semiring::is_one(&one));
388 assert!(zero.value().is_infinite());
389 assert_eq!(*one.value(), 0.0);
390 }
391
392 #[test]
393 fn test_log_addition() {
394 let w1 = LogWeight::new(1.0);
395 let w2 = LogWeight::new(2.0);
396 let result = w1.plus(&w2);
397
398 // -log(exp(-1) + exp(-2)) ≈ 0.687
399 assert!(result.approx_eq(&LogWeight::new(0.6867), 0.001));
400 }
401
402 #[test]
403 fn test_log_multiplication() {
404 let w1 = LogWeight::new(1.0);
405 let w2 = LogWeight::new(2.0);
406 let result = w1.times(&w2);
407
408 assert_eq!(*result.value(), 3.0); // addition in log space
409 }
410
411 #[test]
412 fn test_log_zero_operations() {
413 let w = LogWeight::new(2.0);
414 let zero = LogWeight::zero();
415
416 // Adding zero returns the other weight
417 assert_eq!(w.plus(&zero), w);
418 assert_eq!(zero.plus(&w), w);
419
420 // Multiplying by zero returns zero
421 assert!(Semiring::is_zero(&w.times(&zero)));
422 assert!(Semiring::is_zero(&zero.times(&w)));
423 }
424
425 #[test]
426 fn test_log_one_operations() {
427 let w = LogWeight::new(2.0);
428 let one = LogWeight::one();
429
430 let mul_result = w.times(&one);
431 assert_eq!(mul_result, w);
432 }
433
434 #[test]
435 fn test_log_display() {
436 let w = LogWeight::new(2.5);
437 let zero = LogWeight::zero();
438
439 assert_eq!(format!("{w}"), "2.5");
440 assert_eq!(format!("{zero}"), "∞");
441 }
442
443 #[test]
444 fn test_log_division() {
445 let w1 = LogWeight::new(5.0);
446 let w2 = LogWeight::new(3.0);
447
448 let result = w1.divide(&w2).unwrap();
449 assert_eq!(*result.value(), 2.0);
450
451 // Division by zero should return None
452 let zero = LogWeight::zero();
453 assert!(w1.divide(&zero).is_none());
454 }
455
456 #[test]
457 fn test_log_from_to_probability() {
458 // Test normal probability conversion
459 let prob = 0.5;
460 let log_weight = LogWeight::from_probability(prob);
461 assert!((log_weight.value() - (-prob.ln())).abs() < 1e-10);
462 assert!((log_weight.to_probability() - prob).abs() < 1e-10);
463
464 // Test zero probability
465 let zero_log = LogWeight::from_probability(0.0);
466 assert!(Semiring::is_zero(&zero_log));
467 assert_eq!(zero_log.to_probability(), 0.0);
468
469 // Test very small probability
470 let small_prob = 1e-100;
471 let small_log = LogWeight::from_probability(small_prob);
472 assert!((small_log.to_probability() - small_prob).abs() < small_prob * 1e-10);
473 }
474
475 #[test]
476 fn test_log_properties() {
477 let props = LogWeight::properties();
478 assert!(props.left_semiring);
479 assert!(props.right_semiring);
480 assert!(props.commutative);
481 assert!(!props.idempotent);
482 assert!(!props.path);
483 }
484
485 #[test]
486 fn test_log_approx_eq() {
487 let w1 = LogWeight::new(2.000_001);
488 let w2 = LogWeight::new(2.0);
489
490 assert!(w1.approx_eq(&w2, 0.001));
491 assert!(!w1.approx_eq(&w2, 0.000_000_1));
492 }
493
494 #[test]
495 fn test_log_operator_overloads() {
496 let w1 = LogWeight::new(1.0);
497 let w2 = LogWeight::new(2.0);
498
499 // Test + operator (log-sum-exp)
500 let sum = w1 + w2;
501 assert!(sum.approx_eq(&LogWeight::new(0.6867), 0.001));
502
503 // Test * operator (addition in log space)
504 assert_eq!(w1 * w2, LogWeight::new(3.0));
505 }
506
507 #[test]
508 fn test_log_identity_laws() {
509 let w = LogWeight::new(2.0);
510 let zero = LogWeight::zero();
511 let one = LogWeight::one();
512
513 // Additive identity
514 assert_eq!(w + zero, w);
515 assert_eq!(zero + w, w);
516
517 // Multiplicative identity
518 assert_eq!(w * one, w);
519 assert_eq!(one * w, w);
520
521 // Annihilation by zero
522 assert!(Semiring::is_zero(&(w * zero)));
523 assert!(Semiring::is_zero(&(zero * w)));
524 }
525
526 #[test]
527 fn test_log_semiring_axioms() {
528 let a = LogWeight::new(1.0);
529 let b = LogWeight::new(2.0);
530 let c = LogWeight::new(3.0);
531 let tolerance = 1e-10;
532
533 // Associativity of addition
534 assert!(((a + b) + c).approx_eq(&(a + (b + c)), tolerance));
535
536 // Associativity of multiplication
537 assert_eq!((a * b) * c, a * (b * c));
538
539 // Commutativity of addition
540 assert_eq!(a + b, b + a);
541
542 // Commutativity of multiplication
543 assert_eq!(a * b, b * a);
544
545 // Distributivity (approximate due to log-sum-exp)
546 assert!(((a + b) * c).approx_eq(&((a * c) + (b * c)), tolerance));
547 }
548}