118 lines
3.9 KiB
Go
118 lines
3.9 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package core
|
|
|
|
import "math"
|
|
|
|
// Monotone cubic interpolation. The Fritsch-Carlson tangents keep the
|
|
// curve inside the data's own range between knots: interpolating a
|
|
// monotone series cannot overshoot, which the plain cubic spline does
|
|
// around sharp turns and which is the reason this variant exists.
|
|
|
|
// InterpolateMonotone evaluates the monotone piecewise cubic (PCHIP)
|
|
// through the samples at every query point. xs must be strictly
|
|
// increasing, ys real, and at least two samples long; queries outside
|
|
// [x0, xn] hold the boundary value, matching Interpolate's convention.
|
|
// At every knot the curve passes through the sample, and its slope
|
|
// there never exceeds twice the neighbouring secants, which is what
|
|
// keeps the interpolation inside the local data range.
|
|
func InterpolateMonotone(xs, ys, query *Array) (*Array, error) {
|
|
if xs.dt == Complex || ys.dt == Complex || query.dt == Complex {
|
|
return nil, errf("InterpolateMonotone: complex samples are not supported")
|
|
}
|
|
n := xs.Len()
|
|
if n != ys.Len() || n < 2 {
|
|
return nil, errf("InterpolateMonotone: xs/ys must share length ≥ 2")
|
|
}
|
|
for k := 1; k < n; k++ {
|
|
xk, xprev := xs.FloatAt(k), xs.FloatAt(k-1)
|
|
// NaN defeats the <= ordering test below (every comparison is
|
|
// false), so non-finiteness is refused by name first.
|
|
if math.IsNaN(xk) || math.IsInf(xk, 0) || math.IsNaN(xprev) || math.IsInf(xprev, 0) {
|
|
return nil, errf("InterpolateMonotone: knot %d is not finite", k)
|
|
}
|
|
if xk <= xprev {
|
|
return nil, errf("InterpolateMonotone: xs must be strictly increasing, knot %d repeats", k)
|
|
}
|
|
}
|
|
h := make([]float64, n-1)
|
|
delta := make([]float64, n-1)
|
|
for k := range n - 1 {
|
|
h[k] = xs.FloatAt(k+1) - xs.FloatAt(k)
|
|
delta[k] = (ys.FloatAt(k+1) - ys.FloatAt(k)) / h[k]
|
|
}
|
|
// Fritsch-Carlson tangents: zero where the data turns, the
|
|
// weighted harmonic mean of the neighbouring secants elsewhere,
|
|
// the clamped three-point estimate at the ends.
|
|
m := make([]float64, n)
|
|
switch {
|
|
case n == 2:
|
|
m[0], m[1] = delta[0], delta[0]
|
|
default:
|
|
m[0] = ((2*h[0]+h[1])*delta[0] - h[0]*delta[1]) / (h[0] + h[1])
|
|
if oppositeSigns(m[0], delta[0]) {
|
|
m[0] = 0
|
|
} else if math.Abs(m[0]) > 2*math.Abs(delta[0]) {
|
|
m[0] = 2 * delta[0]
|
|
}
|
|
m[n-1] = ((2*h[n-2]+h[n-3])*delta[n-2] - h[n-3]*delta[n-3]) / (h[n-2] + h[n-3])
|
|
if oppositeSigns(m[n-1], delta[n-2]) {
|
|
m[n-1] = 0
|
|
} else if math.Abs(m[n-1]) > 2*math.Abs(delta[n-2]) {
|
|
m[n-1] = 2 * delta[n-2]
|
|
}
|
|
for k := 1; k < n-1; k++ {
|
|
if delta[k-1]*delta[k] <= 0 {
|
|
m[k] = 0
|
|
continue
|
|
}
|
|
w1 := 2*h[k] + h[k-1]
|
|
w2 := h[k] + 2*h[k-1]
|
|
m[k] = (w1 + w2) / (w1/delta[k-1] + w2/delta[k])
|
|
}
|
|
}
|
|
out := &Array{shape: append([]int{}, query.Shape()...), dt: Float}
|
|
out.alloc(query.Len())
|
|
for i := range query.Len() {
|
|
q := query.FloatAt(i)
|
|
// NaN compares false against both clamps below and would flow
|
|
// through the evaluation as NaN with no error, the same trap
|
|
// InterpolateGrid refuses: an undefined position is a loud
|
|
// error, since there is nothing sensible to clamp it to.
|
|
if math.IsNaN(q) {
|
|
return nil, errf("InterpolateMonotone: query %d is NaN, which cannot be clamped", i)
|
|
}
|
|
lo := 0
|
|
if q <= xs.FloatAt(0) {
|
|
lo = 0
|
|
} else if q >= xs.FloatAt(n-1) {
|
|
lo = n - 2
|
|
} else {
|
|
for lo < n-2 && q > xs.FloatAt(lo+1) {
|
|
lo++
|
|
}
|
|
}
|
|
t := (q - xs.FloatAt(lo)) / h[lo]
|
|
if t < 0 {
|
|
t = 0
|
|
}
|
|
if t > 1 {
|
|
t = 1
|
|
}
|
|
t2, t3 := t*t, t*t*t
|
|
y0, y1 := ys.FloatAt(lo), ys.FloatAt(lo+1)
|
|
v := (2*t3-3*t2+1)*y0 + (t3-2*t2+t)*h[lo]*m[lo] +
|
|
(-2*t3+3*t2)*y1 + (t3-t2)*h[lo]*m[lo+1]
|
|
out.SetFloatAt(i, v)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// oppositeSigns reports whether two values carry strict opposite
|
|
// signs, the condition under which a tangent estimate is discarded as
|
|
// turning.
|
|
func oppositeSigns(a, b float64) bool {
|
|
return (a > 0 && b < 0) || (a < 0 && b > 0)
|
|
}
|