Repository: scipy/scipy

Issue:
BUG: of scipy.interpolate.CubicSpline when `bc_type' is set to `periodic'
in line 701 of file: https://github.com/scipy/scipy/blob/c1372d8aa90a73d8a52f135529293ff4edb98fc8/scipy/interpolate/_cubic.py#L701

                a_m2_m1 = dx[-2]
should be

                a_m2_m1 = dx[-3]

This typo could make the result of some interpolations quite different.


Discussion:
@CastleStar14654 do you have an example where this causes a problem? would be quite useful both to demonstrate the bug and to use as a regression test
@rgommers  Code following would interpolate and extrapolate `x` and `y` periodically, and plot second derivative of y to x.  Ideally, function itself, first derivative & second derivative should be periodic and **continuous**.
```
import scipy.interpolate as ipt
import numpy as np
import matplotlib.pyplot as plt

x = [0.9, 1.3, 1.9, 2.1, 2.6, 3.0, 3.9, 4.4, 4.7, 5.0, 6.0,
     7.0, 8.0, 9.2, 10.5, 11.3, 11.6, 12.0, 12.6, 13.0, 13.3]
y = [1.3, 1.5, 1.85, 2.1, 2.6, 2.7, 2.4, 2.15, 2.05, 2.1,
     2.25, 2.3, 2.25, 1.95, 1.4, 0.9, 0.7, 0.6, 0.5, 0.4, 1.3]
csp = ipt.CubicSpline(x, y, bc_type='periodic', extrapolate='periodic')
newx = np.linspace(0, 15, 2000)
plt.plot(newx, csp.derivative(2)(newx))
plt.show()
```
Current version of scipy would give
<img width="320" alt="wrong" src="https://user-images.githubusercontent.com/50725189/77945621-80199100-72f3-11ea-972e-16279d41ee87.png">
which shows that the second derivative is not continuous at `x = 0.2` and `x = 12.6`.

When `_cubic.py` is modified as I suggested, the plot is
<img width="443" alt="right" src="https://user-images.githubusercontent.com/50725189/77945913-0a61f500-72f4-11ea-9c79-7f26591a617f.png">
and second derivative is continuous at  `x = 0.2` and `x = 12.6`.
Hi! If no one is against it, I will fix the issue.
So I tried to change `a_m2_m1 = dx[-2]` into `a_m2_m1 = dx[-3]`. However, I get an `IndexError` when the number of pair points `(x_i, y_i)` is `n=3`. This is because `dx` is literally `np.diff(x)`, which has shape `x.shape[0] - 1`.
Looking at the code, the case of `n=3` is treated in a specific way when `bc_type="not-a-knot"`:

https://github.com/scipy/scipy/blob/b77138f1f41a0d1190a02e7ba6c2258820a4bf05/scipy/interpolate/_cubic.py#L657-L674

Could it be that also `bc_type="periodic"` has a specific treatment when `n=3`? I am honestly no expert in cubic splines.

EDIT: I actually realized that `bc_type="periodic"` and `n=3` always implies a parabolic curve.
After reading about cubic spline interpolation, I believe that the code I linked just above can be relaxed for the periodic condition when three points are given. Something like:

`if n == 3 and bc[0] in ['not-a-knot', 'periodic' and bc[1] in ['not-a-knot', 'periodic'] and bc[0] == bc[1]:`

However I am unsure on how to proceed. The change directly related to the issue would imply another indirect change. @rgommers, should I directly send a pull request with the changes I wrote about, or wait for further discussion here?
> Could it be that also `bc_type="periodic"` has a specific treatment when `n=3`? I am honestly no expert in cubic splines.
> 
> EDIT: I actually realized that `bc_type="periodic"` and `n=3` always implies a parabolic curve.

Sorry for my late reply.

Actually, `bc_type="periodic"` and `n=3` doesn't mean a parabolic curve. But it does worth a specific treatment.

## An Example
For instance, let's interpolate these data periodically:
|   x | y |
|----:|--:|
| -x1 | 0 |
|   0 | H |
|  x2 | 0 |

Let
- y = A x^3 + B x^2 + C x + H when -x1 < x < 0;
- y = a x^3 + b x^2 + c x + H when 0 < x < x2.

The first and second derivatives should be continuous, so at x = 0
- C = c
- B = b

At x = -x1 or x2
- -A x1^3 + B x1^2 - C x1 + H = 0
- a x2^3 + b x2^2 + c x2 + H = 0
- 3 A x1^2 - 2 B x1 + C = 3 a x2^2 + 2 b x2 + c
- -6 A x1 + 2 B = 6 a x2 + 2 b

Finally, we get
| Variable |                               Value |
|---------:|------------------------------------:|
|        A |                       -2H / x1^2 x2 |
|        a |          -A x1 / x2 = -2H / x1 x2^2 |
|    B = b |            3 A x1 / 2 = -3H / x1 x2 |
|    C = c | A x1(x1 - x2) = -H(x1 - x2) / x1 x2 |

They are not paralobic.
The unique piecewise cubic passing through n=3 points (`x`, `y`) with periodic boundary conditions (which implies `y[0]==y[2]`) is

```
def per_spl(x, y):
    assert len(x) == len(y) == 3
    y[-1] = y[0]   # shortcut
    
    x, y = map(np.asarray, (x, y))
    
    h = x[1:] - x[:-1]
    m = (y[1:] - y[:-1]) / h
    
    s = (m/h).sum() / (1./h).sum()
    return CubicHermiteSpline(x, y, [s, s, s])    
```

Resolving patch:
diff --git a/scipy/interpolate/_cubic.py b/scipy/interpolate/_cubic.py
--- a/scipy/interpolate/_cubic.py
+++ b/scipy/interpolate/_cubic.py
@@ -672,6 +672,12 @@ def __init__(self, x, y, axis=0, bc_type='not-a-knot', extrapolate=None):
 
             s = solve(A, b, overwrite_a=True, overwrite_b=True,
                       check_finite=False)
+        elif n == 3 and bc[0] == 'periodic':
+            # In case when number of points is 3 we should count derivatives
+            # manually
+            s = np.empty((n,) + y.shape[1:], dtype=y.dtype)
+            t = (slope / dxr).sum() / (1. / dxr).sum()
+            s.fill(t)
         else:
             # Find derivative values at each x[i] by solving a tridiagonal
             # system.
@@ -713,7 +719,7 @@ def __init__(self, x, y, axis=0, bc_type='not-a-knot', extrapolate=None):
                 a_m1_0 = dx[-2]  # lower left corner value: A[-1, 0]
                 a_m1_m2 = dx[-1]
                 a_m1_m1 = 2 * (dx[-1] + dx[-2])
-                a_m2_m1 = dx[-2]
+                a_m2_m1 = dx[-3]
                 a_0_m1 = dx[0]
 
                 b[0] = 3 * (dxr[0] * slope[-1] + dxr[-1] * slope[0])


Test patch:
diff --git a/scipy/interpolate/tests/test_polyint.py b/scipy/interpolate/tests/test_polyint.py
--- a/scipy/interpolate/tests/test_polyint.py
+++ b/scipy/interpolate/tests/test_polyint.py
@@ -604,6 +604,27 @@ def test_periodic_eval(self):
         S = CubicSpline(x, y, bc_type='periodic')
         assert_almost_equal(S(1), S(1 + 2 * np.pi), decimal=15)
 
+    def test_second_derivative_continuity_gh_11758(self):
+        # gh-11758: C2 continuity fail
+        x = np.array([0.9, 1.3, 1.9, 2.1, 2.6, 3.0, 3.9, 4.4, 4.7, 5.0, 6.0,
+                      7.0, 8.0, 9.2, 10.5, 11.3, 11.6, 12.0, 12.6, 13.0, 13.3])
+        y = np.array([1.3, 1.5, 1.85, 2.1, 2.6, 2.7, 2.4, 2.15, 2.05, 2.1,
+                      2.25, 2.3, 2.25, 1.95, 1.4, 0.9, 0.7, 0.6, 0.5, 0.4, 1.3])
+        S = CubicSpline(x, y, bc_type='periodic', extrapolate='periodic')
+        self.check_correctness(S, 'periodic', 'periodic')
+
+    def test_three_points(self):
+        # gh-11758: Fails computing a_m2_m1
+        # In this case, s (first derivatives) could be found manually by solving
+        # system of 2 linear equations. Due to solution of this system,
+        # s[i] = (h1m2 + h2m1) / (h1 + h2), where h1 = x[1] - x[0], h2 = x[2] - x[1],
+        # m1 = (y[1] - y[0]) / h1, m2 = (y[2] - y[1]) / h2
+        x = np.array([1.0, 2.75, 3.0])
+        y = np.array([1.0, 15.0, 1.0])
+        S = CubicSpline(x, y, bc_type='periodic')
+        self.check_correctness(S, 'periodic', 'periodic')
+        assert_allclose(S.derivative(1)(x), np.array([-48.0, -48.0, -48.0]))
+
     def test_dtypes(self):
         x = np.array([0, 1, 2, 3], dtype=int)
         y = np.array([-5, 2, 3, 1], dtype=int)