Files

219 lines
6.8 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 signal
import (
"sourcedock.dev/petrbalvin/tensor/internal/base"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
import "slices"
import "sourcedock.dev/petrbalvin/tensor/internal/engine"
// Numerically robust scalar reduction and finite-difference stencils
// for grid data. SumKahan applies Kahan's compensated summation to the
// whole array: the same sum `Sum` computes, but with the running
// rounding error carried alongside, so long chains of similar
// magnitudes stop bleeding digits. The finite-difference stencils
// produce the central-difference gradient and Laplacian of grid data,
// the raw material of explicit PDE steppers.
// SumKahan returns the sum of all elements by Kahan's compensated
// summation. For arrays with mixed magnitudes the result carries more
// of the small terms than a plain left-to-right accumulation.
// core.Complex arrays are refused.
func SumKahan(a *core.Array) (float64, error) {
if a.Dtype() == core.Complex {
return 0, base.Errf("SumKahan: complex arrays are not supported")
}
src := widenFloats(a)
var sum, comp float64
// Bounded by the element count: a rebased view's aliased payload
// runs past its extent.
for i := range a.Len() {
v := src[i]
v -= comp
t := sum + v
comp = (t - sum) - v
sum = t
}
return sum, nil
}
// Gradient1D returns the central-difference derivative of a 1-D
// uniform grid signal y with spacing dx. Interior points use the
// second-order central stencil; the endpoints fall back to the
// first-order one-sided stencil, so the result has the same length as
// the input.
func Gradient1D(y *core.Array, dx float64) (*core.Array, error) {
if y.NDim() != 1 {
return nil, base.Errf("Gradient1D: needs a rank-1 signal, got shape %s", base.ShapeText(y.Shape()))
}
if y.Dtype() == core.Complex {
return nil, base.Errf("Gradient1D: complex arrays are not supported")
}
n := y.Len()
if n < 2 {
return nil, base.Errf("Gradient1D: needs at least 2 points, got %d", n)
}
if dx == 0 {
return nil, base.Errf("Gradient1D: spacing must not be zero")
}
src := widenFloats(y)
out := core.New(core.Float, n)
dst := out.RawFloats()
dst[0] = (src[1] - src[0]) / dx
for i := 1; i < n-1; i++ {
dst[i] = (src[i+1] - src[i-1]) / (2 * dx)
}
dst[n-1] = (src[n-1] - src[n-2]) / dx
return out, nil
}
// Laplacian returns the second-derivative Laplacian of a grid signal.
// 1-D input takes one spacing, 2-D input two (dx, dy for a row-major
// (rows, cols) grid, 5-point stencil), 3-D input three (7-point
// stencil). Boundary points copy their nearest interior value; the
// caller owns the boundary condition. A rank outside 1..3, a zero
// spacing, a missing spacing, or a degenerate extent is an error:
// every axis needs at least 2 points, and every axis of a 2-D or 3-D
// grid at least 3, so the interior stencil and the boundary copies
// have somewhere to stand.
func Laplacian(a *core.Array, spacings ...float64) (*core.Array, error) {
const name = "Laplacian"
if a.Dtype() == core.Complex {
return nil, base.Errf("%s: complex arrays are not supported", name)
}
if len(spacings) < a.NDim() {
return nil, base.Errf("%s: needs %d spacings for rank %d, got %d",
name, a.NDim(), a.NDim(), len(spacings))
}
if slices.Contains(spacings, 0) {
return nil, base.Errf("%s: spacing must not be zero", name)
}
switch a.NDim() {
case 1:
if a.Len() < 2 {
return nil, base.Errf("%s: needs at least 2 points, got %d", name, a.Len())
}
case 2, 3:
for i, d := range a.Shape() {
if d < 3 {
return nil, base.Errf("%s: axis %d has extent %d, the stencil needs at least 3 points",
name, i, d)
}
}
default:
return nil, base.Errf("%s: needs a 1-D, 2-D or 3-D array, got rank %d", name, a.NDim())
}
switch a.NDim() {
case 1:
return laplacian1D(a, spacings[0]), nil
case 2:
return laplacian2D(a, spacings[0], spacings[1]), nil
default:
return laplacian3D(a, spacings[0], spacings[1], spacings[2]), nil
}
}
// laplacian1D applies the 3-point second-difference stencil along the
// single axis. Interior: (y[i+1] − 2y[i] + y[i−1]) / dx². For n = 2
// there is no interior point, and the edge copy leaves both entries
// zero.
func laplacian1D(y *core.Array, dx float64) *core.Array {
n := y.Len()
out := core.New(core.Float, n)
inv := 1 / (dx * dx)
src := widenFloats(y)
dst := out.RawFloats()
for i := 1; i < n-1; i++ {
dst[i] = (src[i+1] - 2*src[i] + src[i-1]) * inv
}
dst[0] = dst[1]
dst[n-1] = dst[n-2]
return out
}
// laplacian2D applies the 5-point stencil on a row-major grid of
// shape (ny, nx) with spacings dx, dy.
func laplacian2D(y *core.Array, dx, dy float64) *core.Array {
ny, nx := y.Shape()[0], y.Shape()[1]
out := core.New(core.Float, ny, nx)
invx := 1 / (dx * dx)
invy := 1 / (dy * dy)
src := widenFloats(y)
dst := out.RawFloats()
engine.ParallelMin(ny, workFloorFor(nx*6), func(s, e int) {
for j := s; j < e; j++ {
for i := 1; i < nx-1; i++ {
idx := j*nx + i
c := src[idx]
acc := (src[idx+1] + src[idx-1] - 2*c) * invx
if j > 0 && j < ny-1 {
acc += (src[(j-1)*nx+i] + src[(j+1)*nx+i] - 2*c) * invy
}
dst[idx] = acc
}
dst[j*nx] = dst[j*nx+1]
dst[j*nx+nx-1] = dst[j*nx+nx-2]
}
})
for i := range nx {
dst[i] = dst[nx+i]
dst[(ny-1)*nx+i] = dst[(ny-2)*nx+i]
}
return out
}
// laplacian3D applies the 7-point stencil on a row-major grid of
// shape (nz, ny, nx) with spacings dx, dy, dz. Boundary planes copy
// their nearest interior plane, the caller owns the boundary
// condition.
func laplacian3D(y *core.Array, dx, dy, dz float64) *core.Array {
nz, ny, nx := y.Shape()[0], y.Shape()[1], y.Shape()[2]
out := core.New(core.Float, nz, ny, nx)
invx := 1 / (dx * dx)
invy := 1 / (dy * dy)
invz := 1 / (dz * dz)
src := widenFloats(y)
dst := out.RawFloats()
engine.ParallelMin(nz, workFloorFor(ny*nx*9), func(zs, ze int) {
for k := zs; k < ze; k++ {
for j := range ny {
for i := 1; i < nx-1; i++ {
idx := (k*ny+j)*nx + i
c := src[idx]
acc := (src[idx+1] + src[idx-1] - 2*c) * invx
if j > 0 && j < ny-1 {
acc += (src[(k*ny+j-1)*nx+i] + src[(k*ny+j+1)*nx+i] - 2*c) * invy
}
if k > 0 && k < nz-1 {
acc += (src[((k-1)*ny+j)*nx+i] + src[((k+1)*ny+j)*nx+i] - 2*c) * invz
}
dst[idx] = acc
}
// The x boundaries copy their nearest interior value.
dst[(k*ny+j)*nx] = dst[(k*ny+j)*nx+1]
dst[(k*ny+j)*nx+nx-1] = dst[(k*ny+j)*nx+nx-2]
}
}
})
// The y boundaries copy their nearest interior row.
for k := range nz {
for i := range nx {
dst[k*ny*nx+i] = dst[(k*ny+1)*nx+i]
dst[((k+1)*ny-1)*nx+i] = dst[((k+1)*ny-2)*nx+i]
}
}
// The z boundaries copy their nearest interior plane.
for j := range ny {
for i := range nx {
dst[j*nx+i] = dst[(ny+j)*nx+i]
dst[((nz-1)*ny+j)*nx+i] = dst[((nz-2)*ny+j)*nx+i]
}
}
return out
}