422 lines
12 KiB
JavaScript
422 lines
12 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
} |