3D Vector Line Integral: From Discrete Samples to Machine Precision
Note (edited): The initial results I posted was made with a corrupted version of the package, giving only 3rd order convergence. With the correct released version, 7th order convergence is recovered.
This is a quick demonstration of application of ConvolutionInterpolations.jl (v0.15.0) to compute a 3D vector line integral entirely from discrete samples, using interpolation, differentiation, and integration from the same package.
The problem
The task is to compute the line integral:
where \mathbf{F}(x,y,z) = (yz,\, xz,\, xy) is a conservative vector field with potential \varphi = xyz, and the curve is a helix
Since \mathbf{F} = \nabla\varphi, the exact answer is
This makes the absolute error trivial to measure: any nonzero result is pure numerical error.
The pipeline
The calculation proceeds in five steps, all handled by ConvolutionInterpolations.jl:
- 3D interpolation: Sample \mathbf{F} on a uniform 3D grid with M points per dimension, build three 3D interpolants (one per field component) using
:b7kernels. - 1D interpolation: Sample the curve \mathbf{r}(t) at N discrete points, build 1D interpolants for x(t), y(t), z(t).
- 1D differentiation: Build derivative interpolants for the tangent vector \mathbf{r}'(t) = (x'(t),\, y'(t),\, z'(t)) using
derivative=1. - Evaluation: At each point on a fine t grid, evaluate \mathbf{F}\bigl(\mathbf{r}(t)\bigr) \cdot \mathbf{r}'(t) by composing the 3D and 1D interpolants.
- 1D integration: Build an antiderivative interpolant of the integrand using
derivative=-1, evaluate at the endpoints.
The code
using ConvolutionInterpolations
using Printf
# Sample the vector field F(x,y,z) = (yz, xz, xy) on a 3D grid
function sample_field(M)
xs = range(-1.5, 1.5, length=M)
ys = range(-1.5, 1.5, length=M)
zs = range(-0.5, 1.5, length=M)
F1 = [y*z for x in xs, y in ys, z in zs]
F2 = [x*z for x in xs, y in ys, z in zs]
F3 = [x*y for x in xs, y in ys, z in zs]
return xs, ys, zs, F1, F2, F3
end
# Compute the line integral for given N (curve samples) and M (field grid)
function line_integral(N, M; kernel_3d=:b7, kernel_1d=:b7)
# Step 1: 3D interpolants for each field component
xs, ys, zs, F1, F2, F3 = sample_field(M)
itp_F1 = convolution_interpolation((xs, ys, zs), F1; kernel=kernel_3d)
itp_F2 = convolution_interpolation((xs, ys, zs), F2; kernel=kernel_3d)
itp_F3 = convolution_interpolation((xs, ys, zs), F3; kernel=kernel_3d)
# Step 2: 1D interpolants for the curve r(t) = (cos(t), sin(t), t/(2pi))
t_grid = range(0.0, 2pi, length=N)
x_data = cos.(t_grid)
y_data = sin.(t_grid)
z_data = collect(t_grid) ./ (2pi)
itp_x = convolution_interpolation(t_grid, x_data; kernel=kernel_1d)
itp_y = convolution_interpolation(t_grid, y_data; kernel=kernel_1d)
itp_z = convolution_interpolation(t_grid, z_data; kernel=kernel_1d)
# Step 3: 1D derivative interpolants for the tangent vector r'(t)
itp_dx = convolution_interpolation(t_grid, x_data; kernel=kernel_1d, derivative=1)
itp_dy = convolution_interpolation(t_grid, y_data; kernel=kernel_1d, derivative=1)
itp_dz = convolution_interpolation(t_grid, z_data; kernel=kernel_1d, derivative=1)
# Step 4: Evaluate integrand F(r(t)) . r'(t) on a fine grid
N_fine = 4 * N
t_fine = range(0.0, 2pi, length=N_fine)
integrand = zeros(N_fine)
for (i, t) in enumerate(t_fine)
cx, cy, cz = itp_x(t), itp_y(t), itp_z(t)
f1, f2, f3 = itp_F1(cx, cy, cz), itp_F2(cx, cy, cz), itp_F3(cx, cy, cz)
dx, dy, dz = itp_dx(t), itp_dy(t), itp_dz(t)
integrand[i] = f1*dx + f2*dy + f3*dz
end
# Step 5: Integrate via antiderivative
itp_int = convolution_interpolation(t_fine, integrand; kernel=kernel_1d, derivative=-1)
return itp_int(2pi) - itp_int(0.0)
end
Results
With M = 20 (field grid) held fixed and sweeping the curve resolution N:
N_curve M_field | |error| rate
------- ------- | ----------- ------
8 20 | 7.131e-05 -
10 20 | 2.364e-05 4.95
12 20 | 9.937e-06 4.75
15 20 | 1.398e-05 -1.53
20 20 | 8.758e-07 9.63
25 20 | 4.564e-07 2.92
30 20 | 3.160e-11 52.53
40 20 | 2.231e-11 1.21
50 20 | 6.192e-12 5.74
60 20 | 1.917e-12 6.43
80 20 | 2.732e-13 6.77
100 20 | 5.793e-14 6.95
160 20 | 1.659e-15 7.56
Clean 7th-order convergence from N = 60 through N = 160, reaching 1.7 \times 10^{-15} at N = 160, essentially machine precision. The jump around N = 25-30 is where the :detect boundary condition polynomial fit gains enough points to start working at full accuracy.
Note on M: the vector field \mathbf{F} = (yz, xz, xy) is polynomial (degree 2), so the :b7 kernel reproduces it exactly regardless of grid resolution. The 3D interpolation is exact to machine precision even at M = 10. All convergence above comes purely from the 1D curve interpolation, differentiation, and integration steps.
Discussion
The observed 7th-order convergence is consistent with the :b7 kernel’s interpolation order. Differentiation loses one order (to 6th), but the antiderivative step recovers it, giving 7th order overall for the integrated result.
The pipeline composes naturally: 3D interpolation for the field, 1D interpolation + differentiation for the curve geometry, and 1D integration for the final scalar. All from the same package, using the same kernel family and the same API (convolution_interpolation with kernel, derivative).
More broadly, vector calculus is a natural application of the per-dimension derivative support. Given a 3D field sampled on a grid, all the standard differential operators (\nabla\mathbf{F}, \nabla\cdot\mathbf{F}, \nabla\times\mathbf{F}, \nabla^2\mathbf{F}) follow directly:
# gradient of a scalar field f(x,y,z)
df_dx = convolution_interpolation((xs, ys, zs), f; kernel=:b7, derivative=(1,0,0))
df_dy = convolution_interpolation((xs, ys, zs), f; kernel=:b7, derivative=(0,1,0))
df_dz = convolution_interpolation((xs, ys, zs), f; kernel=:b7, derivative=(0,0,1))
# grad(f) = [df_dx, df_dy, df_dz]
# divergence of a vector field F = (F1, F2, F3)
dF1_dx = convolution_interpolation((xs, ys, zs), F1; kernel=:b7, derivative=(1,0,0))
dF2_dy = convolution_interpolation((xs, ys, zs), F2; kernel=:b7, derivative=(0,1,0))
dF3_dz = convolution_interpolation((xs, ys, zs), F3; kernel=:b7, derivative=(0,0,1))
# div(F) = dF1_dx(x,y,z) + dF2_dy(x,y,z) + dF3_dz(x,y,z)
# curl of a vector field F = (F1, F2, F3)
dF3_dy = convolution_interpolation((xs, ys, zs), F3; kernel=:b7, derivative=(0,1,0))
dF2_dz = convolution_interpolation((xs, ys, zs), F2; kernel=:b7, derivative=(0,0,1))
dF1_dz = convolution_interpolation((xs, ys, zs), F1; kernel=:b7, derivative=(0,0,1))
dF3_dx = convolution_interpolation((xs, ys, zs), F3; kernel=:b7, derivative=(1,0,0))
dF2_dx = convolution_interpolation((xs, ys, zs), F2; kernel=:b7, derivative=(1,0,0))
dF1_dy = convolution_interpolation((xs, ys, zs), F1; kernel=:b7, derivative=(0,1,0))
# curl(F) = (dF3_dy - dF2_dz, dF1_dz - dF3_dx, dF2_dx - dF1_dy)
# laplacian of a scalar field f(x,y,z)
d2f_dx2 = convolution_interpolation((xs, ys, zs), f; kernel=:b7, derivative=(2,0,0))
d2f_dy2 = convolution_interpolation((xs, ys, zs), f; kernel=:b7, derivative=(0,2,0))
d2f_dz2 = convolution_interpolation((xs, ys, zs), f; kernel=:b7, derivative=(0,0,2))
# laplacian(f) = d2f_dx2(x,y,z) + d2f_dy2(x,y,z) + d2f_dz2(x,y,z)
Each of these returns a callable functor that can be evaluated at any point in the domain.