Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describes**:

| Source | Published under |
| --- | --- |
| branch `main`, with `params.ndarray.version: v0.9.1` in `hugo.yaml` | `https://go-ndarray.github.io/docs/<MAJOR.MINOR>/`, here `/docs/0.9/` |
| branch `main`, with `params.ndarray.version: v0.11.0` in `hugo.yaml` | `https://go-ndarray.github.io/docs/<MAJOR.MINOR>/`, here `/docs/0.11/` |
| the newest version | also `https://go-ndarray.github.io/docs/latest/` |

`https://go-ndarray.github.io/docs/` redirects to `latest/`. Each push to
Expand All @@ -41,22 +41,35 @@ same `versions.json` (mike's format).

## Mirrored pages

Two pages are copies of files in `go-ndarray/ndarray`, kept verbatim so that
Six pages are copies of files in `go-ndarray/ndarray`, kept verbatim so that
re-syncing them is a copy, not an edit:

| Page | Mirrors | What is ours |
| --- | --- | --- |
| `content/en/dtypes.md` | [`docs/design-dtypes.md`](https://github.com/go-ndarray/ndarray/blob/main/docs/design-dtypes.md), below its `#` title | the front matter |
| `content/en/linalg.md` | [`docs/linalg.md`](https://github.com/go-ndarray/ndarray/blob/main/docs/linalg.md), below its `#` title | the front matter |
| `content/en/random.md` | [`docs/random.md`](https://github.com/go-ndarray/ndarray/blob/main/docs/random.md), below its `#` title | the front matter |
| `content/en/roadmap.md` | [`docs/plan-ndarray.md`](https://github.com/go-ndarray/ndarray/blob/main/docs/plan-ndarray.md), below its `#` title | the front matter |
| `content/en/performance.md` | [`docs/perf.md`](https://github.com/go-ndarray/ndarray/blob/main/docs/perf.md), from the heading `## How a pure-Go library can beat NumPy` to the end | the front matter, and the introduction and *Where it stands* summary above that heading |
| `content/en/security.md` | [`SECURITY.md`](https://github.com/go-ndarray/ndarray/blob/main/SECURITY.md), from `## Reporting` to the end | the front matter and the `> Mirrored from … at vX.Y.Z` line; repository-relative references are rewritten as GitHub links (`this repository` → `[the repository](https://github.com/go-ndarray/ndarray/security)`, `` `fuzz_test.go` `` → ``[`fuzz_test.go`](https://github.com/go-ndarray/ndarray/blob/main/fuzz_test.go)``, …) |

To re-sync from a checkout of ndarray at tag `vX.Y.Z` (`$ND`):
The first five are re-synced by one command, from a checkout of ndarray at tag
`vX.Y.Z` (`$ND`); it refuses a checkout that is not at that tag:

```sh
# performance: keep our part above the heading, replace the rest
{ sed '/^## How a pure-Go library can beat NumPy/,$d' content/en/performance.md
sed -n '/^## How a pure-Go library can beat NumPy/,$p' "$ND/docs/perf.md"; } > perf.tmp
mv perf.tmp content/en/performance.md
scripts/sync-mirrors.sh "$ND" vX.Y.Z
```

It writes each page's `> Mirrored from … at vX.Y.Z` line, and rewrites the
source's repository-relative links: a file this site mirrors becomes its page
(`perf.md` → `performance.md`), a package directory (`../linalg`) becomes its
pkg.go.dev page at the tag, any other file a GitHub link at the tag. A relative
link it has no rule for makes it fail, naming the file and line.

Security is re-synced by hand, because its references are prose (`fuzz_test.go`
in backquotes), not links:

```sh
# security: keep our part above "## Reporting", replace the rest, then
# rewrite the repository-relative references as GitHub links again
{ sed '/^## Reporting/,$d' content/en/security.md
Expand Down
39 changes: 25 additions & 14 deletions content/en/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ linkTitle: "Home"
type: docs
cascade:
type: docs
description: "A pure-Go (no cgo) NumPy-style n-dimensional array for float64: views, broadcasting, ufuncs, reductions, linear algebra; multicore + SIMD."
description: "A pure-Go (no cgo) NumPy-style n-dimensional array in 13 dtypes: views, broadcasting, ufuncs, reductions, numpy.linalg, numpy.fft, numpy.random; multicore + SIMD."
---

**A pure-Go (no cgo) NumPy-style n-dimensional array** for `float64` — the
`numpy` equivalent for Go. Creation routines, strided **views that share data**,
broadcasting elementwise ops and ufuncs, reductions, manipulation and linear
algebra, all with cgo disabled.
**A pure-Go (no cgo) NumPy-style n-dimensional array** in **thirteen dtypes**
— the `numpy` equivalent for Go. Creation routines, strided **views that share
data**, broadcasting elementwise ops and ufuncs, reductions, manipulation,
`numpy.linalg`, `numpy.fft` and `numpy.random` (with NumPy's numbers for the
same seed), all with cgo disabled. `float64` is the default dtype and the one
with SIMD kernels.

Ruby has no cgo-free ndarray (`Numo::NArray`, `NMatrix` are C extensions) and
`gonum`'s optimized assembly is amd64-only. go-ndarray pairs a portable pure-Go
Expand All @@ -34,14 +36,18 @@ WebAssembly, with nothing to install — build a pipeline step by step (creation

| Area | Functions / methods |
| --- | --- |
| Dtypes ([page](dtypes.md)) | bool, int8…int64, uint8…uint64, float32/float64, complex64/complex128 with NumPy 2 promotion: `AsType`, `ZerosOf`, `FromSlice`, `Data[T]`/`Item[T]`, weak Go scalars with `Scalar` |
| Creation | `New`, `Zeros`, `Ones`, `Full`, `Arange`, `Linspace`, `Eye`, `Identity`, `FromData` |
| Views & shape | `Slice` (`All`/`R`/`Rng`/`From`/`To`/`Step`), `Reshape`, `Ravel`, `Transpose`, `Copy` |
| Elementwise | `Add`/`Sub`/`Mul`/`Div` (+`*Scalar`, +`*Into`), `Map`, `Neg`, `Abs` |
| Elementwise | `Add`/`Sub`/`Mul`/`Div` (+`*Scalar`, +`*Into`), `Map`, `Neg`, `Abs`, comparisons (`Greater`/`Equal`/… → Bool masks), `Maximum`/`Minimum`, `Logical*`, `Real`/`Imag`/`Conj`/`Angle` |
| Ufuncs | `Sqrt`, `Exp`, `Log`/`Log2`/`Log10`, `Sin`/`Cos`/`Tan`, `Floor`/`Ceil`/`Round`, `Square`, `Power` |
| Reductions | `Sum`, `Mean`, `Max`/`Min`, `Prod`, `ArgMax`/`ArgMin`, `CumSum`/`CumProd`, `Clip`, `Where` (+ per-axis) |
| Indexing | `Slice` (basic indexing), `MaskSelect` (`a[mask]`), `Nonzero` (`flatnonzero`), `Take` (fancy indexing) |
| Manipulation | `Flatten`, `ExpandDims`, `Squeeze`, `Concatenate`, `Stack`, `VStack`, `HStack` |
| Linear algebra | `MatMul`, `Dot`, `Inner`, `Outer` |
| Linear algebra | `MatMul`, `Dot`, `Inner`, `Outer`; package `linalg` ([page](linalg.md)): `Solve`, `Inv`, `Det`, `Slogdet`, `Cholesky`, `QR`, `SVD`, `Eigh`, `Eig`, `Lstsq`, `Pinv`, `MatrixRank`, `Norm`, `Cond`, `MatrixPower`, `MultiDot` |
| Fourier transforms | package `fft`: `numpy.fft` on arrays (`FFT`/`RFFT`/`FFT2`/`FFTN`/… with `n`/`s`, `axes`, `norm`), over [go-fft](https://github.com/go-fft/fft) |
| Random numbers | package `random` ([page](random.md)): PCG64, PCG64DXSM, MT19937, Philox, SFC64; `Generator` (`DefaultRNG`, 40 samplers) and the legacy `RandomState` (`Seed`, `Rand`) |
| Notebooks & storage | `Repr` (prints like NumPy), `Display` (text and HTML), `MarshalBinary`/`UnmarshalBinary` (the same bytes on every architecture) |
| Memory reuse | `NewWorkspace`, `Use`, `Reset`, `Detach` |

## Goroutines (v0.6.0)
Expand Down Expand Up @@ -87,13 +93,15 @@ On an AMD Zen 3 (16 cores), `x + y` on 1 024 elements goes from 2.6 µs to
## Performance & architectures

Large elementwise ops, reductions and products run **multicore** (across
`GOMAXPROCS`). On **amd64, arm64, ppc64le and loong64** the hot loops are
go-asmgen SIMD kernels: sum, add/sub/mul/div, sqrt and the dot product (SSE2 or
AVX2/FMA on amd64, NEON on arm64, VSX on ppc64le, LASX on loong64 when the CPU
has it), max/min too on amd64, and a **panel-packed, cache-blocked GEMM** with
an SIMD-FMA micro-kernel (NEON 4×8; AVX2/FMA 6×8 chosen at run time by a CPUID
probe, SSE2 fallback; VSX 8×8; LASX 8×8). riscv64, s390x and the 32-bit
targets run the same pure-Go code those kernels are tested against. `Exp` and `Log` are ports of Arm's
`GOMAXPROCS`). On **amd64, arm64, ppc64le, loong64, riscv64 and s390x** the hot
loops are go-asmgen SIMD kernels for `float64`: sum, add/sub/mul/div, sqrt and
the dot product (SSE2 or AVX2/FMA on amd64, NEON on arm64, VSX on ppc64le, LASX
on loong64, RVV on riscv64 and the vector facility on s390x, the last three
when the CPU has them), max/min too on amd64, arm64 and s390x (z14 and later),
and a **panel-packed, cache-blocked GEMM** with an SIMD-FMA micro-kernel (NEON
4×8; AVX2/FMA 6×8 chosen at run time by a CPUID probe, SSE2 fallback; VSX 8×8;
LASX 8×8; RVV 6×8; s390x 4×8). The 32-bit targets run the same pure-Go code
those kernels are tested against. `Exp` and `Log` are ports of Arm's
optimized-routines (worst 0.504 and 0.508 ULP measured, against up to 0.88 and 0.72 for Go's `math.Exp`/`math.Log` on arm64), and they are
correct where amd64's `math.Exp` returns +Inf
([golang/go#81995](https://github.com/golang/go/issues/81995)) and `math.Log`
Expand Down Expand Up @@ -132,6 +140,9 @@ ppc64le, riscv64 and loong64 hardware.

## Where to go next

- [Dtypes](dtypes.md) — the thirteen element types and NumPy 2 promotion.
- [Linear algebra](linalg.md) — `numpy.linalg`: algorithms, accuracy, speed.
- [Random numbers](random.md) — `numpy.random`, the same numbers for the same seed.
- [Roadmap](roadmap.md) — the plan and what ships today.
- [Performance](performance.md) — honest benchmarks versus NumPy / OpenBLAS.

Expand Down
183 changes: 183 additions & 0 deletions content/en/dtypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
# Below the "Mirrored from" line, this page is go-ndarray/ndarray
# docs/design-dtypes.md; scripts/sync-mirrors.sh re-syncs it.
title: "Dtypes — thirteen element types, NumPy 2 promotion"
linkTitle: "Dtypes"
weight: 5
description: "Why the dtype is a run-time property of one Array type, how arrays are stored, and where go-ndarray follows or departs from NumPy 2.x promotion."
tags: [dtypes, numpy]
---

> Mirrored from [`docs/design-dtypes.md`](https://github.com/go-ndarray/ndarray/blob/v0.11.0/docs/design-dtypes.md) at v0.11.0.

> Status: implemented (Phase 2). Supersedes decision 4 of
> [plan-ndarray.md](roadmap.md) ("`float64` first").

## Why now

`Array` holds `float64` only. Three things the library is meant to do cannot be
done on that alone:

- **Spectra.** `go-fft` returns `[]complex128`. Without a complex dtype an FFT
result cannot be an array, so the two libraries do not compose.
- **Masks.** Comparisons return `0/1` floats. They work for `MaskSelect` and
`Where`, but a mask cannot be told apart from data, and it costs 8 bytes per
element instead of 1.
- **Memory-bound work.** `float32` halves the bytes moved, which is the whole
cost of the elementwise ops; images arrive as `uint8`; counts and indices are
integers.

The type at the centre of the library is the one thing that gets more
expensive to change every release, and the module is pre-1.0. That is why it
goes first.

## Decision: the dtype is a runtime property of the one `Array` type

```go
type DType uint8 // Bool, Int8…Int64, Uint8…Uint64, Float32, Float64, Complex64, Complex128

a, _ := ndarray.Zeros(3, 4) // Float64, as before
b := a.AsType(ndarray.Complex128)
c, _ := a.Add(b) // Complex128, by NumPy promotion
m, _ := a.Greater(ndarray.Scalar(0)) // Bool
```

The alternative was a generic `Dense[T]`. It was rejected for four reasons:

1. **Promotion cannot be written as a generic method.** `int32 / int32` is
`float64` and `int8 + float32` is `float32`: the result type depends on both
operands. A method on `Dense[T]` cannot return `Dense[U]` for a `U` chosen
from its argument, and methods cannot have their own type parameters. The
arithmetic would have to move to package functions, one per pair of types
in the general case, and NumPy's semantics would be lost or re-encoded.
2. **The consumers are dynamic.** The notebook evaluates cells in an
interpreter, and the Ruby binding (`go-embedded-ruby`) is a dynamic language.
Both hold "an array" and ask it what it is; neither can carry a type
parameter.
3. **Nothing that exists breaks.** `*Array`, its methods and the `float64` hot
path keep their names, signatures and speed. The Ruby binding uses method
expressions such as `(*nd.Array).Sqrt`; those would stop compiling on an
alias of an instantiated generic type.
4. **Static typing is still available where it pays**: generic accessors
`Data[T]`, `FromSlice[T]` and `Item[T]` check the dtype once at the boundary
and hand back a `[]T`.

## Storage

```go
type Array struct {
data []float64 // Float64 storage — the existing hot path, unchanged
ext any // []T for every other dtype, nil for Float64
dtype DType
shape []int
strides []int
offset int
}
```

Keeping `data []float64` as a concrete field means every existing `float64`
kernel, SIMD path and benchmark is untouched. The other dtypes are reached
through one type assertion per operation, not per element. Shape, strides and
views do not depend on the dtype, so slicing, reshape, transpose, broadcast and
the manipulation functions are written once over a generic helper.

Inner loops for the other dtypes are generic Go (`func add[T number](dst, x,
y []T)`). The gc compiler stencils one copy per GC shape, and every numeric type
has its own shape, so these are fully specialised loops. SIMD kernels come
dtype by dtype after correctness, `float32` first.

## Semantics: NumPy 2.x, checked against NumPy

- **Array–array promotion** follows `numpy.result_type`. The full 13×13 table
is generated from NumPy 2.5.3 and committed as a test fixture
(`testdata/promotion.json`, embedded so the emulated CI arches can read it);
the implementation is tested against every cell.
- **Go scalars are weak**, like Python scalars under NEP 50: `AddScalar(1.5)` on
a `float32` array stays `float32`, on an `int8` array becomes `float64`.
`ScalarOf(v, dtype)` makes a strong one.
- **True division** of integers or bools gives `float64`.
- **Reductions:** `Sum`/`Prod`/`CumSum` of bool and signed ints accumulate in
`int64`, of unsigned ints in `uint64`; `Mean` of integers is `float64`;
`Max`/`Min` keep the dtype; arg-reductions are `int64`.
- **Float ufuncs on integers** (`Sqrt`, `Exp`, `Sin` …) give `float64` for
32- and 64-bit ints, `float32` for 16-bit ints.
- **Comparisons return `Bool`.** `MaskSelect` and `Where` accept a `Bool` mask
and, as NumPy does, any other dtype by truthiness, so existing `0/1` masks
keep working.
- **Integer overflow wraps**, as it does in both Go and NumPy.

### Deliberate differences

| NumPy | here | why |
|---|---|---|
| `float16` exists; `sqrt(int8)` is `float16` | no `float16`; `sqrt(int8)`, `sqrt(uint8)` and `sqrt(bool)` give `float32` | Go has no half-precision arithmetic; `float32` is the smallest type that holds the result exactly |
| `round` is half-to-even | half-away-from-zero (unchanged) | documented since v0.1.0 |
| `datetime64`, `str_`, `object`, structured dtypes | absent | out of scope for a numeric library |

## Element access

- `At(idx...) float64` keeps working for every real dtype, converting the
element. On a complex array it panics with a message that names `AtComplex`.
NumPy instead discards the imaginary part with a `ComplexWarning`; Go has no
warnings, and a silent loss is the defect that warning exists to flag.
- `AtComplex(idx...) complex128`, and `Item[T](a, idx...) T` for exact typed
access.
- `Set(v float64, idx...)` converts into the array's dtype.

## Breaking changes

| before | after | migration |
|---|---|---|
| comparisons return a `Float64` 0/1 array | they return `Bool` | `.AsType(Float64)` to get the old values; `MaskSelect` and `Where` need no change |
| `Nonzero` returns `Float64` indices | `Int64`, as `np.flatnonzero` | `.AsType(Float64)`, or `Data[int64]` |
| `ArgMaxAxis`/`ArgMinAxis` return `Float64` indices | `Int64`, as NumPy | same |
| `MaskSelect` takes a 0/1 float mask | it takes any dtype (truthiness) | none |

The only consumer in the fleet, `go-embedded-ruby/ruby` (`internal/vm/ndarray.go`),
does not use comparisons; it is pinned to a June pseudo-version and will be
moved forward in its own pull request.

## What the implementation added

- `Scalar(v)` makes the weak operand from any Go number; the `*Scalar`
methods' `float64` arguments are weak floats.
- `SumAll`/`ProdAll` return a 0-d array in the accumulation dtype, exact for
every dtype; `Sum`/`Prod`/`Mean`/`Max`/`Min` keep returning `float64` and
panic on complex arrays (naming the method to use instead).
- `LogicalNot`/`LogicalAnd`/`LogicalOr`/`LogicalXor`, `Any`, `All`, and
`Real`/`Imag`/`Conj`/`Angle`.
- Float-to-integer casts saturate on every platform. NumPy leaves them
undefined: on arm64 it saturates to 64 bits and then wraps (`+Inf` to `int8`
is `-1`), on x86 `NaN` gives the minimum.
- A `Workspace` serves `Float64` results only; the other dtypes allocate from
the heap for now.
- Only `Float64` has SIMD kernels and multicore fan-out; the other dtypes run
tight generic loops (one per element type, generated by `gen_dtypes.go`).
`MatMul` on them is a plain triple loop.

### Verification

- `testdata/promotion.json`: every one of the 169 `result_type` cells, the
weak-scalar results, and the result dtype of the unary ufuncs and the
reductions, from NumPy 2.5.3. All match, with `float16` read as `float32`.
- `testdata/values.json`: 47 operations on every dtype with edge values
(integer limits, NaN, ±Inf, −0) — 3,147 values. Correctly rounded
operations and every integer or bool result must match to the bit; the
others within 8 machine epsilons of NumPy's result dtype. A deliberately
broken integer `max` fails 48 of them.

## Delivery

1. **Core** — `DType`, storage, `AsType`, creation with a dtype
(`ZerosOf`/`OnesOf`/`FullOf`/`FromSlice[T]`), element access, and every
shape-only operation (reshape, transpose, slice, copy, flatten, expand,
squeeze, concatenate, stack, string form) for every dtype.
2. **Arithmetic** — promotion, the four operators and the scalar forms,
comparisons returning `Bool`, `Where`/`MaskSelect`, reductions with their
accumulator types, unary ufuncs.
3. **Complex** — `Real`/`Imag`/`Conj`/`Angle`, `Abs` of complex to float, and an
`fft` subpackage bridging `go-fft` on arrays (`fft.FFTN(a, axes...)`).
4. **Speed** — `float32` SIMD kernels, then `MatMul` for `float32` and complex.

Each step keeps the 100% coverage gate and the per-arch CI, and is checked
against NumPy before it ships.
Loading
Loading