feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,965 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Reference walks: the bisections, the odometer packing and the
|
||||
// trapezoid sums the fixed-iteration searches, the count-then-fill and
|
||||
// the halved products replace. They pin the exact semantics the fast
|
||||
// forms must reproduce bit for bit: bin clamping with NaN to the
|
||||
// outermost bin, the rightmost insertion rule, the left-segment knot
|
||||
// rule, the row-major coordinate order and the exact accumulation.
|
||||
|
||||
func refBinFloat(ev []float64, v float64) int {
|
||||
if v < ev[0] {
|
||||
return 0
|
||||
}
|
||||
lo, hi := 0, len(ev)-1
|
||||
for lo < hi {
|
||||
mid := int(uint(lo+hi) >> 1)
|
||||
if ev[mid] <= v {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
if lo == 0 {
|
||||
return len(ev) - 2
|
||||
}
|
||||
return lo - 1
|
||||
}
|
||||
|
||||
func refBinInt(ev []int64, v int64) int {
|
||||
if v < ev[0] {
|
||||
return 0
|
||||
}
|
||||
lo, hi := 0, len(ev)-1
|
||||
for lo < hi {
|
||||
mid := int(uint(lo+hi) >> 1)
|
||||
if ev[mid] <= v {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
return lo - 1
|
||||
}
|
||||
|
||||
func refUpperFloat(h []float64, q float64) int {
|
||||
lo, hi := 0, len(h)
|
||||
for lo < hi {
|
||||
mid := int(uint(lo+hi) >> 1)
|
||||
if h[mid] <= q {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
func refUpperInt(h []int64, q int64) int {
|
||||
lo, hi := 0, len(h)
|
||||
for lo < hi {
|
||||
mid := int(uint(lo+hi) >> 1)
|
||||
if h[mid] <= q {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
func refInterpSegment(xv []float64, q float64) int {
|
||||
n := len(xv)
|
||||
if q <= xv[0] {
|
||||
return 0
|
||||
}
|
||||
if q >= xv[n-1] {
|
||||
return n - 2
|
||||
}
|
||||
lo, hi := 1, n
|
||||
for lo < hi {
|
||||
mid := int(uint(lo+hi) >> 1)
|
||||
if xv[mid] < q {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
return lo - 1
|
||||
}
|
||||
|
||||
// refArgwhere collects coordinates the way the single odometer walk did.
|
||||
func refArgwhere(a *Array) []int64 {
|
||||
var rows []int64
|
||||
coord := make([]int, a.NDim())
|
||||
for i := range a.Len() {
|
||||
if !isZero(a, i) {
|
||||
for d := range a.NDim() {
|
||||
rows = append(rows, int64(coord[d]))
|
||||
}
|
||||
}
|
||||
advanceOdometer(coord, a.shape)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// refGather is the per-element accessor walk Gather replaced.
|
||||
func refGather(src *Array, dim int, index *Array) (*Array, error) {
|
||||
out := &Array{shape: index.Shape(), dt: src.dt}
|
||||
out.alloc(index.Len())
|
||||
dst := make([]int, index.NDim())
|
||||
for i := range index.Len() {
|
||||
idx := int(index.ints[index.physIndex(i)])
|
||||
if idx < 0 || idx >= src.shape[dim] {
|
||||
return nil, errf("Gather: index %d out of range for dimension %d of size %d at position %d", idx, dim, src.shape[dim], i)
|
||||
}
|
||||
off := 0
|
||||
for d := range dst {
|
||||
c := dst[d]
|
||||
if d == dim {
|
||||
c = idx
|
||||
}
|
||||
off = off*src.shape[d] + c
|
||||
}
|
||||
out.setFrom(i, src, off)
|
||||
advanceOdometer(dst, index.shape)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// refTake is the validated per-element copy Take replaced.
|
||||
func refTake(a *Array, indices *Array) (*Array, error) {
|
||||
out := &Array{shape: []int{indices.Len()}, dt: a.dt}
|
||||
out.alloc(indices.Len())
|
||||
src := a
|
||||
if !src.isContiguous() {
|
||||
src = src.materialise()
|
||||
}
|
||||
k := intPayload(indices)
|
||||
for i, v := range k {
|
||||
if v < 0 || v >= int64(a.Len()) {
|
||||
return nil, errf("Take: index %d out of range for flat size %d at position %d", v, a.Len(), i)
|
||||
}
|
||||
}
|
||||
switch out.dt {
|
||||
case Int:
|
||||
for i, v := range k {
|
||||
out.ints[i] = src.ints[v]
|
||||
}
|
||||
case Float16:
|
||||
for i, v := range k {
|
||||
out.halves[i] = src.halves[v]
|
||||
}
|
||||
case Float32:
|
||||
for i, v := range k {
|
||||
out.floats32[i] = src.floats32[v]
|
||||
}
|
||||
case Float:
|
||||
for i, v := range k {
|
||||
out.floats[i] = src.floats[v]
|
||||
}
|
||||
default:
|
||||
for i, v := range k {
|
||||
out.complexes[i] = src.complexes[v]
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// probeSearchValues returns the adversarial probe values every search
|
||||
// equivalence test walks: exact edges, the two zeros, neighbours of the
|
||||
// edges, out-of-range ends, both NaN signs, infinities, subnormals and
|
||||
// a deterministic random spread.
|
||||
func probeSearchValues(edges []float64) []float64 {
|
||||
vals := make([]float64, 0, 64)
|
||||
vals = append(vals, edges...)
|
||||
for _, e := range edges {
|
||||
vals = append(vals,
|
||||
math.Nextafter(e, math.Inf(1)),
|
||||
math.Nextafter(e, math.Inf(-1)),
|
||||
e+0,
|
||||
e-0)
|
||||
}
|
||||
vals = append(vals,
|
||||
0, math.Copysign(0, -1),
|
||||
math.Inf(1), math.Inf(-1),
|
||||
math.NaN(), math.Copysign(math.NaN(), -1),
|
||||
5e-324, -5e-324, 1e-320, -1e-320,
|
||||
1e300, -1e300)
|
||||
g := NewGenerator(3)
|
||||
rnd, err := Floats(g, 40)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vals = append(vals, rnd.RawFloats()[:40]...)
|
||||
return vals
|
||||
}
|
||||
|
||||
// TestBinSearchMatchesReference pins AssignBins' float bin selection
|
||||
// against the bisection walk on every edge class: uniform edges, a
|
||||
// single bin, repeated edges, negative ranges, infinite outer edges,
|
||||
// subnormal edges and both large and small magnitudes. NaN edges are out
|
||||
// of scope: an ascending edge set holds none.
|
||||
func TestBinSearchMatchesReference(t *testing.T) {
|
||||
edgeSets := [][]float64{
|
||||
{0, 1},
|
||||
{0, 0.25, 0.5, 0.75, 1},
|
||||
{1, 2, 3},
|
||||
{0, 0, 0, 1, 2, 2},
|
||||
{-5, -3.5, -1, 0, 0, 2},
|
||||
{math.Inf(-1), -1, 0, 1, math.Inf(1)},
|
||||
{0, 5e-324, 1e-320, 1},
|
||||
{1e300, 2e300, 3e300},
|
||||
linspaceVals(0, 1, 256),
|
||||
linspaceVals(-3, 7, 257),
|
||||
}
|
||||
for _, ev := range edgeSets {
|
||||
vals := probeSearchValues(ev)
|
||||
edges := mustFloats(t, ev)
|
||||
a := mustFloats(t, vals)
|
||||
out, err := AssignBins(a, edges)
|
||||
if err != nil {
|
||||
t.Fatalf("AssignBins m=%d: %v", len(ev), err)
|
||||
}
|
||||
got := out.RawInts()[:out.Len()]
|
||||
for i, v := range vals {
|
||||
want := refBinFloat(ev, v)
|
||||
if got[i] != int64(want) {
|
||||
t.Fatalf("edges %v…%v (m=%d), v=%v: bin %d, want %d",
|
||||
ev[0], ev[len(ev)-1], len(ev), v, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBinSearchMatchesReferenceInt pins the int bin selection, native
|
||||
// int64 comparisons included: the extremes, the neighbours above 2^53
|
||||
// that a float64 detour would fold together, and repeated edges.
|
||||
func TestBinSearchMatchesReferenceInt(t *testing.T) {
|
||||
edgeSets := [][]int64{
|
||||
{0, 1},
|
||||
{1, 2, 3},
|
||||
{math.MinInt64, -1, 0, 1, math.MaxInt64},
|
||||
{1 << 53, 1<<53 + 1, 1<<53 + 2},
|
||||
{5, 5, 7, 9, 9},
|
||||
{math.MinInt64, math.MaxInt64},
|
||||
}
|
||||
values := []int64{
|
||||
0, 1, 2, 3, 5, 7, 9,
|
||||
-1, 4, 6, 8, 10,
|
||||
math.MinInt64, math.MaxInt64, math.MinInt64 + 1, math.MaxInt64 - 1,
|
||||
1 << 53, 1<<53 + 1, 1<<53 + 2, 1<<53 + 3,
|
||||
-(1 << 53), -(1<<53 + 1),
|
||||
}
|
||||
for _, ev := range edgeSets {
|
||||
edges, err := FromInts(ev, len(ev))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a, err := FromInts(values, len(values))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := AssignBins(a, edges)
|
||||
if err != nil {
|
||||
t.Fatalf("AssignBins int m=%d: %v", len(ev), err)
|
||||
}
|
||||
got := out.RawInts()[:out.Len()]
|
||||
for i, v := range values {
|
||||
want := refBinInt(ev, v)
|
||||
if got[i] != int64(want) {
|
||||
t.Fatalf("int edges %v, v=%d: bin %d, want %d", ev, v, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssignBinsPinsDocumentedSemantics fixes the observable edge rules
|
||||
// with literals: below-range clamps to bin 0, at-or-above the last edge
|
||||
// clamps to the outermost bin, an exact edge lands in the bin it opens,
|
||||
// and a NaN keeps the outermost bin whatever its sign.
|
||||
func TestAssignBinsPinsDocumentedSemantics(t *testing.T) {
|
||||
edges := mustFloats(t, []float64{0, 1.0 / 3, 2.0 / 3, 1})
|
||||
vals := []float64{
|
||||
-1, 0, 1.0 / 3, 0.5, 2.0 / 3, 0.999, 1, 2,
|
||||
math.NaN(), math.Copysign(math.NaN(), -1),
|
||||
math.Inf(1), math.Inf(-1), math.Copysign(0, -1),
|
||||
}
|
||||
a := mustFloats(t, vals)
|
||||
out, err := AssignBins(a, edges)
|
||||
if err != nil {
|
||||
t.Fatalf("AssignBins: %v", err)
|
||||
}
|
||||
want := []int64{0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 0, 0}
|
||||
for i, w := range want {
|
||||
if got := out.RawInts()[i]; got != w {
|
||||
t.Fatalf("value %v (bits %#x): bin %d, want %d", vals[i], math.Float64bits(vals[i]), got, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchSortedMatchesReference pins the rightmost insertion rule
|
||||
// against the bisection for both operand kinds through the public entry
|
||||
// point, including the int64 range above 2^53, NaN needles, infinite
|
||||
// needles and infinite haystack ends, ties and the two zeros.
|
||||
func TestSearchSortedMatchesReference(t *testing.T) {
|
||||
haySetsF := [][]float64{
|
||||
{1},
|
||||
{0, 0},
|
||||
{1, 2},
|
||||
{-1, 0, 0, 1, 4, 4, 4, 9},
|
||||
{math.Inf(-1), -3, 0, 3, math.Inf(1)},
|
||||
{math.Copysign(0, -1), 0, 0, 1},
|
||||
linspaceVals(0, 1, 33),
|
||||
}
|
||||
needleSetsF := [][]float64{
|
||||
{0, 0.5, 1, 4, 9, 10},
|
||||
{-1, math.Copysign(0, -1), 0, 3},
|
||||
{math.NaN(), math.Copysign(math.NaN(), -1), math.Inf(1), math.Inf(-1)},
|
||||
probeSearchValues([]float64{-1, 0, 1, 4}),
|
||||
}
|
||||
for _, h := range haySetsF {
|
||||
for _, qs := range needleSetsF {
|
||||
hay := mustFloats(t, h)
|
||||
nd := mustFloats(t, qs)
|
||||
out, err := SearchSorted(hay, nd)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchSorted: %v", err)
|
||||
}
|
||||
got := out.RawInts()[:out.Len()]
|
||||
for i, q := range qs {
|
||||
want := refUpperFloat(h, q)
|
||||
if got[i] != int64(want) {
|
||||
t.Fatalf("haystack %v, needle %v: position %d, want %d", h, q, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
haySetsI := [][]int64{
|
||||
{1},
|
||||
{0, 0, 1},
|
||||
{1 << 53, 1<<53 + 1, 1<<53 + 1, 1 << 54},
|
||||
{math.MinInt64, -1, 0, 1, math.MaxInt64},
|
||||
}
|
||||
needleValsI := []int64{
|
||||
0, 1, 2, 1 << 53, 1<<53 + 1, 1<<53 + 2, 1 << 54,
|
||||
math.MinInt64, math.MaxInt64, -1,
|
||||
}
|
||||
for _, h := range haySetsI {
|
||||
hay, err := FromInts(h, len(h))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nd, err := FromInts(needleValsI, len(needleValsI))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := SearchSorted(hay, nd)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchSorted int: %v", err)
|
||||
}
|
||||
got := out.RawInts()[:out.Len()]
|
||||
for i, q := range needleValsI {
|
||||
want := refUpperInt(h, q)
|
||||
if got[i] != int64(want) {
|
||||
t.Fatalf("int haystack %v, needle %d: position %d, want %d", h, q, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
// An int haystack above 2^53 must search natively, and an empty
|
||||
// haystack reports position zero everywhere.
|
||||
hi, err := FromInts([]int64{1 << 53, 1<<53 + 1, 1 << 54}, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ni, err := FromInts([]int64{1 << 53, 1<<53 + 1, 1<<53 + 2}, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := SearchSorted(hi, ni)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchSorted: %v", err)
|
||||
}
|
||||
for i, w := range []int64{1, 2, 2} {
|
||||
if got := out.RawInts()[i]; got != w {
|
||||
t.Fatalf("int needle %d: position %d, want %d", ni.RawInts()[i], got, w)
|
||||
}
|
||||
}
|
||||
empty, err := FromInts(nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err = SearchSorted(empty, ni)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchSorted empty: %v", err)
|
||||
}
|
||||
for i := range 3 {
|
||||
if got := out.RawInts()[i]; got != 0 {
|
||||
t.Fatalf("empty haystack position %d = %d, want 0", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInterpolateSegmentMatchesReference pins the whole per-point
|
||||
// pipeline against the bisection-derived reference across knot classes:
|
||||
// strict ascent, repeated knots at the edges and in the interior, the
|
||||
// minimal two-knot set, and queries on knots, between them, outside the
|
||||
// range and at both infinities. The comparison is the result value's
|
||||
// exact bits, which is the observable contract.
|
||||
func TestInterpolateSegmentMatchesReference(t *testing.T) {
|
||||
knotSets := [][]float64{
|
||||
{0, 1},
|
||||
{0, 1, 2, 3},
|
||||
{0, 0.5, 0.5, 0.5, 1},
|
||||
{0, 0, 1, 2},
|
||||
{0, 1, 2, 2},
|
||||
{-2, -1, -1, 0},
|
||||
{0, 1e-300, 2e-300, 3e-300},
|
||||
linspaceVals(0, 1, 33),
|
||||
}
|
||||
for _, xv := range knotSets {
|
||||
ys := make([]float64, len(xv))
|
||||
for i := range ys {
|
||||
ys[i] = float64(i*i-3*i+1) / 7
|
||||
}
|
||||
queries := probeSearchValues([]float64{xv[0], xv[len(xv)/2], xv[len(xv)-1]})
|
||||
qv := make([]float64, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
if q != q {
|
||||
continue // NaN queries are refused before the search
|
||||
}
|
||||
qv = append(qv, q)
|
||||
}
|
||||
xArr := mustFloats(t, xv)
|
||||
yArr := mustFloats(t, ys)
|
||||
qArr := mustFloats(t, qv)
|
||||
out, err := Interpolate(xArr, yArr, qArr)
|
||||
if err != nil {
|
||||
t.Fatalf("Interpolate knots %v…%v: %v", xv[0], xv[len(xv)-1], err)
|
||||
}
|
||||
for i, q := range qv {
|
||||
lo := refInterpSegment(xv, q)
|
||||
x0, x1 := xv[lo], xv[lo+1]
|
||||
y0, y1 := ys[lo], ys[lo+1]
|
||||
tt := 0.0
|
||||
if x1 > x0 {
|
||||
tt = (q - x0) / (x1 - x0)
|
||||
} else if q > x0 {
|
||||
tt = 1
|
||||
}
|
||||
if tt < 0 {
|
||||
tt = 0
|
||||
}
|
||||
if tt > 1 {
|
||||
tt = 1
|
||||
}
|
||||
want := y0 + tt*(y1-y0)
|
||||
got := out.RawFloats()[i]
|
||||
if math.Float64bits(got) != math.Float64bits(want) {
|
||||
t.Fatalf("knots %v…%v (n=%d), q=%v: value %#x, want %#x",
|
||||
xv[0], xv[len(xv)-1], len(xv), q, math.Float64bits(got), math.Float64bits(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInterpolateMatchesReferencePointwise pins the whole per-point
|
||||
// pipeline: same segment, same t expression, same bits, for random
|
||||
// queries against repeated-knot and strictly ascending knot sets.
|
||||
func TestInterpolateMatchesReferencePointwise(t *testing.T) {
|
||||
for _, knots := range [][]float64{
|
||||
{0, 0.5, 0.5, 1, 2, 2, 3},
|
||||
linspaceVals(-1, 2, 17),
|
||||
} {
|
||||
ys := make([]float64, len(knots))
|
||||
for i := range ys {
|
||||
ys[i] = float64(i*i-3*i+1) / 7
|
||||
}
|
||||
g := NewGenerator(9)
|
||||
rnd, err := Floats(g, 200)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
queries := make([]float64, 0, 220)
|
||||
queries = append(queries, rnd.RawFloats()[:200]...)
|
||||
for _, k := range knots {
|
||||
queries = append(queries, k)
|
||||
}
|
||||
queries = append(queries, -2, 5, math.Inf(1), math.Inf(-1), math.Copysign(0, -1))
|
||||
xv := mustFloats(t, knots)
|
||||
yv := mustFloats(t, ys)
|
||||
qv := mustFloats(t, queries)
|
||||
out, err := Interpolate(xv, yv, qv)
|
||||
if err != nil {
|
||||
t.Fatalf("Interpolate: %v", err)
|
||||
}
|
||||
n := len(knots)
|
||||
for i, q := range queries {
|
||||
lo := refInterpSegment(knots, q)
|
||||
x0, x1 := knots[lo], knots[lo+1]
|
||||
y0, y1 := ys[lo], ys[lo+1]
|
||||
tt := 0.0
|
||||
if x1 > x0 {
|
||||
tt = (q - x0) / (x1 - x0)
|
||||
} else if q > x0 {
|
||||
tt = 1
|
||||
}
|
||||
if tt < 0 {
|
||||
tt = 0
|
||||
}
|
||||
if tt > 1 {
|
||||
tt = 1
|
||||
}
|
||||
want := y0 + tt*(y1-y0)
|
||||
got := out.RawFloats()[i]
|
||||
if math.Float64bits(got) != math.Float64bits(want) {
|
||||
t.Fatalf("knots n=%d, query %v: value %#x, want %#x",
|
||||
n, q, math.Float64bits(got), math.Float64bits(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInterpolateNaNQueryReportsSmallestIndex pins the error contract
|
||||
// under the parallel walk: the first NaN in order names the error.
|
||||
func TestInterpolateNaNQueryReportsSmallestIndex(t *testing.T) {
|
||||
xs := mustFloats(t, []float64{0, 1, 2})
|
||||
ys := mustFloats(t, []float64{0, 1, 2})
|
||||
q := mustFloats(t, []float64{0.5, 0.5, math.NaN(), 0.5, math.NaN(), 0.5})
|
||||
if _, err := Interpolate(xs, ys, q); err == nil || !strings.Contains(err.Error(), "query 2 is NaN") {
|
||||
t.Fatalf("Interpolate NaN error = %v, want the query 2 report", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArgwhereMatchesReference pins the coordinate packing of the
|
||||
// count-then-fill against the single odometer walk, across dtypes,
|
||||
// ranks, densities and a chunk-boundary size; -0.0 counts as zero and
|
||||
// NaN counts as non-zero, as the value tests have always had it.
|
||||
func TestArgwhereMatchesReference(t *testing.T) {
|
||||
shapes := [][]int{
|
||||
{1}, {7}, {5, 4}, {3, 5, 4}, {2, 2, 2, 2}, {80, 80}, {1, 1, 5},
|
||||
}
|
||||
for _, shape := range shapes {
|
||||
n := 1
|
||||
for _, d := range shape {
|
||||
n *= d
|
||||
}
|
||||
for _, density := range []int{0, 1, 2, 3, 64} {
|
||||
// density: 0 all-zero, 1 all-non-zero, k every k-th non-zero.
|
||||
valsF := make([]float64, n)
|
||||
valsI := make([]int64, n)
|
||||
for i := range n {
|
||||
if density == 1 || (density > 1 && i%density == 0) {
|
||||
valsF[i] = float64(i+1) / 3
|
||||
valsI[i] = int64(i) + 1
|
||||
}
|
||||
}
|
||||
if density > 0 && n > 3 {
|
||||
valsF[2] = math.NaN()
|
||||
valsF[3] = math.Copysign(0, -1) // zero
|
||||
valsF[4] = math.Inf(1) // non-zero
|
||||
}
|
||||
af, _ := FromFloats(valsF, shape...)
|
||||
ai, _ := FromInts(valsI, shape...)
|
||||
for _, a := range []*Array{af, ai} {
|
||||
want := refArgwhere(a)
|
||||
out, err := Argwhere(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Argwhere %v density %d: %v", shape, density, err)
|
||||
}
|
||||
got := out.RawInts()[:out.Len()]
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("Argwhere %v density %d: %d coordinates, want %d", shape, density, len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("Argwhere %v density %d: coordinate %d = %d, want %d", shape, density, i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
if out.Shape()[0]*out.Shape()[1] != len(want) || out.Shape()[1] != a.NDim() {
|
||||
t.Fatalf("Argwhere %v: shape %v does not pack %d coordinates of rank %d",
|
||||
shape, out.Shape(), len(want), a.NDim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A rebased view: the walk is bounded by the view's own extent, and
|
||||
// the coordinates are the view's.
|
||||
base := mustFloats(t, []float64{0, 5, 0, 0, 7, 9, 0, 1, 0, 0, 0, 2})
|
||||
view, err := Slice(base, 0, 3, 11)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v2, err := Reshape(view, 2, 4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := refArgwhere(v2)
|
||||
out, err := Argwhere(v2)
|
||||
if err != nil {
|
||||
t.Fatalf("Argwhere view: %v", err)
|
||||
}
|
||||
got := out.RawInts()[:out.Len()]
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("Argwhere view: %d coordinates, want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("Argwhere view: coordinate %d = %d, want %d", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGatherTakeMatchReference pins the parallel payload walks against
|
||||
// the per-element accessor walks, values and error reports alike, over
|
||||
// every dtype, several ranks, repeated and out-of-range indices, view
|
||||
// indices and view sources.
|
||||
func TestGatherTakeMatchReference(t *testing.T) {
|
||||
mk := func(vals []float64, shape ...int) *Array {
|
||||
a, err := FromFloats(vals, shape...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
srcF := mk([]float64{
|
||||
1, 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
}, 3, 4)
|
||||
srcI, _ := FromInts([]int64{
|
||||
1, 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
}, 3, 4)
|
||||
srcC, _ := FromComplexes([]complex128{1, 2i, 3, 4i, 5, 6i}, 3, 2)
|
||||
src3d := mk([]float64{
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
9, 10, 11, 12, 13, 14, 15, 16,
|
||||
}, 2, 2, 4)
|
||||
idxSets := []*Array{
|
||||
mustInts(t, []int64{3, 0, 2, 2, 1, 3}, 3, 2), // for dim 1: (3, 2)
|
||||
mustInts(t, []int64{2, 0, 2, 1}, 2, 2), // for dim 0: (2, 4) reshaped below
|
||||
mustInts(t, []int64{0, 3, 2, 1, 1, 3, 0, 2}, 2, 4),
|
||||
}
|
||||
for _, src := range []*Array{srcF, srcI} {
|
||||
for dim := range 2 {
|
||||
for _, idx := range idxSets {
|
||||
if !gatherCompatible(src.shape, idx.shape, dim) {
|
||||
continue
|
||||
}
|
||||
want, wantErr := refGather(src, dim, idx)
|
||||
got, gotErr := Gather(src, dim, idx)
|
||||
if (wantErr == nil) != (gotErr == nil) || (wantErr != nil && wantErr.Error() != gotErr.Error()) {
|
||||
t.Fatalf("Gather dim %d: error %v, want %v", dim, gotErr, wantErr)
|
||||
}
|
||||
if wantErr == nil && !Equal(got, want) {
|
||||
t.Fatalf("Gather dim %d idx %v: %v, want %v", dim, idx.RawInts(), got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Complex source gathers unchanged; the index keeps the source's
|
||||
// non-dim extent, as gatherCompatible requires.
|
||||
idxC := mustInts(t, []int64{2, 0, 1, 1}, 2, 2)
|
||||
wantC, _ := refGather(srcC, 0, idxC)
|
||||
gotC, err := Gather(srcC, 0, idxC)
|
||||
if err != nil || !Equal(gotC, wantC) {
|
||||
t.Fatalf("Gather complex: %v vs %v (%v)", gotC, wantC, err)
|
||||
}
|
||||
// 3-D source, gather along the interior dimension: the index keeps
|
||||
// the outer and inner extents and varies along dim 1.
|
||||
idx3 := mustInts(t, []int64{1, 0, 1, 1, 0, 1, 0, 0}, 2, 1, 4)
|
||||
want3, _ := refGather(src3d, 1, idx3)
|
||||
got3, err := Gather(src3d, 1, idx3)
|
||||
if err != nil || !Equal(got3, want3) {
|
||||
t.Fatalf("Gather 3-D dim 1: %v vs %v (%v)", got3, want3, err)
|
||||
}
|
||||
// Out-of-range and negative indices: same error, same position.
|
||||
bad := mustInts(t, []int64{0, 4, 2, 1, 0, 0, 0, 0}, 2, 4)
|
||||
_, wantErr := refGather(srcF, 0, bad)
|
||||
_, gotErr := Gather(srcF, 0, bad)
|
||||
if wantErr == nil || gotErr == nil || wantErr.Error() != gotErr.Error() {
|
||||
t.Fatalf("Gather range error: %v, want %v", gotErr, wantErr)
|
||||
}
|
||||
neg := mustInts(t, []int64{1, -1, 0, 0, 0, 0, 0, 0}, 2, 4)
|
||||
_, wantErr = refGather(srcF, 0, neg)
|
||||
_, gotErr = Gather(srcF, 0, neg)
|
||||
if wantErr == nil || gotErr == nil || wantErr.Error() != gotErr.Error() {
|
||||
t.Fatalf("Gather negative error: %v, want %v", gotErr, wantErr)
|
||||
}
|
||||
// A rebased view on the index side: the walk is bounded by the
|
||||
// view's own extent, and the invisible payload tail never reads.
|
||||
idxBase := mustInts(t, []int64{0, 1, 2, 0, 2, 1, 0, 2, 99, 99, 99, 99}, 3, 4)
|
||||
idxView, err := Slice(idxBase, 0, 1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantV, _ := refGather(srcF, 0, idxView)
|
||||
gotV, err := Gather(srcF, 0, idxView)
|
||||
if err != nil || !Equal(gotV, wantV) {
|
||||
t.Fatalf("Gather view index: %v vs %v (%v)", gotV, wantV, err)
|
||||
}
|
||||
|
||||
// Take: every dtype, view source, view indices, range errors.
|
||||
for _, src := range []*Array{srcF, srcI, srcC} {
|
||||
tk := mustInts(t, []int64{int64(src.Len()) - 1, 0, 3, 3, 1}, 5)
|
||||
wantT, wantErr := refTake(src, tk)
|
||||
gotT, gotErr := Take(src, tk)
|
||||
if (wantErr == nil) != (gotErr == nil) || (wantErr != nil && wantErr.Error() != gotErr.Error()) {
|
||||
t.Fatalf("Take: error %v, want %v", gotErr, wantErr)
|
||||
}
|
||||
if wantErr == nil && !Equal(gotT, wantT) {
|
||||
t.Fatalf("Take: %v, want %v", gotT, wantT)
|
||||
}
|
||||
}
|
||||
// Take on a rebased view source: flat indices count from the view's
|
||||
// origin, and the walk is bounded by the view's own extent.
|
||||
longBase := mustFloats(t, []float64{99, 99, 99, 99, 10, 11, 12, 13, 14, 15, 16, 17})
|
||||
viewSrc, err := Slice(longBase, 0, 4, 12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := mustInts(t, []int64{7, 0, 3}, 3)
|
||||
wantT, _ := refTake(viewSrc, tk)
|
||||
gotT, err := Take(viewSrc, tk)
|
||||
if err != nil || !Equal(gotT, wantT) {
|
||||
t.Fatalf("Take view source: %v vs %v (%v)", gotT, wantT, err)
|
||||
}
|
||||
badT := mustInts(t, []int64{1, 2, 99}, 3)
|
||||
_, wantErr = refTake(srcF, badT)
|
||||
_, gotErr = Take(srcF, badT)
|
||||
if wantErr == nil || gotErr == nil || wantErr.Error() != gotErr.Error() {
|
||||
t.Fatalf("Take range error: %v, want %v", gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrateHalvingIsBitIdentical proves the multiplication by 0.5
|
||||
// reproduces the division by 2 bit for bit on adversarial payloads:
|
||||
// subnormals, the signed zeros, infinities, NaN payloads and random
|
||||
// spreads, for a range of spacings. The accumulation order is untouched
|
||||
// either way.
|
||||
func TestIntegrateHalvingIsBitIdentical(t *testing.T) {
|
||||
payloads := [][]float64{
|
||||
{1, 2, 3, 4},
|
||||
{5e-324, 1e-320, 2.5e-320, 1e-308},
|
||||
{math.Copysign(0, -1), 0, 1, math.Copysign(0, -1)},
|
||||
{math.Inf(1), 1, math.Inf(-1), 2},
|
||||
{math.NaN(), 1, 2, math.Copysign(math.NaN(), -1)},
|
||||
{1e308, 1.5e308, 2, 3},
|
||||
{-1e308, 1e308, 1, -1},
|
||||
}
|
||||
g := NewGenerator(21)
|
||||
rnd, err := Floats(g, 128)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payloads = append(payloads, rnd.RawFloats()[:128])
|
||||
for _, dx := range []float64{1, 0.5, 2, 1e-300, 1e300, -3, math.NaN(), math.Inf(1)} {
|
||||
for _, y := range payloads {
|
||||
yArr := mustFloats(t, y)
|
||||
gotTotal, err := Integrate(yArr, dx)
|
||||
if err != nil {
|
||||
t.Fatalf("Integrate: %v", err)
|
||||
}
|
||||
var want float64
|
||||
for i := 1; i < len(y); i++ {
|
||||
want += (y[i-1] + y[i]) / 2
|
||||
}
|
||||
want *= dx
|
||||
if math.Float64bits(gotTotal) != math.Float64bits(want) {
|
||||
t.Fatalf("Integrate dx=%v payload %v…: %#x, want %#x",
|
||||
dx, y[0], math.Float64bits(gotTotal), math.Float64bits(want))
|
||||
}
|
||||
gotCum, err := CumulativeIntegrate(yArr, dx)
|
||||
if err != nil {
|
||||
t.Fatalf("CumulativeIntegrate: %v", err)
|
||||
}
|
||||
ov := make([]float64, len(y))
|
||||
for i := 1; i < len(y); i++ {
|
||||
ov[i] = ov[i-1] + (y[i-1]+y[i])/2*dx
|
||||
}
|
||||
for i := range y {
|
||||
if math.Float64bits(gotCum.RawFloats()[i]) != math.Float64bits(ov[i]) {
|
||||
t.Fatalf("CumulativeIntegrate dx=%v at %d: %#x, want %#x",
|
||||
dx, i, math.Float64bits(gotCum.RawFloats()[i]), math.Float64bits(ov[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestArgwhereVariantsMatchReference pins every packing variant the
|
||||
// probe walks, production included, against the single odometer walk:
|
||||
// the coordinates, their order and the packed shape must match whatever
|
||||
// the buffers do on the way.
|
||||
func TestArgwhereVariantsMatchReference(t *testing.T) {
|
||||
variants := append(argwhereStyles(),
|
||||
struct {
|
||||
name string
|
||||
run func(*Array) *Array
|
||||
}{"merge-capped-1024", func(a *Array) *Array { return probeArgwhereMerge(a, 1024) }},
|
||||
)
|
||||
shapes := [][]int{{1}, {7}, {5, 4}, {3, 5, 4}, {80, 80}, {2, 2, 2, 2}}
|
||||
for _, shape := range shapes {
|
||||
n := 1
|
||||
for _, d := range shape {
|
||||
n *= d
|
||||
}
|
||||
for _, density := range []int{0, 1, 2, 64} {
|
||||
valsF := make([]float64, n)
|
||||
for i := range n {
|
||||
if density == 1 || (density > 1 && i%density == 0) {
|
||||
valsF[i] = float64(i+1) / 3
|
||||
}
|
||||
}
|
||||
if density > 0 && n > 4 {
|
||||
valsF[2] = math.NaN()
|
||||
valsF[3] = math.Copysign(0, -1)
|
||||
valsF[4] = math.Inf(1)
|
||||
}
|
||||
a, _ := FromFloats(valsF, shape...)
|
||||
want := refArgwhere(a)
|
||||
for _, v := range variants {
|
||||
out := v.run(a)
|
||||
if out == nil {
|
||||
t.Fatalf("%s %v density %d: nil result", v.name, shape, density)
|
||||
}
|
||||
got := out.RawInts()[:out.Len()]
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("%s %v density %d: %d coordinates, want %d", v.name, shape, density, len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("%s %v density %d: coordinate %d = %d, want %d", v.name, shape, density, i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A rebased view: bounded by the view's own extent for every variant.
|
||||
base := mustFloats(t, []float64{0, 5, 0, 0, 7, 9, 0, 1, 0, 0, 0, 2})
|
||||
view, err := Slice(base, 0, 3, 11)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v2, err := Reshape(view, 2, 4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := refArgwhere(v2)
|
||||
for _, v := range variants {
|
||||
out := v.run(v2)
|
||||
got := out.RawInts()[:out.Len()]
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("%s view: %d coordinates, want %d", v.name, len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("%s view: coordinate %d = %d, want %d", v.name, i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSortRadixConfigsBitIdentical pins every probe radix configuration
|
||||
// against the production Sort and ArgSort on adversarial fixtures: the
|
||||
// sorted values bit for bit (NaN payloads included) and the
|
||||
// permutations exactly, whatever crew cap and digit width carry them.
|
||||
func TestSortRadixConfigsBitIdentical(t *testing.T) {
|
||||
mkInts := func(vals []int64) *Array {
|
||||
a, err := FromInts(vals, len(vals))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
intRand := benchIntsShape(1000, 5000)
|
||||
intWide := benchIntsShape(1<<30, 5000)
|
||||
intParallel := benchIntsShape(1000, 20000)
|
||||
floatRand := benchFloatsShape(5000)
|
||||
floatParallel := benchFloatsShape(20000)
|
||||
fixtures := []*Array{
|
||||
intRand,
|
||||
intWide,
|
||||
intParallel,
|
||||
mkInts([]int64{math.MinInt64, math.MaxInt64, 0, -1, 1, 1 << 53, 1<<53 + 1, math.MinInt64 + 1, math.MaxInt64 - 1, 5, 5, 5}),
|
||||
mkInts([]int64{42, 42, 42, 42, 42}),
|
||||
floatRand,
|
||||
floatParallel,
|
||||
mustFloats(t, []float64{math.NaN(), math.Copysign(math.NaN(), -1), math.Copysign(0, -1), 0, math.Inf(1), math.Inf(-1), 1, 1, 2.5, 2.5, -3, 0.1}),
|
||||
mustFloats(t, []float64{7, 7, 7, 7, 7}),
|
||||
}
|
||||
cfgs := probeSortConfigs()
|
||||
for _, a := range fixtures {
|
||||
wantSort, err := Sort(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantIdx, err := ArgSort(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range cfgs {
|
||||
var gotSort *Array
|
||||
if a.dt == Int {
|
||||
gotSort = probeSortInt(a, c.wcap, c.bits)
|
||||
} else {
|
||||
gotSort = probeSortFloat(a, c.wcap, c.bits)
|
||||
}
|
||||
if gotSort.Len() != wantSort.Len() {
|
||||
t.Fatalf("%v %s: sorted length %d, want %d", a.Shape(), c.name, gotSort.Len(), wantSort.Len())
|
||||
}
|
||||
if a.dt == Int {
|
||||
g, w := gotSort.RawInts()[:gotSort.Len()], wantSort.RawInts()[:wantSort.Len()]
|
||||
for i := range w {
|
||||
if g[i] != w[i] {
|
||||
t.Fatalf("int fixture %v %s: sorted[%d] = %d, want %d", a.Shape(), c.name, i, g[i], w[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g, w := gotSort.RawFloats()[:gotSort.Len()], wantSort.RawFloats()[:wantSort.Len()]
|
||||
for i := range w {
|
||||
if math.Float64bits(g[i]) != math.Float64bits(w[i]) {
|
||||
t.Fatalf("float fixture %v %s: sorted[%d] = %#x, want %#x", a.Shape(), c.name, i, math.Float64bits(g[i]), math.Float64bits(w[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
gotIdx := probeArgSort(a, c.wcap, c.bits)
|
||||
g, w := gotIdx.RawInts()[:gotIdx.Len()], wantIdx.RawInts()[:wantIdx.Len()]
|
||||
for i := range w {
|
||||
if g[i] != w[i] {
|
||||
t.Fatalf("fixture %v %s: argsort[%d] = %d, want %d", a.Shape(), c.name, i, g[i], w[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// linspaceVals builds n evenly spaced values, the deterministic edge
|
||||
// ladder the bin equivalence tests walk.
|
||||
func linspaceVals(start, stop float64, n int) []float64 {
|
||||
a, err := Linspace(start, stop, n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return append([]float64(nil), a.RawFloats()[:n]...)
|
||||
}
|
||||
|
||||
func mustInts(t *testing.T, vals []int64, shape ...int) *Array {
|
||||
t.Helper()
|
||||
a, err := FromInts(vals, shape...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
Reference in New Issue
Block a user