Skip to content

Commit b077f32

Browse files
committed
fix: Correct periodic cubic spline
It is now assumed that for periodic cubic splines the start and end point match. During slope calculation the end point data is discarded, since it's redundant. Afterwards, the start point slope value is filled here. Signed-off-by: Sietze van Buuren <s.van.buuren@gmail.com>
1 parent feb551a commit b077f32

File tree

2 files changed

+75
-26
lines changed

2 files changed

+75
-26
lines changed

cubinterpp/periodic_spline.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,45 @@
33
import numpy as np
44
import mlpyqtgraph as mpg
55
import cubinterpp.cubinterpp_py as cubinterpp # cubinterpp_py is a pybind11 module
6+
from scipy.interpolate import CubicSpline
7+
8+
9+
def test_data(case='circle'):
10+
""" Generate test data for periodic spline interpolation """
11+
match case:
12+
case 'circle':
13+
nx = 5
14+
x = np.array([1.0, 0.0, -1.0, 0.0, 1.0])
15+
y = np.array([0.0, 1.0, 0.0, -1.0, 0.0])
16+
case _:
17+
nx = 10
18+
x = np.array([2.0, 5.0, 8.0, 9.0, 7.0, 6.0, 8.0, 10.0, 7.0, 2.0])
19+
y = np.array([4.0, 0.5, 3.0, 6.0, 7.0, 5.0, 4.0, 6.0, 8.0, 4.0])
20+
21+
t = np.linspace(0.0, 1.0, nx)
22+
23+
return t, x, y
624

725

826
@mpg.plotter
927
def main():
10-
x = np.array([2.0, 5.0, 8.0, 9.0, 7.0, 6.0, 8.0, 10.0, 7.0, 2.0])
11-
y = np.array([4.0, 0.5, 3.0, 6.0, 7.0, 5.0, 4.0, 6.0, 8.0, 4.0])
12-
t = np.linspace(0.0, 1.0, 10)
28+
29+
t, x, y = test_data('arbitrary')
1330

1431
spline_x = cubinterpp.NaturalPeriodicSpline1D(t, x)
1532
spline_y = cubinterpp.NaturalPeriodicSpline1D(t, y)
1633
t_interp = np.linspace(0.0, 1.0, 500)
1734
x_interp = spline_x.evaln(t_interp)
1835
y_interp = spline_y.evaln(t_interp)
1936

37+
x_interp_scipy = CubicSpline(t, x, bc_type='periodic')(t_interp)
38+
y_interp_scipy = CubicSpline(t, y, bc_type='periodic')(t_interp)
39+
2040
mpg.figure(title='Periodic Spline Interpolation')
2141
mpg.plot(x_interp, y_interp)
42+
mpg.plot(x_interp_scipy, y_interp_scipy, style='--')
2243
mpg.plot(x, y, width=0, symbol='o', symbol_color='r', symbol_size=6)
44+
mpg.legend('cubinterpp', 'scipy', 'data points')
2345
mpg.gca().grid = True
2446

2547

include/slopes.hpp

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -271,19 +271,32 @@ template <typename T, typename Tx, typename Tf>
271271
struct setNaturalSplineBoundaryCondition<BoundaryConditionType::Periodic, T, Tx, Tf> {
272272
using Vector = std::vector<T>;
273273
constexpr void operator()(const Tx& x, const Tf& f, Vector& a, Vector& b, Vector& c, Vector& d) const {
274-
const auto nx = x.size();
275-
276-
T dx1 = x[1] - x[0];
277-
T dxN = x[nx-1] - x[nx-2];
278274

279-
b[0] = 1.0;
280-
a[0] = -1.0; // (lower-left corner)
281-
d[0] = 0.0;
275+
// Periodic boundary conditions, it is assumed that f[0] == f[nx-1] !
282276

283-
a[nx-1] = 1.0/dxN;
284-
b[nx-1] = 2.0*(1.0/dxN + 1.0/dx1);
285-
c[nx-1] = 1.0/dx1; // (upper-right corner)
286-
d[nx-1] = 3.0*((f[nx-1] - f[nx-2])/(dxN*dxN) + (f[1] - f[0])/(dx1*dx1));
277+
const auto nsys = a.size();
278+
279+
// Row 0
280+
{
281+
T dxL = x.back() - x[nsys-1];
282+
T dxR = x[1] - x[0];
283+
284+
a[0] = 1.0/dxL; // lower-left corner
285+
b[0] = 2.0*(1.0/dxL + 1.0/dxR);
286+
c[0] = 1.0/dxR;
287+
d[0] = 3.0*((f[0] - f[nsys-1])/(dxL*dxL) + (f[1] - f[0])/(dxR*dxR));
288+
}
289+
290+
// Last row (nsys-1)
291+
{
292+
T dxL = x[nsys-1] - x[nsys-2];
293+
T dxR = x.back() - x[nsys-1];
294+
295+
a[nsys-1] = 1.0/dxL;
296+
b[nsys-1] = 2.0*(1.0/dxL + 1.0/dxR);
297+
c[nsys-1] = 1.0/dxR; // upper-right corner
298+
d[nsys-1] = 3.0*((f[nsys-1] - f[nsys-2])/(dxL*dxL) + (f[0] - f[nsys-1])/(dxR*dxR));
299+
}
287300

288301
cyclic_thomas_algorithm<T>(a, b, c, d);
289302
}
@@ -297,26 +310,40 @@ std::vector<T> natural_spline_slopes(const Tx x, const Tf f)
297310

298311
const auto nx = x.size();
299312

313+
// Periodic with duplicated endpoint → reduce system
314+
constexpr bool is_periodic = (BC == BoundaryConditionType::Periodic);
315+
const auto nsys = is_periodic ? nx - 1 : nx;
316+
300317
// vectors that fill up the tridiagonal matrix
301-
std::vector<T> a(nx, T{0}); // first value of a is only used for periodic BC
302-
std::vector<T> b(nx, T{0});
303-
std::vector<T> c(nx, T{0}); // last value of c is only used for periodic BC
318+
std::vector<T> a(nsys, T{0}); // first value of a is only used for periodic BC
319+
std::vector<T> b(nsys, T{0});
320+
std::vector<T> c(nsys, T{0}); // last value of c is only used for periodic BC
304321
// right hand side
305-
std::vector<T> d(nx, T{0});
322+
std::vector<T> d(nsys, T{0});
306323

307-
// rows i = 1, ..., nx-2
308-
for (auto i = 1; i < nx-1; ++i)
324+
for (auto i = 1; i < nsys-1; ++i)
309325
{
310-
T dxi = x[i] - x[i-1];
311-
T dxi1 = x[i+1] - x[i];
312-
a[i] = 1.0/dxi;
313-
b[i] = 2.0*(1.0/dxi + 1.0/dxi1);
314-
c[i] = 1.0/dxi1;
315-
d[i] = 3.0*((f[i] - f[i-1])/(dxi*dxi) + (f[i+1] - f[i])/(dxi1*dxi1));
326+
T dxL = x[i] - x[i-1];
327+
T dxR = x[i+1] - x[i];
328+
a[i] = 1.0/dxL;
329+
b[i] = 2.0*(1.0/dxL + 1.0/dxR);
330+
c[i] = 1.0/dxR;
331+
d[i] = 3.0*((f[i] - f[i-1])/(dxL*dxL) + (f[i+1] - f[i])/(dxR*dxR));
316332
}
317333

318334
setNaturalSplineBoundaryCondition<BC, T, Tx, Tf>{}(x, f, a, b, c, d);
319335

336+
if constexpr (is_periodic)
337+
{
338+
std::vector<T> full(nx);
339+
for (auto i = 0; i < nsys; ++i)
340+
{
341+
full[i] = d[i];
342+
}
343+
full[nx-1] = full[0]; // duplicate endpoint slope
344+
return full;
345+
}
346+
320347
return d;
321348
}
322349

0 commit comments

Comments
 (0)