feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user