UNPKG

@allmaps/transform

Version:

Coordinate transformation functions

220 lines (219 loc) 10 kB
import { arrayMatrixSize, newArrayMatrix, newBlockArrayMatrix, pasteArrayMatrix, transposeArrayMatrix } from '@allmaps/stdlib'; import { Polynomial1 } from './Polynomial1.js'; import { BaseIndependentLinearWeightsTransformation } from './BaseIndependentLinearWeightsTransformation.js'; import { solveIndependentlyInverse } from '../shared/solve-functions.js'; /** * 2D Radial Basis Functions transformation * * See notebook https://observablehq.com/d/0b57d3b587542794 for code source and explanation */ export class RBF extends BaseIndependentLinearWeightsTransformation { kernelFunction; normFunction; epsilon; coefsArrayMatrices; coefsArrayMatrix; coefsArrayMatricesSize; coefsArrayMatrixSize; weightsArrays; rbfWeightsArrays; affineWeightsArrays; constructor(sourcePoints, destinationPoints, kernelFunction, normFunction, type, epsilon) { super(sourcePoints, destinationPoints, type, 3); this.kernelFunction = kernelFunction; this.normFunction = normFunction; this.epsilon = epsilon; // Note: getCoefsArrayMatrices can not be moved to the parent class's constructor // since for this class it uses properties (normFunction, kernelFunction, epsilon) // which are only defined after super() this.coefsArrayMatrices = this.getCoefsArrayMatrices(); this.coefsArrayMatrix = this.coefsArrayMatrices[0]; this.coefsArrayMatricesSize = this.coefsArrayMatrices.map((coefsArrayMatrix) => arrayMatrixSize(coefsArrayMatrix)); this.coefsArrayMatrixSize = arrayMatrixSize(this.coefsArrayMatrix); } getDestinationPointsArrays() { return [ [...this.destinationPoints, [0, 0], [0, 0], [0, 0]].map((value) => value[0]), [...this.destinationPoints, [0, 0], [0, 0], [0, 0]].map((value) => value[1]) ]; } getCoefsArrayMatrix() { // Pre-compute kernelsArrayMatrix: fill normsArrayMatrix // with the point to point distances between all control points const normsArrayMatrix = newArrayMatrix(this.pointCount, this.pointCount, 0); for (let i = 0; i < this.pointCount; i++) { for (let j = 0; j < this.pointCount; j++) { normsArrayMatrix[i][j] = this.normFunction(this.sourcePoints[i], this.sourcePoints[j]); } } // If it's not provided, and if it's an input to the kernelFunction, // compute epsilon as the average distance between the control points if (this.epsilon === undefined) { const normsSum = normsArrayMatrix .map((row) => row.reduce((a, c) => a + c, 0)) .reduce((a, c) => a + c, 0); this.epsilon = normsSum / (Math.pow(this.pointCount, 2) - this.pointCount); } // Finish the computation of kernelsArrayMatrix by applying the requested kernel function const kernelCoefsArrayMatrix = newArrayMatrix(this.pointCount, this.pointCount, 0); for (let i = 0; i < this.pointCount; i++) { for (let j = 0; j < this.pointCount; j++) { kernelCoefsArrayMatrix[i][j] = this.kernelFunction(normsArrayMatrix[i][j], { epsilon: this.epsilon }); } } // Construct Nx3 affineCoefsArrayMatrix // 1 x0 y0 // 1 x1 y1 // 1 x2 y2 // ... let affineCoefsArrayMatrix = newArrayMatrix(this.pointCount, 3, 0); for (let i = 0; i < this.pointCount; i++) { affineCoefsArrayMatrix = pasteArrayMatrix(affineCoefsArrayMatrix, i, 0, [ Polynomial1.getPolynomial1SourcePointCoefsArray(this.sourcePoints[i]) ]); } // Construct 3x3 zerosArrayMatrix const zerosArrayMatrix = newArrayMatrix(3, 3, 0); // Combine kernelsArrayMatrix and affineCoefsArrayMatrix // into new coefsArrayMatrix, to include the affine transformation const coefsArrayMatrix = newBlockArrayMatrix([ [kernelCoefsArrayMatrix, affineCoefsArrayMatrix], [transposeArrayMatrix(affineCoefsArrayMatrix), zerosArrayMatrix] ]); return coefsArrayMatrix; } /** * Get 1x(N+3) coefsArray, populating the (N+3)x(N+3) coefsArrayMatrix * * The coefsArray has a 1xN kernel part and a 1x3 affine part. * * @param sourcePoint */ getSourcePointCoefsArray(sourcePoint) { return [ ...this.getRbfKernelSourcePointCoefsArray(sourcePoint), ...Polynomial1.getPolynomial1SourcePointCoefsArray(sourcePoint) ]; } getRbfKernelSourcePointCoefsArray(sourcePoint) { const kernelSourcePointCoefsArray = []; for (let i = 0; i < this.pointCount; i++) { kernelSourcePointCoefsArray.push(this.kernelFunction(this.normFunction(this.sourcePoints[i], sourcePoint), { epsilon: this.epsilon })); } return kernelSourcePointCoefsArray; } setWeightsArrays(weightsArrays, epsilon) { if (epsilon) { this.epsilon = epsilon; } super.setWeightsArrays(weightsArrays); } /** * Solve the x and y components independently. * * This uses the exact inverse to compute (for each component, using the same coefs for both) * the exact solution for the system of linear equations * which is (in general) invertable to an exact solution. * * This wil result in a weights array for each component with rbf weights and affine weights. */ solve() { this.weightsArrays = solveIndependentlyInverse(this.coefsArrayMatrix, this.destinationPointsArrays); this.processWeightsArrays(); } processWeightsArrays() { if (!this.weightsArrays) { throw new Error('Weights not computed'); } this.rbfWeightsArrays = this.weightsArrays.map((array) => array.slice(0, this.pointCount)); this.affineWeightsArrays = this.weightsArrays.map((array) => array.slice(this.pointCount)); } evaluateFunction(newSourcePoint) { if (!this.weightsArrays) { this.solve(); } if (!this.rbfWeightsArrays || !this.affineWeightsArrays) { throw new Error('RBF weights not computed'); } const rbfWeights = this.rbfWeightsArrays; const affineWeights = this.affineWeightsArrays; // Compute the distances of that point to all control points const newDistances = this.sourcePoints.map((sourcePoint) => this.normFunction(newSourcePoint, sourcePoint)); // Sum the weighted contributions of the input point const newDestinationPoint = [0, 0]; for (let i = 0; i < 2; i++) { // Apply the weights to the new distances newDestinationPoint[i] = newDistances.reduce((sum, dist, index) => sum + this.kernelFunction(dist, { epsilon: this.epsilon }) * rbfWeights[i][index], 0); // Add the affine part newDestinationPoint[i] += affineWeights[i][0] + affineWeights[i][1] * newSourcePoint[0] + affineWeights[i][2] * newSourcePoint[1]; } return newDestinationPoint; } evaluatePartialDerivativeX(newSourcePoint) { if (!this.weightsArrays) { this.solve(); } if (!this.rbfWeightsArrays || !this.affineWeightsArrays) { throw new Error('RBF weights not computed'); } const rbfWeights = this.rbfWeightsArrays; const affineWeights = this.affineWeightsArrays; // Compute the distances of that point to all control points const newDistances = this.sourcePoints.map((sourcePoint) => this.normFunction(newSourcePoint, sourcePoint)); // Sum the weighted contributions of the input point const newDestinationPointPartDerX = [0, 0]; for (let i = 0; i < 2; i++) { // Apply the weights to the new distances newDestinationPointPartDerX[i] = newDistances.reduce((sum, dist, index) => sum + (dist === 0 ? 0 : this.kernelFunction(dist, { derivative: 1, epsilon: this.epsilon }) * ((newSourcePoint[0] - this.sourcePoints[index][0]) / dist) * rbfWeights[i][index]), 0); // Add the affine part newDestinationPointPartDerX[i] += affineWeights[i][1]; } return newDestinationPointPartDerX; } evaluatePartialDerivativeY(newSourcePoint) { if (!this.weightsArrays) { this.solve(); } if (!this.rbfWeightsArrays || !this.affineWeightsArrays) { throw new Error('RBF weights not computed'); } const rbfWeights = this.rbfWeightsArrays; const affineWeights = this.affineWeightsArrays; // Compute the distances of that point to all control points const newDistances = this.sourcePoints.map((sourcePoint) => this.normFunction(newSourcePoint, sourcePoint)); // Sum the weighted contributions of the input point const newDestinationPointPartDerY = [0, 0]; for (let i = 0; i < 2; i++) { // Apply the weights to the new distances newDestinationPointPartDerY[i] = newDistances.reduce((sum, dist, index) => sum + (dist === 0 ? 0 : this.kernelFunction(dist, { derivative: 1, epsilon: this.epsilon }) * ((newSourcePoint[1] - this.sourcePoints[index][1]) / dist) * rbfWeights[i][index]), 0); // Add the affine part newDestinationPointPartDerY[i] += affineWeights[i][2]; } return newDestinationPointPartDerY; } }