use std::fmt::{Debug, Display};
use std::str::FromStr;
use crate::fst_type::WeightType;
pub const DELTA: f32 = 1.0 / 1024.0;
pub const LEFT_SEMIRING: u64 = 0x0000000000000001;
pub const RIGHT_SEMIRING: u64 = 0x0000000000000002;
pub const SEMIRING: u64 = LEFT_SEMIRING | RIGHT_SEMIRING;
pub const COMMUTATIVE: u64 = 0x0000000000000004;
pub const IDEMPOTENT: u64 = 0x0000000000000008;
pub const PATH: u64 = 0x0000000000000010;
pub trait Weight: Clone + PartialEq + Debug + Display + FromStr {
type ReverseWeight: Weight;
fn zero() -> Self;
fn one() -> Self;
fn no_weight() -> Self;
fn type_name() -> WeightType;
fn properties() -> u64;
fn plus(&self, rhs: &Self) -> Self;
fn times(&self, rhs: &Self) -> Self;
fn reverse(&self) -> Self::ReverseWeight;
fn is_member(&self) -> bool;
fn approx_equal(&self, other: &Self, delta: f32) -> bool;
fn quantize(&self, delta: f32) -> Self;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DivideType {
Left,
Right,
Any,
}
pub trait WeightIo: Sized {
fn read<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self>;
fn write<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()>;
}
pub trait Divide: Weight {
fn divide(&self, rhs: &Self, typ: DivideType) -> Self;
}
pub trait Minus: Weight {
fn minus(&self, rhs: &Self) -> Self;
}
pub trait LeftSemiring: Weight {}
pub trait RightSemiring: Weight {}
pub trait IdempotentWeight: Weight {}
pub trait PathWeight: IdempotentWeight {}
pub trait CommutativeWeight: Weight {}
#[inline]
pub fn natural_less<W: IdempotentWeight>(lhs: &W, rhs: &W) -> bool {
lhs != rhs && lhs.plus(rhs) == *lhs
}
pub fn power<W: Weight>(weight: &W, n: usize) -> W {
let mut result = W::one();
for _ in 0..n {
result = result.times(weight);
}
result
}
#[derive(Debug, Clone)]
pub struct Adder<W: Weight> {
sum: W,
}
impl<W: Weight> Adder<W> {
pub fn new() -> Self {
Self { sum: W::zero() }
}
#[inline]
pub fn add(&mut self, w: &W) {
self.sum = self.sum.plus(w);
}
#[inline]
pub fn sum(&self) -> W {
self.sum.clone()
}
#[inline]
pub fn reset(&mut self, w: W) {
self.sum = w;
}
}
impl<W: Weight> Default for Adder<W> {
fn default() -> Self {
Self::new()
}
}
macro_rules! impl_weight_convert {
($from:ident, $to:ident, $closure:expr) => {
impl From<$from> for $to {
#[inline(always)]
fn from(w: $from) -> Self {
$closure(w)
}
}
};
}
pub(crate) use impl_weight_convert;
#[cfg(any(test, feature = "axioms"))]
pub mod axioms {
use super::*;
const DELTA: f32 = 1e-4;
fn same<W: Weight>(lhs: &W, rhs: &W, axiom: &str, context: &str) {
assert!(
lhs.approx_equal(rhs, DELTA),
"{axiom} failed for {context}: {lhs} vs {rhs}"
);
}
pub fn check<W: Weight>(samples: &[W]) {
let mut values = vec![W::zero(), W::one()];
values.extend(samples.iter().cloned());
for value in &values {
assert!(
value.is_member(),
"sample {value} is not a member of {}",
W::type_name()
);
}
let props = W::properties();
let type_name = W::type_name();
for a in &values {
let context = format!("{type_name}: a={a}");
same(
&a.plus(&W::zero()),
a,
"zero is a right identity for plus",
&context,
);
same(
&W::zero().plus(a),
a,
"zero is a left identity for plus",
&context,
);
same(
&a.times(&W::one()),
a,
"one is a right identity for times",
&context,
);
same(
&W::one().times(a),
a,
"one is a left identity for times",
&context,
);
same(
&a.times(&W::zero()),
&W::zero(),
"zero annihilates on the right",
&context,
);
same(
&W::zero().times(a),
&W::zero(),
"zero annihilates on the left",
&context,
);
if props & IDEMPOTENT != 0 {
same(&a.plus(a), a, "plus is idempotent", &context);
}
for b in &values {
let context = format!("{type_name}: a={a}, b={b}");
same(&a.plus(b), &b.plus(a), "plus is commutative", &context);
if props & COMMUTATIVE != 0 {
same(&a.times(b), &b.times(a), "times is commutative", &context);
}
if props & PATH != 0 {
let sum = a.plus(b);
assert!(
sum.approx_equal(a, DELTA) || sum.approx_equal(b, DELTA),
"the path property failed for {context}: a+b={sum}"
);
}
for c in &values {
let context = format!("{type_name}: a={a}, b={b}, c={c}");
same(
&a.plus(b).plus(c),
&a.plus(&b.plus(c)),
"plus is associative",
&context,
);
same(
&a.times(b).times(c),
&a.times(&b.times(c)),
"times is associative",
&context,
);
if props & LEFT_SEMIRING != 0 {
same(
&a.times(&b.plus(c)),
&a.times(b).plus(&a.times(c)),
"times distributes over plus on the left",
&context,
);
}
if props & RIGHT_SEMIRING != 0 {
same(
&b.plus(c).times(a),
&b.times(a).plus(&c.times(a)),
"times distributes over plus on the right",
&context,
);
}
}
}
}
}
pub fn check_divide<W: Weight + Divide>(samples: &[W]) {
let mut values = vec![W::one()];
values.extend(samples.iter().cloned());
let props = W::properties();
for a in &values {
for b in &values {
let c = a.times(b);
if !c.is_member() {
continue;
}
if props & LEFT_SEMIRING != 0 {
let recovered = c.divide(a, DivideType::Left);
if recovered.is_member() {
assert!(
a.times(&recovered).approx_equal(&c, DELTA),
"left divide failed for a={a}, b={b}: recovered {recovered}"
);
}
}
if props & RIGHT_SEMIRING != 0 {
let recovered = c.divide(b, DivideType::Right);
if recovered.is_member() {
assert!(
recovered.times(b).approx_equal(&c, DELTA),
"right divide failed for a={a}, b={b}: recovered {recovered}"
);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::weights::float_weight::{LogWeight, TropicalWeight};
#[derive(Debug, Clone, PartialEq)]
struct Liar(f32);
impl std::fmt::Display for Liar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for Liar {
type Err = std::num::ParseFloatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(Liar)
}
}
impl Weight for Liar {
type ReverseWeight = Liar;
fn zero() -> Self {
Liar(0.0)
}
fn one() -> Self {
Liar(1.0)
}
fn no_weight() -> Self {
Liar(f32::NAN)
}
fn type_name() -> crate::fst_type::WeightType {
crate::fst_type::WeightType::new("liar")
}
fn properties() -> u64 {
SEMIRING | COMMUTATIVE | IDEMPOTENT | PATH
}
fn plus(&self, rhs: &Self) -> Self {
Liar(self.0 + rhs.0)
}
fn times(&self, rhs: &Self) -> Self {
Liar(self.0 * rhs.0)
}
fn reverse(&self) -> Self {
self.clone()
}
fn is_member(&self) -> bool {
!self.0.is_nan()
}
fn approx_equal(&self, other: &Self, delta: f32) -> bool {
(self.0 - other.0).abs() <= delta
}
fn quantize(&self, _delta: f32) -> Self {
self.clone()
}
}
#[test]
#[should_panic(expected = "plus is idempotent")]
fn the_axiom_harness_rejects_a_weight_that_lies() {
axioms::check(&[Liar(1.0), Liar(2.0)]);
}
#[test]
fn tropical_satisfies_the_axioms_it_claims() {
assert_eq!(
TropicalWeight::properties(),
SEMIRING | COMMUTATIVE | IDEMPOTENT | PATH
);
axioms::check(&[
TropicalWeight(0.5),
TropicalWeight(2.0),
TropicalWeight(-1.5),
]);
axioms::check_divide(&[TropicalWeight(0.5), TropicalWeight(2.0)]);
}
#[test]
fn log_satisfies_the_axioms_it_claims() {
assert_eq!(LogWeight::properties(), SEMIRING | COMMUTATIVE);
axioms::check(&[LogWeight(0.5), LogWeight(2.0), LogWeight(-1.5)]);
axioms::check_divide(&[LogWeight(0.5), LogWeight(2.0)]);
}
#[test]
fn power_is_the_iterated_product() {
let weight = TropicalWeight(1.5);
assert_eq!(power(&weight, 0), TropicalWeight::one());
assert_eq!(power(&weight, 1), weight);
assert_eq!(power(&weight, 3), TropicalWeight(4.5));
for n in 1..8 {
assert_eq!(power(&weight, n), power(&weight, n - 1).times(&weight));
}
}
#[test]
fn the_natural_order_is_strict_and_total_on_a_path_semiring() {
let values = [
TropicalWeight::zero(),
TropicalWeight(3.0),
TropicalWeight(1.0),
TropicalWeight::one(),
];
for a in &values {
assert!(!natural_less(a, a), "the order must be strict");
for b in &values {
assert_eq!(natural_less(a, b), a.value() < b.value(), "{a} vs {b}");
if a != b {
assert!(natural_less(a, b) || natural_less(b, a));
}
}
}
}
#[test]
fn the_adder_accumulates_with_plus() {
let mut adder = Adder::new();
assert_eq!(adder.sum(), TropicalWeight::zero());
for value in [3.0, 1.0, 2.0] {
adder.add(&TropicalWeight(value));
}
assert_eq!(adder.sum(), TropicalWeight(1.0), "tropical plus is min");
adder.reset(TropicalWeight(0.5));
assert_eq!(adder.sum(), TropicalWeight(0.5));
}
#[test]
fn the_property_bits_match_openfst() {
assert_eq!(LEFT_SEMIRING, 0x01);
assert_eq!(RIGHT_SEMIRING, 0x02);
assert_eq!(SEMIRING, 0x03);
assert_eq!(COMMUTATIVE, 0x04);
assert_eq!(IDEMPOTENT, 0x08);
assert_eq!(PATH, 0x10);
}
}