/** * Convergence Detection and Metrics Validation Test Suite * * Tests the convergence detection system against known test cases * with expected convergence behavior. */ const { ConvergenceDetector } = require('../../src/convergence/convergence-detector'); const { MetricsReporter } = require('../../src/convergence/metrics-reporter'); const { createSolver } = require('../../src/solver'); class ConvergenceValidator { constructor() { this.testCases = this.generateTestCases(); this.results = []; } /** * Generate test cases with known convergence properties */ generateTestCases() { return [ { name: 'Well-conditioned Diagonal Matrix', description: 'Identity matrix should converge in 1 iteration', matrix: this.createIdentityMatrix(10), rhs: Array(10).fill(1), expectedIterations: 1, expectedConvergence: true, expectedRate: 0.0, tolerance: 1e-10 }, { name: 'Simple Diagonal Matrix', description: 'Diagonal matrix with 2s on diagonal', matrix: this.createDiagonalMatrix(5, 2.0), rhs: [2, 4, 6, 8, 10], expectedIterations: 1, expectedConvergence: true, expectedRate: 0.0, tolerance: 1e-10 }, { name: 'Strongly Diagonal Dominant', description: 'Matrix with strong diagonal dominance', matrix: this.createStronglyDiagonalDominant(8), rhs: Array(8).fill(1), expectedIterations: { min: 1, max: 10 }, expectedConvergence: true, expectedRate: { min: 0.0, max: 0.3 }, tolerance: 1e-8 }, { name: 'Weakly Diagonal Dominant', description: 'Matrix with weak diagonal dominance', matrix: this.createWeaklyDiagonalDominant(6), rhs: Array(6).fill(1), expectedIterations: { min: 10, max: 100 }, expectedConvergence: true, expectedRate: { min: 0.3, max: 0.9 }, tolerance: 1e-6 }, { name: 'Symmetric Positive Definite', description: 'Well-conditioned SPD matrix', matrix: this.createSPDMatrix(5), rhs: [1, 2, 3, 4, 5], expectedIterations: { min: 1, max: 20 }, expectedConvergence: true, expectedRate: { min: 0.0, max: 0.5 }, tolerance: 1e-8 }, { name: 'Near-singular Matrix', description: 'Poorly conditioned matrix', matrix: this.createNearSingularMatrix(4), rhs: [1, 1, 1, 1], expectedIterations: { min: 50, max: 1000 }, expectedConvergence: false, // May not converge expectedRate: { min: 0.8, max: 1.0 }, tolerance: 1e-4, maxIterations: 200 } ]; } /** * Run all validation tests */ async runValidation() { console.log('๐Ÿงช Running Convergence Validation Tests'); console.log('=' .repeat(60)); for (const testCase of this.testCases) { console.log(`\n๐Ÿ“‹ Test: ${testCase.name}`); console.log(` ${testCase.description}`); try { const result = await this.runSingleTest(testCase); this.results.push(result); this.printTestResult(result); } catch (error) { console.log(` โŒ ERROR: ${error.message}`); this.results.push({ testCase: testCase.name, passed: false, error: error.message }); } } this.printSummary(); return this.results; } /** * Run a single test case */ async runSingleTest(testCase) { const solver = await createSolver({ matrix: testCase.matrix, method: 'jacobi', tolerance: testCase.tolerance, maxIterations: testCase.maxIterations || 1000, verbose: false }); const result = await solver.solve(testCase.rhs); // Validate convergence behavior const validation = this.validateResult(result, testCase); return { testCase: testCase.name, expected: testCase, actual: { iterations: result.iterations, converged: result.converged, convergenceRate: result.convergenceRate, residual: result.residual, reductionFactor: result.reductionFactor, grade: result.performanceGrade }, validation, passed: validation.overall }; } /** * Validate result against expected behavior */ validateResult(result, testCase) { const checks = { convergence: this.checkConvergence(result.converged, testCase.expectedConvergence), iterations: this.checkIterations(result.iterations, testCase.expectedIterations), convergenceRate: this.checkConvergenceRate(result.convergenceRate, testCase.expectedRate), residual: this.checkResidual(result.residual, testCase.tolerance), reductionFactor: this.checkReductionFactor(result.reductionFactor) }; const passedChecks = Object.values(checks).filter(c => c.passed).length; const totalChecks = Object.keys(checks).length; return { ...checks, overall: passedChecks >= totalChecks - 1, // Allow one check to fail score: `${passedChecks}/${totalChecks}` }; } checkConvergence(actual, expected) { const passed = actual === expected; return { passed, message: passed ? 'โœ“ Convergence as expected' : `โœ— Expected ${expected}, got ${actual}` }; } checkIterations(actual, expected) { if (typeof expected === 'number') { const passed = actual === expected; return { passed, message: passed ? 'โœ“ Iterations as expected' : `โœ— Expected ${expected}, got ${actual}` }; } else { const passed = actual >= expected.min && actual <= expected.max; return { passed, message: passed ? 'โœ“ Iterations in range' : `โœ— Expected ${expected.min}-${expected.max}, got ${actual}` }; } } checkConvergenceRate(actual, expected) { if (typeof expected === 'number') { const passed = Math.abs(actual - expected) < 0.1; return { passed, message: passed ? 'โœ“ Convergence rate as expected' : `โœ— Expected ~${expected}, got ${actual}` }; } else { const passed = actual >= expected.min && actual <= expected.max; return { passed, message: passed ? 'โœ“ Convergence rate in range' : `โœ— Expected ${expected.min}-${expected.max}, got ${actual.toFixed(3)}` }; } } checkResidual(actual, tolerance) { const passed = actual <= tolerance * 10; // Allow some tolerance slack return { passed, message: passed ? 'โœ“ Residual acceptable' : `โœ— Residual ${actual.toExponential(2)} too large` }; } checkReductionFactor(actual) { const passed = actual >= 0 && actual <= 1.0; return { passed, message: passed ? 'โœ“ Reduction factor valid' : `โœ— Invalid reduction factor ${actual}` }; } /** * Print individual test result */ printTestResult(result) { const status = result.passed ? 'โœ… PASS' : 'โŒ FAIL'; console.log(` ${status} (${result.validation.score})`); if (result.passed) { console.log(` Iterations: ${result.actual.iterations}, Convergence: ${result.actual.convergenceRate.toFixed(1)}%`); console.log(` Grade: ${result.actual.grade}, Reduction: ${result.actual.reductionFactor.toExponential(2)}`); } else { console.log(' Issues:'); Object.entries(result.validation).forEach(([key, check]) => { if (key !== 'overall' && key !== 'score' && !check.passed) { console.log(` ${check.message}`); } }); } } /** * Print validation summary */ printSummary() { console.log('\n' + '='.repeat(60)); console.log('\n๐Ÿ“Š VALIDATION SUMMARY'); const passed = this.results.filter(r => r.passed).length; const total = this.results.length; const percentage = (passed / total * 100).toFixed(1); console.log(`\nOverall: ${passed}/${total} tests passed (${percentage}%)`); if (passed === total) { console.log('๐ŸŽ‰ All convergence validation tests passed!'); console.log('โœ“ Convergence detection is working correctly'); console.log('โœ“ Metrics reporting is accurate'); console.log('โœ“ Early stopping is functioning'); } else { console.log('โš ๏ธ Some tests failed - convergence system needs attention'); const failed = this.results.filter(r => !r.passed); console.log('\nFailed tests:'); failed.forEach(f => { console.log(` - ${f.testCase}: ${f.error || 'Validation failed'}`); }); } console.log('\n' + '='.repeat(60)); } // Matrix generation utilities createIdentityMatrix(size) { const matrix = Array(size).fill(0).map(() => Array(size).fill(0)); for (let i = 0; i < size; i++) { matrix[i][i] = 1.0; } return { data: matrix, rows: size, cols: size, format: 'dense' }; } createDiagonalMatrix(size, diagonalValue) { const matrix = Array(size).fill(0).map(() => Array(size).fill(0)); for (let i = 0; i < size; i++) { matrix[i][i] = diagonalValue; } return { data: matrix, rows: size, cols: size, format: 'dense' }; } createStronglyDiagonalDominant(size) { const matrix = Array(size).fill(0).map(() => Array(size).fill(0)); for (let i = 0; i < size; i++) { let rowSum = 0; // Add off-diagonal elements for (let j = 0; j < size; j++) { if (i !== j) { const value = (Math.random() - 0.5) * 0.2; // Small off-diagonal elements matrix[i][j] = value; rowSum += Math.abs(value); } } // Set diagonal to be much larger than row sum matrix[i][i] = rowSum * 3 + 2.0; } return { data: matrix, rows: size, cols: size, format: 'dense' }; } createWeaklyDiagonalDominant(size) { const matrix = Array(size).fill(0).map(() => Array(size).fill(0)); for (let i = 0; i < size; i++) { let rowSum = 0; // Add larger off-diagonal elements for (let j = 0; j < size; j++) { if (i !== j) { const value = (Math.random() - 0.5) * 0.8; // Larger off-diagonal elements matrix[i][j] = value; rowSum += Math.abs(value); } } // Set diagonal to barely dominate matrix[i][i] = rowSum + 0.1; } return { data: matrix, rows: size, cols: size, format: 'dense' }; } createSPDMatrix(size) { // Create A = B^T * B + I to ensure SPD const B = Array(size).fill(0).map(() => Array(size).fill(0).map(() => Math.random() - 0.5) ); const matrix = Array(size).fill(0).map(() => Array(size).fill(0)); for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { let sum = 0; for (let k = 0; k < size; k++) { sum += B[k][i] * B[k][j]; } matrix[i][j] = sum; if (i === j) matrix[i][j] += 1.0; // Add identity for positive definiteness } } return { data: matrix, rows: size, cols: size, format: 'dense' }; } createNearSingularMatrix(size) { const matrix = Array(size).fill(0).map(() => Array(size).fill(0)); // Create a matrix with very small singular values for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { matrix[i][j] = Math.random() * 0.1; } // Set diagonal to be barely non-zero matrix[i][i] = 0.001 + Math.random() * 0.01; } return { data: matrix, rows: size, cols: size, format: 'dense' }; } } // Export for use in tests module.exports = { ConvergenceValidator }; // Run validation if called directly if (require.main === module) { const validator = new ConvergenceValidator(); validator.runValidation().then(results => { const passed = results.filter(r => r.passed).length; process.exit(passed === results.length ? 0 : 1); }).catch(error => { console.error('Validation failed:', error); process.exit(1); }); }