#[derive(Debug, Clone)]
pub struct SparseCut {
indices: Vec<usize>,
values: Vec<f64>,
state_dimension: usize,
}
impl SparseCut {
#[must_use]
pub fn from_dense(dense: &[f64]) -> Self {
let mut indices = Vec::new();
let mut values = Vec::new();
for (j, &c) in dense.iter().enumerate() {
if c != 0.0 {
indices.push(j);
values.push(c);
}
}
Self {
indices,
values,
state_dimension: dense.len(),
}
}
#[must_use]
pub fn empty(state_dimension: usize) -> Self {
Self {
indices: Vec::new(),
values: Vec::new(),
state_dimension,
}
}
#[must_use]
pub fn to_dense(&self) -> Vec<f64> {
let mut dense = vec![0.0; self.state_dimension];
for (&j, &v) in self.indices.iter().zip(self.values.iter()) {
dense[j] = v;
}
dense
}
#[must_use]
#[inline]
pub fn nnz(&self) -> usize {
self.indices.len()
}
#[must_use]
#[inline]
pub fn state_dimension(&self) -> usize {
self.state_dimension
}
#[must_use]
#[inline]
pub fn indices(&self) -> &[usize] {
&self.indices
}
#[must_use]
#[inline]
pub fn values(&self) -> &[f64] {
&self.values
}
#[must_use]
pub fn dot(&self, state: &[f64]) -> f64 {
debug_assert_eq!(
state.len(),
self.state_dimension,
"SparseCut::dot: state length {} != state_dimension {}",
state.len(),
self.state_dimension
);
let mut sum = 0.0_f64;
for (&j, &v) in self.indices.iter().zip(self.values.iter()) {
sum += v * state[j];
}
sum
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::cast_precision_loss)]
mod tests {
use super::SparseCut;
#[test]
fn from_dense_drops_exact_zeros() {
let sparse = SparseCut::from_dense(&[1.0, 0.0, 0.0, 3.0, 0.0]);
assert_eq!(sparse.nnz(), 2);
assert_eq!(sparse.indices(), &[0, 3]);
assert_eq!(sparse.values(), &[1.0, 3.0]);
assert_eq!(sparse.state_dimension(), 5);
}
#[test]
fn from_dense_all_zero() {
let sparse = SparseCut::from_dense(&[0.0, 0.0, 0.0]);
assert_eq!(sparse.nnz(), 0);
assert!(sparse.indices().is_empty());
assert!(sparse.values().is_empty());
assert_eq!(sparse.state_dimension(), 3);
}
#[test]
fn from_dense_all_nonzero() {
let sparse = SparseCut::from_dense(&[1.0, 2.0, 3.0]);
assert_eq!(sparse.nnz(), 3);
assert_eq!(sparse.indices(), &[0, 1, 2]);
assert_eq!(sparse.values(), &[1.0, 2.0, 3.0]);
}
#[test]
fn from_dense_empty() {
let sparse = SparseCut::from_dense(&[]);
assert_eq!(sparse.nnz(), 0);
assert_eq!(sparse.state_dimension(), 0);
}
#[test]
fn from_dense_single_nonzero() {
let sparse = SparseCut::from_dense(&[0.0, 5.0, 0.0]);
assert_eq!(sparse.nnz(), 1);
assert_eq!(sparse.indices(), &[1]);
assert_eq!(sparse.values(), &[5.0]);
}
#[test]
fn from_dense_preserves_near_zero() {
let sparse = SparseCut::from_dense(&[1e-300, 0.0, -1e-300]);
assert_eq!(sparse.nnz(), 2);
assert_eq!(sparse.indices(), &[0, 2]);
assert_eq!(sparse.values(), &[1e-300, -1e-300]);
}
#[test]
fn to_dense_round_trip() {
let original = vec![1.0, 0.0, 3.0, 0.0, 5.0];
let sparse = SparseCut::from_dense(&original);
let reconstructed = sparse.to_dense();
assert_eq!(reconstructed, original);
}
#[test]
fn to_dense_round_trip_all_zero() {
let original = vec![0.0, 0.0, 0.0];
let sparse = SparseCut::from_dense(&original);
let reconstructed = sparse.to_dense();
assert_eq!(reconstructed, original);
}
#[test]
fn to_dense_round_trip_all_nonzero() {
let original = vec![1.0, 2.0, 3.0, 4.0];
let sparse = SparseCut::from_dense(&original);
let reconstructed = sparse.to_dense();
assert_eq!(reconstructed, original);
}
#[test]
fn empty_creates_zero_nnz() {
let sparse = SparseCut::empty(5);
assert_eq!(sparse.nnz(), 0);
assert_eq!(sparse.state_dimension(), 5);
assert_eq!(sparse.to_dense(), vec![0.0; 5]);
}
#[test]
fn dot_matches_dense() {
let dense = vec![1.0, 0.0, 3.0, 0.0, 5.0];
let state = vec![2.0, 7.0, 4.0, 9.0, 6.0];
let sparse = SparseCut::from_dense(&dense);
let dense_dot: f64 = dense.iter().zip(state.iter()).map(|(a, b)| a * b).sum();
let sparse_dot = sparse.dot(&state);
assert_eq!(sparse_dot, dense_dot);
}
#[test]
fn dot_all_zero_is_zero() {
let sparse = SparseCut::from_dense(&[0.0, 0.0, 0.0]);
let state = vec![1.0, 2.0, 3.0];
assert_eq!(sparse.dot(&state), 0.0);
}
#[test]
fn dot_empty_is_zero() {
let sparse = SparseCut::from_dense(&[]);
assert_eq!(sparse.dot(&[]), 0.0);
}
#[test]
fn dot_single_element() {
let sparse = SparseCut::from_dense(&[0.0, 3.0, 0.0]);
let state = vec![10.0, 5.0, 20.0];
assert_eq!(sparse.dot(&state), 15.0); }
#[test]
fn dot_bit_identical_to_dense_with_multiple_patterns() {
let patterns: Vec<Vec<f64>> = vec![
vec![1.0, 0.0, 2.0, 0.0, 3.0],
vec![0.0, 0.0, 0.0, 0.0, 0.0],
vec![1.0, 1.0, 1.0, 1.0, 1.0],
vec![0.0, 0.0, 0.0, 0.0, 7.0],
vec![9.0, 0.0, 0.0, 0.0, 0.0],
vec![-1.0, 2.0, -3.0, 4.0, -5.0],
vec![1e-100, 0.0, 1e100, 0.0, -1e-100],
];
let state = vec![0.5, 1.5, 2.5, 3.5, 4.5];
for dense in &patterns {
let sparse = SparseCut::from_dense(dense);
let dense_dot: f64 = dense.iter().zip(state.iter()).map(|(a, b)| a * b).sum();
let sparse_dot = sparse.dot(&state);
assert_eq!(
sparse_dot, dense_dot,
"mismatch for pattern {dense:?}: sparse={sparse_dot}, dense={dense_dot}"
);
}
}
#[test]
fn clone_produces_independent_copy() {
let sparse = SparseCut::from_dense(&[1.0, 0.0, 3.0]);
let cloned = sparse.clone();
assert_eq!(cloned.nnz(), sparse.nnz());
assert_eq!(cloned.indices(), sparse.indices());
assert_eq!(cloned.values(), sparse.values());
assert_eq!(cloned.state_dimension(), sparse.state_dimension());
}
}