class ConvergenceDetector {
constructor(options = {}) {
this.tolerance = options.tolerance || 1e-10;
this.maxIterations = options.maxIterations || 1000;
this.relativeToleranceEnabled = options.relativeToleranceEnabled !== false;
this.minIterations = options.minIterations || 1;
this.stagnationThreshold = options.stagnationThreshold || 1e-14;
this.convergenceWindowSize = options.convergenceWindowSize || 10;
this.reset();
}
reset() {
this.iteration = 0;
this.residualHistory = [];
this.convergenceRateHistory = [];
this.relativeResidualHistory = [];
this.initialResidualNorm = null;
this.rhsNorm = null;
this.isConverged = false;
this.stagnationDetected = false;
this.divergenceDetected = false;
this.startTime = Date.now();
this.lastUpdateTime = Date.now();
}
initialize(rhs) {
this.rhsNorm = this.vectorNorm(rhs);
if (this.rhsNorm === 0) {
console.warn('Zero RHS vector detected - using absolute residual tolerance');
this.relativeToleranceEnabled = false;
}
}
computeResidual(matrix, solution, rhs) {
const Ax = this.multiplyMatrixVector(matrix, solution);
return rhs.map((bi, i) => bi - Ax[i]);
}
computeRelativeResidualNorm(residual) {
const residualNorm = this.vectorNorm(residual);
if (this.relativeToleranceEnabled && this.rhsNorm > 0) {
return residualNorm / this.rhsNorm;
} else {
return residualNorm;
}
}
update(matrix, solution, rhs) {
this.iteration++;
this.lastUpdateTime = Date.now();
const residual = this.computeResidual(matrix, solution, rhs);
const residualNorm = this.vectorNorm(residual);
const relativeResidualNorm = this.computeRelativeResidualNorm(residual);
this.residualHistory.push(residualNorm);
this.relativeResidualHistory.push(relativeResidualNorm);
if (this.iteration === 1) {
this.initialResidualNorm = relativeResidualNorm;
}
const convergenceRate = this.computeConvergenceRate();
this.convergenceRateHistory.push(convergenceRate);
this.checkConvergence(relativeResidualNorm);
this.checkStagnation();
this.checkDivergence();
const metrics = {
iteration: this.iteration,
residualNorm: residualNorm,
relativeResidualNorm: relativeResidualNorm,
convergenceRate: convergenceRate,
isConverged: this.isConverged,
stagnationDetected: this.stagnationDetected,
divergenceDetected: this.divergenceDetected,
shouldStop: this.shouldStop(),
reductionFactor: this.getReductionFactor(),
estimatedIterationsRemaining: this.estimateIterationsRemaining(),
elapsedTime: this.lastUpdateTime - this.startTime,
iterationsPerSecond: this.iteration / ((this.lastUpdateTime - this.startTime) / 1000)
};
return metrics;
}
computeConvergenceRate() {
if (this.relativeResidualHistory.length < 2) {
return 0.0;
}
const current = this.relativeResidualHistory[this.relativeResidualHistory.length - 1];
const previous = this.relativeResidualHistory[this.relativeResidualHistory.length - 2];
if (previous === 0 || current === 0) {
return 0.0;
}
const singleStepRate = current / previous;
if (this.relativeResidualHistory.length >= this.convergenceWindowSize) {
const windowStart = this.relativeResidualHistory.length - this.convergenceWindowSize;
const windowEnd = this.relativeResidualHistory.length - 1;
const initialWindow = this.relativeResidualHistory[windowStart];
const finalWindow = this.relativeResidualHistory[windowEnd];
if (initialWindow > 0 && finalWindow > 0) {
const averageRate = Math.pow(finalWindow / initialWindow, 1.0 / (this.convergenceWindowSize - 1));
return averageRate;
}
}
return singleStepRate;
}
checkConvergence(relativeResidualNorm) {
if (this.iteration < this.minIterations) {
this.isConverged = false;
return;
}
this.isConverged = relativeResidualNorm < this.tolerance;
}
checkStagnation() {
if (this.residualHistory.length < this.convergenceWindowSize) {
return;
}
const recentResiduals = this.residualHistory.slice(-this.convergenceWindowSize);
const maxRecent = Math.max(...recentResiduals);
const minRecent = Math.min(...recentResiduals);
if (maxRecent > 0 && (maxRecent - minRecent) / maxRecent < this.stagnationThreshold) {
this.stagnationDetected = true;
}
}
checkDivergence() {
if (this.residualHistory.length < 5) {
return;
}
const current = this.residualHistory[this.residualHistory.length - 1];
const previous = this.residualHistory[this.residualHistory.length - 2];
const initial = this.residualHistory[0];
if (current > 1000 * initial || (previous > 0 && current / previous > 10)) {
this.divergenceDetected = true;
}
}
shouldStop() {
return this.isConverged ||
this.iteration >= this.maxIterations ||
this.stagnationDetected ||
this.divergenceDetected;
}
getReductionFactor() {
if (this.initialResidualNorm === null || this.initialResidualNorm === 0) {
return 1.0;
}
const current = this.relativeResidualHistory[this.relativeResidualHistory.length - 1] || 0;
return current / this.initialResidualNorm;
}
estimateIterationsRemaining() {
if (this.isConverged) {
return 0;
}
const currentResidual = this.relativeResidualHistory[this.relativeResidualHistory.length - 1];
const convergenceRate = this.convergenceRateHistory[this.convergenceRateHistory.length - 1];
if (!currentResidual || !convergenceRate || convergenceRate >= 1.0 || convergenceRate <= 0) {
return this.maxIterations - this.iteration;
}
const iterationsNeeded = Math.log(this.tolerance / currentResidual) / Math.log(convergenceRate);
return Math.max(0, Math.min(iterationsNeeded, this.maxIterations - this.iteration));
}
getConvergenceReport() {
const current = this.relativeResidualHistory[this.relativeResidualHistory.length - 1] || 0;
const avgConvergenceRate = this.convergenceRateHistory.length > 0
? this.convergenceRateHistory.reduce((a, b) => a + b, 0) / this.convergenceRateHistory.length
: 0;
return {
iterations: this.iteration,
finalResidual: current,
initialResidual: this.initialResidualNorm,
reductionFactor: this.getReductionFactor(),
averageConvergenceRate: avgConvergenceRate,
converged: this.isConverged,
stagnated: this.stagnationDetected,
diverged: this.divergenceDetected,
tolerance: this.tolerance,
relativeToleranceUsed: this.relativeToleranceEnabled,
elapsedTime: this.lastUpdateTime - this.startTime,
residualHistory: [...this.residualHistory],
convergenceRateHistory: [...this.convergenceRateHistory]
};
}
vectorNorm(vector) {
return Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
}
multiplyMatrixVector(matrix, vector) {
const result = new Array(matrix.rows).fill(0);
if (matrix.format === 'dense') {
for (let i = 0; i < matrix.rows; i++) {
for (let j = 0; j < matrix.cols; j++) {
result[i] += matrix.data[i][j] * vector[j];
}
}
} else if (matrix.format === 'coo') {
for (let k = 0; k < matrix.data.values.length; k++) {
const row = matrix.data.rowIndices[k];
const col = matrix.data.colIndices[k];
const val = matrix.data.values[k];
result[row] += val * vector[col];
}
} else if (matrix.format === 'csr') {
for (let i = 0; i < matrix.rows; i++) {
const start = matrix.data.rowPointers[i];
const end = matrix.data.rowPointers[i + 1];
for (let k = start; k < end; k++) {
const col = matrix.data.colIndices[k];
const val = matrix.data.values[k];
result[i] += val * vector[col];
}
}
}
return result;
}
}
module.exports = { ConvergenceDetector };