UNPKG

xen-dev-utils

Version:

Utility functions used by the Scale Workshop ecosystem

801 lines 27.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.solveDiophantine = exports.respell = exports.nearestPlane = exports.canonical = exports.defactoredHnf = exports.fractionalMatsub = exports.fractionalMatadd = exports.fractionalMatscale = exports.matsub = exports.matadd = exports.matscale = exports.minor = exports.fractionalTranspose = exports.fractionalDet = exports.det = exports.fractionalMatmul_ = exports.fractionalMatmul = exports.matmul = exports.fractionalInv = exports.inv = exports.fractionalEye = exports.eye = exports.fractionalLenstraLenstraLovasz = exports.lenstraLenstraLovasz = exports.fractionalGram = exports.gram = void 0; const fraction_1 = require("./fraction"); const monzo_1 = require("./monzo"); const number_array_1 = require("./number-array"); const hnf_1 = require("./hnf"); /** * Perform Gram–Schmidt process without normalization. * @param basis Array of basis elements. * @param epsilon Threshold for zero. * @returns The orthogonalized basis and its dual (duals of near-zero basis elements are coerced to zero). */ function gram(basis, epsilon = 1e-12) { const ortho = []; const squaredLengths = []; const dual = []; for (let i = 0; i < basis.length; ++i) { ortho.push([...basis[i]]); for (let j = 0; j < i; ++j) { ortho[i] = (0, monzo_1.sub)(ortho[i], (0, monzo_1.scale)(ortho[j], (0, number_array_1.dot)(ortho[i], dual[j]))); } squaredLengths.push((0, number_array_1.dot)(ortho[i], ortho[i])); if (squaredLengths[i] > epsilon) { dual.push((0, monzo_1.scale)(ortho[i], 1 / squaredLengths[i])); } else { dual.push((0, monzo_1.scale)(ortho[i], 0)); } } return { ortho, squaredLengths, dual }; } exports.gram = gram; /** * Perform Gram–Schmidt process without normalization. * @param basis Array of rational basis elements. * @returns The orthogonalized basis and its dual as arrays of fractions (duals of zero basis elements are coerced to zero). */ function fractionalGram(basis) { const ortho = []; const squaredLengths = []; const dual = []; for (let i = 0; i < basis.length; ++i) { ortho.push(basis[i].map(f => new fraction_1.Fraction(f))); for (let j = 0; j < i; ++j) { ortho[i] = (0, monzo_1.fractionalSub)(ortho[i], (0, monzo_1.fractionalScale)(ortho[j], (0, monzo_1.fractionalDot)(ortho[i], dual[j]))); } squaredLengths.push((0, monzo_1.fractionalDot)(ortho[i], ortho[i])); if (squaredLengths[i].n) { dual.push((0, monzo_1.fractionalScale)(ortho[i], squaredLengths[i].inverse())); } else { dual.push((0, monzo_1.fractionalScale)(ortho[i], squaredLengths[i])); } } return { ortho, squaredLengths, dual }; } exports.fractionalGram = fractionalGram; /** * Preform Lenstra-Lenstra-Lovász basis reduction. * @param basis Array of basis elements. * @param delta Lovász coefficient. * @param epsilon Threshold for zero. * @param maxIterations Maximum number of iterations to perform. * @returns The basis processed to be short and nearly orthogonal alongside Gram-Schmidt coefficients. */ function lenstraLenstraLovasz(basis, delta = 0.75, epsilon = 1e-12, maxIterations = 10000) { // https://en.wikipedia.org/wiki/Lenstra%E2%80%93Lenstra%E2%80%93Lov%C3%A1sz_lattice_basis_reduction_algorithm#LLL_algorithm_pseudocode basis = basis.map(row => [...row]); let { ortho, squaredLengths, dual } = gram(basis, epsilon); let k = 1; while (k < basis.length && maxIterations--) { for (let j = k - 1; j >= 0; --j) { const mu = (0, number_array_1.dot)(basis[k], dual[j]); if (Math.abs(mu) > 0.5) { basis[k] = (0, monzo_1.sub)(basis[k], (0, monzo_1.scale)(basis[j], Math.round(mu))); ({ ortho, squaredLengths, dual } = gram(basis, epsilon)); } } const mu = (0, number_array_1.dot)(basis[k], dual[k - 1]); if (squaredLengths[k] > (delta - mu * mu) * squaredLengths[k - 1] || !squaredLengths[k - 1]) { k++; } else { const bk = basis[k]; basis[k] = basis[k - 1]; basis[k - 1] = bk; ({ ortho, squaredLengths, dual } = gram(basis, epsilon)); k = Math.max(k - 1, 1); } } return { basis, gram: { ortho, squaredLengths, dual, }, }; } exports.lenstraLenstraLovasz = lenstraLenstraLovasz; const HALF = new fraction_1.Fraction(1, 2); /** * Preform Lenstra-Lenstra-Lovász basis reduction using rational numbers. * @param basis Array of rational basis elements. * @param delta Lovász coefficient. * @param maxIterations Maximum number of iterations to perform. * @returns The basis processed to be short and nearly orthogonal alongside Gram-Schmidt coefficients. */ function fractionalLenstraLenstraLovasz(basis, delta = '3/4', maxIterations = 10000) { const result = basis.map(row => row.map(f => new fraction_1.Fraction(f))); const delta_ = new fraction_1.Fraction(delta); let { ortho, squaredLengths, dual } = fractionalGram(result); let k = 1; while (k < result.length && maxIterations--) { for (let j = k - 1; j >= 0; --j) { const mu = (0, monzo_1.fractionalDot)(result[k], dual[j]); if (mu.abs().compare(HALF) > 0) { result[k] = (0, monzo_1.fractionalSub)(result[k], (0, monzo_1.fractionalScale)(result[j], mu.round())); ({ ortho, squaredLengths, dual } = fractionalGram(result)); } } const mu = (0, monzo_1.fractionalDot)(result[k], dual[k - 1]); if (squaredLengths[k].compare(delta_.sub(mu.mul(mu)).mul(squaredLengths[k - 1])) > 0 || !squaredLengths[k - 1].n) { k++; } else { const bk = result[k]; result[k] = result[k - 1]; result[k - 1] = bk; ({ ortho, squaredLengths, dual } = fractionalGram(result)); k = Math.max(k - 1, 1); } } return { basis: result, gram: { ortho, squaredLengths, dual, }, }; } exports.fractionalLenstraLenstraLovasz = fractionalLenstraLenstraLovasz; /** * Return a 2-D array with ones on the diagonal and zeros elsewhere. * @param N Number of rows in the output. * @param M Number of columns in the output. * @param k Index of the diagonal. * @returns An array where all elements are equal to zero, except for the `k`-th diagonal, whose values are equal to one. */ function eye(N, M, k = 0) { M ?? (M = N); const result = []; for (let i = 0; i < N; ++i) { result.push(Array(M).fill(0)); if (i >= k && i + k < M) { result[i][i + k] = 1; } } return result; } exports.eye = eye; /** * Return a 2-D array with ones on the diagonal and zeros elsewhere. * @param N Number of rows in the output. * @param M Number of columns in the output. * @param k Index of the diagonal. * @returns An array where all elements are equal to zero, except for the `k`-th diagonal, whose values are equal to one. */ function fractionalEye(N, M, k = 0) { M ?? (M = N); const result = []; for (let i = 0; i < N; ++i) { const row = []; for (let j = 0; j < M; ++j) { if (j === i + k) { row.push(new fraction_1.Fraction(1)); } else { row.push(new fraction_1.Fraction(0)); } } result.push(row); } return result; } exports.fractionalEye = fractionalEye; // XXX: I'm sorry. This matrix inversion algorithm is not particularly sophisticated. Existing solutions just come with too much bloat. /** * Compute the (multiplicative) inverse of a matrix. * @param matrix Matrix to be inverted. * @returns The multiplicative inverse. * @throws An error if the matrix is not square or not invertible. */ function inv(matrix) { let width = 0; const height = matrix.length; for (const row of matrix) { width = Math.max(width, row.length); } if (width !== height) { throw new Error('Non-square matrix'); } const result = []; for (let i = 0; i < height; ++i) { result.push(Array(width).fill(0)); result[i][i] = 1; } // Don't modify input matrix = matrix.map(row => [...row]); // Coerce missing entries to zeros for (let y = 0; y < height; ++y) { for (let x = matrix[y].length; x < width; ++x) { matrix[y][x] = 0; } } // Put ones along the diagonal, zeros in the lower triangle for (let x = 0; x < width; ++x) { // Maintain row echelon form by pivoting on the most dominant row. let pivot; let s = 0; for (let y = x; y < height; ++y) { if (Math.abs(matrix[y][x]) > Math.abs(s)) { pivot = y; s = matrix[y][x]; } } if (pivot === undefined) { throw new Error('Matrix is singular'); } if (x !== pivot) { let temp = matrix[pivot]; matrix[pivot] = matrix[x]; matrix[x] = temp; temp = result[pivot]; result[pivot] = result[x]; result[x] = temp; } if (s !== 1) { s = 1 / s; matrix[x] = matrix[x].map(a => a * s); result[x] = result[x].map(a => a * s); } for (let y = x + 1; y < height; ++y) { s = matrix[y][x]; if (s) { result[y] = result[y].map((a, i) => a - s * result[x][i]); // Ignore entries that are not used later on. for (let i = x + 1; i < width; ++i) { matrix[y][i] -= s * matrix[x][i]; } // Full row operation for reference: // matrix[y] = matrix[y].map((a, i) => a - s * matrix[x][i]); } } } // Eliminate remaining entries in the upper triangle for (let x = width - 1; x > 0; --x) { for (let y = x - 1; y >= 0; --y) { const s = matrix[y][x]; if (s) { // No need to keep track of these entries anymore. // matrix[y] = matrix[y].map((a, i) => a - s * matrix[x][i]); result[y] = result[y].map((a, i) => a - s * result[x][i]); } } } return result; } exports.inv = inv; /** * Compute the (multiplicative) inverse of a matrix. * @param matrix Matrix to be inverted. * @returns The multiplicative inverse. * @throws An error if the matrix is not square or not invertible. */ function fractionalInv(matrix) { let width = 0; const height = matrix.length; for (const row of matrix) { width = Math.max(width, row.length); } if (width !== height) { throw new Error('Non-square matrix'); } const result = []; for (let i = 0; i < height; ++i) { const row = []; for (let j = 0; j < width; ++j) { if (i === j) { row.push(new fraction_1.Fraction(1)); } else { row.push(new fraction_1.Fraction(0)); } } result.push(row); } // Don't modify input const matrix_ = matrix.map(row => row.map(f => new fraction_1.Fraction(f))); // Coerce missing entries to zeros for (let y = 0; y < height; ++y) { for (let x = matrix_[y].length; x < width; ++x) { matrix_[y][x] = new fraction_1.Fraction(0); } } // Put ones along the diagonal, zeros in the lower triangle for (let x = 0; x < width; ++x) { let s = matrix_[x][x]; if (!s.n) { // Row echelon form (pivoting makes no difference over rationals) // TODO: Figure out if there's a strategy to avoid blowing safe limits during manipulation. for (let y = x + 1; y < height; ++y) { if (matrix_[y][x].n) { let temp = matrix_[y]; matrix_[y] = matrix_[x]; matrix_[x] = temp; temp = result[y]; result[y] = result[x]; result[x] = temp; break; } } s = matrix_[x][x]; if (!s.n) { throw new Error('Matrix is singular'); } } if (!s.isUnity()) { matrix_[x] = matrix_[x].map(a => a.div(s)); result[x] = result[x].map(a => a.div(s)); } for (let y = x + 1; y < height; ++y) { s = matrix_[y][x]; if (s.n) { result[y] = result[y].map((a, i) => a.sub(s.mul(result[x][i]))); // Ignore entries that are not used later on. for (let i = x + 1; i < width; ++i) { matrix_[y][i] = matrix_[y][i].sub(s.mul(matrix_[x][i])); } // Full row operation for reference: // matrix_[y] = matrix_[y].map((a, i) => a.sub(s.mul(matrix_[x][i]))); } } } // Eliminate remaining entries in the upper triangle for (let x = width - 1; x > 0; --x) { for (let y = x - 1; y >= 0; --y) { const s = matrix_[y][x]; if (s.n) { // No need to keep track of these entries anymore. // matrix_[y] = matrix_[y].map(...); result[y] = result[y].map((a, i) => a.sub(s.mul(result[x][i]))); } } } return result; } exports.fractionalInv = fractionalInv; function matmul(A, B) { let numVectors = 0; if (!Array.isArray(A[0])) { A = [A]; numVectors++; } if (!Array.isArray(B[0])) { B = B.map(c => [c]); numVectors++; } const result = matmul_(A, B); if (numVectors === 1) { return result.flat(); } else if (numVectors === 2) { return result[0][0]; } return result; } exports.matmul = matmul; function matmul_(A, B) { const height = A.length; let width = 0; for (const row of B) { width = Math.max(width, row.length); } let n = 0; for (const row of A) { n = Math.max(n, row.length); } B = [...B]; while (B.length < n) { B.push([]); } const result = []; for (let i = 0; i < height; ++i) { const row = Array(width).fill(0); const rowA = A[i]; for (let j = 0; j < width; ++j) { for (let k = 0; k < rowA.length; ++k) { row[j] += rowA[k] * (B[k][j] ?? 0); } } result.push(row); } return result; } function fractionalMatmul(A, B) { let numVectors = 0; if (!Array.isArray(A[0])) { A = [A]; numVectors++; } if (!Array.isArray(B[0])) { B = B.map(c => [c]); numVectors++; } const result = fractionalMatmul_(A, B); if (numVectors === 1) { return result.flat(); } else if (numVectors === 2) { return result[0][0]; } return result; } exports.fractionalMatmul = fractionalMatmul; function fractionalMatmul_(A, B) { const height = A.length; let width = 0; for (const row of B) { width = Math.max(width, row.length); } let n = 0; for (const row of A) { n = Math.max(n, row.length); } const B_ = B.map(row => row.map(f => new fraction_1.Fraction(f))); while (B_.length < n) { B_.push([]); } for (const row of B_) { while (row.length < width) { row.push(new fraction_1.Fraction(0)); } } const result = []; for (let i = 0; i < height; ++i) { const row = []; while (row.length < width) { row.push(new fraction_1.Fraction(0)); } const rowA = A[i].map(f => new fraction_1.Fraction(f)); for (let j = 0; j < width; ++j) { for (let k = 0; k < rowA.length; ++k) { row[j] = row[j].add(rowA[k].mul(B_[k][j])); } } result.push(row); } return result; } exports.fractionalMatmul_ = fractionalMatmul_; /** * Compute the determinant of a matrix. * @param matrix Array of arrays of numbers to calculate determinant for. * @returns The determinant. */ function det(matrix) { let width = 0; const height = matrix.length; for (const row of matrix) { width = Math.max(width, row.length); } if (width !== height) { throw new Error('Non-square matrix'); } matrix = matrix.map(row => [...row]); let result = 1; for (let x = 0; x < width; ++x) { // Maintain row echelon form by pivoting on the most dominant row. let pivot; let d = 0; for (let y = x; y < height; ++y) { if (Math.abs(matrix[y][x]) > Math.abs(d)) { pivot = y; d = matrix[y][x]; } } if (pivot === undefined) { return 0; } if (x !== pivot) { const temp = matrix[pivot]; matrix[pivot] = matrix[x]; matrix[x] = temp; result = -result; } result *= d; d = 1 / d; for (let y = x + 1; y < height; ++y) { const row = matrix[y]; const s = row[x] * d; if (s) { // Skip over entries that are not used later. const upperRow = matrix[x]; for (let i = x + 1; i < upperRow.length; ++i) { row[i] -= s * upperRow[i]; } // Full row operation for reference: // matrix[y] = matrix[y].map((a, i) => a - s * (matrix[x][i] ?? 0)); } } } return result; } exports.det = det; /** * Compute the determinant of a matrix with rational entries. * @param matrix Array of arrays of fractions to calculate determinant for. * @returns The determinant. */ function fractionalDet(matrix) { let width = 0; const height = matrix.length; for (const row of matrix) { width = Math.max(width, row.length); } if (width !== height) { throw new Error('Non-square matrix'); } const matrix_ = matrix.map(row => row.map(f => new fraction_1.Fraction(f))); for (const row of matrix_) { while (row.length < width) { row.push(new fraction_1.Fraction(0)); } } let result = new fraction_1.Fraction(1); for (let x = 0; x < width; ++x) { let d = matrix_[x][x]; if (!d.n) { // Row echelon form for (let y = x + 1; y < height; ++y) { if (matrix_[y][x].n) { const temp = matrix_[y]; matrix_[y] = matrix_[x]; matrix_[x] = temp; result = result.neg(); break; } } d = matrix_[x][x]; if (!d.n) { return new fraction_1.Fraction(0); } } result = result.mul(d); for (let y = x + 1; y < height; ++y) { const row = matrix_[y]; const s = row[x].div(d); if (s.n) { // Skip over entries that are not used later. const upperRow = matrix_[x]; for (let i = x + 1; i < width; ++i) { row[i] = row[i].sub(s.mul(upperRow[i])); } } } } return result; } exports.fractionalDet = fractionalDet; /** * Transpose a 2-D matrix with rational entries (swap rows and columns). * @param matrix Matrix to transpose. * @returns The transpose. */ function fractionalTranspose(matrix) { let width = 0; for (const row of matrix) { width = Math.max(row.length, width); } const result = []; for (let i = 0; i < width; ++i) { const row = []; for (let j = 0; j < matrix.length; ++j) { row.push(new fraction_1.Fraction(matrix[j][i] ?? 0)); } result.push(row); } return result; } exports.fractionalTranspose = fractionalTranspose; /** * Obtain the minor a matrix. * @param matrix The input matrix. * @param i The row to remove. * @param j The column to remove. * @returns The spliced matrix. */ function minor(matrix, i, j) { matrix = matrix.map(row => [...row]); matrix.splice(i, 1); for (const row of matrix) { row.splice(j, 1); } return matrix; } exports.minor = minor; /** * Scale a matrix by a scalar. * @param matrix The matrix to scale. * @param amount The amount to scale by. * @returns The scalar multiple. */ function matscale(matrix, amount) { return matrix.map(row => (0, monzo_1.scale)(row, amount)); } exports.matscale = matscale; /** * Add two matrices. * @param A The first matrix. * @param B The second matrix. * @returns The sum. */ function matadd(A, B) { const result = []; const numRows = Math.max(A.length, B.length); for (let i = 0; i < numRows; ++i) { result.push((0, monzo_1.add)(A[i] ?? [], B[i] ?? [])); } return result; } exports.matadd = matadd; /** * Subtract two matrices. * @param A The matrix to subtract from. * @param B The matrix to subtract by. * @returns The difference. */ function matsub(A, B) { const result = []; const numRows = Math.max(A.length, B.length); for (let i = 0; i < numRows; ++i) { result.push((0, monzo_1.sub)(A[i] ?? [], B[i] ?? [])); } return result; } exports.matsub = matsub; /** * Scale a matrix by a scalar. * @param matrix The matrix to scale. * @param amount The amount to scale by. * @returns The scalar multiple. */ function fractionalMatscale(matrix, amount) { return matrix.map(row => (0, monzo_1.fractionalScale)(row, amount)); } exports.fractionalMatscale = fractionalMatscale; /** * Add two matrices. * @param A The first matrix. * @param B The second matrix. * @returns The sum. */ function fractionalMatadd(A, B) { const result = []; const numRows = Math.max(A.length, B.length); for (let i = 0; i < numRows; ++i) { result.push((0, monzo_1.fractionalAdd)(A[i] ?? [], B[i] ?? [])); } return result; } exports.fractionalMatadd = fractionalMatadd; /** * Subtract two matrices. * @param A The matrix to subtract from. * @param B The matrix to subtract by. * @returns The difference. */ function fractionalMatsub(A, B) { const result = []; const numRows = Math.max(A.length, B.length); for (let i = 0; i < numRows; ++i) { result.push((0, monzo_1.fractionalSub)(A[i] ?? [], B[i] ?? [])); } return result; } exports.fractionalMatsub = fractionalMatsub; // Finds the Hermite normal form and 'defactors' it. // Defactoring is also known as saturation. // This removes torsion from the map. // Algorithm as described by: // // Clément Pernet and William Stein. // Fast Computation of HNF of Random Integer Matrices. // Journal of Number Theory. // https://doi.org/10.1016/j.jnt.2010.01.017 // See section 8. /** * Compute the Hermite normal form with torsion removed. * @param M The input matrix. * @returns The defactored Hermite normal form. */ function defactoredHnf(M) { // Need to convert to bigint so that intermediate results don't blow up. const bigM = M.map(row => row.map(BigInt)); const K = (0, hnf_1.hnf)((0, hnf_1.transpose)(bigM)); while (K.length > M.length) { K.pop(); } const determinant = (0, hnf_1.integerDet)(K); if (determinant === 1n) { return (0, hnf_1.hnf)(bigM).map(row => row.map(Number)); } const S = inv((0, hnf_1.transpose)(K).map(row => row.map(Number))); const D = matmul(S, M).map(row => row.map(Math.round)); return (0, hnf_1.hnf)(D); } exports.defactoredHnf = defactoredHnf; /** * Compute the canonical form of the input. * @param M Input maps. * @returns Defactored Hermite normal form or the antitranspose sandwich for commas bases. */ function canonical(M) { for (const row of M) { if (row.length < M.length) { // Comma basis return (0, hnf_1.antitranspose)(defactoredHnf((0, hnf_1.antitranspose)(M))); } } return defactoredHnf(M); } exports.canonical = canonical; // Babai's nearest plane algorithm for solving approximate CVP // `basis` should be LLL reduced first /** * Solve approximate CVP using Babai's nearest plane algorithm. * @param v Vector to reduce. * @param basis LLL basis to reduce with. * @param dual Optional precalculated geometric duals of the orthogonalized basis. * @returns The reduced vector. */ function nearestPlane(v, basis, dual) { // Body moved to respell to save a sub() call. return (0, monzo_1.sub)(v, respell(v, basis, dual)); } exports.nearestPlane = nearestPlane; /** * Respell a monzo represting a rational number to a simpler form. * @param monzo Array of prime exponents to simplify. * @param commas Monzos representing near-unisons to simplify by. Should be LLL reduced to work properly. * @param commaOrthoDuals Optional precalculated geometric duals of the orthogonalized comma basis. * @returns An array of prime exponents representing a simpler rational number. */ function respell(monzo, commas, commaOrthoDuals) { if (commaOrthoDuals === undefined) { commaOrthoDuals = gram(commas).dual; } monzo = [...monzo]; for (let i = commaOrthoDuals.length - 1; i >= 0; --i) { const mu = (0, number_array_1.dot)(monzo, commaOrthoDuals[i]); monzo = (0, monzo_1.sub)(monzo, (0, monzo_1.scale)(commas[i], Math.round(mu))); } return monzo; } exports.respell = respell; function solveDiophantine(A, b) { const hasMultiple = Array.isArray(b[0]); const B = hasMultiple ? (0, hnf_1.padMatrix)(b).M : b.map(c => [c]); // Need to convert to bigint so that intermediate results don't blow up. const { width, height, M } = (0, hnf_1.padMatrix)(A.map(row => row.map(BigInt))); for (let i = 0; i < height; ++i) { M[i].push(...B[i].map(BigInt)); } const H = (0, hnf_1.hnf)(M); while (H.length > width) { H.pop(); } const c = []; for (const row of H) { c.push(row.splice(width, row.length - width).map(Number)); } const S = inv(H.map(row => row.map(Number))); const sol = matmul(S, c).map(row => row.map(Math.round)); // Check solution(s). const BS = matmul(A, sol); for (let i = 0; i < B.length; ++i) { if (!(0, monzo_1.monzosEqual)(B[i], BS[i])) { throw new Error('Could not solve system'); } } return hasMultiple ? sol : sol.map(row => row[0]); } exports.solveDiophantine = solveDiophantine; //# sourceMappingURL=basis.js.map