feat: initial release
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-03 10:00:00 +02:00
commit af4ee19703
617 changed files with 191195 additions and 0 deletions
+117
View File
@@ -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)
}