UNPKG

node-red-contrib-prib-functions

Version:
699 lines (696 loc) 24.8 kB
const logger = new (require("node-red-contrib-logger"))("Matrix"); logger.sendInfo("Copyright 2022 Jaroslav Peter Prib"); const typedArrays= {Array:Array,Int8Array:Int8Array,Uint8Array:Uint8Array,Uint8ClampedArray:Uint8ClampedArray,Int16Array:Int16Array,Uint16Array:Uint16Array, Int32Array:Int32Array,Uint32Array:Uint32Array,Float32Array:Float32Array, Float64Array:Float64Array,BigInt64Array:BigInt64Array,BigUint64Array:BigUint64Array} Object.keys(typedArrays).map(t=>typedArrays[t]).forEach(object=>{ if(object.prototype.setAll==null) Object.defineProperty(object.prototype, "setAll", { value(call,size=this.length) { let i=size while(i) this[--i]=call(); return this; }, writable: true, configurable: true }) if(object.prototype.setOne==null) Object.defineProperty(object.prototype, "setOne", { value(call,size=this.length) { let i=size while(i) this[--i]=1; return this; }, writable: true, configurable: true }) if(object.prototype.setZero==null) Object.defineProperty(object.prototype, "setZero", { value(call,size=this.length) { let i=size while(i) this[--i]=0; return this; }, writable: true, configurable: true }) if(object.prototype.setRandom==null) Object.defineProperty(object.prototype, "setRandom", { value(size=this.length) { return this.setAll(Math.random,size) }, writable: true, configurable: true }) }) const zeroFloat32Value=1e-6; function Matrix(rows,columns,fill,dataType="Float32Array") { this.dataType=dataType if(rows instanceof Array) { this.rows=rows.length; if(this.rows==0) throw Error("expected rows") const row=rows[0]; if(!row instanceof Array) throw Error("expected row to be array of columns") this.columns=row.length; if(this.columns==0) throw Error("expected Columns") this.createVector().fillArray(rows); return this; } if(rows instanceof Object) { Object.assign(this,rows); this.dataType??="Float32Array" } else { this.rows=rows; this.columns=columns; } if(this.columns) this.columns=parseInt(this.columns); if(this.rows)this.rows=parseInt(this.rows) if(this.rowsMax) this.rowsMax=parseInt(this.rowsMax) this.createVector(); if(fill) { if(fill instanceof Function) this.setAll(fill) else this.vector.set(fill) } return this; } Matrix.prototype.add=function(matrix){ this.forEachCellPairSet(matrix,(cellA,cellB)=>cellA+cellB) return this; } Matrix.prototype.addCell=function(row,column,value){ this.vector[this.getIndex(row,column)]+=value; return this; } Matrix.prototype.addRow=function(row,vectorIn){ if(vectorIn & row>=0){ this.forRowCells(row,(value,column,offset,vector)=>vector[offset]+=vectorIn[column]); return this; } const vector=vectorIn?vectorIn:row; if(this.size==this.sizeMax){ this.vector.copyWithin(0,this.columns,this.sizeMax); this.rows--; this.size-=this.columns; } this.vector.set(vector, this.rows*this.columns); this.rows++; this.size+=this.columns; return this; } Matrix.prototype.addRow2Row=function(rowA,rowB,factor=1,startColumn=0,endColumn=this.columns-1){ const diff=(rowA-rowB)*this.columns; this.forRowCells(rowB,(value,column,offset,vector)=>vector[offset]+=vector[offset+diff]*factor,startColumn,endColumn); return this; } Matrix.prototype.backwardSubstitution=function(){ const vector=new typedArrays[this.dataType](this.rows); for(let row=this.rows-1; row>=0; row--) { vector[row] = this.get(row,this.rows); for(let column=row+1; column<this.rows; column++) { vector[row] -= this.get(row,column)*vector[column]; } vector[row] /= this.get(row,row); } return vector; } Matrix.prototype.clone=function(){ const matrix= new Matrix({rows:this.rows,columns:this.columns,size:this.size,sizeMax:this.sizeMax}) matrix.vector.set(this.vector,0); return matrix } Matrix.prototype.consoleLog=function(label){ console.log({label:label,rows:this.rows,columns:this.columns,size:this.size,sizeMax:this.sizeMax}) return this; } Matrix.prototype.createLike=function(matrix){ return new Matrix({rows:this.rows,columns:this.columns,size:this.size,sizeMax:this.sizeMax}) } Matrix.prototype.createForEachCellPairSet=function(matrix,call){ const result=this.createLike(); if(matrix instanceof Matrix){ if(this.rows!=matrix.rows) throw Error("number of rows different"); if(this.columns!=matrix.columns) throw Error("number of columns different"); for(let offset=0;offset<this.size;offset++) result.vector[offset]=call.apply(this,[this.vector[offset],matrix.vector[offset]]); return result; } if(this.rows!=matrix.length) throw Error("number of rows different"); if(this.columns!=matrix[0].length) throw Error("number of columns different"); for(let offset=0,row=0;row<this.rows;row++) { for(let column=0;column<this.columns;column++) { result.vector[offset]=call.apply(this,[this.vector[offset],matrix[row][column]]); offset++ } } return result; } Matrix.prototype.createVector=function(){ if(this.size==null) { if(this.rows==null){ if(this.rowsMax==null) throw Error("rows or rowsMax not specified") this.rows=0; this.sizeMax=this.rowsMax*this.columns; } if(this.columns==null) throw Error("columns not specified") } else { if(this.columns==null) throw Error("columns not specified") if(this.columns==0) throw Error("columns = 0") if(this.rows==null){ this.rows=0; } } this.size=this.rows*this.columns if(this.sizeMax==null) this.sizeMax=this.size if(this.sizeMax==null) throw Error("max size not specified or calculated") this.vector=new typedArrays[this.dataType](this.sizeMax); return this; } Matrix.prototype.divideCell=function(row,column,value){ this.vector[this.getIndex(row,column)]/=value; return this; } Matrix.prototype.divideRow=function(row,factor,startColumn=0,endColumn=this.columns-1){ this.determinant/=factor; this.forRowCells(row,(value,column,offset,vector)=>vector[offset]/=factor,startColumn,endColumn); return this; } Matrix.prototype.equalsNearly=function(matrix,precision=6){ const thisObject=this; if(matrix instanceof Matrix){ if(this.rows!=matrix.rows) throw Error("rows counts not equal actual: "+this.rows+" expected: "+matrix.rows) if(this.columns!=matrix.columns) throw Error("columns counts not equal actual: "+this.columns+" expected: "+matrix.columns) this.forEachCell((value,row,column,vector,offset)=>{ try{ thisObject.equalsNearlyValues(value,matrix.vector[offset],precision) } catch(ex) { throw Error("row: "+row+" column: "+column+", cell values "+ex.message) } }); } else { if(this.rows!=matrix.length) throw Error("rows counts not equal actual: "+this.rows+" expected: "+matrix.length) if(this.columns!=matrix[0].length) throw Error("columns counts not equal actual: "+this.columns+" expected: "+matrix[0].length) this.forEachCell((value,row,column)=>{ try{ thisObject.equalsNearlyValues(value,matrix[row][column],precision) } catch(ex) { throw Error("row: "+row+" column: "+column+", cell values "+ex.message) } }); } return this; } Matrix.prototype.equalsNearlyValues=function(x,y,precision=6){ if(x==0){ if(y.toFixed(precision)==0) return this; } else { if((y/x).toFixed(precision)==1) return this; } throw Error(x+" != "+y); return this; } Matrix.prototype.equalsNearlyVector=function(vector,precision=6,baseVector=this.vector){ baseVector.forEach((v,i,a)=>{ try{ this.equalsNearlyValues(v,vector[i],precision) } catch(ex) { throw Error('not equal, index '+i+' left: '+v+' right: '+vector[i]); } }) return this; } Matrix.prototype.fill=function(value, start, end){ this.vector.fill(value, start, end); return this; } Matrix.prototype.fillArray=function(a){ const matrix=this; a.forEach((columns)=>matrix.addRow(columns)) return this; } Matrix.prototype.findColumnRow=function(column,call,startRow=0,endRow=this.rows-1){ let offset=startRow*this.columns+column; for(let row=startRow;row<=this.rows;row++){ if(call.apply(this,[this.vector[offset],row,column,offset,this.vector])) { return row; } offset+=this.columns; } return -1; } Matrix.prototype.findRowColumn=function(row,call,startColumn=0,endColumn=this.columns-1){ let offset=row*this.columns+startColumn; for(let column=startColumn;column<=endColumn;column++){ if(call.apply(this,[this.vector[offset],column,offset,this.vector])) return column; offset++; } return -1; } Matrix.prototype.forColumnCells=function(column,call,startRow=0,endRow=this.rows-1){ let offset=startRow*this.columns+column; for(let row=startRow;row<=endRow;row++){ call.apply(this,[this.vector[offset],row,offset,this.vector]); offset+=this.columns; } return this; } Matrix.prototype.forEachCell=function(call){ for(let offset=0,row=0;row<this.rows;row++) { for(let column=0;column<this.columns;column++) { call.apply(this,[this.vector[offset],row,column,this.vector,offset,this]); offset++ } } return this; } Matrix.prototype.forEachCellLowerTriangle=function(call){ this.testIsSquare(); for(let row=0;row<this.rows;row++) { let offset=row*this.columns; for(let column=0;column<=row;column++) { call.apply(this,[this.vector[offset],row,column,this.vector,offset,this]); offset++ } } return this; } Matrix.prototype.forEachCellPairSet=function(matrix,call){ if(matrix instanceof Matrix){ if(this.rows!=matrix.rows) throw Error("number of rows different"); if(this.columns!=matrix.columns) throw Error("number of columns different"); for(let offset=0;offset<this.size;offset++) this.vector[offset]=call.apply(this,[this.vector[offset],matrix.vector[offset]]); return this; } if(this.rows!=matrix.length) throw Error("number of rows different"); if(this.columns!=matrix[0].length) throw Error("number of columns different"); for(let offset=0,row=0;row<this.rows;row++) { for(let column=0;column<this.columns;column++) { this.vector[offset]=call.apply(this,[this.vector[offset],matrix[row][column]]); offset++ } } return this; } Matrix.prototype.forEachCellUpperTriangle=function(call){ this.testIsSquare(); for(let row=0;row<this.rows;row++) { let offset=row*this.columns; for(let column=row;column<=this.columns;column++) { call.apply(this,[this.vector[offset],row,column,this.vector,offset,this]); offset++ } } return this; } Matrix.prototype.forEachRow=function(call){ for(let row=0;row<this.rows;row++) { const rowVector=this.getRow(row); call.apply(this,[rowVector,row,this]) } return this; } Matrix.prototype.forRowCells=function(row,call,startColumn=0,endColumn=this.columns-1){ let offset=row*this.columns+startColumn; for(let column=startColumn;column<=endColumn;column++){ call.apply(this,[this.vector[offset],column,offset,this.vector]); offset++; } return this; } Matrix.prototype.forwardElimination=function(){ let maxValue,maxRow; for(let pivotRowColumn=0; pivotRowColumn<this.rows; pivotRowColumn++) { maxRow=pivotRowColumn; // Initialize maximum value and index for pivot maxValue=Math.abs(this.get(maxRow,pivotRowColumn)); for(let row=pivotRowColumn+1; row<this.rows; row++){ const value=Math.abs(this.get(row,pivotRowColumn)) if(value>maxValue) { maxValue=value; maxRow=row; } } // if a principal diagonal element is zero then matrix is singular + division-by-zero later if(this.get(pivotRowColumn,maxRow)==0) { throw Error("Singular matrix, "+(this.get(pivotRowColumn,this.columns)==0?"may have infinitely many solutions":"inconsistent system")) } if(maxRow!=pivotRowColumn) this.swapRows(pivotRowColumn, maxRow); pivotValue=this.get(pivotRowColumn,pivotRowColumn); for(let row=pivotRowColumn+1; row<this.rows; row++){ const factor=this.get(row,pivotRowColumn)/pivotValue; for(let column=pivotRowColumn+1; column<this.columns; column++){ this.subtractCell(row,column,this.get(pivotRowColumn,column)*factor); } this.set(row,pivotRowColumn,0); } } return this; } Matrix.prototype.gaussianElimination=function(){ return this.forwardElimination().backwardSubstitution(); } Matrix.prototype.get=function(row, column){ return this.vector[row*this.columns+column]; } Matrix.prototype.getAdjoint=function(){ if(this.columns==1) return new Matrix([[1]]); const adjoint=this.createLike(); for(let offset=0,row=0;row<adjoint.rows;row++) { for(let column=0;column<adjoint.columns;column++) { const temp=this.getCofactor(column,row); // get reverse adjoint.vector[offset]=((row+column)%2==0)?temp.getDeterminant():-temp.getDeterminant(); offset++ } } return adjoint; } Matrix.prototype.getCofactor=function(cellRow,cellColumn){ const matrixSize=this.rows-1; const matrix=new Matrix(matrixSize,matrixSize) const startLength=cellColumn; const endLength=matrixSize-cellColumn; const columnOffsetPart2=cellColumn+1; let thisRowOffset=0;matrixRowOffset=0; for(let row=0;row<this.rows;row++) { if(row!==cellRow){ const vectorStart=this.vector.slice(thisRowOffset,thisRowOffset+startLength); const thisRowOffsetPart2=thisRowOffset+columnOffsetPart2; const vectorEnd=this.vector.slice(thisRowOffsetPart2,thisRowOffsetPart2+endLength); matrix.vector.set(vectorStart,matrixRowOffset); matrix.vector.set(vectorEnd,matrixRowOffset+cellColumn) matrixRowOffset+=matrixSize; //next rows } thisRowOffset+=this.rows } return matrix; } Matrix.prototype.getComplementMinor=function(cellRow, cellColumn){ const matrix = new Matrix(this.rows-1,this.columns-1); for(let row=0,column=0,sourceRow=0; sourceRow<this.rows; sourceRow++) { if(sourceRow==cellRow) continue; for(let sourceColumn=0; sourceColumn<this.columns; sourceColum++) { if(sourceColum==cellColumn) continue; // In the first sourceRow Data ellipsis of column matrix.set(row,column,this.get(sourceRow, sourceColumn)); column++; if(column >= matrix.columns ) { column=0; row++; } } } return matrix; } Matrix.prototype.getDeterminant=function(){ if(this.determinant) return this.determinant this.testIsSquare(); return this.setDeterminant(); } Matrix.prototype.getDeterminantUsingCofactor=function(){ this.determinant=0; if(this.rows>2) { let sign=1; for(let column=0; column<this.columns; column++) { this.determinant += sign*this.vector[column]*this.getCofactor(0,column).getDeterminantUsingCofactor(); sign=-sign; } } else { this.determinant=this.vector[0]*this.vector[3]- this.vector[1]*this.vector[2]; } return this.determinant; } Matrix.prototype.getDeterminantUsingRowEchelonForm=function(){ if(this.rows==1) return this.vector[0]; const matrix=this.clone(); matrix.determinant=parseFloat(1); //force use of float matrix.rowEchelonForm(); this.determinant=1/matrix.determinant; return this.determinant; } Matrix.prototype.getIdentity=function(){ const identity=this.createLike(); for(let offset=0;offset<identity.size;offset+=identity.columns+1) identity.vector[offset]=1; return identity; } Matrix.prototype.getIndex=function(row, column){ return row*this.columns+column; } Matrix.prototype.getInverse=Matrix.prototype.getInverseGaussJordan; Matrix.prototype.getInverseAdjointMethod=function(){ const determinant=this.getDeterminant(); if(determinant==0) throw Error("Singular matrix, can't find its inverse"); const adjoint=this.getAdjoint(); for(let offset=0;offset<adjoint.size;offset++) { adjoint.vector[offset]/=determinant; } return adjoint; } Matrix.prototype.getInverseGaussJordan=function(){ this.testIsSquare(); const matrix = new Matrix(this.rows, this.columns * 2); for (let row=0,offset=this.columns; row<this.rows; ++row) { matrix.setRow(this.getRow(row),row); matrix.vector[offset+row]=1; offset+=matrix.columns } matrix.reducedRowEchelonForm(); return matrix.getMatrix(0,this.columns,this.rows,this.columns); } Matrix.prototype.getMatrix=function(row,column,rows,columns){ const matrix = new Matrix(rows,columns); for(let matrixOffset=0,thisOffset=row*this.columns+column; row<this.rows; ++row,thisOffset+=this.columns,matrixOffset+=rows) { matrix.vector.set(this.vector.subarray(thisOffset,thisOffset+columns),matrixOffset) } return matrix; } Matrix.prototype.getRow=function(row){ const start=row*this.columns; return this.vector.subarray(start,start+this.columns); } Matrix.prototype.getVandermonde=function(vectorIn,columns){ const matrix = new Matrix(vectorIn.length,columns); matrix.forEachCell((cell,row,column,vector,offset,object)=>vector[offset]=vectorIn[row]**column) return matrix; } Matrix.prototype.getZeroed=function(row, column){ const offset=row*this.columns+column; const value=this.vector[offset]; if(value==0 || Math.abs(value)>zeroFloat32Value) return value; this.vector[offset]=0; return 0; } Matrix.prototype.maxAbsColumn=function(column,startRow=0){ let rowColumnOffset=startRow*this.columns+column; let maxValue=Math.abs(this.vector[rowColumnOffset]); rowColumnOffset+=this.columns; while (rowColumnOffset<this.size) { const value=Math.abs(this.vector[rowColumnOffset]) if(value>maxValue) { maxValue=value; maxRowColumnOffset=rowColumnOffset; }; rowColumnOffset+=this.columns; } return {row:(maxRowColumnOffset-column)/this.columns,value:this.vector[maxRowColumnOffset]} } Matrix.prototype.maxColumn=function(column,startRow=0){ let rowColumnOffset=startRow*this.columns+column; let maxValue=this.vector[rowColumnOffset]; rowColumnOffset+=this.columns; while (rowColumnOffset<this.size) { const value=this.vector[rowColumnOffset]; if(value>maxValue) { maxValue=value; maxRowColumnOffset=rowColumnOffset; }; rowColumnOffset+=this.columns; } return {row:(maxRowColumnOffset-column)/this.columns,value:this.vector[maxRowColumnOffset]} } Matrix.prototype.multiply=function(rightMatrix){ const leftMatrix=this; if(leftMatrix.columns!=rightMatrix.rows) throw Error(leftMatrix.columns+ "columns != "+rightMatrix.rows+" rows of argument"); const result=new Matrix({rows:leftMatrix.rows,columns:rightMatrix.columns}) result.forEachCell((cell,row,column)=>{ const value=leftMatrix.reduceRow(row, (value,rowCell,leftRow,leftColumn)=> value+rowCell*rightMatrix.get(leftColumn,column) ); result.set(row,column,value); }) return result; } Matrix.prototype.multiplyRow=function(row,factor){ this.determinant*=factor; this.forRowCells(row,(cell,column,offset,vector)=>vector[offset]*=factor) return this; } Matrix.prototype.norm=function(){ return Math.sqrt(this.reduce((aggregate,cell)=>aggregate+cell*cell)) } Matrix.prototype.reduce=function(call,aggregate=0){ this.forEachCell((cell,row,column,vector,offset,object)=> aggregate=call.apply(this,[aggregate,cell,row,column,vector,offset,object]) ); return aggregate; } Matrix.prototype.reducedRowEchelonForm=function(){ const lastColumn=this.columns-1; for(let pivotColumn=0,pivotRow=0;pivotRow<this.rows;pivotRow++){ if(this.rows<=pivotRow) throw Error("logic error") let row=pivotRow; while(this.getZeroed(row,pivotColumn)==0){ row++ if(this.rows==row){ row=pivotRow; pivotColumn++ if(pivotColumn>=this.columns) { if(row==this.rows-1) return this; throw Error("row all zeroes which is not last row") } } } if(row!==pivotRow) this.swapRows(row,pivotRow) const factor=this.get(pivotRow,pivotColumn) for(let column=pivotColumn;column<this.columns;column++) this.divideCell(pivotRow,column,factor) if(pivotColumn==lastColumn) break; for(let row=0;row<this.rows;row++){ if(row==pivotRow) continue; const factor=-this.getZeroed(row,pivotColumn); if(factor==0) continue; for(let column=pivotColumn;column<this.columns;column++){ const value=this.get(pivotRow,column); if(value) this.addCell(row,column,factor*value) } } if(++pivotColumn>this.columns) break; } return this; } Matrix.prototype.rowEchelonForm=function(){ const columns=this.columns; let pivotRow=0,pivotColumn=0,lastRowAdj=this.rows-1; nextPivot: while(pivotRow<=lastRowAdj & pivotColumn<columns){ let pivotValue=this.getZeroed(pivotRow, pivotColumn); if(pivotValue==0) { const i=this.findRowColumn(pivotRow,value=>Math.abs(value)>zeroFloat32Value,pivotColumn+1,lastRowAdj) //none zeroe? if(i==-1){ if(pivotRow==lastRowAdj) { pivotColumn++; continue nextPivot; } this.swapRows(pivotRow,lastRowAdj--); if(lastRowAdj<=1) return this; continue nextPivot; } const row=this.findColumnRow(pivotColumn,value=>Math.abs(value)>zeroFloat32Value,pivotRow+1,lastRowAdj); if(row==-1) { pivotColumn++; continue nextPivot; } this.swapRows(pivotRow,row); pivotValue=this.get(pivotRow, pivotColumn); } this.divideRow(pivotRow,pivotValue,pivotColumn); for(let row=pivotRow+1; row<=lastRowAdj; row++) { const factor=-this.get(row, pivotColumn); this.addRow2Row(pivotRow,row,factor,pivotColumn); } pivotRow++; pivotColumn++; } return this; } Matrix.prototype.reduceRow=function(row,call,value=0){ this.forRowCells(row,(cell,column,offset,vector)=>{ value=call.apply(this,[value,cell,row,column]); }); return value; } Matrix.prototype.set=function(row,column,value){ this.vector[this.getIndex(row,column)]=value; return this; } Matrix.prototype.setAll=function(call){ for(let offset=0,row=0;row<this.rows;row++) { for(let column=0;column<this.columns;column++) { this.vector[offset]=call.apply(this,[row,column,this.vector[offset],this.vector,offset,this]); offset++ } } this.rows=this.rowsMax this.size=this.sizeMax return this; } Matrix.prototype.setDeterminant=function(){ this.determinant=this.getDeterminantUsingRowEchelonForm(); return this.determinant; } Matrix.prototype.setIdentity=function(){ if(this.columns!=this.rowsMax) throw Error("number of columns not equal rows") this.setZero() for(let offset=0;offset<this.size;offset+=this.columns+1) this.vector[offset]=1; return this; } Matrix.prototype.setWithFunction=function(f){ this.vector[f](this.sizeMax) this.rows=this.rowsMax this.size=this.sizeMax; return this; } Matrix.prototype.setOne=function(){ return this.setWithFunction("setOne") } Matrix.prototype.setRandom=function(){ return this.setWithFunction("setRandom") } Matrix.prototype.setRow=function(vector,row){ this.vector.set(vector, row*this.columns); return this; } Matrix.prototype.setRunningSum=function(){ this.forEachCellLowerTriangle((cell,row,column,vector,offset,object)=>vector[offset]=1); } Matrix.prototype.setZero=function(){ return this.setWithFunction("setZero") } Matrix.prototype.substract=function(matrix){ this.forEachCellPairSet(matrix,(cellA,cellB)=>cellA-cellB) return this; } Matrix.prototype.subtractCell=function(row,column,value){ this.vector[this.getIndex(row,column)]-=value; return this; } Matrix.prototype.sumRow=function(row){ return this.reduceRow(row,(value,cell)=>value+cell); } Matrix.prototype.swapRows=function(rowA,rowB){ this.determinant=-this.determinant; const startRowA=rowA*this.columns; const rowAVector=this.vector.slice(startRowA,startRowA+this.columns); const startRowB=rowB*this.columns this.vector.copyWithin(startRowA,startRowB,startRowB+this.columns); this.vector.set(rowAVector,startRowB); return this; } Matrix.prototype.testIsSquare=function(){ if(this.rows!=this.columns) throw Error("not square matrix"); return this; } Matrix.prototype.toArray=function(precision=6){ const result=[]; this.forEachRow((row,index)=>{ const columns=[]; row.forEach(value=>columns.push(value)) result.push(columns) }) return result; } Matrix.prototype.transpose=function(){ const matrix=new Matrix({rows:this.columns,columns:this.rows}) this.forEachCell((cell,row,column)=>matrix.set(column,row,cell)) return matrix; } module.exports=Matrix;