geolog-zeta-fork/tests/proptest_tensor.rs

477 lines
15 KiB
Rust
Raw Normal View History

2026-02-26 11:50:51 +01:00
//! Property tests for tensor operations
//!
//! Tests algebraic properties of tensor operations using proptest.
mod generators;
use generators::{TensorParams, arb_sparse_tensor, arb_tensor_pair_same_dims, arb_sparse_tensor_with_dims};
use geolog::tensor::{SparseTensor, TensorExpr, conjunction, exists, conjunction_all, disjunction_all};
use proptest::prelude::*;
// ============================================================================
// SparseTensor Basic Properties
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(1024))]
/// Empty tensor has no tuples
#[test]
fn empty_tensor_is_empty(dims in proptest::collection::vec(1usize..10, 0..4)) {
let tensor = SparseTensor::empty(dims.clone());
prop_assert!(tensor.is_empty());
prop_assert_eq!(tensor.len(), 0);
prop_assert_eq!(tensor.dims, dims);
}
/// Scalar true contains the empty tuple
#[test]
fn scalar_true_contains_empty(_seed in any::<u64>()) {
let tensor = SparseTensor::scalar(true);
prop_assert!(tensor.contains(&[]));
prop_assert_eq!(tensor.len(), 1);
prop_assert!(tensor.dims.is_empty());
}
/// Scalar false is empty
#[test]
fn scalar_false_is_empty(_seed in any::<u64>()) {
let tensor = SparseTensor::scalar(false);
prop_assert!(!tensor.contains(&[]));
prop_assert!(tensor.is_empty());
}
/// Insert/remove roundtrip
#[test]
fn insert_remove_roundtrip(
dims in proptest::collection::vec(1usize..5, 1..3),
tuple_idx in any::<prop::sample::Index>(),
) {
let mut tensor = SparseTensor::empty(dims.clone());
// Generate a valid tuple
let tuple: Vec<usize> = dims.iter()
.map(|&d| tuple_idx.index(d.max(1)))
.collect();
prop_assert!(!tensor.contains(&tuple));
tensor.insert(tuple.clone());
prop_assert!(tensor.contains(&tuple));
tensor.remove(&tuple);
prop_assert!(!tensor.contains(&tuple));
}
/// Generated tensor has valid tuples (within dimension bounds)
#[test]
fn generated_tensor_valid_tuples(
tensor in arb_sparse_tensor(TensorParams::default())
) {
for tuple in tensor.iter() {
prop_assert_eq!(tuple.len(), tensor.dims.len());
for (i, &val) in tuple.iter().enumerate() {
prop_assert!(val < tensor.dims[i], "tuple value {} >= dim {}", val, tensor.dims[i]);
}
}
}
}
// ============================================================================
// TensorExpr Product Properties
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
/// Product of empty tensors is empty
#[test]
fn product_with_empty_is_empty(
tensor in arb_sparse_tensor(TensorParams { max_dims: 2, max_dim_size: 5, max_tuples: 10 })
) {
let empty = SparseTensor::empty(vec![3]);
let expr = TensorExpr::Product(vec![
TensorExpr::leaf(tensor),
TensorExpr::leaf(empty),
]);
let result = expr.materialize();
prop_assert!(result.is_empty());
}
/// Product with scalar true is identity (dims extended but tuples preserved)
#[test]
fn product_with_scalar_true(
tensor in arb_sparse_tensor(TensorParams { max_dims: 2, max_dim_size: 5, max_tuples: 10 })
) {
let scalar_true = SparseTensor::scalar(true);
let orig_len = tensor.len();
let orig_dims = tensor.dims.clone();
let expr = TensorExpr::Product(vec![
TensorExpr::leaf(tensor),
TensorExpr::leaf(scalar_true),
]);
let result = expr.materialize();
prop_assert_eq!(result.len(), orig_len);
prop_assert_eq!(result.dims, orig_dims);
}
/// Empty product is scalar true
#[test]
fn empty_product_is_scalar_true(_seed in any::<u64>()) {
let expr = TensorExpr::Product(vec![]);
let result = expr.materialize();
prop_assert!(result.contains(&[]));
prop_assert_eq!(result.len(), 1);
}
/// Product dimensions are concatenation
#[test]
fn product_dims_concatenate(
t1 in arb_sparse_tensor(TensorParams { max_dims: 2, max_dim_size: 4, max_tuples: 5 }),
t2 in arb_sparse_tensor(TensorParams { max_dims: 2, max_dim_size: 4, max_tuples: 5 }),
) {
let expected_dims: Vec<usize> = t1.dims.iter().chain(t2.dims.iter()).copied().collect();
let expr = TensorExpr::Product(vec![
TensorExpr::leaf(t1),
TensorExpr::leaf(t2),
]);
let result = expr.materialize();
prop_assert_eq!(result.dims, expected_dims);
}
}
// ============================================================================
// Sum (Disjunction) Properties
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
/// Sum is commutative
#[test]
fn sum_commutative(
(t1, t2) in arb_tensor_pair_same_dims(TensorParams { max_dims: 2, max_dim_size: 5, max_tuples: 10 })
) {
let sum1 = TensorExpr::Sum(vec![
TensorExpr::leaf(t1.clone()),
TensorExpr::leaf(t2.clone()),
]).materialize();
let sum2 = TensorExpr::Sum(vec![
TensorExpr::leaf(t2),
TensorExpr::leaf(t1),
]).materialize();
prop_assert_eq!(sum1, sum2);
}
/// Sum is idempotent (T T = T)
#[test]
fn sum_idempotent(
tensor in arb_sparse_tensor(TensorParams { max_dims: 2, max_dim_size: 5, max_tuples: 10 })
) {
let sum = TensorExpr::Sum(vec![
TensorExpr::leaf(tensor.clone()),
TensorExpr::leaf(tensor.clone()),
]).materialize();
prop_assert_eq!(sum, tensor);
}
/// Sum with empty is identity
#[test]
fn sum_with_empty_is_identity(
tensor in arb_sparse_tensor(TensorParams { max_dims: 2, max_dim_size: 5, max_tuples: 10 })
) {
let empty = SparseTensor::empty(tensor.dims.clone());
let sum = TensorExpr::Sum(vec![
TensorExpr::leaf(tensor.clone()),
TensorExpr::leaf(empty),
]).materialize();
prop_assert_eq!(sum, tensor);
}
/// Empty sum is scalar false
#[test]
fn empty_sum_is_scalar_false(_seed in any::<u64>()) {
let sum = TensorExpr::Sum(vec![]).materialize();
prop_assert!(sum.is_empty());
}
/// Sum extent is union of extents
#[test]
fn sum_is_union(
(t1, t2) in arb_tensor_pair_same_dims(TensorParams { max_dims: 2, max_dim_size: 5, max_tuples: 10 })
) {
let sum = TensorExpr::Sum(vec![
TensorExpr::leaf(t1.clone()),
TensorExpr::leaf(t2.clone()),
]).materialize();
// Every tuple in t1 should be in sum
for tuple in t1.iter() {
prop_assert!(sum.contains(tuple));
}
// Every tuple in t2 should be in sum
for tuple in t2.iter() {
prop_assert!(sum.contains(tuple));
}
// Every tuple in sum should be in t1 or t2
for tuple in sum.iter() {
prop_assert!(t1.contains(tuple) || t2.contains(tuple));
}
}
}
// ============================================================================
// Conjunction Properties
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
/// Conjunction with scalar true is identity (modulo variable naming)
#[test]
fn conjunction_with_true(
tensor in arb_sparse_tensor_with_dims(vec![3, 3], 5)
) {
let vars = vec!["x".to_string(), "y".to_string()];
let scalar_true = SparseTensor::scalar(true);
let (expr, result_vars) = conjunction(
TensorExpr::leaf(tensor.clone()),
&vars,
TensorExpr::leaf(scalar_true),
&[],
);
let result = expr.materialize();
prop_assert_eq!(result_vars, vars);
prop_assert_eq!(result, tensor);
}
/// Conjunction with scalar false is empty
#[test]
fn conjunction_with_false(
tensor in arb_sparse_tensor_with_dims(vec![3, 3], 5)
) {
let vars = vec!["x".to_string(), "y".to_string()];
let scalar_false = SparseTensor::scalar(false);
let (expr, _result_vars) = conjunction(
TensorExpr::leaf(tensor),
&vars,
TensorExpr::leaf(scalar_false),
&[],
);
let result = expr.materialize();
prop_assert!(result.is_empty());
}
/// Conjunction is commutative (on shared variables)
#[test]
fn conjunction_commutative(
t1 in arb_sparse_tensor_with_dims(vec![3, 4], 5),
t2 in arb_sparse_tensor_with_dims(vec![4, 5], 5),
) {
let vars1 = vec!["x".to_string(), "y".to_string()];
let vars2 = vec!["y".to_string(), "z".to_string()];
let (expr1, _vars_result1) = conjunction(
TensorExpr::leaf(t1.clone()),
&vars1,
TensorExpr::leaf(t2.clone()),
&vars2,
);
let (expr2, _vars_result2) = conjunction(
TensorExpr::leaf(t2),
&vars2,
TensorExpr::leaf(t1),
&vars1,
);
let result1 = expr1.materialize();
let result2 = expr2.materialize();
// Same number of tuples (though variable order may differ)
prop_assert_eq!(result1.len(), result2.len());
}
}
// ============================================================================
// Exists (Contraction) Properties
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
/// Exists on non-existent variable is identity
#[test]
fn exists_nonexistent_var(
tensor in arb_sparse_tensor_with_dims(vec![3, 3], 5)
) {
let vars = vec!["x".to_string(), "y".to_string()];
let (expr, result_vars) = exists(TensorExpr::leaf(tensor.clone()), &vars, "z");
let result = expr.materialize();
prop_assert_eq!(result_vars, vars);
prop_assert_eq!(result, tensor);
}
/// Exists reduces arity by 1
#[test]
fn exists_reduces_arity(
tensor in arb_sparse_tensor_with_dims(vec![3, 4], 8)
) {
let vars = vec!["x".to_string(), "y".to_string()];
let (expr, result_vars) = exists(TensorExpr::leaf(tensor), &vars, "y");
let result = expr.materialize();
prop_assert_eq!(result_vars, vec!["x"]);
prop_assert_eq!(result.arity(), 1);
prop_assert_eq!(result.dims, vec![3]);
}
/// Exists on scalar is identity
#[test]
fn exists_on_scalar(value in any::<bool>()) {
let tensor = SparseTensor::scalar(value);
let (expr, result_vars) = exists(TensorExpr::leaf(tensor.clone()), &[], "x");
let result = expr.materialize();
prop_assert!(result_vars.is_empty());
prop_assert_eq!(result, tensor);
}
/// Double exists is same as single exists (idempotent on same var)
#[test]
fn exists_idempotent(
tensor in arb_sparse_tensor_with_dims(vec![3, 4], 8)
) {
let vars = vec!["x".to_string(), "y".to_string()];
let (expr1, vars1) = exists(TensorExpr::leaf(tensor.clone()), &vars, "y");
let (expr2, vars2) = exists(expr1, &vars1, "y");
let result = expr2.materialize();
prop_assert_eq!(vars2, vec!["x"]);
prop_assert_eq!(result.arity(), 1);
}
}
// ============================================================================
// Fusion Tests (Contract(Product(...)))
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
/// Fused join produces same result as naive evaluation
#[test]
fn fused_join_correctness(
t1 in arb_sparse_tensor_with_dims(vec![5, 5], 10),
t2 in arb_sparse_tensor_with_dims(vec![5, 5], 10),
) {
let vars1 = vec!["x".to_string(), "y".to_string()];
let vars2 = vec!["y".to_string(), "z".to_string()];
// This creates Contract(Product(...)) which gets fused
let (conj_expr, conj_vars) = conjunction(
TensorExpr::leaf(t1.clone()),
&vars1,
TensorExpr::leaf(t2.clone()),
&vars2,
);
let (result_expr, _result_vars) = exists(conj_expr, &conj_vars, "y");
let result = result_expr.materialize();
// Verify result is correct by checking each tuple
for tuple in result.iter() {
let x = tuple[0];
let z = tuple[1];
// Should exist some y such that t1(x,y) and t2(y,z)
let mut found = false;
for y in 0..5 {
if t1.contains(&[x, y]) && t2.contains(&[y, z]) {
found = true;
break;
}
}
prop_assert!(found, "tuple {:?} in result but no witness y", tuple);
}
// And every valid (x,z) should be in result
for x in 0..5 {
for z in 0..5 {
let mut should_be_in_result = false;
for y in 0..5 {
if t1.contains(&[x, y]) && t2.contains(&[y, z]) {
should_be_in_result = true;
break;
}
}
prop_assert_eq!(
result.contains(&[x, z]),
should_be_in_result,
"({}, {}) expected {} but got {}",
x, z, should_be_in_result, result.contains(&[x, z])
);
}
}
}
}
// ============================================================================
// Disjunction Helper Tests
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
/// disjunction_all with empty is scalar false
#[test]
fn disjunction_all_empty(_seed in any::<u64>()) {
let (expr, vars) = disjunction_all(vec![]);
let result = expr.materialize();
prop_assert!(vars.is_empty());
prop_assert!(result.is_empty());
}
/// disjunction_all with single element is identity
#[test]
fn disjunction_all_single(
tensor in arb_sparse_tensor_with_dims(vec![3, 3], 5)
) {
let vars = vec!["x".to_string(), "y".to_string()];
let (expr, result_vars) = disjunction_all(vec![
(TensorExpr::leaf(tensor.clone()), vars.clone())
]);
let result = expr.materialize();
prop_assert_eq!(result_vars, vars);
prop_assert_eq!(result, tensor);
}
/// conjunction_all with empty is scalar true
#[test]
fn conjunction_all_empty(_seed in any::<u64>()) {
let (expr, vars) = conjunction_all(vec![]);
let result = expr.materialize();
prop_assert!(vars.is_empty());
prop_assert!(result.contains(&[]));
prop_assert_eq!(result.len(), 1);
}
}