Pure-Go n-dimensional array library with a NumPy-compatible API — zero cgo, no external dependencies, nothing but the standard library. Values are bit-exact against NumPy where semantics allow, every behaviour is checked against a live NumPy 2.5.4 session, and the same code builds to WebAssembly.
English | 简体中文
a, _ := num.FromSlice([]float64{1, 2, 3, 4, 5, 6})
m, _ := a.Reshape([]int{2, 3})
b, _ := num.Transpose(m) // 3×2 view, no copy
_ = b.AsContiguous() // explicit copy when you need one
s, _ := num.Sum(m, []int{0}, false) // [5, 7, 9]
// npy interop: the file NumPy writes is the file this reads, and vice versa
_ = num.SaveNPY("a.npy", m)
back, _ := num.LoadNPY("a.npy")- NumPy semantics, Go ergonomics: broadcasting, dtype promotion (NEP 50
rules), views vs copies,
axis/keepdims, NaN propagation, integer overflow wrap-around — all matched against measured NumPy behaviour, not guessed. - Bit-exact where it matters: reductions use pairwise summation (so
Summatchesnp.sumto the last bit for the same input order), integer arithmetic wraps,-0.0and NaN payloads survive,sign(-0.0)is+0like NumPy. - Both I/O directions verified:
.npy/.npz/ raw bytes / text. The reader accepts v1/v2/v3 headers; the writer emits headers NumPy parses without complaint (checked by loading our files in a live Python session). - Useful beyond arithmetic:
linalg(matmul/einsum/solve/inv/det/eig/svd),random(bit-exact MT19937RandomState),fft(radix-2 + Bluestein), masked arrays (ma), sorting/statistics, padding, convolution, set ops,polyfit/unwrap, and a WASM bridge. - WASM ready:
wasm/buildssimple-num.wasmplus a Node/JS API — no server, no C toolchain (CGO_ENABLED=0 go build ./...passes).
Note
🚀 Initial release — canary testing in progress. If you hit any problem, please open an issue!
| Capability | Status |
|---|---|
dtypes: bool, int8…int64, uint8…uint64, float16/32/64, complex64/128, fixed-width S/U, datetime64 |
✅ |
Views: Reshape / Transpose / Squeeze / ExpandDims / SwapAxes / BroadcastTo / Sub (zero-copy), explicit AsContiguous |
✅ |
Indexing: basic slices, fancy indexing, boolean masks, Take / TakeAlongAxis / Put / PutMask / Place |
✅ |
Element-wise ops with broadcasting + NEP 50 promotion: arithmetic, comparison, logical, bitwise, rounding, transcendentals (tanh/cbrt/log2/expm1/log1p/sinc, …) |
✅ |
Reductions over any axis with keepdims: Sum/Prod/Mean/Var/Std/Min/Max/AllTrue/Any/Median plus the Nan* family |
✅ |
Sorting/statistics: Sort/Argsort/Median/Percentile (all 13 NumPy methods)/Quantile/NanPercentile/Diff/Unique (+axis)/SearchSorted/Digitize/Histogram/Histogram2D/Bincount(weights) |
✅ |
Shape manipulation: Atleast1D/2D/3D, Flip/Roll/Repeat/Tile, Split/ArraySplit/SplitAt/H·V·DSplit, Append/Insert/Delete, Stack/VStack/HStack/ColumnStack/DStack/Concatenate |
✅ |
Linear algebra: Dot/MatMul/Inner/Outer/Einsum/Tensordot, Det/Inv/Solve (LU with partial pivoting), Norm (vector 0/1/2/±inf; matrix fro/1/2/-2/inf), Eigvalsh/Eigh (cyclic Jacobi), Svd/Svdvals (one-sided Jacobi) |
✅ |
Floating-point helpers: IsClose/AllClose, Invert, Round(decimals), Clip, Where, Select, Piecewise, IsNan/IsInf/IsFinite/IsReal |
✅ |
Set ops: IsIn/Intersect1D/Union1D/Setdiff1D/Setxor1D (NaN never matches, like NumPy) |
✅ |
Interpolation/signal: Interp, MeshGrid, Gradient, Convolve/Correlate (full/same/valid), Trapz, Polyval, Polyfit, Unwrap |
✅ |
random: bit-exact NumPy legacy RandomState/MT19937 — Seed/RandomSample/StandardNormal/Uniform/Normal/RandInt/Permutation/Shuffle |
✅ |
fft: FFT/IFFT/RFFT/IRFFT/FFTFreq — radix-2 for powers of two, Bluestein for any other length (O(n log n)) |
✅ |
ma (masked arrays): data + mask, Filled/Compressed/Count, masked Sum/Mean/Min/Max/Median/Var/Std, predicates MaskedEqual/Less/Greater/Inside/Outside |
✅ |
I/O: SaveNPY/LoadNPY/SaveNPYBytes/LoadNPYBytes, SaveZ/SaveZCompressed/LoadZ (npz), FromBuffer (zero-copy), FromFile/ToFile, SaveTxt/LoadTxt |
✅ |
ufunc protocol: out= via BinaryOut/UnaryOut, where= via BinaryWhere/UnaryWhere, Reduce/Accumulate/UfuncOuter |
✅ |
WASM bridge (wasm/): arrays as {dtype, shape, data}, op/unary/reduce/linalg/cumulative dispatchers, npy round-trip |
✅ |
Measured on one Windows/amd64 machine, float64, single process. Method: each
case is iterated until ≥400 ms of total time, then divided by the iteration
count — single-shot timing on this platform is quantised too coarsely to trust.
| Case | Time | Note |
|---|---|---|
Add 1e6 contiguous |
1.21 ms | ~21 GB/s |
Add 1e6 with scalar broadcast |
1.19 ms | fast path |
Sum 1e6 (pairwise) |
1.15 ms | |
Take 1e6 from 1e6 indices |
8.41 ms | gather |
Cast f8→f4 1e6 |
0.55 ms | typed bulk kernel |
CastUnsafe f8→i4 1e6 |
0.94 ms | |
LoadNPY 1e6 f8 |
1.89 ms | |
Arange 1e6 |
3.09 ms | |
LinSpace 1e6 |
1.08 ms | |
AsContiguous on a transposed 1000×1000 |
5.54 ms | |
MatMul 512³ |
6.0 ms | row-parallel (≈44 GFLOP/s) |
Einsum ij,jk->ik 512³ |
6.3 ms | contraction graph, on par with MatMul |
FFT n=1024 |
0.055 ms | radix-2 |
FFT n=4000 |
0.77 ms | Bluestein (256 ms with the previous naive DFT) |
Notes on the numbers that matter:
MatMulis pure Go without SIMD. Cache blocking and inner-loop unrolling were both tried and measured slower (6.99 / 6.32 ms vs 6.01 ms); the Go compiler does not vectorise these loops, so the four-row unroll plus row parallelism is where this plateaus.Einsumused to enumerate every label combination (4522 ms at 512³). It now builds a greedy contraction tree and lands onMatMul: ~700× faster and bit-identical toMatMulat 64/128/256.FFTfor non-power-of-two lengths used a naive O(n²) DFT; Bluestein brings n=4000 from 256 ms to 0.77 ms (~330×), with ~1e-14 relative agreement againstnp.fft.fft.
go get github.com/knoweather/simple-numStandard library only — CGO_ENABLED=0 go build ./... works on windows/amd64,
linux/amd64, darwin/arm64 and windows/arm64 (cross-compilation is part of the
release gate).
FromSlice is generic and the dtype follows the Go type, so nothing is silently
widened: []int8 stays int8, []float32 stays float32, []complex128 stays
complex128, and []string becomes a fixed-width U<n> array. Every operator
has a typed kernel per dtype, so int8 arithmetic wraps exactly like NumPy.
A scalar takes its dtype from the Go type it is written as:
num.MustFromSlice([]int8{127}).MustAddScalar(int8(1)) // int8, wraps to -128
num.MustFromSlice([]int8{127}).MustAddScalar(1) // int64 (Go's 1 is int)That differs from NumPy, where a Python scalar is "weak" and keeps the array's dtype. Here the dtype is visible in the Go code, which keeps results predictable.
For scripts and one-off pipelines every operation also has a Must* form that
panics on error and returns *Array, so calls chain left to right:
v := num.MustFromSlice([]float64{1, 2, 3, 4, 5, 6}).
MustReshape(2, 3).
MustTranspose().
MustSumAll().
MustFloat() // 21The panic value is an error that names the operation and wraps the usual
sentinels, so recover plus errors.Is(err, ErrShape) still works. The
error-returning API is unchanged and remains the default for library code.
sh wasm/build.sh # Linux / macOS / Git Bash → wasm/simple-num.wasm + wasm_exec.js
pwsh wasm/build.ps1 # Windows PowerShell
node wasm/smoke/smoke.mjs # build, load, run the JS API assertionsThe bridge publishes globalThis.simpleNum with array objects crossing the
boundary as {dtype, shape, data} and dispatchers for op / unary / reduce /
linalg / cumulative plus npy in/out. It is a curated subset of the Go API —
wasm/README.md lists exactly what is exposed today and what is
planned (npz, percentile, full Eigh/Svd, Einsum, masked arrays, FFT, random…).
Nothing here is "looks right" — every behaviour is pinned against a live NumPy.
The idea is simple: for each test case the same operation runs in Python with
NumPy 2.5.4, and its output bytes plus the inputs are saved into
xcheck/reference/ (.npy files + a JSON manifest). The Go test then compares
our result against those saved bytes. Where an operation is not bit-exact by
nature (an FFT or an LU solve can differ in the last bits), the case declares a
relative tolerance instead.
-
NumPy reference cross-check (
xcheck/): 517 cases / 524 subtests produced byxcheck/gen_reference.py, covering integers/floats/complex, broadcasting, ±0/NaN/Inf, integer overflow, axis reductions, gather/mask indexing, multi-output ops and the whole function surface. Two comparison regions: bitwise (exactly equal, NaN payloads included) and relative tolerance (FFT, LU,sinc). -
Unregistered-op guard: a manifest entry whose op has no registered implementation fails the test instead of silently counting as a pass.
-
Cross-cutting audits: read-only ops never mutate their inputs; transposed/sliced views produce identical results to contiguous copies, including writes back into views; parallel
MatMulis bit-stable acrossGOMAXPROCSvalues. -
Coverage: 85.3% of statements (gate: 85%).
-
I/O interop is asserted in both directions: NumPy loads files this library writes, and this library loads files NumPy writes.
- No structured/record dtypes, no
timedelta64arithmetic, nonditer;datetime64is limited to construct/index/compare. linalgis dense real-valued only: no complex solvers, nolstsq, no general (non-symmetric)eig, noqr/cholesky.Polyfitsolves normal equations, so it squares the condition number — fine for low degrees, not for large ones or widexranges.Einsumdoes not support the ellipsis form (...); repeated labels inside one operand (diagonal) fall back to the enumeration path (correct, O(∏ dims)).Convolve/Correlateare 1-D only and O(n·m); no FFT-based fast path.macovers construction, masked reductions, arithmetic and predicates — notma.corrcoef(NumPy's own result there can exceed ±1, which is not a behaviour worth reproducing).- Text I/O (
SaveTxt/LoadTxt) handles 1-D/2-D, a delimiter andskiprows; nousecols, no per-column dtypes.
array.go, dtype.go, view.go, broadcast.go, ufunc.go core: memory, dtype, views, dispatch
impl_*.go operator kernels by domain
xcheck/ NumPy-generated reference data + harness
wasm/ WebAssembly bridge and JS smoke test
Legend: ✅ available ·
| Area | Supported | Not yet (planned) |
|---|---|---|
| dtypes | bool, int8…int64, uint8…uint64, float16/32/64, complex64/128, fixed-width S/U, datetime64 |
float128 🚧, timedelta64 🚧, structured/record dtypes 🚧, object 🚧 |
| creation | FromSlice, Zeros, Ones, Empty, Full, Eye, Identity, Arange, LinSpace, LogSpace, GeomSpace, MeshGrid, Diag, Diagflat |
FromIter 🚧, FromString 🚧, Vander 🚧, Tri 🚧 |
| shape & views | Reshape, Ravel, Transpose, Squeeze, ExpandDims, SwapAxes, BroadcastTo, Sub (all zero-copy where possible) |
Moveaxis 🚧, Resize 🚧, BroadcastArrays 🚧 |
| manipulation | Concatenate, Stack/VStack/HStack/ColumnStack/DStack, Split/ArraySplit/SplitAt/H·V·DSplit, Append/Insert/Delete, Flip/FlipUD/FlipLR, Roll, Rot90, Moveaxis, Resize, BroadcastArrays, Repeat, Tile, Pad (10 modes incl. stat_length), Atleast1D/2D/3D |
— |
| indexing | basic slices, fancy indexing, boolean masks, Take, TakeAlongAxis, Put, PutAlongAxis, PutMask, Place, FillDiagonal, Compress, Choose, TrilIndices/TriuIndices, DiagIndices/DiagIndicesFrom, UnravelIndex, Nonzero, Argwhere, Flatnonzero, CountNonzero |
— |
| arithmetic | Add/Subtract/Multiply/Div/FloorDivide/Remainder/Power, Negative/Positive/Abs/Fabs/Square/Reciprocal, Maximum/Minimum, Clip, Round/Around, Where/Select/Piecewise |
— |
| comparison & logic | Equal/NotEqual/Less/LessEqual/Greater/GreaterEqual, LogicalAnd/Or/Xor/Not, AllTrue/Any, IsNan/IsInf/IsFinite/IsReal, IsClose/AllClose |
— |
| bitwise & shifts | BitwiseAnd/BitwiseOr/BitwiseXor/LeftShift/RightShift/Invert (integers and bool; invalid shift counts yield 0 like NumPy) |
— |
| transcendental | Exp/Exp2/Log/Log2/Log10/Expm1/Log1p/Sqrt/Cbrt/Sin/Cos/Tan/Arcsin/Arccos/Arctan/Arctan2/Sinh/Cosh/Tanh/Arcsinh/Arccosh/Arctanh/Sinc, Deg2Rad/Rad2Deg, Sign, Floor/Ceil/Trunc/Rint |
— |
| reductions | Sum/Prod/Mean/Var/Std/Min/Max/ArgMin/ArgMax/Median/Cumsum/Cumprod, Average (weights), PtP, all Nan* variants (incl. NanProd/NanCumsum/NanToNum), any axis + keepdims |
— |
| statistics | Percentile (all 13 methods), Quantile, Median, Histogram, Histogram2D, Bincount(weights), Cov, CorrCoef, Diff |
CorrCoef of masked data 🚧 |
| sorting & sets | Sort, Argsort, Partition, Argpartition, SearchSorted, Digitize, Unique (+axis), UniqueIdx (returns index/counts), IsIn/Intersect1D/Union1D/Setdiff1D/Setxor1D |
— |
| linalg | Dot, MatMul (batched, real or complex), Inner, Outer, Einsum, Tensordot, Det, Inv/Solve (real and complex), Norm, Cond, MatrixRank, MultiDot, Vdot, Eigvalsh/Eigh (symmetric), Eigvals/Eig (general, Hessenberg + shifted QR, complex results like NumPy), Svd, Svdvals, Slogdet, Kron, QR, Cholesky, Pinv, Lstsq (returns x/residuals/rank/s) |
Tensorsolve 🚧, N-D complex dot 🚧 |
| random | bit-exact legacy RandomState/MT19937: Seed, RandomSample, StandardNormal, Uniform, Normal, RandInt, Permutation, Shuffle |
Generator/PCG64 family 🚧, Choice/Beta/Gamma/Poisson etc. 🚧 |
| fft | FFT/IFFT/RFFT/IRFFT/FFTFreq (radix-2 + Bluestein, any length), FFT2/IFFT2, FFTN/IFFTN (any axis subset), RFFT2/IRFFT2, RFFTN/IRFFTN, HFFT/IHFFT, FFTShift/IFFTShift |
— |
| masked arrays | MaskedArray (data+mask), Filled/Compressed/Count, masked Sum/Mean/Min/Max/Median/Var/Std, MaskedAdd/Subtract/Multiply/Divide, MaskedInvalid/MaskedWhere, predicates |
ma.corrcoef 🚧 (see limitations), masked_equal-style constructors beyond the 8 provided |
| I/O | SaveNPY/LoadNPY (+ bytes), SaveZ/SaveZCompressed/LoadZ, FromBuffer (zero-copy), FromFile/ToFile, SaveTxt/LoadTxt, Genfromtxt/GenfromtxtBytes (delimiter, skip_header/skip_footer, usecols, missing → NaN) |
Memmap 🚧, per-column dtypes 🚧 |
| ufunc protocol | BinaryOut/UnaryOut (out=), BinaryWhere/UnaryWhere (where=), Reduce, Accumulate, UfuncOuter |
at/buffers/signature keywords 🚧 |
| string & datetime | construction, indexing, comparison, datetime64 unit handling |
string ufuncs 🚧, timedelta64 arithmetic 🚧 |
| iteration & broadcast objects | — | nditer 🚧, vectorize/frompyfunc 🚧, broadcast_arrays 🚧 |
| error control | fail-loud errors with sentinels (ErrShape, ErrDtype, ErrValue, ErrIndex, ErrZeroDiv) |
errstate/seterr 🚧 |
- Package-level state is initialised once and read-only afterwards. The dtype
promotion table, ufunc registries and kernel maps are built in
init(); there is no lazy initialisation and nosync.Onceneeded. Concurrent calls into the package are therefore safe. - Arrays are not internally synchronised — exactly like NumPy. Distinct
arrays can be used from different goroutines freely; concurrent writes to the
same array (or to aliasing views of it) are the caller's responsibility.
Read-only concurrent access to one array is tested (
TestConcurrentReadSameArray). MatMulparallelism is contained: rows are split across goroutines and each worker writes only its own rows, so no locking is needed; results are bit-identical acrossGOMAXPROCSvalues (TestMatMulParallelStability).- File handles are closed on every path (
deferimmediately after a successful open).TestNoFileHandleLeakperforms 20 000 load/save rounds across.npy/.npz/text — well past the Windows handle limit — and additionally deletes a file it just wrote to prove no handle is still held. - Views keep their backing buffer alive. A view of a 1 GB array keeps that
1 GB reachable; copy with
AsContiguous/Copyif you only need a slice.FromBufferdoes not copy either, so the caller owns the byte slice lifetime. - Memory is not pooled. Every result is a fresh allocation sized exactly to
the result shape; there are no hidden caches whose growth could surprise you.
Reuse buffers explicitly with the
out=helpers (BinaryOut/UnaryOut).
| Doc | Contents |
|---|---|
wasm/README.md |
WASM entry: build, JS API, what is exposed vs planned, smoke test |
Releases use annotated tags of the form v0.<minor>.<patch>. Each release
increments only the last digit (v0.1.0 → v0.1.1 → … → v0.1.9); when that
digit would pass 9 the counter carries into the minor component
(v0.1.9 → v0.2.0).
MIT — see LICENSE. Semantics were derived from observed NumPy behaviour and re-implemented in Go; no NumPy source is vendored.