geolog-zeta-fork/tests/proptest_tensor.rs
2026-02-26 11:50:51 +01:00

477 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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);
}
}