// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package core import ( "fmt" "math" "strconv" "sourcedock.dev/petrbalvin/tensor/internal/engine" ) // Scalar boxes a single numeric result whose dtype is known only at // runtime: Sum of an int array is an int, of a float array a // float, of a complex array a complex. Int, Float and Complex unpack it; // IsFloat and IsComplex say which. type Scalar struct { isFloat bool isComplex bool i int64 f float64 c complex128 } // Int returns the scalar as an int64. It converts a float or complex // scalar by truncation, mirroring Go's conversions; use IsFloat and // IsComplex when the distinction matters. func (s Scalar) Int() int64 { if s.isFloat { return int64(s.f) } if s.isComplex { return int64(real(s.c)) } return s.i } // Float returns the scalar as a float64, converting int and complex // scalars (a complex scalar contributes its real part). func (s Scalar) Float() float64 { if s.isFloat { return s.f } if s.isComplex { return real(s.c) } return float64(s.i) } // Complex returns the scalar as a complex128, converting real scalars. func (s Scalar) Complex() complex128 { if s.isComplex { return s.c } if s.isFloat { return complex(s.f, 0) } return complex(float64(s.i), 0) } // IsFloat reports whether the scalar came from a float computation. func (s Scalar) IsFloat() bool { return s.isFloat } // IsComplex reports whether the scalar came from a complex computation. func (s Scalar) IsComplex() bool { return s.isComplex } // String renders the scalar with its dtype, as in "int 7" or // "complex (4-2i)". The complex form is the one %v prints, matching // Array.String: a single sign between the parts, never "+-". func (s Scalar) String() string { switch { case s.isComplex: return "complex " + fmt.Sprintf("%v", s.c) case s.isFloat: return "float " + strconv.FormatFloat(s.f, 'g', -1, 64) default: return "int " + strconv.FormatInt(s.i, 10) } } // Sum returns the sum of all elements. An int sum wraps on overflow like // Go's int64 arithmetic; the narrow integer widths widen // exactly and accumulate in int64, answered as an Int scalar, and a bool // sum counts its true elements into the same Int scalar; float16 and // float32 sums accumulate in float64 and answer a float scalar; // an empty array sums to zero. // // The fold is partitioned by length alone, never by the worker count, so // the same input answers the same scalar on any machine and under any // worker setting: the range is cut into fixed chunks, each chunk // accumulates into four interleaved partials, and the chunk results // combine through a balanced pairwise tree over the chunk indices // (treeSum). The tree is the shape the distributed reduction shares: a // range cut at chunk boundaries into shards answers the same bits as // this fold, because shard partials and array chunks are entries of one // tree combined by one function. The integer sum is exact under any // order. The floating-point fold rounds differently from the single // chain it replaced, not always in the same direction: the interleaved // partials shorten each dependency chain and the tree adds a logarithmic // number of combination roundings, which measured within a small factor // of the chain at every length tried and better than it once the chain // is long enough for its own roundings to accumulate. Only the array's // own elements take part: a rebased view's payload may run past its // element count, and those invisible tail slots never contribute. func Sum(a *Array) Scalar { n := a.Len() switch a.dt { case Int: return Scalar{i: intFoldSum(a.ints[:n])} case Bool: // The bool sum counts the true elements, answered as the Int // scalar every integer-class reduction answers. return Scalar{i: boolFoldCount(a.bools[:n])} case Int8: return Scalar{i: intFoldSumNarrow(a.i8s[:n])} case Uint8: return Scalar{i: intFoldSumNarrow(a.u8s[:n])} case Int16: return Scalar{i: intFoldSumNarrow(a.i16s[:n])} case Uint16: return Scalar{i: intFoldSumNarrow(a.u16s[:n])} case Int32: return Scalar{i: intFoldSumNarrow(a.i32s[:n])} case Uint32: return Scalar{i: intFoldSumNarrow(a.u32s[:n])} case Float16: // The half sum accumulates in float64 and answers a float // scalar, exactly as the float32 sum does: every // widening is exact, so the fold sees the same addends an // accessor walk would hand over. return Scalar{isFloat: true, f: floatFoldSumF16(a.halves[:n])} case Float32: return Scalar{isFloat: true, f: floatFoldSumF32(a.floats32[:n])} case Float: return Scalar{isFloat: true, f: floatFoldSum(a.floats[:n])} default: return Scalar{isComplex: true, c: complexFoldSum(a.complexes[:n])} } } // foldChunk is the element count one partial fold carries. It is a // constant of the arithmetic: the partition follows from the length // alone, never from the worker count, so the same input answers the // same scalar on any machine and under any worker setting. const foldChunk = 1 << 16 // foldParts is the number of chunks a length is cut into, bounded so the // partial table stays small for very long arrays. func foldParts(n int) int { if n <= foldChunk { return 1 } parts := min((n+foldChunk-1)/foldChunk, 1<<12) return parts } // FoldParts reports how many blocks the canonical reduction partition // cuts a range of n elements into. The partition is a function of the // length alone, so it is the same on every machine and under any worker // setting; the spmd package cuts distributed data on these boundaries, // which is what makes a sharded reduction compose into the single-array // fold's exact bits. func FoldParts(n int) int { return foldParts(n) } // FoldBoundary reports the index where block c of the canonical // partition of n elements begins; block c spans [FoldBoundary(n, c), // FoldBoundary(n, c+1)). Block 0 begins at 0 and FoldBoundary(n, // FoldParts(n)) is n. func FoldBoundary(n, c int) int { return c * n / foldParts(n) } // TreeSum combines reduction partials through the balanced pairwise // tree the folds combine their chunk partials with: the node over a // range splits at its midpoint and adds the two halves' nodes, left // before right. The value of any contiguous range of partials is one // node of the tree, so partials gathered from a sharded cut combine // into the single-array fold's exact bits; the spmd package feeds it // the block values the shards folded. func TreeSum[N int64 | float64 | complex128](vals []N) N { return treeSum(vals) } // FoldRange is one canonical block's fold over a float64 payload: the // four interleaved chains combined as a balanced pair. Shards fold // their own blocks with it, so a block's value is the same bits // wherever its elements live. func FoldRange(src []float64) float64 { return foldRange(src) } // FoldRangeF32 is FoldRange over a float32 payload; every widening is // exact. func FoldRangeF32(src []float32) float64 { return foldRangeF32(src) } // FoldRangeF16 is FoldRange over a half payload's raw bit patterns; // every widening is exact. func FoldRangeF16(src []uint16) float64 { return foldRangeF16(src) } // FoldRangeC128 is FoldRange over a complex payload. func FoldRangeC128(src []complex128) complex128 { return foldRangeC128(src) } // ExtremeRange is the serial extremum rule on one canonical block of a // float64 payload: seed past the leading NaNs, keep the first strictly // better value, and report whether the block holds a candidate at all. func ExtremeRange(src []float64, greater bool) (float64, bool) { return floatExtreme(src, greater) } // ExtremeRangeF32 is ExtremeRange over a float32 payload; every // widening is exact. func ExtremeRangeF32(src []float32, greater bool) (float64, bool) { return float32Extreme(src, greater) } // ExtremeRangeF16 is ExtremeRange over a half payload's raw bit // patterns; every widening is exact. func ExtremeRangeF16(src []uint16, greater bool) (float64, bool) { return halfExtreme(src, greater) } // CombineExtrema combines the blocks' extrema with the serial walk's // strict comparison in index order: a tie keeps the earlier block's // value, a block with no candidate contributes nothing, and an array // with no candidate anywhere answers the last block's fallback, which // is the whole array's last element. The extrema the spmd shards fold // combine through it, so a sharded extremum is the single-array // extremum bit for bit. func CombineExtrema(vals []float64, oks []bool, greater bool) float64 { return combineExtreme(vals, oks, greater) } // FloatScalar boxes a float64 as the scalar the float reductions // answer. func FloatScalar(f float64) Scalar { return Scalar{isFloat: true, f: f} } // IntScalar boxes an int64 as the scalar the integer-class reductions // answer. func IntScalar(i int64) Scalar { return Scalar{i: i} } // ComplexScalar boxes a complex128 as the scalar the complex // reductions answer. func ComplexScalar(c complex128) Scalar { return Scalar{isComplex: true, c: c} } // TreeProd combines partial products through the balanced midpoint // tree, multiplying the left half before the right: partials gathered // from a sharded cut combine into the single-array product's exact // bits. The float32 partials multiply natively in float32, the way the // float32 product fold keeps. func TreeProd[N int64 | float64 | float32](vals []N) N { return treeProd(vals) } // TreeProdHalf combines half-precision partial products: the values // carry exact half bits in float64 and every combine narrows through // half, the per-step rounding the half product fold keeps. func TreeProdHalf(vals []float64) float64 { return treeProdHalf(vals) } // FoldProd is one canonical block's product over a float64 payload. // Shards fold their own blocks with it, so a block's product is the // same bits wherever its elements live. func FoldProd(src []float64) float64 { return foldProdRange(src) } // FoldProdF32 is FoldProd over a float32 payload, multiplied natively // in float32. func FoldProdF32(src []float32) float32 { return foldProdRangeF32(src) } // FoldProdF16 is FoldProd over a half payload's raw bit patterns, with // the per-step half rounding the line product keeps; the answer is an // exact half value carried in float64. func FoldProdF16(src []uint16) float64 { return foldProdRangeF16(src) } // FoldProdI64 is FoldProd over an int64 payload; the wrapping product // is exact under any grouping. func FoldProdI64(src []int64) int64 { return foldProdRangeI64(src) } // FoldNormPower is one canonical block's sum of |v|^p over a float64 // payload, with the per-element arithmetic the norm fold keeps. func FoldNormPower(src []float64, p float64) float64 { return foldNormPowerRange(src, p) } // FoldNormPowerF32 is FoldNormPower over a float32 payload; every // widening is exact. func FoldNormPowerF32(src []float32, p float64) float64 { return foldNormPowerRangeF32(src, p) } // FoldNormPowerF16 is FoldNormPower over a half payload's raw bit // patterns; every widening is exact. func FoldNormPowerF16(src []uint16, p float64) float64 { return foldNormPowerRangeF16(src, p) } // FoldNormPowerI64 is FoldNormPower over an int64 payload. func FoldNormPowerI64(src []int64, p float64) float64 { return foldNormPowerRangeI64(src, p) } // NormRoot closes a power sum into the norm: Sqrt for p = 2, the sum // for p = 1, Pow of the sum for every other exponent. func NormRoot(sum, p float64) float64 { return normRoot(sum, p) } // FoldDot is one canonical block's dot product over float64 payloads // of equal length. func FoldDot(x, y []float64) float64 { return foldDotRange(x, y) } // FoldDotF32 is FoldDot over float32 payloads; every product is exact // in float64. func FoldDotF32(x, y []float32) float64 { return foldDotRangeF32(x, y) } // FoldDotF16 is FoldDot over half payloads' raw bit patterns; every // product is exact in float64. func FoldDotF16(x, y []uint16) float64 { return foldDotRangeF16(x, y) } // FoldDotC128 is FoldDot over complex payloads. func FoldDotC128(x, y []complex128) complex128 { return foldDotRangeC128(x, y) } // FoldDotI64 is FoldDot over int64 payloads: the wrapping products and // the wrapping sums are exact under any grouping. func FoldDotI64(x, y []int64) int64 { var s int64 for i := range x { s += x[i] * y[i] } return s } // foldRange runs one chunk's fold over a float payload: four interleaved // chains, so the adds of a long array overlap instead of queueing on one // adder, combined as ((s0+s1)+(s2+s3)), the pairing a balanced tree // gives. Each chain carries a quarter of the chunk, so the dependency // chain is four times shorter and the combination costs three roundings; // against the single chain it replaced the total error measured within a // small factor either way, so this form is chosen for the throughput and // not for a claimed accuracy win. func foldRange(src []float64) float64 { var s0, s1, s2, s3 float64 i := 0 for ; i+4 <= len(src); i += 4 { s0 += src[i] s1 += src[i+1] s2 += src[i+2] s3 += src[i+3] } for ; i < len(src); i++ { s0 += src[i] } return (s0 + s1) + (s2 + s3) } // treeSum combines chunk partials with a balanced pairwise tree over // their indices: the node over a range splits at its midpoint and adds // the two halves' nodes, left before right. The shape depends on nothing // but the partial count, and the value of any contiguous range of // partials is one node of the tree. That property is what lets a // reduction cut at chunk boundaries into shards reproduce this fold // exactly, for any number of shards: the spmd package gathers the shard // partials and combines them with this same function. func treeSum[N int64 | float64 | complex128](vals []N) N { return treeSumRange(vals, 0, len(vals)) } // treeSumRange is one node of the treeSum tree over the partials [l, r). func treeSumRange[N int64 | float64 | complex128](vals []N, l, r int) N { if r-l == 1 { return vals[l] } m := l + (r-l)/2 return treeSumRange(vals, l, m) + treeSumRange(vals, m, r) } // treeProd combines partial products through the balanced midpoint // tree treeSum uses, multiplying the left half before the right. The // shape depends on nothing but the partial count, so partial products // gathered from a sharded cut combine into the single-array product's // exact bits; the spmd package feeds it the block values the shards // folded. func treeProd[N int64 | float64 | float32](vals []N) N { return treeProdRange(vals, 0, len(vals)) } func treeProdRange[N int64 | float64 | float32](vals []N, l, r int) N { if r-l == 1 { return vals[l] } m := l + (r-l)/2 return treeProdRange(vals, l, m) * treeProdRange(vals, m, r) } // treeProdHalf is treeProd for a half-precision product: the partials // carry exact half values and every combine narrows through half, the // per-step rounding the half product fold keeps. func treeProdHalf(vals []float64) float64 { return treeProdHalfRange(vals, 0, len(vals)) } func treeProdHalfRange(vals []float64, l, r int) float64 { if r-l == 1 { return vals[l] } m := l + (r-l)/2 return halfRound(treeProdHalfRange(vals, l, m) * treeProdHalfRange(vals, m, r)) } // halfRound narrows through half and back, the per-step rounding the // half precision folds keep. func halfRound(f float64) float64 { return HalfToFloat64(HalfFromFloat64(f)) } // foldProdRangeI64 is one canonical block's product over an int64 // payload: wrapping multiplication is associative, so the grouping // cannot change the value. func foldProdRangeI64(src []int64) int64 { m := int64(1) for _, v := range src { m *= v } return m } // foldProdRange is one canonical block's product over a float64 // payload: a single chain from one, the association the product fold // keeps inside a block. func foldProdRange(src []float64) float64 { m := 1.0 for _, v := range src { m *= v } return m } // foldProdRangeF32 is one canonical block's product over a float32 // payload, multiplied natively in float32. func foldProdRangeF32(src []float32) float32 { m := float32(1) for _, v := range src { m *= v } return m } // foldProdRangeF16 is one canonical block's product over a half // payload: the running product narrows through half before every // combine, exactly the per-step rounding the line fold keeps, and the // answer is an exact half value carried in float64. func foldProdRangeF16(src []uint16) float64 { m := 1.0 for _, v := range src { m = halfRound(m * HalfToFloat64(v)) } return m } // foldNormPowerRange is one canonical block's sum of |v|^p over a // float64 payload, with the same per-element arithmetic the norm fold // keeps: the bare absolute for p = 1, the squared absolute for p = 2, // and Pow of the absolute for every other exponent. func foldNormPowerRange(src []float64, p float64) float64 { switch { case p == 1: var acc float64 for _, v := range src { acc += math.Abs(v) } return acc case p == 2: var acc float64 for _, v := range src { w := math.Abs(v) acc += w * w } return acc default: var acc float64 for _, v := range src { acc += math.Pow(math.Abs(v), p) } return acc } } // foldNormPowerRangeF32 is foldNormPowerRange over a float32 payload; // every widening is exact. func foldNormPowerRangeF32(src []float32, p float64) float64 { switch { case p == 1: var acc float64 for _, v := range src { acc += math.Abs(float64(v)) } return acc case p == 2: var acc float64 for _, v := range src { w := math.Abs(float64(v)) acc += w * w } return acc default: var acc float64 for _, v := range src { acc += math.Pow(math.Abs(float64(v)), p) } return acc } } // foldNormPowerRangeF16 is foldNormPowerRange over a half payload's // raw bit patterns; every widening is exact. func foldNormPowerRangeF16(src []uint16, p float64) float64 { switch { case p == 1: var acc float64 for _, v := range src { acc += math.Abs(HalfToFloat64(v)) } return acc case p == 2: var acc float64 for _, v := range src { w := math.Abs(HalfToFloat64(v)) acc += w * w } return acc default: var acc float64 for _, v := range src { acc += math.Pow(math.Abs(HalfToFloat64(v)), p) } return acc } } // foldNormPowerRangeI64 is foldNormPowerRange over an int64 payload. func foldNormPowerRangeI64(src []int64, p float64) float64 { switch { case p == 1: var acc float64 for _, v := range src { acc += math.Abs(float64(v)) } return acc case p == 2: var acc float64 for _, v := range src { w := math.Abs(float64(v)) acc += w * w } return acc default: var acc float64 for _, v := range src { acc += math.Pow(math.Abs(float64(v)), p) } return acc } } // normRoot closes a power sum into the norm: Sqrt for p = 2, the sum // itself for p = 1, and Pow of the sum for every other exponent, the // closing the norm fold keeps. func normRoot(sum, p float64) float64 { switch { case p == 2: return math.Sqrt(sum) case p == 1: return sum default: return math.Pow(sum, 1/p) } } // foldDotRange is one canonical block's dot product over float64 // payloads of equal length: four interleaved product chains, combined // as a balanced pair. func foldDotRange(x, y []float64) float64 { var s0, s1, s2, s3 float64 i := 0 for ; i+4 <= len(x); i += 4 { s0 += x[i] * y[i] s1 += x[i+1] * y[i+1] s2 += x[i+2] * y[i+2] s3 += x[i+3] * y[i+3] } for ; i < len(x); i++ { s0 += x[i] * y[i] } return (s0 + s1) + (s2 + s3) } // foldDotRangeF32 is foldDotRange over float32 payloads; every product // is exact in float64. func foldDotRangeF32(x, y []float32) float64 { var s0, s1, s2, s3 float64 i := 0 for ; i+4 <= len(x); i += 4 { s0 += float64(x[i]) * float64(y[i]) s1 += float64(x[i+1]) * float64(y[i+1]) s2 += float64(x[i+2]) * float64(y[i+2]) s3 += float64(x[i+3]) * float64(y[i+3]) } for ; i < len(x); i++ { s0 += float64(x[i]) * float64(y[i]) } return (s0 + s1) + (s2 + s3) } // foldDotRangeF16 is foldDotRange over half payloads' raw bit // patterns; every product is exact in float64. func foldDotRangeF16(x, y []uint16) float64 { var s0, s1, s2, s3 float64 i := 0 for ; i+4 <= len(x); i += 4 { s0 += HalfToFloat64(x[i]) * HalfToFloat64(y[i]) s1 += HalfToFloat64(x[i+1]) * HalfToFloat64(y[i+1]) s2 += HalfToFloat64(x[i+2]) * HalfToFloat64(y[i+2]) s3 += HalfToFloat64(x[i+3]) * HalfToFloat64(y[i+3]) } for ; i < len(x); i++ { s0 += HalfToFloat64(x[i]) * HalfToFloat64(y[i]) } return (s0 + s1) + (s2 + s3) } // foldDotRangeC128 is foldDotRange over complex payloads. func foldDotRangeC128(x, y []complex128) complex128 { var s0, s1, s2, s3 complex128 i := 0 for ; i+4 <= len(x); i += 4 { s0 += x[i] * y[i] s1 += x[i+1] * y[i+1] s2 += x[i+2] * y[i+2] s3 += x[i+3] * y[i+3] } for ; i < len(x); i++ { s0 += x[i] * y[i] } return (s0 + s1) + (s2 + s3) } // floatFoldSum sums a float64 payload over the fixed partition. func floatFoldSum(src []float64) float64 { parts := foldParts(len(src)) if parts == 1 { return foldRange(src) } partials := make([]float64, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { partials[c] = foldRange(src[c*len(src)/parts : (c+1)*len(src)/parts]) } }) return treeSum(partials) } // foldRangeF32 is one block's fold over a float32 payload: four // interleaved float64 chains, every widening exact. func foldRangeF32(part []float32) float64 { var s0, s1, s2, s3 float64 i := 0 for ; i+4 <= len(part); i += 4 { s0 += float64(part[i]) s1 += float64(part[i+1]) s2 += float64(part[i+2]) s3 += float64(part[i+3]) } for ; i < len(part); i++ { s0 += float64(part[i]) } return (s0 + s1) + (s2 + s3) } // foldRangeF16 is one block's fold over a half payload's raw bit // patterns, widening each element exactly as it is read. func foldRangeF16(part []uint16) float64 { var s0, s1, s2, s3 float64 i := 0 for ; i+4 <= len(part); i += 4 { s0 += HalfToFloat64(part[i]) s1 += HalfToFloat64(part[i+1]) s2 += HalfToFloat64(part[i+2]) s3 += HalfToFloat64(part[i+3]) } for ; i < len(part); i++ { s0 += HalfToFloat64(part[i]) } return (s0 + s1) + (s2 + s3) } // floatFoldSumF32 sums a float32 payload: every widening to float64 is // exact, so the fold sees the values an accessor walk would hand over. func floatFoldSumF32(src []float32) float64 { parts := foldParts(len(src)) return sumOverParts(len(src), parts, func(c int) float64 { return foldRangeF32(src[c*len(src)/parts : (c+1)*len(src)/parts]) }) } // floatFoldSumF16 sums a half-precision payload, widening each element // exactly as it is read. func floatFoldSumF16(src []uint16) float64 { parts := foldParts(len(src)) return sumOverParts(len(src), parts, func(c int) float64 { return foldRangeF16(src[c*len(src)/parts : (c+1)*len(src)/parts]) }) } // sumOverParts runs the per-chunk fold and combines the results through // the balanced partial tree: the only order-dependent step, and its shape // depends on nothing but the chunk count. func sumOverParts(n, parts int, fold func(c int) float64) float64 { if parts == 1 { return fold(0) } partials := make([]float64, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { partials[c] = fold(c) } }) return treeSum(partials) } // intFoldSum sums an int64 payload: wrapping addition is associative, so // the partition cannot change the value and a plain chunked scan needs no // interleaved partials. func intFoldSum(src []int64) int64 { parts := foldParts(len(src)) if parts == 1 { var s int64 for _, v := range src { s += v } return s } partials := make([]int64, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { var s int64 for _, v := range src[c*len(src)/parts : (c+1)*len(src)/parts] { s += v } partials[c] = s } }) var total int64 for _, v := range partials { total += v } return total } // intFoldSumNarrow sums a narrow integer payload into int64 through the // fixed partition intFoldSum uses; every widening is exact, so the sum // stays machine-independent and the accumulation order changes nothing. func intFoldSumNarrow[T int8 | uint8 | int16 | uint16 | int32 | uint32](src []T) int64 { parts := foldParts(len(src)) if parts == 1 { var s int64 for _, v := range src { s += int64(v) } return s } partials := make([]int64, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { var s int64 for _, v := range src[c*len(src)/parts : (c+1)*len(src)/parts] { s += int64(v) } partials[c] = s } }) var total int64 for _, v := range partials { total += v } return total } // boolFoldCount counts the true elements of a bool payload through the // same fixed partition intFoldSum uses. func boolFoldCount(src []bool) int64 { parts := foldParts(len(src)) if parts == 1 { var s int64 for _, v := range src { if v { s++ } } return s } partials := make([]int64, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { var s int64 for _, v := range src[c*len(src)/parts : (c+1)*len(src)/parts] { if v { s++ } } partials[c] = s } }) var total int64 for _, v := range partials { total += v } return total } // foldRangeC128 is one block's fold over a complex payload: four // interleaved chains, combined as a balanced pair. func foldRangeC128(part []complex128) complex128 { var s0, s1, s2, s3 complex128 i := 0 for ; i+4 <= len(part); i += 4 { s0 += part[i] s1 += part[i+1] s2 += part[i+2] s3 += part[i+3] } for ; i < len(part); i++ { s0 += part[i] } return (s0 + s1) + (s2 + s3) } // complexFoldSum sums a complex payload the way the float fold sums a // real one: fixed chunks, four interleaved partials, partials combined // through the balanced partial tree. func complexFoldSum(src []complex128) complex128 { parts := foldParts(len(src)) if parts == 1 { return foldRangeC128(src) } partials := make([]complex128, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { partials[c] = foldRangeC128(src[c*len(src)/parts : (c+1)*len(src)/parts]) } }) return treeSum(partials) } // Min returns the smallest element; an empty array, or a complex array // (no ordering), is an error. func Min(a *Array) (Scalar, error) { return a.reduceOrder("Min", false) } // Max returns the largest element; an empty array, or a complex array // (no ordering), is an error. func Max(a *Array) (Scalar, error) { return a.reduceOrder("Max", true) } // Mean returns the arithmetic mean of all elements as a float64, // computed in float64 even for int arrays (true division); an empty or // complex array is an error. // // The sum runs through Sum, so every ordinary input rounds exactly as // the standalone Sum does. The one exception is the input whose largest // magnitude could carry the running total past float64's range: there // the plain sum overflows to an Inf the final division cannot undo // (Mean of two MaxFloat64s answered +Inf). Such an input takes a scaled // two-pass form instead: the elements are summed divided by the largest // magnitude and the result rescaled, which stays finite. The scale is // chosen by the pre-pass below, so any input below the overflow // threshold, and every int array whose whole range sits far below // float64's ceiling, keeps the plain path and its digest. func Mean(a *Array) (float64, error) { if a.dt == Complex { return 0, errf("Mean: complex arrays have no float mean") } if a.Len() == 0 { return 0, errf("Mean: an empty array has no mean") } n := a.Len() fn := float64(n) // Every widening in this pre-pass is exact, and NaN fails the // comparison, so a NaN payload never triggers the scaled path and // reaches the caller through Sum's fold as before. The walk reads the // payload directly, which is the value the accessor would hand over. maxAbs := a.maxAbs(n) if maxAbs > math.MaxFloat64/fn && !math.IsInf(maxAbs, 0) { var acc float64 for i := range n { acc += a.floatAt(i) / maxAbs } // The rescale divides by n first: multiplying acc by maxAbs // before the division could overflow again, which is the very // failure this path exists to avoid. return acc / fn * maxAbs, nil } return Sum(a).Float() / fn, nil } // maxAbs returns the largest magnitude among the first n elements, the // seed pre-pass the scaled mean path needs. The dtype dispatch sits // outside the walk and float16 and float32 widen exactly, so every // value is the one an accessor read would hand over, at two // instructions per element instead of a switch on the dtype. // // A magnitude maximum is associative and commutative, and it has no sign // to lose: |+0| and |−0| are the same zero, so unlike the signed extrema // the answer cannot depend on the visit order. The walk therefore takes // the same fixed partition the sums use and combines the partial maxima // with max, which makes it both parallel and deterministic. func (a *Array) maxAbs(n int) float64 { if a.strides != nil { parts := foldParts(n) if parts == 1 { return a.maxAbsSerial(n) } partials := make([]float64, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { lo, hi := c*n/parts, (c+1)*n/parts m := 0.0 for i := lo; i < hi; i++ { if w := math.Abs(a.floatAt(i)); w > m { m = w } } partials[c] = m } }) m := 0.0 for _, v := range partials { m = math.Max(m, v) } return m } switch a.dt { case Int: return foldMaxAbsParts(n, func(lo, hi int) float64 { m := 0.0 for _, v := range a.ints[lo:hi] { if w := math.Abs(float64(v)); w > m { m = w } } return m }) case Float16: return foldMaxAbsParts(n, func(lo, hi int) float64 { m := 0.0 for _, v := range a.halves[lo:hi] { if w := math.Abs(HalfToFloat64(v)); w > m { m = w } } return m }) case Float32: return foldMaxAbsParts(n, func(lo, hi int) float64 { m := 0.0 for _, v := range a.floats32[lo:hi] { if w := math.Abs(float64(v)); w > m { m = w } } return m }) case Float: return foldMaxAbsParts(n, func(lo, hi int) float64 { m := 0.0 for _, v := range a.floats[lo:hi] { if w := math.Abs(v); w > m { m = w } } return m }) case Bool: return foldMaxAbsParts(n, func(lo, hi int) float64 { for _, v := range a.bools[lo:hi] { if v { return 1 } } return 0 }) case Int8: return maxAbsNarrow(a.i8s, n) case Uint8: return maxAbsNarrow(a.u8s, n) case Int16: return maxAbsNarrow(a.i16s, n) case Uint16: return maxAbsNarrow(a.u16s, n) case Int32: return maxAbsNarrow(a.i32s, n) case Uint32: return maxAbsNarrow(a.u32s, n) } // Complex never reaches here: Mean rejects it before the guard. return 0 } // maxAbsNarrow is maxAbs's chunk walk for a narrow integer payload: // every widening is exact, so the magnitude sees the accessor value. func maxAbsNarrow[T int8 | uint8 | int16 | uint16 | int32 | uint32](src []T, n int) float64 { return foldMaxAbsParts(n, func(lo, hi int) float64 { m := 0.0 for _, v := range src[lo:hi] { if w := math.Abs(float64(v)); w > m { m = w } } return m }) } // foldMaxAbsParts runs the magnitude maximum over the fixed partition of // n elements. The chunk closure walks a payload slice, never a function // per element, and the partial maxima combine with max, so the partition // changes nothing. func foldMaxAbsParts(n int, chunk func(lo, hi int) float64) float64 { parts := foldParts(n) if parts == 1 { return chunk(0, n) } partials := make([]float64, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { partials[c] = chunk(c*n/parts, (c+1)*n/parts) } }) m := 0.0 for _, v := range partials { m = math.Max(m, v) } return m } // maxAbsSerial is the strided fallback for a length below the partition // floor. func (a *Array) maxAbsSerial(n int) float64 { var m float64 for i := range n { if v := math.Abs(a.floatAt(i)); v > m { m = v } } return m } // Dot returns the dot product of two 1-D arrays of equal length. // Integer-class pairs produce an int scalar accumulated in int64 (the // products wrap like the int64 kernel's); bool pairs are refused: bool // carries no arithmetic. Float32 operands accumulate in float64 and // produce a float scalar; any float64 operand promotes the // result to float, any complex operand to complex. func Dot(a, b *Array) (Scalar, error) { if a.NDim() != 1 || b.NDim() != 1 { return Scalar{}, errf("Dot: needs 1-D arrays, got shapes %s and %s", shapeText(a.shape), shapeText(b.shape)) } if a.Len() != b.Len() { return Scalar{}, errf("Dot: length mismatch %d vs %d", a.Len(), b.Len()) } n := a.Len() dt := promote(a.dt, b.dt) if dt == Bool { return Scalar{}, errf("Dot: bool arrays have no arithmetic") } switch { case dt == Int && a.dt == Int && b.dt == Int: return Scalar{i: intFoldDot(a.ints[:n], b.ints[:n])}, nil case intClass(dt): // Every integer-class pair widens exactly into int64; the // products wrap exactly as the int64 kernel's do, and wrapping // addition is associative, so the fixed partition changes // nothing. if a.dt == b.dt && a.dt != Bool && a.isContiguous() && b.isContiguous() { switch a.dt { case Int: return Scalar{i: intFoldDot(a.ints[:n], b.ints[:n])}, nil case Int8: return Scalar{i: narrowFoldDot(a.i8s[:n], b.i8s[:n])}, nil case Uint8: return Scalar{i: narrowFoldDot(a.u8s[:n], b.u8s[:n])}, nil case Int16: return Scalar{i: narrowFoldDot(a.i16s[:n], b.i16s[:n])}, nil case Uint16: return Scalar{i: narrowFoldDot(a.u16s[:n], b.u16s[:n])}, nil case Int32: return Scalar{i: narrowFoldDot(a.i32s[:n], b.i32s[:n])}, nil case Uint32: return Scalar{i: narrowFoldDot(a.u32s[:n], b.u32s[:n])}, nil } } parts := foldParts(n) return Scalar{i: foldPartsOver(n, parts, func(c int) int64 { lo, hi := c*n/parts, (c+1)*n/parts var s int64 for i := lo; i < hi; i++ { s += a.intAt(i) * b.intAt(i) } return s })}, nil } switch dt { case Float16: var s float64 if a.dt == Float16 && b.dt == Float16 { // Both payloads hold half bit patterns at their flat index, // so the kernel streams the raw slices; every product is // exact in float64 either way. as, bs := a.halves[:n], b.halves[:n] s = foldDotF16(as, bs) } else { for i := range n { s += a.floatAt(i) * b.floatAt(i) } } return Scalar{isFloat: true, f: s}, nil case Float32: var s float64 if a.dt == Float32 && b.dt == Float32 { // Both payloads hold float32 at their flat index, so the // kernel streams the raw slices; every product is exact in // float64 either way, so the values match accessor reads. as, bs := a.floats32[:n], b.floats32[:n] s = foldDotF32(as, bs) } else { for i := range n { s += a.floatAt(i) * b.floatAt(i) } } return Scalar{isFloat: true, f: s}, nil case Float: var s float64 if a.dt == Float && b.dt == Float { as, bs := a.floats[:n], b.floats[:n] s = foldDotF64(as, bs) } else { for i := range n { s += a.floatAt(i) * b.floatAt(i) } } return Scalar{isFloat: true, f: s}, nil default: var s complex128 if a.dt == Complex && b.dt == Complex { as, bs := a.complexes[:n], b.complexes[:n] s = foldDotC128(as, bs) } else { for i := range n { s += a.complexAt(i) * b.complexAt(i) } } return Scalar{isComplex: true, c: s}, nil } } // reduceOrder walks the array keeping the smallest (greater=false) or // largest (greater=true) element per dtype. An empty array is an error, // and so is a complex array: it has no ordering. The comparisons are // inlined per dtype: a closure per element would dominate the walk. // reduceOrder walks the array keeping the smallest (greater=false) or // largest (greater=true) element per dtype. An empty array is an error, // and so is a complex array: it has no ordering. // // The walk is partitioned by length alone, never by the worker count, so // the same input answers the same element on any machine: the range is // cut into fixed chunks, each chunk applies the serial rule to its own // range, and the chunks combine in index order with the same strict // comparison. A tie therefore keeps the earlier chunk's element and, in a // chunk, the earlier element's, which is what the serial walk kept; the // rules for a NaN (never a candidate) and an all-NaN array (the last // element, where the seed walk ends) are preserved exactly, zeros // included. func (a *Array) reduceOrder(name string, greater bool) (Scalar, error) { if a.dt == Complex { return Scalar{}, errf("%s: complex arrays have no ordering", name) } if a.Len() == 0 { return Scalar{}, errf("%s: an empty array has no %s", name, name) } n := a.Len() switch a.dt { case Int: return Scalar{i: foldExtreme(n, greater, func(lo, hi int) (int64, bool) { is := a.ints[lo:hi] best := is[0] if greater { for _, v := range is[1:] { if v > best { best = v } } } else { for _, v := range is[1:] { if v < best { best = v } } } return best, true })}, nil case Bool: return Scalar{i: foldExtremeBool(a.bools, n, greater)}, nil case Int8: return Scalar{i: foldExtremeNarrow(a.i8s, n, greater)}, nil case Uint8: return Scalar{i: foldExtremeNarrow(a.u8s, n, greater)}, nil case Int16: return Scalar{i: foldExtremeNarrow(a.i16s, n, greater)}, nil case Uint16: return Scalar{i: foldExtremeNarrow(a.u16s, n, greater)}, nil case Int32: return Scalar{i: foldExtremeNarrow(a.i32s, n, greater)}, nil case Uint32: return Scalar{i: foldExtremeNarrow(a.u32s, n, greater)}, nil } // The float folds read the payload slices directly: float16 and // float32 widen exactly, so the raw walk sees the same values the // accessor would hand over, in the same order. var f float64 switch a.dt { case Float16: f = foldExtreme(n, greater, func(lo, hi int) (float64, bool) { return halfExtreme(a.halves[lo:hi], greater) }) case Float32: f = foldExtreme(n, greater, func(lo, hi int) (float64, bool) { return float32Extreme(a.floats32[lo:hi], greater) }) default: f = foldExtreme(n, greater, func(lo, hi int) (float64, bool) { return floatExtreme(a.floats[lo:hi], greater) }) } return Scalar{isFloat: true, f: f}, nil } // floatExtreme is the serial fold's rule applied to one chunk of a float64 // payload: skip the leading NaNs to seed, then keep the first strictly // better value. A chunk holding no non-NaN reports its last element, where // the serial seed walk would have stopped, and says there is no candidate. func floatExtreme(src []float64, greater bool) (float64, bool) { best, k := src[0], 1 for math.IsNaN(best) && k < len(src) { best = src[k] k++ } if math.IsNaN(best) { return src[len(src)-1], false } if greater { for _, v := range src[k:] { if v > best { best = v } } } else { for _, v := range src[k:] { if v < best { best = v } } } return best, true } // float32Extreme is floatExtreme over a float32 payload; every widening is // exact, so the comparisons see the values an accessor read would. func float32Extreme(src []float32, greater bool) (float64, bool) { best, k := float64(src[0]), 1 for math.IsNaN(best) && k < len(src) { best = float64(src[k]) k++ } if math.IsNaN(best) { return float64(src[len(src)-1]), false } if greater { for _, v := range src[k:] { if w := float64(v); w > best { best = w } } } else { for _, v := range src[k:] { if w := float64(v); w < best { best = w } } } return best, true } // halfExtreme is floatExtreme over a half-precision payload. func halfExtreme(src []uint16, greater bool) (float64, bool) { best, k := HalfToFloat64(src[0]), 1 for math.IsNaN(best) && k < len(src) { best = HalfToFloat64(src[k]) k++ } if math.IsNaN(best) { return HalfToFloat64(src[len(src)-1]), false } if greater { for _, v := range src[k:] { if w := HalfToFloat64(v); w > best { best = w } } } else { for _, v := range src[k:] { if w := HalfToFloat64(v); w < best { best = w } } } return best, true } // foldExtreme runs a chunk fold over the fixed partition of n elements // and combines the partials with the serial walk's rule, so the answer // is the serial walk's element whatever the worker count. A chunk with // no candidate contributes nothing; an array with no candidate at all // is all NaN and answers the last element, which is what the final // chunk stored. func foldExtreme[N int64 | float64](n int, greater bool, chunk func(lo, hi int) (N, bool)) N { parts := foldParts(n) vals := make([]N, parts) oks := make([]bool, parts) if parts == 1 { vals[0], oks[0] = chunk(0, n) } else { engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { vals[c], oks[c] = chunk(c*n/parts, (c+1)*n/parts) } }) } return combineExtreme(vals, oks, greater) } // combineExtreme is foldExtreme's combination: the partials compare in // index order with the strict rule the serial walk used, so a tie keeps // the earlier block's value; a block with no candidate contributes // nothing; a whole with no candidate answers the last block's stored // fallback. func combineExtreme[N int64 | float64](vals []N, oks []bool, greater bool) N { best, have := vals[0], false for c := range len(vals) { if !oks[c] { continue } if !have || (greater && vals[c] > best) || (!greater && vals[c] < best) { best, have = vals[c], true } } if have { return best } return vals[len(vals)-1] } // foldExtremeNarrow applies reduceOrder's chunk rule to a narrow integer // payload: the comparison runs in the payload's own type, so no value // ever meets a float64 rounding, and the winner widens exactly into the // int64 the fold combines. func foldExtremeNarrow[T int8 | uint8 | int16 | uint16 | int32 | uint32](src []T, n int, greater bool) int64 { return foldExtreme(n, greater, func(lo, hi int) (int64, bool) { is := src[lo:hi] best := is[0] if greater { for _, v := range is[1:] { if v > best { best = v } } } else { for _, v := range is[1:] { if v < best { best = v } } } return int64(best), true }) } // foldExtremeBool is foldExtremeNarrow for a bool payload: false below // true, widened to the 0/1 the Int scalar carries. Go orders no bool // with < or >, so the strict improvement writes its own logic. func foldExtremeBool(src []bool, n int, greater bool) int64 { return foldExtreme(n, greater, func(lo, hi int) (int64, bool) { is := src[lo:hi] best := is[0] if greater { for _, v := range is[1:] { if v && !best { best = v } } } else { for _, v := range is[1:] { if !v && best { best = v } } } if best { return 1, true } return 0, true }) } // foldPartsOver runs a per-chunk fold over the fixed partition of n // elements and combines the results through the balanced partial tree: // the only ordering step, and its shape depends on the chunk count // alone. The chunk boundaries c·n/parts are a function of the length and // the part count, so the total is the same on any machine and under any // worker setting. func foldPartsOver[N int64 | float64 | complex128](n, parts int, fold func(c int) N) N { if parts == 1 { return fold(0) } partials := make([]N, parts) engine.Parallel(parts, func(cs, ce int) { for c := cs; c < ce; c++ { partials[c] = fold(c) } }) return treeSum(partials) } // intFoldDot is the integer dot product: wrapping addition is // associative, so the partition cannot change the value. func intFoldDot(x, y []int64) int64 { n := len(x) parts := foldParts(n) return foldPartsOver(n, parts, func(c int) int64 { lo, hi := c*n/parts, (c+1)*n/parts var s int64 for i := lo; i < hi; i++ { s += x[i] * y[i] } return s }) } // narrowFoldDot is the same-dtype narrow integer dot product: the // products run in int64 over the exact widenings, exactly what the // accessor fold's intAt reads produce, so the partition and the value // are the accessor fold's own. func narrowFoldDot[T int8 | uint8 | int16 | uint16 | int32 | uint32](x, y []T) int64 { n := len(x) parts := foldParts(n) return foldPartsOver(n, parts, func(c int) int64 { lo, hi := c*n/parts, (c+1)*n/parts var s int64 for i := lo; i < hi; i++ { s += int64(x[i]) * int64(y[i]) } return s }) } // foldDotF64 is the float64 dot product: four interleaved product chains // per chunk, combined as a balanced pair. func foldDotF64(x, y []float64) float64 { n := len(x) parts := foldParts(n) return foldPartsOver(n, parts, func(c int) float64 { lo, hi := c*n/parts, (c+1)*n/parts return foldDotRange(x[lo:hi], y[lo:hi]) }) } // foldDotF32 is the float32 dot product: every product is exact in // float64, so the accumulation sees the values an accessor read would. func foldDotF32(x, y []float32) float64 { n := len(x) parts := foldParts(n) return foldPartsOver(n, parts, func(c int) float64 { lo, hi := c*n/parts, (c+1)*n/parts return foldDotRangeF32(x[lo:hi], y[lo:hi]) }) } // foldDotF16 is the half-precision dot product, widening each element // exactly as it is read. func foldDotF16(x, y []uint16) float64 { n := len(x) parts := foldParts(n) return foldPartsOver(n, parts, func(c int) float64 { lo, hi := c*n/parts, (c+1)*n/parts return foldDotRangeF16(x[lo:hi], y[lo:hi]) }) } // foldDotC128 is the complex dot product with the same partition. func foldDotC128(x, y []complex128) complex128 { n := len(x) parts := foldParts(n) return foldPartsOver(n, parts, func(c int) complex128 { lo, hi := c*n/parts, (c+1)*n/parts return foldDotRangeC128(x[lo:hi], y[lo:hi]) }) }