Property Testing Tutorial

This tutorial will teach you how to use Matrix Toolkit’s property testing capabilities to verify mathematical properties of matrices.

Introduction

Matrix properties are mathematical characteristics that describe the structure and behavior of matrices. Matrix Toolkit provides automated tools to:

  1. Check if a matrix has specific properties

  2. Verify that generated matrices satisfy their expected properties

  3. Search for matrices based on properties

  4. Run comprehensive test suites

Why Property Testing?

Property testing is crucial for:

  • Numerical Algorithm Development: Ensure test matrices have required properties

  • Reproducibility: Verify matrix generators produce correct outputs

  • Debugging: Identify when computations violate expected properties

  • Algorithm Selection: Choose appropriate algorithms based on matrix properties

Tutorial Setup

import numpy as np
from matrix_toolkit.anymatrix import AnyMatrix, MatrixProperties

# Initialize
am = AnyMatrix()

Part 1: Basic Property Checking

Step 1: Check Single Property

# Generate a symmetric matrix
A = am.generate('core/beta', 10)

# Check if symmetric
is_symmetric = MatrixProperties.is_symmetric(A)
print(f"Is symmetric: {is_symmetric}")

# Check if positive definite
is_pd = MatrixProperties.is_positive_definite(A)
print(f"Is positive definite: {is_pd}")

Expected Output:

Is symmetric: True
Is positive definite: True

Step 2: Understanding Tolerance

Property checks use floating-point comparisons with tolerance:

# Create a nearly symmetric matrix
A_exact = np.array([[1, 2], [2, 3]])
A_noisy = A_exact + np.random.randn(2, 2) * 1e-12

# Default tolerance (1e-10)
print(f"Symmetric (default): {MatrixProperties.is_symmetric(A_noisy)}")

# Stricter tolerance
print(f"Symmetric (strict): {MatrixProperties.is_symmetric(A_noisy, tol=1e-14)}")

# Looser tolerance
print(f"Symmetric (loose): {MatrixProperties.is_symmetric(A_noisy, tol=1e-10)}")

Key Takeaway: Adjust tolerance based on your numerical precision requirements.

Part 2: Automated Property Verification

Step 3: Verify Expected Properties

# Generate a matrix
matrix_id = 'core/fourier'
F = am.generate(matrix_id, 8)

# Get expected properties
expected_props = am.properties(matrix_id)
print(f"Expected properties: {expected_props}")

# Verify all properties
results = MatrixProperties.check_properties(F, expected_props)

print("\nVerification Results:")
for prop, passed in results.items():
    status = "✓ PASS" if passed else "✗ FAIL"
    print(f"  {prop:20s}: {status}")

Expected Output:

Expected properties: ['unitary', 'symmetric']

Verification Results:
  unitary             : ✓ PASS
  symmetric           : ✓ PASS

Step 4: Handling Failed Checks

def verify_and_report(matrix_id, size):
    """Generate matrix and verify properties with detailed reporting"""

    # Generate
    matrix = am.generate(matrix_id, size)

    # Get expected properties
    expected = am.properties(matrix_id)

    # Verify
    results = MatrixProperties.check_properties(matrix, expected, tol=1e-10)

    # Report
    print(f"\nMatrix: {matrix_id} (size={size})")
    print("-" * 50)

    all_passed = True
    for prop, passed in results.items():
        if passed:
            print(f"  ✓ {prop}")
        else:
            print(f"  ✗ {prop} FAILED")
            all_passed = False

    if all_passed:
        print("\n✓ All properties verified successfully!")
    else:
        print("\n✗ Some properties failed verification!")
        print("  This may indicate:")
        print("  - Numerical precision issues")
        print("  - Implementation bugs")
        print("  - Incorrect property tags")

    return all_passed

# Test multiple matrices
test_cases = [
    ('core/beta', 10),
    ('matlab/hilbert', 5),
    ('core/fourier', 16)
]

for matrix_id, size in test_cases:
    verify_and_report(matrix_id, size)

Part 3: Comprehensive Testing

Step 5: Test Across Sizes

def test_across_sizes(matrix_id, sizes):
    """Test matrix properties across different sizes"""

    expected_props = am.properties(matrix_id)
    results = {}

    print(f"\nTesting {matrix_id} across sizes: {sizes}")
    print("=" * 60)

    for size in sizes:
        try:
            # Generate
            matrix = am.generate(matrix_id, size)

            # Verify
            prop_results = MatrixProperties.check_properties(
                matrix, expected_props
            )

            # Store
            all_passed = all(prop_results.values())
            results[size] = {
                'success': True,
                'all_passed': all_passed,
                'details': prop_results
            }

            # Report
            status = "✓" if all_passed else "✗"
            print(f"  Size {size:4d}: {status}")

        except Exception as e:
            results[size] = {
                'success': False,
                'error': str(e)
            }
            print(f"  Size {size:4d}: ERROR - {e}")

    return results

# Run tests
results = test_across_sizes('core/beta', [10, 20, 50, 100, 200])

Step 6: Batch Testing

def batch_test_matrices(matrix_ids, size=10):
    """Test multiple matrices at once"""

    print(f"\nBatch Testing {len(matrix_ids)} matrices (size={size})")
    print("=" * 70)

    summary = {
        'passed': [],
        'failed': [],
        'errors': []
    }

    for matrix_id in matrix_ids:
        try:
            # Generate and verify
            matrix = am.generate(matrix_id, size)
            expected = am.properties(matrix_id)
            results = MatrixProperties.check_properties(matrix, expected)

            # Categorize
            if all(results.values()):
                summary['passed'].append(matrix_id)
                print(f"  ✓ {matrix_id:30s} PASS")
            else:
                summary['failed'].append(matrix_id)
                failed_props = [p for p, v in results.items() if not v]
                print(f"  ✗ {matrix_id:30s} FAIL: {failed_props}")

        except Exception as e:
            summary['errors'].append((matrix_id, str(e)))
            print(f"  ! {matrix_id:30s} ERROR: {e}")

    # Summary
    print("\n" + "=" * 70)
    print(f"Summary:")
    print(f"  Passed: {len(summary['passed'])}")
    print(f"  Failed: {len(summary['failed'])}")
    print(f"  Errors: {len(summary['errors'])}")

    return summary

# Test all core matrices
core_matrices = am.list('core')
summary = batch_test_matrices(core_matrices[:20], size=10)

Part 4: Custom Property Tests

Step 7: Define Custom Properties

def is_diagonally_dominant(matrix, tol=1e-10):
    """Check if matrix is diagonally dominant"""
    n = matrix.shape[0]

    for i in range(n):
        row_sum = np.sum(np.abs(matrix[i, :])) - np.abs(matrix[i, i])
        if np.abs(matrix[i, i]) < row_sum - tol:
            return False

    return True

def is_m_matrix(matrix, tol=1e-10):
    """Check if matrix is an M-matrix"""
    # Non-positive off-diagonals
    n = matrix.shape[0]
    for i in range(n):
        for j in range(n):
            if i != j and matrix[i, j] > tol:
                return False

    # Positive eigenvalues
    eigenvalues = np.linalg.eigvals(matrix)
    if not np.all(eigenvalues.real > -tol):
        return False

    return True

# Test custom properties
A = am.generate('regtools/deriv2', 20).toarray()

print(f"Diagonally dominant: {is_diagonally_dominant(A)}")
print(f"M-matrix: {is_m_matrix(-A)}")  # Note: deriv2 gives -M-matrix

Step 8: Create Property Test Suite

class PropertyTestSuite:
    """Custom test suite for matrix properties"""

    def __init__(self):
        self.tests = {}
        self.results = {}

    def add_test(self, name, checker_func, description=""):
        """Add a property test"""
        self.tests[name] = {
            'checker': checker_func,
            'description': description
        }

    def run_tests(self, matrix, verbose=True):
        """Run all tests on a matrix"""
        if verbose:
            print(f"\nRunning {len(self.tests)} property tests")
            print("=" * 60)

        results = {}

        for name, test in self.tests.items():
            try:
                passed = test['checker'](matrix)
                results[name] = passed

                if verbose:
                    status = "✓" if passed else "✗"
                    print(f"  {status} {name:30s} : {test['description']}")

            except Exception as e:
                results[name] = None
                if verbose:
                    print(f"  ! {name:30s} : ERROR - {e}")

        self.results = results
        return results

    def summary(self):
        """Print summary of test results"""
        if not self.results:
            print("No tests have been run yet")
            return

        passed = sum(1 for v in self.results.values() if v is True)
        failed = sum(1 for v in self.results.values() if v is False)
        errors = sum(1 for v in self.results.values() if v is None)

        print(f"\nTest Summary:")
        print(f"  Total:  {len(self.results)}")
        print(f"  Passed: {passed}")
        print(f"  Failed: {failed}")
        print(f"  Errors: {errors}")

# Create suite
suite = PropertyTestSuite()

# Add standard tests
suite.add_test('symmetric', MatrixProperties.is_symmetric,
               'A = A^T')
suite.add_test('positive_definite', MatrixProperties.is_positive_definite,
               'x^T A x > 0')
suite.add_test('orthogonal', MatrixProperties.is_orthogonal,
               'Q^T Q = I')

# Add custom tests
suite.add_test('diagonally_dominant', is_diagonally_dominant,
               '|a_ii| >= sum|a_ij|')

# Run tests
A = am.generate('core/beta', 20)
results = suite.run_tests(A)
suite.summary()

Part 5: Debugging Failed Properties

Step 9: Diagnose Property Failures

def diagnose_symmetry(matrix, tol=1e-10):
    """Diagnose why a matrix might not be symmetric"""

    print("\nSymmetry Diagnosis")
    print("=" * 60)

    # Compute difference
    diff = matrix - matrix.T

    # Maximum difference
    max_diff = np.max(np.abs(diff))
    print(f"Maximum |A - A^T|: {max_diff:.2e}")

    # Where is the maximum?
    i, j = np.unravel_index(np.argmax(np.abs(diff)), diff.shape)
    print(f"Location: ({i}, {j})")
    print(f"A[{i},{j}] = {matrix[i,j]:.6f}")
    print(f"A[{j},{i}] = {matrix[j,i]:.6f}")
    print(f"Difference: {diff[i,j]:.2e}")

    # Is it close to symmetric?
    if max_diff < tol:
        print(f"\n✓ Matrix IS symmetric (within tolerance {tol:.2e})")
    elif max_diff < 100 * tol:
        print(f"\n~ Matrix is NEARLY symmetric")
        print(f"  Consider using tolerance >= {max_diff:.2e}")
    else:
        print(f"\n✗ Matrix is NOT symmetric")
        print(f"  Maximum difference {max_diff:.2e} >> tolerance {tol:.2e}")

# Test
A = am.generate('core/beta', 10)
A_perturbed = A + np.random.randn(10, 10) * 1e-11

diagnose_symmetry(A_perturbed, tol=1e-10)

Part 6: Real-World Application

Step 10: Complete Testing Workflow

import json
from datetime import datetime

class MatrixPropertyTester:
    """Complete property testing framework"""

    def __init__(self, am):
        self.am = am
        self.test_history = []

    def test_matrix(self, matrix_id, size, custom_tests=None):
        """Run complete test on a matrix"""

        # Record start
        test_record = {
            'matrix_id': matrix_id,
            'size': size,
            'timestamp': datetime.now().isoformat(),
            'results': {}
        }

        try:
            # Generate matrix
            matrix = self.am.generate(matrix_id, size)
            test_record['generated'] = True

            # Get expected properties
            expected = self.am.properties(matrix_id)

            # Verify expected properties
            results = MatrixProperties.check_properties(matrix, expected)
            test_record['results']['expected'] = results

            # Run custom tests if provided
            if custom_tests:
                custom_results = {}
                for test_name, test_func in custom_tests.items():
                    try:
                        custom_results[test_name] = test_func(matrix)
                    except Exception as e:
                        custom_results[test_name] = f"ERROR: {e}"
                test_record['results']['custom'] = custom_results

            # Summary
            all_expected_passed = all(results.values())
            test_record['summary'] = {
                'expected_passed': all_expected_passed,
                'total_tests': len(results)
            }

        except Exception as e:
            test_record['generated'] = False
            test_record['error'] = str(e)

        # Save to history
        self.test_history.append(test_record)

        return test_record

    def generate_report(self, filename=None):
        """Generate test report"""

        report = {
            'total_tests': len(self.test_history),
            'timestamp': datetime.now().isoformat(),
            'tests': self.test_history
        }

        if filename:
            with open(filename, 'w') as f:
                json.dump(report, f, indent=2)
            print(f"Report saved to {filename}")

        return report

    def print_summary(self):
        """Print summary of all tests"""

        print("\n" + "=" * 70)
        print("PROPERTY TESTING SUMMARY")
        print("=" * 70)

        passed = sum(1 for t in self.test_history
                    if t.get('generated') and
                    t.get('summary', {}).get('expected_passed'))

        print(f"Total tests run: {len(self.test_history)}")
        print(f"Passed: {passed}")
        print(f"Failed: {len(self.test_history) - passed}")

# Use the tester
tester = MatrixPropertyTester(am)

# Run tests
matrices_to_test = [
    ('core/beta', 10),
    ('core/fourier', 16),
    ('matlab/hilbert', 5),
    ('regtools/deriv2', 50),
]

for matrix_id, size in matrices_to_test:
    result = tester.test_matrix(matrix_id, size)
    status = "✓" if result.get('summary', {}).get('expected_passed') else "✗"
    print(f"{status} {matrix_id:30s} (size={size})")

# Generate report
tester.print_summary()
tester.generate_report('property_test_report.json')

Exercises

  1. Exercise 1: Write a property checker for tridiagonal matrices

  2. Exercise 2: Create a test that verifies orthogonality by checking \(Q^T Q = I\)

  3. Exercise 3: Implement a property checker for Toeplitz matrices

  4. Exercise 4: Write a comprehensive test suite for all nilpotent matrices

Next Steps

Summary

You’ve learned:

  • ✓ How to check individual matrix properties

  • ✓ How to verify expected properties automatically

  • ✓ How to run comprehensive test suites

  • ✓ How to create custom property tests

  • ✓ How to diagnose and debug failed properties

  • ✓ How to implement real-world testing workflows