UNPKG

gis-tools-ts

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

569 lines 19.2 kB
import { averageInterpolation, defaultGetInterpolateCurrentValue } from './index.js'; import { lRGBToGamma, sRGBToLinear } from '../../index.js'; /** * # Kriging Distance Weighting Interpolation * * ## Description * Given a reference of data, interpolate a point using kriging distance weighting * Uses the {@link KrigingInterpolator} * * ## Usage * ```ts * import { krigingInterpolation, PointIndexFast } from 'gis-tools-ts'; * import type { VectorPoint } from 'gis-tools-ts'; * * // We have m-value data that we want to interpolate * interface TempData { temp: number; } * * const pointIndex = new PointIndexFast<TempData>(); * // add lots of points * pointIndex.insertLonLat(lon, lat, data); * // .... * * // given a point we are interested in * const point: VectorPoint = { x: 20, y: -40 }; * // get a collection of points relative to the point * const data = await pointIndex.searchRadius(point.x, point.y, radius); * * // interpolate * const interpolatedValue = krigingInterpolation<TempData>(point, data, (p) => p.m.temp); * ``` * @param point - point to interpolate * @param refData - reference data to interpolate from * @param getValue - function to get value from reference data. Can be the z value or a property in the m-values * defaults to function that returns the z value or 0 if the z value is undefined * @param model - kriging model * @param sigma2 - variance * @param alpha - diffuse * @returns - the interpolated value */ export function krigingInterpolation(point, refData, getValue = defaultGetInterpolateCurrentValue, model = 'gaussian', sigma2 = 0, alpha = 100) { const interpolator = new KrigingInterpolator(refData, model, getValue, sigma2, alpha); return interpolator.predict(point); } /** * Helper function for {@link krigingInterpolation} on RGB(A) data. * Light in RGB data is logarithmically weighted, so we need to expand each component by n^2 to * get the correct weight for each component. * @param point - Point to interpolate * @param refData - Reference data points * @param model - Kriging model * @param sigma2 - Variance * @param alpha - Diffuse * @returns - The interpolated RGBA data. */ export function rgbaKrigingInterpolation(point, refData, model = 'gaussian', sigma2 = 0, alpha = 100) { if (refData.length === 0) return { r: 0, g: 0, b: 0, a: 255 }; const rData = krigingInterpolation(point, refData, (p) => sRGBToLinear(p.m?.r ?? 0), model, sigma2, alpha); const gData = krigingInterpolation(point, refData, (p) => sRGBToLinear(p.m?.g ?? 0), model, sigma2, alpha); const bData = krigingInterpolation(point, refData, (p) => sRGBToLinear(p.m?.b ?? 0), model, sigma2, alpha); const a = averageInterpolation(point, refData, (p) => p.m?.a ?? 255); return { r: lRGBToGamma(rData), g: lRGBToGamma(gData), b: lRGBToGamma(bData), a, }; } /** * # Kriging Interpolator * * ## Description * Interpolation using the Kriging method. Find the best fit curve for a given set of data. * You can either compute the semivariance or the variogram (expected value). * * The various variogram models can be interpreted as kernel functions for 2-dimensional coordinates * a, b and parameters nugget, range, sill and A. Reparameterized as a linear function, with * w = [nugget, (sill-nugget)/range], this becomes: * - **Gaussian**: `k(a,b) = w[0] + w[1] * ( 1 - exp{ -( ||a-b|| / range )2 / A } )` * - **Exponential**: `k(a,b) = w[0] + w[1] * ( 1 - exp{ -( ||a-b|| / range ) / A } )` * - **Spherical**: `k(a,b) = w[0] + w[1] * ( 1.5 * ( ||a-b|| / range ) - 0.5 * ( ||a-b|| / range )3 )` * * Notice the σ2 (sigma2) and α (alpha) variables, these correspond to the variance parameters of * the gaussian process and the prior of the variogram model, respectively. A diffuse α prior is * typically used; a formal mathematical definition of the model is provided below. * * The variance parameter α of the prior distribution for w should be manually set, according to: * - `w ~ N(w|0, αI)` * * Using the fitted kernel function hyperparameters and setting K as the Gram matrix, the prior and * likelihood for the gaussian process become: * - `y ~ N(y|0, K)` * - `t|y ~ N(t|y, σ2I) * * ## Usage * ```ts * import { KrigingInterpolator } from 'gis-tools-ts'; * import type { VectorPoint } from 'gis-tools-ts'; * * // We have m-value data that we want to interpolate * interface TempData { temp: number; } * * // given a point we are interested in * const point: VectorPoint = { x: 20, y: -40 }; * // get a collection of points relative to the point * const data: VectorPoint<TempData>[] = [...]; * * // interpolate * const interpolator = new KrigingInterpolator<TempData>(data, 'gaussian', (p) => p.m.temp); * const interpolatedValue = interpolator.predict(point); * ``` * * ## Links * - https://pro.arcgis.com/en/pro-app/latest/tool-reference/3d-analyst/how-kriging-works.htm */ export class KrigingInterpolator { refData; t = []; nugget = 0; range = 0; sill = 0; A = 1 / 3; K = []; M = []; n = 0; model; /** * @param refData - reference data to interpolate from * @param model - kriging model * @param getValue - function to get value from reference data. Can be the z value or a property in the m-values * @param sigma2 - variance parameter of the gaussian model * @param alpha - diffuse α prior of the variogram model */ constructor(refData, model = 'gaussian', getValue = defaultGetInterpolateCurrentValue, sigma2 = 0, alpha = 100) { this.refData = refData; if (refData.length === 0) throw new Error('Reference data cannot be empty.'); const { abs, pow } = Math; this.t = refData.map((p) => getValue(p)); // setup model switch (model) { case 'exponential': this.model = krigingVariogramExponential; break; case 'spherical': this.model = krigingVariogramSpherical; break; case 'gaussian': default: this.model = krigingVariogramGaussian; break; } // Lag distance/semivariance let i, j, k, l; const { t } = this; let n = t.length; const distance = new Array((n * n - n) / 2); for (i = 0, k = 0; i < n; i++) { const { x: xi, y: yi } = this.refData[i]; for (j = 0; j < i; j++, k++) { const { x: xj, y: yj } = this.refData[j]; distance[k] = new Array(2); distance[k][0] = pow(pow(xi - xj, 2) + pow(yi - yj, 2), 0.5); distance[k][1] = abs(t[i] - t[j]); } } distance.sort((a, b) => a[0] - b[0]); this.range = distance[(n * n - n) / 2 - 1][0]; // Bin lag distance const lags = (n * n - n) / 2 > 30 ? 30 : (n * n - n) / 2; const tolerance = this.range / lags; const lag = rep([0], lags); const semi = rep([0], lags); if (lags < 30) { for (l = 0; l < lags; l++) { lag[l] = distance[l][0]; semi[l] = distance[l][1]; } } else { for (i = 0, j = 0, k = 0, l = 0; i < lags && j < (n * n - n) / 2; i++, k = 0) { while (distance[j][0] <= (i + 1) * tolerance) { lag[l] += distance[j][0]; semi[l] += distance[j][1]; j++; k++; if (j >= (n * n - n) / 2) break; } if (k > 0) { lag[l] /= k; semi[l] /= k; l++; } } if (l < 2) return; // Error: Not enough points } // feature transformation n = l; this.range = lag[n - 1] - lag[0]; const X = rep([1], 2 * n); const Y = new Array(n); const A = this.A; for (i = 0; i < n; i++) { switch (model) { case 'gaussian': X[i * 2 + 1] = 1.0 - Math.exp(-(1.0 / A) * Math.pow(lag[i] / this.range, 2)); break; case 'exponential': X[i * 2 + 1] = 1.0 - Math.exp((-(1.0 / A) * lag[i]) / this.range); break; case 'spherical': X[i * 2 + 1] = 1.5 * (lag[i] / this.range) - 0.5 * Math.pow(lag[i] / this.range, 3); break; } Y[i] = semi[i]; } // Least squares const Xt = krigingMatrixTranspose(X, n, 2); let Z = krigingMatrixMultiply(Xt, X, 2, n, 2); Z = krigingMatrixAdd(Z, krigingMatrixDiag(1 / alpha, 2), 2, 2); const cloneZ = Z.slice(0); if (krigingMatrixChol(Z, 2)) { krigingMatrixChol2inv(Z, 2); } else { const solved = krigingMatrixSolve(cloneZ, 2); if (!solved) throw Error('Cholesky decomposition failed'); Z = cloneZ; } const W = krigingMatrixMultiply(krigingMatrixMultiply(Z, Xt, 2, 2, n), Y, 2, n, 1); // Variogram parameters this.nugget = W[0]; this.sill = W[1] * this.range + this.nugget; n = this.n = this.refData.length; // Gram matrix with prior const K = new Array(n * n); for (i = 0; i < n; i++) { const { x: refiX, y: refiY } = this.refData[i]; for (j = 0; j < i; j++) { const { x: refjX, y: refjY } = this.refData[j]; K[i * n + j] = this.model(Math.pow(Math.pow(refiX - refjX, 2) + Math.pow(refiY - refjY, 2), 0.5), this.nugget, this.range, this.sill, this.A); K[j * n + i] = K[i * n + j]; } K[i * n + i] = this.model(0, this.nugget, this.range, this.sill, this.A); } // Inverse penalized Gram matrix projected to target vector let C = krigingMatrixAdd(K, krigingMatrixDiag(sigma2, n), n, n); const cloneC = C.slice(0); if (krigingMatrixChol(C, n)) { krigingMatrixChol2inv(C, n); } else { krigingMatrixSolve(cloneC, n); C = cloneC; } // Copy unprojected inverted matrix as K const Ks = C.slice(0); const M = krigingMatrixMultiply(C, t, n, n, 1); this.K = Ks; this.M = M; } /** * Model prediction * @param point - point to interpolate to * @returns - predicted value */ predict(point) { const { x, y } = point; const k = new Array(this.n); for (let i = 0; i < this.n; i++) { const { x: refX, y: refY } = this.refData[i]; k[i] = this.model(Math.pow(Math.pow(x - refX, 2) + Math.pow(y - refY, 2), 0.5), this.nugget, this.range, this.sill, this.A); } return krigingMatrixMultiply(k, this.M, 1, this.n, 1)[0]; } /** * Variance prediction * @param point - point to interpolate to * @returns - predicted variance */ variance(point) { const { x, y } = point; let i; const k = new Array(this.n); for (i = 0; i < this.n; i++) { const { x: refX, y: refY } = this.refData[i]; k[i] = this.model(Math.pow(Math.pow(x - refX, 2) + Math.pow(y - refY, 2), 0.5), this.nugget, this.range, this.sill, this.A); } return (this.model(0, this.nugget, this.range, this.sill, this.A) + krigingMatrixMultiply(krigingMatrixMultiply(k, this.K, 1, this.n, this.n), k, 1, this.n, 1)[0]); } } /** * Matrix diagonal algebra * @param c - diagonal value * @param n - matrix size * @returns - diagonal matrix */ function krigingMatrixDiag(c, n) { let i; const Z = rep([0], n * n); for (i = 0; i < n; i++) Z[i * n + i] = c; return Z; } /** * Matrix transpose * @param X - matrix * @param n - matrix row size * @param m - matrix column size * @returns - transposed matrix */ function krigingMatrixTranspose(X, n, m) { let i; let j; const Z = new Array(m * n); for (i = 0; i < n; i++) { for (j = 0; j < m; j++) { Z[j * n + i] = X[i * m + j]; } } return Z; } /** * Matrix addition * @param X - first matrix * @param Y - second matrix * @param n - matrix row size * @param m - matrix column size * @returns - added matrix */ function krigingMatrixAdd(X, Y, n, m) { let i; let j; const Z = new Array(n * m); for (i = 0; i < n; i++) { for (j = 0; j < m; j++) { Z[i * m + j] = X[i * m + j] + Y[i * m + j]; } } return Z; } /** * Naive matrix multiplication * @param X - matrix X * @param Y - matrix Y * @param n - matrix row size * @param m - matrix X column size * @param p - matrix Y column size * @returns - multiplied matrix */ function krigingMatrixMultiply(X, Y, n, m, p) { let i, j, k; const Z = new Array(n * p); for (i = 0; i < n; i++) { for (j = 0; j < p; j++) { Z[i * p + j] = 0; for (k = 0; k < m; k++) { Z[i * p + j] += X[i * m + k] * Y[k * p + j]; } } } return Z; } /** * Cholesky decomposition * @param X - matrix * @param n - matrix size * @returns - true if successful */ function krigingMatrixChol(X, n) { let i; let j; let k; const p = new Array(n); for (i = 0; i < n; i++) p[i] = X[i * n + i]; for (i = 0; i < n; i++) { for (j = 0; j < i; j++) { p[i] -= X[i * n + j] * X[i * n + j]; } if (p[i] <= 0) return false; p[i] = Math.sqrt(p[i]); for (j = i + 1; j < n; j++) { for (k = 0; k < i; k++) { X[j * n + i] -= X[j * n + k] * X[i * n + k]; } X[j * n + i] /= p[i]; } } for (i = 0; i < n; i++) X[i * n + i] = p[i]; return true; } /** * Inversion of cholesky decomposition * @param X - matrix * @param n - matrix size */ function krigingMatrixChol2inv(X, n) { let i, j, k, sum; for (i = 0; i < n; i++) { X[i * n + i] = 1 / X[i * n + i]; for (j = i + 1; j < n; j++) { sum = 0; for (k = i; k < j; k++) { sum -= X[j * n + k] * X[k * n + i]; } X[j * n + i] = sum / X[j * n + j]; } } for (i = 0; i < n; i++) { for (j = i + 1; j < n; j++) { X[i * n + j] = 0; } } for (i = 0; i < n; i++) { X[i * n + i] *= X[i * n + i]; for (k = i + 1; k < n; k++) { X[i * n + i] += X[k * n + i] * X[k * n + i]; } for (j = i + 1; j < n; j++) { for (k = j; k < n; k++) { X[i * n + j] += X[k * n + i] * X[k * n + j]; } } } for (i = 0; i < n; i++) { for (j = 0; j < i; j++) { X[i * n + j] = X[j * n + i]; } } } /** * Inversion via gauss-jordan elimination * @param X - matrix * @param n - matrix size * @returns - true if successful */ function krigingMatrixSolve(X, n) { const m = n; const b = new Array(n * n); const indxc = new Array(n); const indxr = new Array(n); const ipiv = new Array(n); let i, icol = 0, irow = 0, j, k, l, ll; let big, dum, pivinv, temp; for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { if (i === j) b[i * n + j] = 1; else b[i * n + j] = 0; } } for (j = 0; j < n; j++) ipiv[j] = 0; for (i = 0; i < n; i++) { big = 0; for (j = 0; j < n; j++) { if (ipiv[j] !== 1) { for (k = 0; k < n; k++) { if (ipiv[k] === 0) { if (Math.abs(X[j * n + k]) >= big) { big = Math.abs(X[j * n + k]); irow = j; icol = k; } } } } } ++ipiv[icol]; if (irow !== icol) { for (l = 0; l < n; l++) { temp = X[irow * n + l]; X[irow * n + l] = X[icol * n + l]; X[icol * n + l] = temp; } for (l = 0; l < m; l++) { temp = b[irow * n + l]; b[irow * n + l] = b[icol * n + l]; b[icol * n + l] = temp; } } indxr[i] = irow; indxc[i] = icol; if (X[icol * n + icol] === 0) return false; // Singular pivinv = 1 / X[icol * n + icol]; X[icol * n + icol] = 1; for (l = 0; l < n; l++) X[icol * n + l] *= pivinv; for (l = 0; l < m; l++) b[icol * n + l] *= pivinv; for (ll = 0; ll < n; ll++) { if (ll !== icol) { dum = X[ll * n + icol]; X[ll * n + icol] = 0; for (l = 0; l < n; l++) X[ll * n + l] -= X[icol * n + l] * dum; for (l = 0; l < m; l++) b[ll * n + l] -= b[icol * n + l] * dum; } } } for (l = n - 1; l >= 0; l--) { if (indxr[l] !== indxc[l]) { for (k = 0; k < n; k++) { temp = X[k * n + indxr[l]]; X[k * n + indxr[l]] = X[k * n + indxc[l]]; X[k * n + indxc[l]] = temp; } } } return true; } /** * Variogram Gaussian Model * @param h - distance * @param nugget - nugget * @param range - range * @param sill - sill * @param A - A * @returns - predicted gaussian value */ function krigingVariogramGaussian(h, nugget, range, sill, A) { return nugget + ((sill - nugget) / range) * (1.0 - Math.exp(-(1.0 / A) * Math.pow(h / range, 2))); } /** * Variogram Exponential Model * @param h - distance * @param nugget - nugget * @param range - range * @param sill - sill * @param A - A * @returns - predicted exponential value */ function krigingVariogramExponential(h, nugget, range, sill, A) { return nugget + ((sill - nugget) / range) * (1.0 - Math.exp(-(1.0 / A) * (h / range))); } /** * Variogram Spherical Model * @param h - distance * @param nugget - nugget * @param range - range * @param sill - sill * @param _A - A (unused) * @returns - predicted spherical value */ function krigingVariogramSpherical(h, nugget, range, sill, _A) { if (h > range) return nugget + (sill - nugget) / range; return nugget + ((sill - nugget) / range) * (1.5 * (h / range) - 0.5 * Math.pow(h / range, 3)); } /** * Creates a new array of n size that is all set to the first element of arr * @param arr - array * @param n - number * @returns - array all set to the first element of arr */ function rep(arr, n) { return Array(n).fill(arr[0]); } //# sourceMappingURL=kriging.js.map