|
| 1 | +import std / [math, algorithm, tables, sequtils, strutils] |
| 2 | +import arraymancer |
| 3 | +import ./utils |
| 4 | + |
| 5 | + |
| 6 | +type |
| 7 | + RbfFunc* = proc (r: Tensor[float], epsilon: float): Tensor[float] |
| 8 | + RbfBaseType*[T] = object |
| 9 | + points*: Tensor[float] # (n_points, n_dim) |
| 10 | + values*: Tensor[T] # (n_points, n_values) |
| 11 | + coeffs*: Tensor[float] # (n_points, n_values) |
| 12 | + epsilon*: float |
| 13 | + f*: RbfFunc |
| 14 | + |
| 15 | + RbfGrid*[T] = object |
| 16 | + indices*: seq[seq[int]] |
| 17 | + values*: Tensor[T] |
| 18 | + points*: Tensor[float] |
| 19 | + gridSize*, gridDim*: int |
| 20 | + gridDelta*: float |
| 21 | + |
| 22 | + RbfType*[T] = object |
| 23 | + limits*: tuple[upper: Tensor[float], lower: Tensor[float]] |
| 24 | + grid*: RbfGrid[RbfBaseType[T]] |
| 25 | + nValues*: int |
| 26 | + |
| 27 | +template km(point: Tensor[float], index: int, delta: float): int = |
| 28 | + int(ceil(point[0, index] / delta)) |
| 29 | + |
| 30 | +iterator neighbours*[T](grid: RbfGrid[T], k: int, searchLevels: int = 1): int = |
| 31 | + # TODO: Create product iterator that doesn't need to allocate 3^gridDim seqs |
| 32 | + let directions = @[toSeq(-searchLevels .. searchLevels)].cycle(grid.gridDim) |
| 33 | + for dir in product(directions): |
| 34 | + block loopBody: |
| 35 | + var kNeigh = k |
| 36 | + for i, x in dir: |
| 37 | + let step = grid.gridSize ^ (grid.gridDim - i - 1) |
| 38 | + for level in 1 .. searchLevels: |
| 39 | + if (k div step) mod grid.gridSize == level - 1 and x <= -level: |
| 40 | + break loopBody |
| 41 | + elif (k div step) mod grid.gridSize == grid.gridSize - level and x >= level: |
| 42 | + break loopBody |
| 43 | + kNeigh += x * step |
| 44 | + if kNeigh >= 0 and kNeigh < grid.gridSize ^ grid.gridDim: |
| 45 | + yield kNeigh |
| 46 | + |
| 47 | + |
| 48 | +iterator neighboursExcludingCenter*[T](grid: RbfGrid[T], k: int): int = |
| 49 | + for x in grid.neighbours(k): |
| 50 | + if x != k: |
| 51 | + yield x |
| 52 | + |
| 53 | +proc findIndex*[T](grid: RbfGrid[T], point: Tensor[float]): int = |
| 54 | + result = km(point, grid.gridDim - 1, grid.gridDelta) - 1 |
| 55 | + for i in 0 ..< grid.gridDim - 1: |
| 56 | + result += (km(point, i, grid.gridDelta) - 1) * grid.gridSize ^ (grid.gridDim - i - 1) |
| 57 | + |
| 58 | +proc constructMeshedPatches*[T](grid: RbfGrid[T]): Tensor[float] = |
| 59 | + meshgrid(@[arraymancer.linspace(0 + grid.gridDelta / 2, 1 - grid.gridDelta / 2, grid.gridSize)].cycle(grid.gridDim)) |
| 60 | + |
| 61 | +template dist2(p1, p2: Tensor[float]): float = |
| 62 | + var result = 0.0 |
| 63 | + for i in 0 ..< p1.shape[1]: |
| 64 | + let diff = p1[0, i] - p2[0, i] |
| 65 | + result += diff * diff |
| 66 | + result |
| 67 | + |
| 68 | +proc findAllWithin*[T](grid: RbfGrid[T], x: Tensor[float], rho: float): seq[int] = |
| 69 | + assert x.shape.len == 2 and x.shape[0] == 1 |
| 70 | + let index = grid.findIndex(x) |
| 71 | + let searchLevels = (rho / grid.gridDelta).ceil.int |
| 72 | + for k in grid.neighbours(index, searchLevels): |
| 73 | + for i in grid.indices[k]: |
| 74 | + if dist2(x, grid.points[i, _]) <= rho*rho: |
| 75 | + result.add i |
| 76 | + |
| 77 | +proc findAllBetween*[T](grid: RbfGrid[T], x: Tensor[float], rho1, rho2: float): seq[int] = |
| 78 | + assert x.shape.len == 2 and x.shape[0] == 1 |
| 79 | + assert rho2 > rho1 |
| 80 | + let index = grid.findIndex(x) |
| 81 | + let searchLevels = (rho2 / grid.gridDelta).ceil.int |
| 82 | + for k in grid.neighbours(index, searchLevels): |
| 83 | + for i in grid.indices[k]: |
| 84 | + let d = dist2(x, grid.points[i, _]) |
| 85 | + if rho1*rho1 <= d and d <= rho2*rho2: |
| 86 | + result.add i |
| 87 | + |
| 88 | +proc newRbfGrid*[T](points: Tensor[float], values: Tensor[T], gridSize: int = 0): RbfGrid[T] = |
| 89 | + let nPoints = points.shape[0] |
| 90 | + let nDims = points.shape[1] |
| 91 | + let gridSize = |
| 92 | + if gridSize > 0: |
| 93 | + gridSize |
| 94 | + else: |
| 95 | + max(int(round(pow(nPoints.float, 1 / nDims) / 2)), 1) |
| 96 | + let delta = 1 / gridSize |
| 97 | + result = RbfGrid[T](gridSize: gridSize, gridDim: nDims, gridDelta: delta, indices: newSeq[seq[int]](gridSize ^ nDims)) |
| 98 | + for row in 0 ..< nPoints: |
| 99 | + let index = result.findIndex(points[row, _]) |
| 100 | + result.indices[index].add row |
| 101 | + result.values = values |
| 102 | + result.points = points |
| 103 | + |
| 104 | +# Idea: blocked distance matrix for better cache friendliness |
| 105 | +proc distanceMatrix(p1, p2: Tensor[float]): Tensor[float] = |
| 106 | + ## Returns distance matrix of shape (n_points, n_points) |
| 107 | + let n_points1 = p1.shape[0] |
| 108 | + let n_points2 = p2.shape[0] |
| 109 | + let n_dims = p1.shape[1] |
| 110 | + result = newTensor[float](n_points2, n_points1) |
| 111 | + for i in 0 ..< n_points2: |
| 112 | + for j in 0 ..< n_points1: |
| 113 | + var r2 = 0.0 |
| 114 | + for k in 0 ..< n_dims: |
| 115 | + let diff = p2[i,k] - p1[j,k] |
| 116 | + r2 += diff * diff |
| 117 | + result[i, j] = sqrt(r2) |
| 118 | + |
| 119 | +template compactRbfFuncScalar*(r: float, epsilon: float): float = |
| 120 | + (1 - r/epsilon) ^ 4 * (4*r/epsilon + 1) * float(r < epsilon) |
| 121 | + |
| 122 | +proc compactRbfFunc*(r: Tensor[float], epsilon: float): Tensor[float] = |
| 123 | + result = map_inline(r): |
| 124 | + let xeps = x / epsilon |
| 125 | + let temp = (1 - xeps) |
| 126 | + let temp2 = temp * temp |
| 127 | + temp2*temp2 * (4*xeps + 1) * float(xeps < 1) |
| 128 | + |
| 129 | +proc newRbfBase*[T](points: Tensor[float], values: Tensor[T], rbfFunc: RbfFunc = compactRbfFunc, epsilon: float = 1): RbfBaseType[T] = |
| 130 | + assert points.shape[0] == values.shape[0] |
| 131 | + let dist = distanceMatrix(points, points) |
| 132 | + let A = rbfFunc(dist, epsilon) |
| 133 | + let coeffs = solve(A, values) |
| 134 | + result = RbfBaseType[T](points: points, values: values, coeffs: coeffs, epsilon: epsilon, f: rbfFunc) |
| 135 | + |
| 136 | +proc eval*[T](rbf: RbfBaseType[T], x: Tensor[float]): Tensor[T] = |
| 137 | + let dist = distanceMatrix(rbf.points, x) |
| 138 | + let A = rbf.f(dist, rbf.epsilon) |
| 139 | + result = A * rbf.coeffs |
| 140 | + |
| 141 | +proc scalePoint*(x: Tensor[float], limits: tuple[upper: Tensor[float], lower: Tensor[float]]): Tensor[float] = |
| 142 | + let lower = limits.lower -. 0.01 |
| 143 | + let upper = limits.upper +. 0.01 |
| 144 | + (x -. lower) /. (upper - lower) |
| 145 | + |
| 146 | +proc newRbf*[T](points: Tensor[float], values: Tensor[T], gridSize: int = 0, rbfFunc: RbfFunc = compactRbfFunc, epsilon: float = 1): RbfType[T] = |
| 147 | + ## Construct a Radial basis function interpolator using Partition of Unity. |
| 148 | + ## points: The positions of the data points. Shape: (nPoints, nDims) |
| 149 | + ## values: The values at the points. Can be multivalued. Shape: (nPoints, nValues) |
| 150 | + ## gridSize: The number of cells along each dimension. Setting it to the default 0 will automatically choose a value based on the number of points. |
| 151 | + ## rbfFunc: The RBF function that accepts shape parameter. Default is a C^2 compactly supported function. |
| 152 | + ## epsilon: shape parameter. Default 1. |
| 153 | + assert points.shape[0] == values.shape[0] |
| 154 | + assert points.shape.len == 2 and values.shape.len == 2 |
| 155 | + let upperLimit = max(points, 0) |
| 156 | + let lowerLimit = min(points, 0) |
| 157 | + let limits = (upper: upperLimit, lower: lowerLimit) |
| 158 | + let scaledPoints = points.scalePoint(limits) |
| 159 | + let dataGrid = newRbfGrid(scaledPoints, values, gridSize) |
| 160 | + let patchPoints = dataGrid.constructMeshedPatches() |
| 161 | + let nPatches = patchPoints.shape[0] |
| 162 | + var patchRbfs: seq[RbfBaseType[T]] #= newTensor[RbfBaseType[T]](nPatches, 1) |
| 163 | + var patchIndices: seq[int] |
| 164 | + for i in 0 ..< nPatches: |
| 165 | + let indices = dataGrid.findAllWithin(patchPoints[i, _], dataGrid.gridDelta) |
| 166 | + if indices.len > 0: |
| 167 | + patchRbfs.add newRbfBase(dataGrid.points[indices,_], values[indices, _], epsilon=epsilon) |
| 168 | + patchIndices.add i |
| 169 | + |
| 170 | + let patchGrid = newRbfGrid(patchPoints[patchIndices, _], patchRbfs.toTensor.unsqueeze(1), gridSize) |
| 171 | + result = RbfType[T](limits: limits, grid: patchGrid, nValues: values.shape[1]) |
| 172 | + |
| 173 | +proc eval*[T](rbf: RbfType[T], x: Tensor[float]): Tensor[T] = |
| 174 | + assert x.shape.len == 2 |
| 175 | + assert (not ((x <=. rbf.limits.upper) and (x >=. rbf.limits.lower))).astype(int).sum() == 0, "Some of your points are outside the allowed limits" |
| 176 | + |
| 177 | + let nPoints = x.shape[0] |
| 178 | + let x = x.scalePoint(rbf.limits) |
| 179 | + result = newTensor[T](nPoints, rbf.nValues) |
| 180 | + for row in 0 ..< nPoints: |
| 181 | + let p = x[row, _] |
| 182 | + let indices = rbf.grid.findAllWithin(p, rbf.grid.gridDelta) |
| 183 | + if indices.len > 0: |
| 184 | + var c = 0.0 |
| 185 | + for i in indices: |
| 186 | + let center = rbf.grid.points[i, _] |
| 187 | + let r = sqrt(dist2(p, center)) |
| 188 | + let ci = compactRbfFuncScalar(r, rbf.grid.gridDelta) |
| 189 | + c += ci |
| 190 | + let val = rbf.grid.values[i, 0].eval(p) |
| 191 | + result[row, _] = result[row, _] + ci * val |
| 192 | + result[row, _] = result[row, _] / c |
| 193 | + else: |
| 194 | + result[row, _] = T(Nan) # allow to pass default value to newRbf? |
| 195 | + |
| 196 | +proc evalAlt*[T](rbf: RbfType[T], x: Tensor[float]): Tensor[T] = |
| 197 | + assert x.shape.len == 2 |
| 198 | + assert (not ((x <=. rbf.limits.upper) and (x >=. rbf.limits.lower))).astype(int).sum() == 0, "Some of your points are outside the allowed limits" |
| 199 | + |
| 200 | + let nPoints = x.shape[0] |
| 201 | + let x = x.scalePoint(rbf.limits) |
| 202 | + result = newTensor[T](nPoints, rbf.nValues) |
| 203 | + var c = newTensor[float](nPoints, 1) |
| 204 | + var isSet = newTensor[bool](nPoints, 1) |
| 205 | + let nPatches = rbf.grid.points.shape[0] |
| 206 | + let pointGrid = newRbfGrid(x, x, rbf.grid.gridSize) |
| 207 | + for row in 0 ..< nPatches: |
| 208 | + let center = rbf.grid.points[row, _] |
| 209 | + let indices = pointGrid.findAllWithin(center, rbf.grid.gridDelta) |
| 210 | + if indices.len > 0: |
| 211 | + let vals = rbf.grid.values[row, 0].eval(x[indices, _]) |
| 212 | + for i, index in indices: |
| 213 | + let r = sqrt(dist2(center, x[index, _])) |
| 214 | + let ci = compactRbfFuncScalar(r, rbf.grid.gridDelta) |
| 215 | + result[index, _] = result[index, _] + ci * vals[i, _] |
| 216 | + c[index] += ci |
| 217 | + isSet[index, 0] = true |
| 218 | + |
| 219 | + result /.= c |
| 220 | + result[not isSet, _] = T(NaN) |
0 commit comments