Files
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

119 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package core
import (
"math"
"math/bits"
)
// Multilinear interpolation over a regular grid of any rank: the
// rank-2 generalisation of Interpolate2D's bilinear read, one weight
// per axis, clamped at the boundary.
// InterpolateGrid reads the grid at every query point by multilinear
// interpolation. grid has one axis per dimension (each at least two
// samples); origins[i] and steps[i] place axis i, with steps strictly
// positive; queries is a rank-2 (m × rank) matrix, one row per query,
// axes in the grid's own order. Queries outside the grid clamp to the
// boundary, matching Interpolate's convention; a position that comes
// out NaN is an error, since there is nothing sensible to clamp it to.
// A rank above 12 is an error: 2^12 corners per query is where this
// implementation stays honest about its cost.
//
// The grid and the queries are widened once and the walk reads the
// payload slices, the same values the accessors returned; the axes
// combine bottom up, deepest first, which is the order the recursive
// walk evaluated, so the bits are the recursive walk's.
func InterpolateGrid(grid *Array, origins, steps []float64, queries *Array) (*Array, error) {
const name = "InterpolateGrid"
dims := grid.NDim()
if dims < 1 {
return nil, errf("%s: the grid must have at least one axis", name)
}
if dims > 12 {
return nil, errf("%s: the grid has %d axes, above the 12 this multilinear read supports", name, dims)
}
if len(origins) != dims || len(steps) != dims {
return nil, errf("%s: origins and steps need %d entries each, got %d and %d", name, dims, len(origins), len(steps))
}
shape := grid.Shape()
for i := range dims {
if !isFiniteStep(steps[i]) || steps[i] <= 0 {
return nil, errf("%s: axis %d needs a positive finite step, got %g", name, i, steps[i])
}
if shape[i] < 2 {
return nil, errf("%s: axis %d needs at least two samples, got %d", name, i, shape[i])
}
}
if queries.NDim() != 2 || queries.Shape()[1] != dims {
return nil, errf("%s: queries must be rank 2 with %d columns, got shape %s", name, dims, shapeText(queries.Shape()))
}
if grid.dt == Complex || queries.dt == Complex {
return nil, errf("%s: complex grids are not supported", name)
}
m := queries.Shape()[0]
out := &Array{shape: []int{m}, dt: Float}
out.alloc(m)
gv, qv := floatPayload(grid), floatPayload(queries)
// The axis strides and the corner flats are functions of the shape
// alone: precomputed once, the corner table stepped by the lowest
// cleared bit so no query re-walks the axes to find its corners.
strides := make([]int, dims)
strides[dims-1] = 1
for i := dims - 2; i >= 0; i-- {
strides[i] = strides[i+1] * shape[i+1]
}
corners := 1 << dims
flat := make([]int, corners)
for c := 1; c < corners; c++ {
flat[c] = flat[c&(c-1)] + strides[bits.TrailingZeros32(uint32(c))]
}
buf := make([]float64, corners)
idx := make([]int, dims)
frac := make([]float64, dims)
for q := range m {
base := 0
for i := range dims {
t := (qv[q*dims+i] - origins[i]) / steps[i]
// NaN compares false against both clamps below and converts
// to the platform's indefinite integer, which then indexes
// far outside the grid: the clamp contract only holds for
// the infinities, so an undefined position is a loud error.
if math.IsNaN(t) {
return nil, errf("%s: query %d axis %d is NaN, which cannot be clamped", name, q, i)
}
if t < 0 {
t = 0
}
if t > float64(shape[i]-1) {
t = float64(shape[i] - 1)
}
idx[i] = min(int(t), shape[i]-2)
frac[i] = t - float64(idx[i])
base += idx[i] * strides[i]
}
// The corners seed the deepest level and the axes combine from
// the last one up: buf[l] takes the lo corner first, the hi
// corner second, the operand order the recursive walk kept.
for c := range corners {
buf[c] = gv[base+flat[c]]
}
for i := dims - 1; i >= 0; i-- {
w := frac[i]
lo := 1 - w
for l := range 1 << i {
buf[l] = lo*buf[l] + w*buf[l+1<<i]
}
}
out.floats[q] = buf[0]
}
return out, nil
}
// isFiniteStep guards the step validation against NaN and infinity.
func isFiniteStep(s float64) bool {
return !math.IsNaN(s) && !math.IsInf(s, 0)
}