#[derive(Clone, Debug, PartialEq)]
pub enum FactorError {
Empty,
RepeatedVariable { var: usize, times: usize },
OutOfRange { var: usize, n: usize },
NonFinite,
}
impl core::fmt::Display for FactorError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FactorError::Empty => write!(f, "a factor needs at least one variable"),
FactorError::RepeatedVariable { var, times } => write!(
f,
"variable {var} appears {times} times in one factor; s*s = 1, so this is not the \
factor you wrote -- an even count collapses it to a constant and an odd count \
lowers its order"
),
FactorError::OutOfRange { var, n } => {
write!(f, "variable {var} is out of range for a model of {n} variables")
}
FactorError::NonFinite => write!(f, "a factor's weight must be finite"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Factor {
vars: Vec<u32>,
weight: f64,
}
impl Factor {
pub fn new(vars: &[usize], weight: f64, n: usize) -> Result<Factor, FactorError> {
if vars.is_empty() {
return Err(FactorError::Empty);
}
if !weight.is_finite() {
return Err(FactorError::NonFinite);
}
for &v in vars {
if v >= n {
return Err(FactorError::OutOfRange { var: v, n });
}
}
let mut sorted: Vec<usize> = vars.to_vec();
sorted.sort_unstable();
let mut i = 0;
while i < sorted.len() {
let mut j = i + 1;
while j < sorted.len() && sorted[j] == sorted[i] {
j += 1;
}
if j - i > 1 {
return Err(FactorError::RepeatedVariable { var: sorted[i], times: j - i });
}
i = j;
}
Ok(Factor { vars: vars.iter().map(|&v| v as u32).collect(), weight })
}
pub fn vars(&self) -> impl Iterator<Item = usize> + '_ {
self.vars.iter().map(|&v| v as usize)
}
pub fn arity(&self) -> usize {
self.vars.len()
}
pub fn weight(&self) -> f64 {
self.weight
}
pub fn energy(&self, s: &[i8]) -> f64 {
let mut p = 1.0;
for &v in &self.vars {
p *= s[v as usize] as f64;
}
-self.weight * p
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_repeated_variable_cannot_be_built() {
let e = Factor::new(&[0, 1, 0], 1.0, 4).unwrap_err();
assert_eq!(e, FactorError::RepeatedVariable { var: 0, times: 2 });
assert!(e.to_string().contains("variable 0 appears 2 times"));
assert!(matches!(
Factor::new(&[2, 2, 2], 1.0, 4),
Err(FactorError::RepeatedVariable { var: 2, times: 3 })
));
assert!(Factor::new(&[1, 1], 1.0, 4).is_err());
}
#[test]
fn repetition_really_would_have_changed_the_factor() {
let honest = Factor::new(&[1], 1.0, 4).unwrap();
for mask in 0..16usize {
let s: Vec<i8> = (0..4).map(|i| if mask >> i & 1 == 1 { 1 } else { -1 }).collect();
let would_have_been = -1.0 * (s[0] as f64 * s[1] as f64 * s[0] as f64);
assert_eq!(would_have_been, honest.energy(&s));
}
}
#[test]
fn other_malformed_factors_are_rejected() {
assert_eq!(Factor::new(&[], 1.0, 4), Err(FactorError::Empty));
assert_eq!(Factor::new(&[9], 1.0, 4), Err(FactorError::OutOfRange { var: 9, n: 4 }));
assert_eq!(Factor::new(&[0], f64::NAN, 4), Err(FactorError::NonFinite));
assert_eq!(Factor::new(&[0], f64::INFINITY, 4), Err(FactorError::NonFinite));
}
#[test]
fn arity_two_matches_the_ising_convention() {
let f = Factor::new(&[0, 1], 0.75, 2).unwrap();
for (a, b) in [(1i8, 1i8), (1, -1), (-1, 1), (-1, -1)] {
let s = [a, b];
assert_eq!(f.energy(&s), -0.75 * a as f64 * b as f64);
}
}
#[test]
fn a_positive_weight_prefers_the_product_to_be_plus_one() {
let f = Factor::new(&[0, 1, 2], 1.0, 3).unwrap();
assert_eq!(f.energy(&[1, 1, 1]), -1.0, "aligned should be LOW energy");
assert_eq!(f.energy(&[1, 1, -1]), 1.0, "product -1 should be HIGH energy");
let anti = Factor::new(&[0, 1, 2], -1.0, 3).unwrap();
assert_eq!(anti.energy(&[1, 1, 1]), 1.0);
assert_eq!(anti.energy(&[1, 1, -1]), -1.0);
}
#[test]
fn higher_order_factors_are_expressible() {
let f = Factor::new(&[0, 1, 2, 3, 4], 2.0, 8).unwrap();
assert_eq!(f.arity(), 5);
assert_eq!(f.vars().collect::<Vec<_>>(), vec![0, 1, 2, 3, 4]);
assert_eq!(f.energy(&[1, 1, 1, 1, 1, -1, -1, -1]), -2.0);
assert_eq!(f.energy(&[-1, 1, 1, 1, 1, -1, -1, -1]), 2.0);
}
}