Files

119 lines
4.2 KiB
Go
Raw Permalink Normal View History

2026-09-03 10:00:00 +02:00
// 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)
}