'use strict'; const rational = require('./rational'); const merge = require('./merge'); const generate = require('./generate'); /** * Pass a 2-dimensional array that will return a function accepting indices to access the matrix * * @param mat array that initializes the matrix * @returns function with the array initialized and access to method that perform operations on the matrix */ function matrix(mat) { if (!Array.isArray(mat)) { throw new Error('Input should be of type array'); } let _matrix = function() { let args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments)); return read(mat, args); } return Object.assign(_matrix, _mat(mat)); } matrix.gen = generate; /** * Private function that returns an object containing methods * that perform operations on the matrix * * @param mat array that initializes the matrix * @returns object of methods performing matrix operations */ function _mat(mat) { return { size: () => size(mat), add: (operand) => operate(mat, operand, addition), sub: (operand) => operate(mat, operand, subtraction), mul: (operand) => operate(mat, operand, multiplication), div: (operand) => operate(mat, operand, division), prod: (operand) => prod(mat, operand), trans: () => trans(mat), set: function() { let args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments)); return { to: (val) => replace(mat, val, args) } }, det: () => determinant(mat), inv: () => invert(mat), merge: merge(mat), map: (func) => map(mat, func), equals: (operand) => equals(mat, operand), }; } module.exports = matrix; /** * Calculates the size of the array across each dimension * * @param mat input matrix that initialized the function * @returns size of the matrix as an array */ function size(mat) { let s = []; while (Array.isArray(mat)) { s.push(mat.length); mat = mat[0]; } return s; } /** * Private function to calculate the dimensions of the matrix * * @param mat input matrix that initializes the function * @returns integer indicating the number of dimensions */ function dimensions(mat) { return size(mat).length; } /** * Outputs the original matrix or a particular element or a matrix that is part of the original * * @param mat input matrix that initializes the function * @param args indices to access one or more array elements * @returns array or single element */ function read(mat, args) { if (args.length === 0) { return mat; } else { return extract(mat, args); } } /** * Private function to extract a single element or a matrix that is part of the original * * @param mat input matrix that initializes the function * @param args indices to access one or more array elements * @returns array or single element */ function extract(mat, args) { let dim = dimensions(mat); for (let i = 0; i < dim; i++) { let d = args[i]; if (d === undefined) { break; } if (Array.isArray(d)) { // if an element of args is an array, more extraction is needed mat = extractRange(mat, d, i); } else if (Number.isInteger(d)) { if (dimensions(mat) > 1 && i > 0) { mat = mat.map(function(elem) { return [elem[d]]; }); } else { mat = mat[d]; } } } return mat; } /** * Private function to extract a portion of the array based on the specified range * * @param mat input matrix that initialized the function * @param arg single argument containing the range specified as an array * @param ind the current index of the arguments while extracting the matrix * @returns array from the specified range */ function extractRange(mat, arg, ind) { if (!arg.length) { return mat; } else if (arg.length === 2) { let reverse = arg[0] > arg[1]; let first = (!reverse) ? arg[0] : arg[1]; let last = (!reverse) ? arg[1]: arg[0]; if (dimensions(mat) > 1 && ind > 0) { return mat.map(function(elem) { if (reverse) { return elem.slice(first, last+1).reverse(); } return elem.slice(first, last+1); }) } else { mat = mat.slice(first, last+1); return (reverse && mat.reverse()) || mat; } } } /** * Replaces the specified index in the matrix with the specified value * * @param mat input matrix that initialized the function * @param value specified value that replace current value at index or indices * @param args index or indices passed in arguments to initialized function * @returns replaced matrix */ function replace(mat, value, args) { //TODO: Clean this function up let result = clone(mat); let prev = args[0]; let start = prev[0] || 0; let end = prev[1] && prev[1] + 1 || mat.length; if (!Array.isArray(prev) && args.length === 1) { result[prev].fill(value); } else if (args.length === 1) { for (let ind = start; ind < end; ind++) { result[ind].fill(value); } } for (let i = 1; i < args.length; i++) { let first = Array.isArray(args[i]) ? args[i][0] || 0 : args[i]; let last = Array.isArray(args[i]) ? args[i][1] && args[i][1] + 1 || mat[0].length : args[i] + 1; if (!Array.isArray(prev)) { result[prev].fill(value, first, last); } else { for (let ind = start; ind < end; ind++) { result[ind].fill(value, first, last); } } } return result; } /** * Operates on two matrices of the same size * * @param mat input matrix that initialized the function * @param operand second matrix with which operation is performed * @param operation function performing the desired operation * @returns result of the operation */ function operate(mat, operand, operation) { let result = []; let op = operand(); for (let i = 0; i < mat.length; i++) { let op1 = mat[i]; let op2 = op[i]; result.push(op1.map(function(elem, ind) { return operation(elem, op2[ind]); })); } return result; } /** * Finds the product of two matrices * * @param mat input matrix that initialized the function * @param operand second matrix with which operation is performed * @returns the product of the two matrices */ function prod(mat, operand) { let op1 = mat; let op2 = operand(); let size1 = size(op1); let size2 = size(op2); let result = []; if (size1[1] === size2[0]) { for (let i = 0; i < size1[0]; i++) { result[i] = []; for (let j = 0; j < size2[1]; j++) { for (let k = 0; k < size1[1]; k++) { if (result[i][j] === undefined) { result[i][j] = 0; } result[i][j] += multiplication(op1[i][k], op2[k][j]); } } } } return result; } /** * Returns the transpose of a matrix, swaps rows with columns * * @param mat input matrix that initialized the function * @returns a matrix with rows and columns swapped from the original matrix */ function trans(mat) { let input = mat; let s = size(mat); let output = []; for (let i = 0; i < s[0]; i++) { for (let j = 0; j < s[1]; j++) { if (Array.isArray(output[j])) { output[j].push(input[i][j]); } else { output[j] = [input[i][j]]; } } } return output; } /** * Private method to clone the matrix * * @param mat input matrix that initialized the function * @returns cloned matrix */ function clone(mat) { let result = []; for (let i = 0; i < mat.length; i++) { result.push(mat[i].slice(0)); } return result; } /** * Performs addition * * @param op1 first operand * @param op2 second operand * @returns result */ function addition(op1, op2) { return op1 + op2; } /** * Performs subtraction * * @param op1 first operand * @param op2 second operand * @returns result */ function subtraction(op1, op2) { return op1 - op2; } /** * Performs multiplication * * @param op1 first operand * @param op2 second operand * @returns result */ function multiplication(op1, op2) { return op1 * op2; } /** * Performs division * * @param op1 first operand * @param op2 second operand * @returns result */ function division(op1, op2) { return op1/op2; } /** * Computes the determinant using row reduced echelon form * Works best if the elements are integers or rational numbers * The matrix must be a square * * @param mat input matrix that initialized the function * @returns determinant value as a number */ function determinant(mat) { let rationalized = rationalize(mat); let siz = size(mat); let det = rational(1); let sign = 1; for (let i = 0; i < siz[0] - 1; i++) { for (let j = i + 1; j < siz[0]; j++) { if (rationalized[j][i].num === 0) { continue; } if (rationalized[i][i].num === 0) { interchange(rationalized, i, j); sign = -sign; continue; } let temp = rationalized[j][i].div(rationalized[i][i]); temp = rational(Math.abs(temp.num), temp.den); if (Math.sign(rationalized[j][i].num) === Math.sign(rationalized[i][i].num)) { temp = rational(-temp.num, temp.den); } for (let k = 0; k < siz[1]; k++) { rationalized[j][k] = temp.mul(rationalized[i][k]).add(rationalized[j][k]); } } } det = rationalized.reduce((prev, curr, index) => prev.mul(curr[index]), rational(1)); return sign * det.num/det.den; } /** * Interchanges one row index with another on passed matrix * * @param mat input matrix * @param ind1 one of the row indices to exchange * @param ind2 one of the row indices to exchange */ function interchange(mat, ind1, ind2) { let temp = mat[ind1]; mat[ind1] = mat[ind2]; mat[ind2] = temp; } /** * Inverts the input square matrix using row reduction technique * Works best if the elements are integers or rational numbers * The matrix has to be a square and non-singular * * @param mat input matrix * @returns inverse of the input matrix */ function invert(mat) { let rationalized = rationalize(mat); let siz = size(mat); let result = rationalize(generate(1).diag(siz[0])); // row reduction let i = 0; let j = 0; while (j < siz[0]) { if (rationalized[i][j].num === 0) { for (let k = i + 1; k < siz[0]; k++) { if (rationalized[k][j].num !== 0) { interchange(rationalized, i, k); interchange(result, i, k); } } } if (rationalized[i][j].num !== 0) { if (rationalized[i][j].num !== 1 || rationalized[i][j].den !== 1) { let factor = rational(rationalized[i][j].num, rationalized[i][j].den); for (let col = 0; col < siz[1]; col++) { rationalized[i][col] = rationalized[i][col].div(factor); result[i][col] = result[i][col].div(factor); } } for (let k = i + 1; k < siz[0]; k++) { let temp = rationalized[k][j]; for (let col = 0; col < siz[1]; col++) { rationalized[k][col] = rationalized[k][col].sub(temp.mul(rationalized[i][col])); result[k][col] = result[k][col].sub(temp.mul(result[i][col])); } } } i += 1; j += 1; } // Further reduction to convert rationalized to identity let last = siz[0] - 1; if (rationalized[last][last].num !== 1 || rationalized[last][last].den !== 1) { let factor = rational(rationalized[last][last].num, rationalized[last][last].den); for (let col = 0; col < siz[1]; col++) { rationalized[last][col] = rationalized[last][col].div(factor); result[last][col] = result[last][col].div(factor); } } for (let i = siz[0] - 1; i > 0; i--) { for (let j = i - 1; j >= 0; j--) { let temp = rational(-rationalized[j][i].num, rationalized[j][i].den); for (let k = 0; k < siz[1]; k++) { rationalized[j][k] = temp.mul(rationalized[i][k]).add(rationalized[j][k]); result[j][k] = temp.mul(result[i][k]).add(result[j][k]); } } } return derationalize(result); } /** * Applies a given function over the matrix, elementwise. Similar to Array.map() * The supplied function is provided 4 arguments: * the current element, * the row index, * the col index, * the matrix. * * @param mat input matrix * @returns matrix of same dimensions with values altered by the function. */ function map(mat, func) { const s = size(mat); const result = []; for (let i = 0; i < s[0]; i++) { if(Array.isArray(mat[i])) { result[i] = []; for (let j = 0; j < s[1]; j++) { result[i][j] = func(mat[i][j], [i, j], mat); } } else { result[i] = func(mat[i], [i, 0], mat); } } return result; } /** * Converts a matrix of numbers to all rational type objects * * @param mat input matrix * @returns matrix with elements of type rational */ function rationalize(mat) { let rationalized = []; mat.forEach((row, ind) => { rationalized.push(row.map((ele) => rational(ele))); }); return rationalized; } /** * Converts a rationalized matrix to all numerical values * * @param mat input matrix * @returns matrix with numerical values */ function derationalize(mat) { let derationalized = []; mat.forEach((row, ind) => { derationalized.push(row.map((ele) => ele.num/ele.den)); }); return derationalized; } /** * Checks the equality of two matrices * @param mat input matrix * @param operand second matrix */ function equals(mat, operand) { let op1 = mat; let op2 = operand(); let size1 = size(op1); let size2 = size(op2); if (!size1.every((val, ind) => val === size2[ind])) { return false; } return op1.every((val, ind1) => val.every((ele, ind2) => Math.abs(ele - op2[ind1][ind2]) < 1e-10)); }