//! 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::()) { 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::()) { 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::(), ) { let mut tensor = SparseTensor::empty(dims.clone()); // Generate a valid tuple let tuple: Vec = 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::()) { 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 = 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::()) { 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::()) { 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::()) { 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::()) { 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); } }