use std::collections::BTreeMap;
use crate::{DocId, DocSet};
pub trait Semiring: Clone {
fn zero() -> Self;
fn one() -> Self;
fn plus(&self, other: &Self) -> Self;
fn times(&self, other: &Self) -> Self;
fn is_zero(&self) -> bool;
}
impl Semiring for bool {
fn zero() -> Self {
false
}
fn one() -> Self {
true
}
fn plus(&self, other: &Self) -> Self {
*self || *other
}
fn times(&self, other: &Self) -> Self {
*self && *other
}
fn is_zero(&self) -> bool {
!*self
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LogSemiring(f64);
impl LogSemiring {
pub fn from_log(value: f64) -> Option<Self> {
(!value.is_nan()).then_some(Self(value))
}
pub fn from_weight(weight: f64) -> Option<Self> {
if weight.is_nan() || weight < 0.0 {
return None;
}
if weight == 0.0 {
return Some(Self::zero());
}
Some(Self(weight.ln()))
}
pub fn log_value(self) -> f64 {
self.0
}
pub fn weight(self) -> f64 {
self.0.exp()
}
}
impl Semiring for LogSemiring {
fn zero() -> Self {
Self(f64::NEG_INFINITY)
}
fn one() -> Self {
Self(0.0)
}
fn plus(&self, other: &Self) -> Self {
if self.is_zero() {
return *other;
}
if other.is_zero() {
return *self;
}
if self.0 == f64::INFINITY || other.0 == f64::INFINITY {
return Self(f64::INFINITY);
}
let maximum = self.0.max(other.0);
Self(maximum + ((self.0 - maximum).exp() + (other.0 - maximum).exp()).ln())
}
fn times(&self, other: &Self) -> Self {
if self.is_zero() || other.is_zero() {
Self::zero()
} else {
Self(self.0 + other.0)
}
}
fn is_zero(&self) -> bool {
self.0 == f64::NEG_INFINITY
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RelationEntry<K> {
pub doc_id: DocId,
pub value: K,
}
impl<K> RelationEntry<K> {
pub fn new(doc_id: DocId, value: K) -> Self {
Self { doc_id, value }
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Relation<K> {
entries: Vec<RelationEntry<K>>,
}
impl<K> Relation<K> {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn entries(&self) -> &[RelationEntry<K>] {
&self.entries
}
pub fn get(&self, doc_id: DocId) -> Option<&K> {
self.entries
.binary_search_by_key(&doc_id, |entry| entry.doc_id)
.ok()
.map(|index| &self.entries[index].value)
}
pub fn support(&self) -> DocSet {
DocSet::from_sorted_unchecked(self.entries.iter().map(|entry| entry.doc_id).collect())
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> std::slice::Iter<'_, RelationEntry<K>> {
self.entries.iter()
}
}
impl<K: Semiring> Relation<K> {
pub fn from_support(support: &DocSet) -> Self {
Self::from_terms(
support
.iter()
.map(|doc_id| RelationEntry::new(doc_id, K::one())),
)
}
pub fn from_terms<I>(terms: I) -> Self
where
I: IntoIterator<Item = RelationEntry<K>>,
{
let mut values = BTreeMap::<DocId, K>::new();
for term in terms {
if term.value.is_zero() {
continue;
}
values
.entry(term.doc_id)
.and_modify(|value| *value = value.plus(&term.value))
.or_insert(term.value);
}
let entries = values
.into_iter()
.filter_map(|(doc_id, value)| {
(!value.is_zero()).then_some(RelationEntry { doc_id, value })
})
.collect();
Self { entries }
}
pub fn singleton(doc_id: DocId, value: K) -> Self {
Self::from_terms([RelationEntry::new(doc_id, value)])
}
pub fn plus(&self, other: &Self) -> Self {
let mut entries = Vec::with_capacity(self.len() + other.len());
let (mut left, mut right) = (0, 0);
while left < self.len() && right < other.len() {
match self.entries[left].doc_id.cmp(&other.entries[right].doc_id) {
std::cmp::Ordering::Less => {
entries.push(self.entries[left].clone());
left += 1;
}
std::cmp::Ordering::Equal => {
let value = self.entries[left].value.plus(&other.entries[right].value);
if !value.is_zero() {
entries.push(RelationEntry::new(self.entries[left].doc_id, value));
}
left += 1;
right += 1;
}
std::cmp::Ordering::Greater => {
entries.push(other.entries[right].clone());
right += 1;
}
}
}
entries.extend_from_slice(&self.entries[left..]);
entries.extend_from_slice(&other.entries[right..]);
Self { entries }
}
pub fn times(&self, other: &Self) -> Self {
let mut entries = Vec::with_capacity(self.len().min(other.len()));
let (mut left, mut right) = (0, 0);
while left < self.len() && right < other.len() {
match self.entries[left].doc_id.cmp(&other.entries[right].doc_id) {
std::cmp::Ordering::Less => left += 1,
std::cmp::Ordering::Equal => {
let value = self.entries[left].value.times(&other.entries[right].value);
if !value.is_zero() {
entries.push(RelationEntry::new(self.entries[left].doc_id, value));
}
left += 1;
right += 1;
}
std::cmp::Ordering::Greater => right += 1,
}
}
Self { entries }
}
}
impl From<&DocSet> for Relation<bool> {
fn from(support: &DocSet) -> Self {
Self::from_support(support)
}
}
impl From<DocSet> for Relation<bool> {
fn from(support: DocSet) -> Self {
Self::from_support(&support)
}
}
impl<K> IntoIterator for Relation<K> {
type Item = RelationEntry<K>;
type IntoIter = std::vec::IntoIter<RelationEntry<K>>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl<'a, K> IntoIterator for &'a Relation<K> {
type Item = &'a RelationEntry<K>;
type IntoIter = std::slice::Iter<'a, RelationEntry<K>>;
fn into_iter(self) -> Self::IntoIter {
self.entries.iter()
}
}
#[cfg(test)]
mod tests {
use super::{LogSemiring, Relation, RelationEntry, Semiring};
use crate::DocSet;
#[test]
fn boolean_relation_lifts_set_union_and_intersection() {
let left = Relation::<bool>::from_support(&DocSet::from(vec![1, 3]));
let right = Relation::<bool>::from_support(&DocSet::from(vec![2, 3]));
assert_eq!(left.plus(&right).support(), DocSet::from(vec![1, 2, 3]));
assert_eq!(left.times(&right).support(), DocSet::from(vec![3]));
}
#[test]
fn duplicate_terms_are_combined_and_zero_is_not_stored() {
let relation = Relation::from_terms([
RelationEntry::new(1, false),
RelationEntry::new(2, true),
RelationEntry::new(2, true),
]);
assert_eq!(relation.support(), DocSet::from(vec![2]));
assert_eq!(relation.get(2), Some(&true));
}
#[test]
fn log_semiring_uses_log_sum_exp_and_log_space_multiplication() {
let point_two = LogSemiring::from_weight(0.2).unwrap();
let point_three = LogSemiring::from_weight(0.3).unwrap();
let sum = point_two.plus(&point_three);
let product = point_two.times(&point_three);
assert!((sum.weight() - 0.5).abs() < 1e-12);
assert!((product.weight() - 0.06).abs() < 1e-12);
assert!(LogSemiring::from_log(f64::NAN).is_none());
}
}