Skip to content

Commit a17b80c

Browse files
Merge pull request #33 from SciNim/rbf
Radial Basis functions
2 parents aaa7bb6 + 6b8ac4f commit a17b80c

File tree

5 files changed

+284
-3
lines changed

5 files changed

+284
-3
lines changed

src/numericalnim/interpolate.nim

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import arraymancer, cdt/[dt, vectors, edges, types]
33
import
44
./utils,
55
./common/commonTypes,
6-
./private/arraymancerOverloads
6+
./private/arraymancerOverloads,
7+
./rbf
8+
9+
export rbf
710

811
type
912
InterpolatorType*[T] = ref object

src/numericalnim/rbf.nim

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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)

src/numericalnim/utils.nim

+25
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,31 @@ proc meshgridFlat*[T](x, y: Tensor[T]): (Tensor[T], Tensor[T]) =
410410
result[0][i+j*nx] = x[i]
411411
result[1][i+j*nx] = y[j]
412412

413+
proc meshgridInternal[T](x1, x2: Tensor[T]): Tensor[T] =
414+
assert x2.squeeze().shape.len == 1
415+
assert x1.shape.len in [1, 2]
416+
let x1 =
417+
if x1.shape.len == 2:
418+
x1
419+
else:
420+
x1.unsqueeze(1)
421+
let len1 = x1.shape[0]
422+
let cols1 = x1.shape[1]
423+
let len2 = x2.shape[0]
424+
result = newTensor[T](len1 * len2, cols1 + 1)
425+
for i in 0 ..< len2:
426+
result[i*len1 ..< (i+1)*len1, 0 ..< cols1] = x1
427+
result[i*len1 ..< (i+1)*len1, ^1] = x2[i]
428+
429+
proc meshgrid*[T](ts: varargs[Tensor[T]]): Tensor[T] =
430+
if ts.len == 1:
431+
result = ts[0]
432+
elif ts.len == 0:
433+
assert false, "No input was given to meshgrid!"
434+
else:
435+
result = ts[0]
436+
for x in ts[1..^1]:
437+
result = meshgridInternal(result, x)
413438

414439
proc isClose*[T](y1, y2: T, tol: float = 1e-3): bool {.inline.} =
415440
let diff = calcError(y1, y2)

tests/test_interpolate.nim

+28-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import unittest, math, sequtils
22
import numericalnim
33
import arraymancer
44

5-
65
proc f(x: float): float = sin(x)
76
proc df(x: float): float = cos(x)
87
let t = linspace(0.0, 10.0, 100)
@@ -410,4 +409,31 @@ test "Trilinear f = x*y*z T: Tensor[float]":
410409
for k in z:
411410
check abs(spline.eval(i, j, k)[0] - i*j*k) < 1e-12
412411
check abs(spline.eval(i, j, k)[1] - i*j*k) < 1e-12
413-
check abs(spline.eval(i, j, k)[2] - 1) < 1e-16
412+
check abs(spline.eval(i, j, k)[2] - 1) < 1e-16
413+
414+
test "rbfBase f=x*y*z":
415+
let pos = meshgrid(arraymancer.linspace(0.0, 1.0, 5), arraymancer.linspace(0.0, 1.0, 5), arraymancer.linspace(0.0, 1.0, 5))
416+
let vals = pos[_, 0] *. pos[_, 1] *. pos[_, 2]
417+
let rbfObj = newRbfBase(pos, vals)
418+
419+
# We want test points in the interior to avoid the edges
420+
let xTest = meshgrid(arraymancer.linspace(0.1, 0.9, 10), arraymancer.linspace(0.1, 0.9, 10), arraymancer.linspace(0.1, 0.9, 10))
421+
let yTest = rbfObj.eval(xTest)
422+
let yCorrect = xTest[_, 0] *. xTest[_, 1] *. xTest[_, 2]
423+
for x in abs(yCorrect - yTest):
424+
check x < 0.16
425+
check mean_squared_error(yTest, yCorrect) < 2e-4
426+
427+
test "rbf f=x*y*z":
428+
let pos = meshgrid(arraymancer.linspace(0.0, 1.0, 5), arraymancer.linspace(0.0, 1.0, 5), arraymancer.linspace(0.0, 1.0, 5))
429+
let vals = pos[_, 0] *. pos[_, 1] *. pos[_, 2]
430+
let rbfObj = newRbf(pos, vals)
431+
432+
# We want test points in the interior to avoid the edges
433+
let xTest = meshgrid(arraymancer.linspace(0.1, 0.9, 10), arraymancer.linspace(0.1, 0.9, 10), arraymancer.linspace(0.1, 0.9, 10))
434+
let yTest = rbfObj.eval(xTest)
435+
let yCorrect = xTest[_, 0] *. xTest[_, 1] *. xTest[_, 2]
436+
for x in abs(yCorrect - yTest):
437+
check x < 0.03
438+
check mean_squared_error(yTest, yCorrect) < 1e-4
439+

tests/test_utils.nim

+7
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,10 @@ test "meshgridFlat":
8484
check gridX == [0, 1, 2, 0, 1, 2, 0, 1, 2].toTensor
8585
check gridY == [3, 3, 3, 4, 4, 4, 5, 5, 5].toTensor
8686

87+
test "meshgrid":
88+
let x = [0, 1].toTensor
89+
let y = [2, 3].toTensor
90+
let z = [4, 5].toTensor
91+
let grid = meshgrid(x, y, z)
92+
check grid == [[0, 2, 4], [1, 2, 4], [0, 3, 4], [1, 3, 4], [0, 2, 5], [1, 2, 5], [0, 3, 5], [1, 3, 5]].toTensor
93+

0 commit comments

Comments
 (0)