/** * Core matrix operations for sublinear-time solvers */ import { SolverError, ErrorCodes } from './types.js'; export class MatrixOperations { /** * Validates matrix format and properties */ static validateMatrix(matrix) { if (!matrix) { throw new SolverError('Matrix is required', ErrorCodes.INVALID_MATRIX); } if (matrix.rows <= 0 || matrix.cols <= 0) { throw new SolverError('Matrix dimensions must be positive', ErrorCodes.INVALID_DIMENSIONS); } if (matrix.format === 'dense') { const dense = matrix; if (!Array.isArray(dense.data) || dense.data.length !== dense.rows) { throw new SolverError('Dense matrix data must be array of rows', ErrorCodes.INVALID_MATRIX); } for (let i = 0; i < dense.rows; i++) { if (!Array.isArray(dense.data[i]) || dense.data[i].length !== dense.cols) { throw new SolverError(`Row ${i} has invalid length`, ErrorCodes.INVALID_MATRIX); } } } else if (matrix.format === 'coo') { const sparse = matrix; const { values, rowIndices, colIndices } = sparse; if (!Array.isArray(values) || !Array.isArray(rowIndices) || !Array.isArray(colIndices)) { throw new SolverError('COO matrix must have values, rowIndices, and colIndices arrays', ErrorCodes.INVALID_MATRIX); } if (values.length !== rowIndices.length || values.length !== colIndices.length) { throw new SolverError('COO matrix arrays must have same length', ErrorCodes.INVALID_MATRIX); } // Check indices are valid for (let i = 0; i < rowIndices.length; i++) { if (rowIndices[i] < 0 || rowIndices[i] >= sparse.rows) { throw new SolverError(`Invalid row index ${rowIndices[i]}`, ErrorCodes.INVALID_MATRIX); } if (colIndices[i] < 0 || colIndices[i] >= sparse.cols) { throw new SolverError(`Invalid column index ${colIndices[i]}`, ErrorCodes.INVALID_MATRIX); } } } else { throw new SolverError(`Unsupported matrix format: ${matrix.format}`, ErrorCodes.INVALID_MATRIX); } } /** * Matrix-vector multiplication: result = matrix * vector */ static multiplyMatrixVector(matrix, vector) { this.validateMatrix(matrix); if (vector.length !== matrix.cols) { throw new SolverError(`Vector length ${vector.length} does not match matrix columns ${matrix.cols}`, ErrorCodes.INVALID_DIMENSIONS); } const result = new Array(matrix.rows).fill(0); if (matrix.format === 'dense') { const dense = matrix; for (let i = 0; i < matrix.rows; i++) { for (let j = 0; j < matrix.cols; j++) { result[i] += dense.data[i][j] * vector[j]; } } } else if (matrix.format === 'coo') { const sparse = matrix; for (let k = 0; k < sparse.values.length; k++) { const row = sparse.rowIndices[k]; const col = sparse.colIndices[k]; const val = sparse.values[k]; result[row] += val * vector[col]; } } return result; } /** * Get matrix entry at (row, col) */ static getEntry(matrix, row, col) { this.validateMatrix(matrix); if (row < 0 || row >= matrix.rows || col < 0 || col >= matrix.cols) { throw new SolverError(`Index (${row}, ${col}) out of bounds`, ErrorCodes.INVALID_DIMENSIONS); } if (matrix.format === 'dense') { const dense = matrix; return dense.data[row][col]; } else if (matrix.format === 'coo') { const sparse = matrix; for (let k = 0; k < sparse.values.length; k++) { if (sparse.rowIndices[k] === row && sparse.colIndices[k] === col) { return sparse.values[k]; } } return 0; // Implicit zero } return 0; } /** * Get diagonal entry at position i */ static getDiagonal(matrix, i) { return this.getEntry(matrix, i, i); } /** * Extract diagonal as vector */ static getDiagonalVector(matrix) { if (matrix.rows !== matrix.cols) { throw new SolverError('Matrix must be square to extract diagonal', ErrorCodes.INVALID_DIMENSIONS); } const diagonal = new Array(matrix.rows); for (let i = 0; i < matrix.rows; i++) { diagonal[i] = this.getDiagonal(matrix, i); } return diagonal; } /** * Get row sum for diagonal dominance check */ static getRowSum(matrix, row, excludeDiagonal = false) { this.validateMatrix(matrix); if (row < 0 || row >= matrix.rows) { throw new SolverError(`Row index ${row} out of bounds`, ErrorCodes.INVALID_DIMENSIONS); } let sum = 0; if (matrix.format === 'dense') { const dense = matrix; for (let j = 0; j < matrix.cols; j++) { if (!excludeDiagonal || j !== row) { sum += Math.abs(dense.data[row][j]); } } } else if (matrix.format === 'coo') { const sparse = matrix; for (let k = 0; k < sparse.values.length; k++) { if (sparse.rowIndices[k] === row) { const col = sparse.colIndices[k]; if (!excludeDiagonal || col !== row) { sum += Math.abs(sparse.values[k]); } } } } return sum; } /** * Get column sum for diagonal dominance check */ static getColumnSum(matrix, col, excludeDiagonal = false) { this.validateMatrix(matrix); if (col < 0 || col >= matrix.cols) { throw new SolverError(`Column index ${col} out of bounds`, ErrorCodes.INVALID_DIMENSIONS); } let sum = 0; if (matrix.format === 'dense') { const dense = matrix; for (let i = 0; i < matrix.rows; i++) { if (!excludeDiagonal || i !== col) { sum += Math.abs(dense.data[i][col]); } } } else if (matrix.format === 'coo') { const sparse = matrix; for (let k = 0; k < sparse.values.length; k++) { if (sparse.colIndices[k] === col) { const row = sparse.rowIndices[k]; if (!excludeDiagonal || row !== col) { sum += Math.abs(sparse.values[k]); } } } } return sum; } /** * Check if matrix is diagonally dominant */ static checkDiagonalDominance(matrix) { this.validateMatrix(matrix); if (matrix.rows !== matrix.cols) { return { isRowDD: false, isColDD: false, strength: 0 }; } let isRowDD = true; let isColDD = true; let minRowStrength = Infinity; let minColStrength = Infinity; for (let i = 0; i < matrix.rows; i++) { const diagonal = Math.abs(this.getDiagonal(matrix, i)); const rowOffDiagonalSum = this.getRowSum(matrix, i, true); const colOffDiagonalSum = this.getColumnSum(matrix, i, true); if (diagonal === 0) { isRowDD = false; isColDD = false; minRowStrength = 0; minColStrength = 0; break; } const rowStrength = diagonal - rowOffDiagonalSum; const colStrength = diagonal - colOffDiagonalSum; if (rowStrength < 0) { isRowDD = false; } else { minRowStrength = Math.min(minRowStrength, rowStrength / diagonal); } if (colStrength < 0) { isColDD = false; } else { minColStrength = Math.min(minColStrength, colStrength / diagonal); } } const strength = Math.max(isRowDD ? minRowStrength : 0, isColDD ? minColStrength : 0); return { isRowDD, isColDD, strength }; } /** * Check if matrix is symmetric */ static isSymmetric(matrix, tolerance = 1e-10) { this.validateMatrix(matrix); if (matrix.rows !== matrix.cols) { return false; } // For sparse matrices, this is more complex - we'd need to compare all entries if (matrix.format === 'dense') { const dense = matrix; for (let i = 0; i < matrix.rows; i++) { for (let j = i + 1; j < matrix.cols; j++) { if (Math.abs(dense.data[i][j] - dense.data[j][i]) > tolerance) { return false; } } } return true; } // For sparse matrices, check symmetry by comparing entries for (let i = 0; i < matrix.rows; i++) { for (let j = i + 1; j < matrix.cols; j++) { const entry_ij = this.getEntry(matrix, i, j); const entry_ji = this.getEntry(matrix, j, i); if (Math.abs(entry_ij - entry_ji) > tolerance) { return false; } } } return true; } /** * Calculate sparsity ratio (fraction of zero entries) */ static calculateSparsity(matrix) { this.validateMatrix(matrix); const totalEntries = matrix.rows * matrix.cols; if (matrix.format === 'dense') { const dense = matrix; let nonZeros = 0; for (let i = 0; i < matrix.rows; i++) { for (let j = 0; j < matrix.cols; j++) { if (Math.abs(dense.data[i][j]) > 1e-15) { nonZeros++; } } } return 1 - (nonZeros / totalEntries); } else if (matrix.format === 'coo') { const sparse = matrix; return 1 - (sparse.values.length / totalEntries); } return 0; } /** * Analyze matrix properties */ static analyzeMatrix(matrix) { this.validateMatrix(matrix); const dominance = this.checkDiagonalDominance(matrix); const isSymmetric = this.isSymmetric(matrix); const sparsity = this.calculateSparsity(matrix); let dominanceType = 'none'; if (dominance.isRowDD && dominance.isColDD) { dominanceType = 'row'; // Prefer row if both } else if (dominance.isRowDD) { dominanceType = 'row'; } else if (dominance.isColDD) { dominanceType = 'column'; } return { isDiagonallyDominant: dominance.isRowDD || dominance.isColDD, dominanceType, dominanceStrength: dominance.strength, isSymmetric, sparsity, size: { rows: matrix.rows, cols: matrix.cols } }; } /** * Convert dense matrix to COO sparse format */ static denseToSparse(dense, tolerance = 1e-15) { const values = []; const rowIndices = []; const colIndices = []; for (let i = 0; i < dense.rows; i++) { for (let j = 0; j < dense.cols; j++) { const value = dense.data[i][j]; if (Math.abs(value) > tolerance) { values.push(value); rowIndices.push(i); colIndices.push(j); } } } return { rows: dense.rows, cols: dense.cols, values, rowIndices, colIndices, format: 'coo' }; } /** * Convert COO sparse matrix to dense format */ static sparseToDense(sparse) { const data = Array(sparse.rows).fill(null).map(() => Array(sparse.cols).fill(0)); for (let k = 0; k < sparse.values.length; k++) { const row = sparse.rowIndices[k]; const col = sparse.colIndices[k]; const val = sparse.values[k]; data[row][col] = val; } return { rows: sparse.rows, cols: sparse.cols, data, format: 'dense' }; } }