728 lines
25 KiB
Markdown
728 lines
25 KiB
Markdown
# Tensor
|
||||
|
|
|
|||
|
|
Tensor is a scientific computing library for Go: n-dimensional arrays,
|
|||
|
|
dense and sparse linear algebra, differential equation solvers,
|
|||
|
|
quadrature, statistics, signal transforms, optimisation, deterministic
|
|||
|
|
scientific charts and a reverse-mode differentiable core, built for the
|
|||
|
|
natural sciences: cosmology, astronomy, quantum, particle and nuclear
|
|||
|
|
physics, condensed matter and materials science, chemistry, biology and
|
|||
|
|
genetics. It is pure Go with no cgo, no GPU stack and no third-party
|
|||
|
|
dependency, and it is deterministic by contract: parallel kernels
|
|||
|
|
reduce in a fixed order, so the same program with the same seed
|
|||
|
|
produces bit-identical output run to run.
|
|||
|
|
|
|||
|
|
## What Tensor optimises for
|
|||
|
|
|
|||
|
|
Tensor is not built to win a speed contest. It keeps no benchmark
|
|||
|
|
tables against other libraries, NumPy included: they are not the
|
|||
|
|
competition, and racing them would settle nothing. The only
|
|||
|
|
performance numbers in this repository compare Tensor against its own
|
|||
|
|
previous revisions, under the discipline in
|
|||
|
|
[docs/BENCHMARKING.md](docs/BENCHMARKING.md), so that a change is
|
|||
|
|
judged by what it costs and never by an impression. Speed is an
|
|||
|
|
engineering duty, not the goal.
|
|||
|
|
|
|||
|
|
The goal is a tool a scientist can trust with their numbers:
|
|||
|
|
|
|||
|
|
- **Determinism, everywhere, without exceptions.** The same program
|
|||
|
|
with the same seed produces bit-identical output, run to run, on any
|
|||
|
|
core count and under any worker setting. Parallel kernels reduce in
|
|||
|
|
an order derived from the data alone, never from the machine, and
|
|||
|
|
the generator is pinned and stable across Go releases. A number that
|
|||
|
|
moves between runs is not a result.
|
|||
|
|
- **Portability, without variants.** Pure Go on the standard library:
|
|||
|
|
no cgo, no third-party dependency. One portable build, no build
|
|||
|
|
flags, no CPU feature probes; there is no fast edition and no slow
|
|||
|
|
edition of the truth to keep in agreement.
|
|||
|
|
- **Precision, measured rather than claimed.** Narrow element types
|
|||
|
|
accumulate in float64, and where two algorithms round differently
|
|||
|
|
the choice is settled against high-precision referents, `big.Float`
|
|||
|
|
arithmetic at hundreds of bits. Every solver carries its
|
|||
|
|
exact-reference or residual check in the test suite.
|
|||
|
|
|
|||
|
|
This is also why the GPU is not the path for Tensor. GPU execution is
|
|||
|
|
non-deterministic by construction: the order of a reduction follows
|
|||
|
|
the hardware topology and the device scheduler instead of the data,
|
|||
|
|
and the arithmetic carries no exactness guarantee to hold, the
|
|||
|
|
graphics APIs themselves permitting results several ULP off. A
|
|||
|
|
library contracted to exact reproducibility does not hand its numbers
|
|||
|
|
to hardware that cannot sign for them.
|
|||
|
|
|
|||
|
|
## Features
|
|||
|
|
|
|||
|
|
- **Arrays**: immutable, row-major, n-dimensional arrays of int64,
|
|||
|
|
IEEE 754 half precision, float32, float64 and complex128 with a
|
|||
|
|
strict promotion ladder and loud shape errors; slicing views,
|
|||
|
|
gathers, scatters, sorting, Einsum, interpolation and the special
|
|||
|
|
functions from the gamma family to the elliptic integrals.
|
|||
|
|
- **Linear algebra (`linalg`)**: dense factorisations and
|
|||
|
|
eigenproblems in real, complex and general arithmetic, matrix
|
|||
|
|
functions, regularised and truncated solves, the rank-revealing QR,
|
|||
|
|
sparse CSR solvers with ILU preconditioning, sparse direct
|
|||
|
|
Cholesky and LU with fill-reducing orderings and the rank-one
|
|||
|
|
update and downdate, LSQR and LSMR least squares, Lanczos and
|
|||
|
|
Arnoldi eigensolvers, the Krylov action of a matrix exponential,
|
|||
|
|
polynomial fitting and natural cubic splines.
|
|||
|
|
- **Differential equations (`integrate`)**: adaptive Dormand-Prince,
|
|||
|
|
stiff systems from variable-step BDF2 up to variable-order BDF 1 to
|
|||
|
|
5, the L-stable Rosenbrock-Wanner ROS4, index-1
|
|||
|
|
differential-algebraic systems in mass-matrix form, symplectic
|
|||
|
|
integrators from velocity Verlet through Yoshida's fourth order to
|
|||
|
|
the implicit midpoint, event detection, boundary values by shooting
|
|||
|
|
and by adaptive Lobatto collocation, Gauss-Legendre quadrature,
|
|||
|
|
globally adaptive cubature, heat and wave evolution in one and two
|
|||
|
|
space dimensions, flux-limited advection, and finite-element
|
|||
|
|
Poisson solves on triangular and tetrahedral meshes.
|
|||
|
|
- **Signal and transforms (`signal`)**: FFTs of any length,
|
|||
|
|
multi-dimensional and real-input transforms, cosine and sine
|
|||
|
|
transforms, the NUFFT, Welch, spectrogram and Lomb-Scargle spectra,
|
|||
|
|
the Hilbert envelope, decimation and resampling, Butterworth,
|
|||
|
|
Chebyshev, inverse Chebyshev and elliptic filter design, the window
|
|||
|
|
catalogue, zero-phase filtfilt, median and rank filters, Haar and
|
|||
|
|
Daubechies wavelets, continuous wavelets, Kalman filtering, AR and
|
|||
|
|
ARMA estimation, convolutions, pooling and stencils, and spectral
|
|||
|
|
Poisson solves.
|
|||
|
|
- **Statistics (`stats`)**: CDFs, quantiles and draws for the normal,
|
|||
|
|
exponential, gamma, chi-square, Student t, Poisson and binomial
|
|||
|
|
laws; density, CDF and quantile for Weibull, lognormal and Pareto;
|
|||
|
|
the negative binomial PMF, CDF and quantile; the Dirichlet density,
|
|||
|
|
mean, mode and draws; the noncentral chi-square, F and t families
|
|||
|
|
through their density, CDF and quantile; histograms and rolling
|
|||
|
|
windows, robust descriptives, Welch's t-test, Kolmogorov-Smirnov,
|
|||
|
|
Mann-Whitney U, one-way ANOVA, bootstrap intervals, rank
|
|||
|
|
correlations and multiple-testing corrections, multivariate
|
|||
|
|
normals, kernel density, Gaussian processes, clustering, PCA, and
|
|||
|
|
linear, weighted, logistic, Poisson, lasso, elastic-net, Huber and
|
|||
|
|
quantile regression with the classical inference beside Theil-Sen's
|
|||
|
|
robust pair.
|
|||
|
|
- **Optimisation (`optim`)**: Levenberg-Marquardt with an optional
|
|||
|
|
analytic Jacobian, L-BFGS with box bounds, linearly constrained
|
|||
|
|
minimisation through the augmented Lagrangian with nonlinear
|
|||
|
|
equality and inequality rows, Nelder-Mead, differential evolution,
|
|||
|
|
CMA-ES and simulated annealing, the revised simplex and active-set
|
|||
|
|
quadratic programming, and Brent, Newton, Broyden quasi-Newton and
|
|||
|
|
damped-Newton root finding.
|
|||
|
|
- **Differentiable core (`grad`)**: a reverse-mode graph over the
|
|||
|
|
arithmetic surface, the matrix products and the transforms, complex
|
|||
|
|
Wirtinger differentiation, Hessians, Newton-CG on the graph,
|
|||
|
|
Hamiltonian Monte Carlo and adjoint ODE sensitivities.
|
|||
|
|
- **Data (`io`)**: CSV, FITS images and tables, HDF5 datasets read
|
|||
|
|
and written (both superblock generations, chunked storage, deflate
|
|||
|
|
and shuffle filters, attributes and string data), the NetCDF
|
|||
|
|
classic model, and memory-mapped arrays that let a data cube far
|
|||
|
|
larger than RAM open instantly.
|
|||
|
|
- **Worked examples**: thirteen runnable workflows in `examples/`,
|
|||
|
|
each solving its problem end to end: ODE parameter fitting by
|
|||
|
|
adjoint sensitivities, PSF deconvolution, Hamiltonian Monte Carlo
|
|||
|
|
sampling, spectral analysis, wavelet denoising, the exact pendulum
|
|||
|
|
period through the elliptic integral, a Helmholtz system on the
|
|||
|
|
complex sparse solvers, quasi-Monte Carlo integration, heat and
|
|||
|
|
wave evolution, regression inference, a FITS star field, a NetCDF
|
|||
|
|
climate round trip, and an FFT tour.
|
|||
|
|
|
|||
|
|
## Experimental distributed computing
|
|||
|
|
|
|||
|
|
**This is an experiment.** The `spmd` package carries a revolutionary
|
|||
|
|
technological concept: one program running on many ranks, in one
|
|||
|
|
process or across machines, where the order of a reduction is a
|
|||
|
|
function of the data alone, so a sharded reduction carries the
|
|||
|
|
single-array reduction's exact bits at any world size. The concept is
|
|||
|
|
not fully verified and remains the subject of research. The
|
|||
|
|
single-machine library is the settled product; the distributed surface
|
|||
|
|
is new, its behaviour and performance on real networks are not yet
|
|||
|
|
measured, and it is expected to change as the research moves.
|
|||
|
|
|
|||
|
|
What the experiment carries today, and what already holds: the
|
|||
|
|
movement collectives (`Broadcast`, `Scatter`, `Gather`, `AllGather`)
|
|||
|
|
and two families of reductions whose answers the test suite, the
|
|||
|
|
oracle digests and the loopback cluster tests in CI pin against the
|
|||
|
|
single-array reductions they must equal. What the suite cannot reach,
|
|||
|
|
the bandwidth and behaviour of real networks above all, is exactly
|
|||
|
|
what the research is for. Read the contract in
|
|||
|
|
[docs/API.md](docs/API.md) before you build on it, and treat its edges
|
|||
|
|
as open questions rather than finished answers.
|
|||
|
|
|
|||
|
|
The shortest form of the experiment, four ranks over one million
|
|||
|
|
values, with the sharded sum equal to the single-array sum bit for
|
|||
|
|
bit:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/spmd"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
err := spmd.Launch(4, func(w *spmd.World) error {
|
|||
|
|
const n = 1000000
|
|||
|
|
vals := make([]float64, n)
|
|||
|
|
for i := range vals {
|
|||
|
|
vals[i] = float64((i*7919)%2001-1000) / 7.0
|
|||
|
|
}
|
|||
|
|
whole, _ := tensor.FromFloats(vals, n)
|
|||
|
|
span, err := spmd.Partition(n, w.Size(), w.Rank())
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
local, err := tensor.Slice(whole, 0, span.Lo, span.Hi)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
got, err := w.AllReduceShards(local, span, spmd.Sum)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if w.Rank() == 0 {
|
|||
|
|
want := tensor.Sum(whole)
|
|||
|
|
fmt.Printf("size %d: sharded %v equals single-array %v: %v\n",
|
|||
|
|
w.Size(), got.Float(), want.Float(), got.Float() == want.Float())
|
|||
|
|
}
|
|||
|
|
return w.Barrier()
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Launch` swaps for `Listen` and `Join` when the ranks are processes on
|
|||
|
|
different machines, and nothing else in the program moves.
|
|||
|
|
|
|||
|
|
## Install
|
|||
|
|
|
|||
|
|
As a library:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
go get sourcedock.dev/petrbalvin/tensor
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Requires Go 1.27.1 or newer, the exact version `go.mod` declares.
|
|||
|
|
|
|||
|
|
One import covers everything: the root package re-exports the exported
|
|||
|
|
surface of every domain package, so `tensor.SVD` and `linalg.SVD` name
|
|||
|
|
the same function. A domain package may also be imported on its own
|
|||
|
|
for a narrow dependency graph; the general array constructors live in
|
|||
|
|
the root package (`linalg.ArrayFromFloatsSafe` is the one exported
|
|||
|
|
outside it), and the arrays are the same type either way, because
|
|||
|
|
`tensor.Array` is an alias for the core array, not a wrapper around
|
|||
|
|
it. Nothing is lost by mixing the two styles.
|
|||
|
|
|
|||
|
|
## Quick start
|
|||
|
|
|
|||
|
|
A stiff relaxation with a slow forcing, integrated to the analytic
|
|||
|
|
answer:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
// y' = -1e5·(y - cos t): a transient of width 1e-5 under a slow
|
|||
|
|
// forcing. The adaptive BDF2 steps over the transient and then
|
|||
|
|
// follows the forcing; an explicit scheme is pinned to the
|
|||
|
|
// stability limit h < 2e-5 for the whole run.
|
|||
|
|
f := func(t float64, y *tensor.Array) (*tensor.Array, error) {
|
|||
|
|
return tensor.FromFloats([]float64{-1e5 * (y.FloatAt(0) - math.Cos(t))}, 1)
|
|||
|
|
}
|
|||
|
|
y0, _ := tensor.FromFloats([]float64{0}, 1)
|
|||
|
|
end, err := tensor.IntegrateBDF2(f, 0, 1, y0, tensor.ODEOptions{MaxSteps: 2000})
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
exact := (1e10*math.Cos(1) + 1e5*math.Sin(1)) / (1e10 + 1)
|
|||
|
|
fmt.Printf("y(1) = %.12f, error %.2e\n", end.FloatAt(0),
|
|||
|
|
math.Abs(end.FloatAt(0)-exact))
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The stiff solver lands on the exact value `(k²·cos 1 + k·sin 1)/(k² + 1)`
|
|||
|
|
inside a 2000-step budget where the explicit pair, pinned to its
|
|||
|
|
stability limit, cannot follow the run at all. It prints
|
|||
|
|
`y(1) = 0.540310721007, error 4.83e-10`.
|
|||
|
|
|
|||
|
|
## Usage
|
|||
|
|
|
|||
|
|
Tensor's behaviour is a contract, not a convention:
|
|||
|
|
|
|||
|
|
- **Immutable arrays.** No operation mutates its inputs; results are
|
|||
|
|
fresh arrays, and views never alias a buffer a later step could
|
|||
|
|
rewrite. A returned array is yours alone, which is also what makes
|
|||
|
|
them safe to share between goroutines.
|
|||
|
|
- **Errors, not lies.** Singular systems, exhausted step budgets,
|
|||
|
|
malformed files and impossible shapes come back as errors prefixed
|
|||
|
|
`tensor: ` that say what happened. Nothing is silently truncated,
|
|||
|
|
clamped or filled.
|
|||
|
|
- **Determinism.** The generator is xoshiro256++ seeded through
|
|||
|
|
splitmix64, stable across Go releases, because the standard library
|
|||
|
|
does not promise stable output and reproducibility is the point of
|
|||
|
|
a seed. Parallel kernels keep their reduction order fixed, so a
|
|||
|
|
result does not move with the core count; `SetNumCPU(n)` pins the
|
|||
|
|
worker count for containers and small machines.
|
|||
|
|
- **Scientific scope.** Tensor is for the natural sciences and for
|
|||
|
|
nothing else. It carries nothing for artificial intelligence,
|
|||
|
|
economics or finance: no neural-network machinery, no training
|
|||
|
|
loops, no market or portfolio helpers.
|
|||
|
|
The convolution and pooling functions are signal-processing
|
|||
|
|
stencils (PSF deconvolution, image filtering), not model layers.
|
|||
|
|
Differentiation exists because fitting parameters to data and
|
|||
|
|
sensitivity analysis are scientific tools.
|
|||
|
|
|
|||
|
|
What follows is one short program per package, each complete and
|
|||
|
|
runnable as written. The full surface, option by option, is in
|
|||
|
|
[docs/API.md](docs/API.md), and the longer workflows are the thirteen
|
|||
|
|
programs in [`examples/`](examples).
|
|||
|
|
|
|||
|
|
### Arrays
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
y, err := tensor.FromFloats([]float64{1, 2, 3, 4, 5, 6}, 2, 3)
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
// A slice is a read-only view: no copy, and no way for a later
|
|||
|
|
// step to write through it.
|
|||
|
|
cols, _ := tensor.Slice(y, 1, 1, 3)
|
|||
|
|
// Reductions name the axis, and the axis disappears from the shape.
|
|||
|
|
rowSum, _ := tensor.SumAxis(y, 1)
|
|||
|
|
// The dtype ladder promotes on request, never implicitly downward.
|
|||
|
|
halves, _ := tensor.Astype(y, tensor.Float32)
|
|||
|
|
|
|||
|
|
fmt.Println(y.Shape(), rowSum, halves.Dtype())
|
|||
|
|
fmt.Println(cols)
|
|||
|
|
fmt.Println(y)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Shape` reports the extents, `Dtype` the element type, `Len` the
|
|||
|
|
element count. Element-wise operations (`Add`, `Mul`, `Exp`, `Sqrt`,
|
|||
|
|
the comparisons, `Where`) and the shape moves (`Reshape`, `Transpose`,
|
|||
|
|
`Concat`, `Stack`, `Pad`) all take and return whole arrays, so a
|
|||
|
|
formula reads as one expression per line.
|
|||
|
|
|
|||
|
|
### Linear algebra
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
a, _ := tensor.FromFloats([]float64{4, 1, 1, 3}, 2, 2)
|
|||
|
|
b, _ := tensor.FromFloats([]float64{1, 2}, 2)
|
|||
|
|
|
|||
|
|
// One LU with partial pivoting; a singular matrix is an error.
|
|||
|
|
x, err := tensor.Solve(a, b)
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
// The symmetric eigenproblem returns ascending values and the
|
|||
|
|
// orthonormal eigenvectors as columns.
|
|||
|
|
values, vectors, _ := tensor.Eigen(a)
|
|||
|
|
// The Cholesky factor for reuse across right-hand sides.
|
|||
|
|
l, _ := tensor.Cholesky(a)
|
|||
|
|
|
|||
|
|
fmt.Println(x, values)
|
|||
|
|
fmt.Println(l, vectors.Shape())
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Sparse systems go through the same array type: `SparseFrom` builds the
|
|||
|
|
COO form, `CSRFromCOO` and `CSCFromCOO` the compressed views,
|
|||
|
|
`NewSparseCholesky` and `NewSparseLU` the direct factorisations under a
|
|||
|
|
fill-reducing ordering, `NewSparseILU` the preconditioner, and
|
|||
|
|
`SpSolve`, `SpSolveBiCGSTAB`, `SpLSQR` and `SpEigen` the iterative
|
|||
|
|
solvers.
|
|||
|
|
|
|||
|
|
### Differential equations
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
// The harmonic oscillator as a first-order system: y = (position,
|
|||
|
|
// velocity), so y' = (velocity, -position).
|
|||
|
|
f := func(t float64, y *tensor.Array) (*tensor.Array, error) {
|
|||
|
|
return tensor.FromFloats([]float64{y.FloatAt(1), -y.FloatAt(0)}, 2)
|
|||
|
|
}
|
|||
|
|
y0, _ := tensor.FromFloats([]float64{0, 1}, 2)
|
|||
|
|
|
|||
|
|
// Event detection is a watch on the trajectory, so the crossing
|
|||
|
|
// time comes from the interpolant rather than from the step grid.
|
|||
|
|
hits, end, err := tensor.IntegrateODEEvents(f, 0, 4, y0,
|
|||
|
|
[]tensor.ODEWatch{{
|
|||
|
|
Function: func(t float64, y *tensor.Array) (float64, error) {
|
|||
|
|
return y.FloatAt(0), nil
|
|||
|
|
},
|
|||
|
|
Direction: -1, // falling crossings only
|
|||
|
|
}}, tensor.ODEOptions{})
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
fmt.Printf("first minimum at t = %.6f, want pi = %.6f\n", hits[0].Time, math.Pi)
|
|||
|
|
fmt.Printf("y(4) = %.6f\n", end.FloatAt(0))
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`IntegrateODE` is the adaptive Dormand-Prince pair, `IntegrateBDF2`
|
|||
|
|
and `IntegrateBDFVar` the stiff routes, `IntegrateROS4` the L-stable
|
|||
|
|
one, `IntegrateDAE` the mass-matrix form. Quadrature is
|
|||
|
|
`IntegrateFunction` and `IntegrateND`, the boundary value problems are
|
|||
|
|
`IntegrateBoundary` and `SolveBoundaryCollocation`, and the turnkey
|
|||
|
|
time steppers are the `IntegrateHeat1D/2D`, `IntegrateWave1D/2D` and
|
|||
|
|
advection families.
|
|||
|
|
|
|||
|
|
### Transforms, filters and spectra
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
const (
|
|||
|
|
fs = 1000.0
|
|||
|
|
n = 1000
|
|||
|
|
)
|
|||
|
|
samples := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
t := float64(i) / fs
|
|||
|
|
samples[i] = math.Sin(2*math.Pi*50*t) + 0.25*math.Sin(2*math.Pi*120*t)
|
|||
|
|
}
|
|||
|
|
x, _ := tensor.FromFloats(samples, n)
|
|||
|
|
|
|||
|
|
// Welch's averaged periodogram: windowed segments, one-sided.
|
|||
|
|
freqs, psd, err := tensor.WelchPSD(x, fs, 256, 128, "hann")
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
// Bin 0 is the mean; the peak above it is the tone.
|
|||
|
|
rest, _ := tensor.Slice(psd, 0, 1, psd.Len())
|
|||
|
|
idx, _ := tensor.ArgMax(rest)
|
|||
|
|
fmt.Printf("peak at %.1f Hz, bin spacing %.1f Hz\n",
|
|||
|
|
freqs.FloatAt(idx+1), freqs.FloatAt(1))
|
|||
|
|
|
|||
|
|
// A Butterworth design plus filtfilt: zero phase, so no lag.
|
|||
|
|
b, a, _ := tensor.ButterworthLowPass(4, fs, 60)
|
|||
|
|
clean, _ := tensor.Filtfilt(b, a, x)
|
|||
|
|
peakIn, _ := tensor.Max(x)
|
|||
|
|
peakOut, _ := tensor.Max(clean)
|
|||
|
|
fmt.Printf("input peak %.3f, filtered peak %.3f\n", peakIn.Float(), peakOut.Float())
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The Fourier family covers `FFT`/`IFFT` of any length, `FFT2`, `FFT3`,
|
|||
|
|
`FFTN`, the real-input `RFFT`/`IRFFT`, the cosine and sine transforms,
|
|||
|
|
the Haar and Daubechies wavelets, the analytic `CWT`, and the STFT,
|
|||
|
|
spectrogram and Lomb-Scargle estimators beside Welch.
|
|||
|
|
|
|||
|
|
### Statistics
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
// y = 2 + 3x with noise, the intercept column first.
|
|||
|
|
design, _ := tensor.FromFloats([]float64{
|
|||
|
|
1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 1, 5,
|
|||
|
|
}, 6, 2)
|
|||
|
|
y, _ := tensor.FromFloats([]float64{2.1, 4.9, 8.2, 11.1, 13.8, 17.2}, 6)
|
|||
|
|
|
|||
|
|
fit, err := tensor.LinearRegression(design, y)
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
fmt.Printf("slope %.3f ± %.3f, t = %.2f, p = %.2g, R2 = %.4f\n",
|
|||
|
|
fit.Coefficients[1], fit.StandardErrors[1],
|
|||
|
|
fit.TStatistics[1], fit.PValues[1], fit.RSquared)
|
|||
|
|
|
|||
|
|
// Distributions answer one call each, with the tail the caller asks for.
|
|||
|
|
fmt.Printf("P(Z <= 1.96) = %.4f, t(10) 97.5%% = %.4f\n",
|
|||
|
|
tensor.NormalCDF(1.96), mustQuantile(tensor.StudentTQuantile(0.975, 10)))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Quantiles can fail on a parameter outside their domain, so the
|
|||
|
|
// example carries the error rather than dropping it.
|
|||
|
|
func mustQuantile(q float64, err error) float64 {
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
return q
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The models are `LinearRegression`, `WeightedLinearRegression`,
|
|||
|
|
`LogisticRegression`, `PoissonRegression`, `Lasso`, `ElasticNet`,
|
|||
|
|
`HuberRegression` and `QuantileRegression`, each returning its
|
|||
|
|
coefficient table with the standard errors and p-values beside it;
|
|||
|
|
`TheilSenRegression` returns the robust intercept and slope as a pair.
|
|||
|
|
The tests are `WelchTTest`, `KolmogorovSmirnovTest`, `MannWhitneyU`,
|
|||
|
|
`ANOVAOneWay` and `ChiSquareGoodnessOfFit`; the multivariate tools are
|
|||
|
|
`PCA`, `KMeans`, `GaussianMixture` and `GaussianProcessRegression`.
|
|||
|
|
|
|||
|
|
### Optimisation
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
// Fit A·exp(−k·t) to six noisy observations by least squares.
|
|||
|
|
ts := []float64{0, 1, 2, 3, 4, 5}
|
|||
|
|
obs := []float64{2.0, 1.22, 0.74, 0.45, 0.27, 0.17}
|
|||
|
|
residual := func(p *tensor.Array) (*tensor.Array, error) {
|
|||
|
|
out := make([]float64, len(ts))
|
|||
|
|
for i, t := range ts {
|
|||
|
|
out[i] = p.FloatAt(0)*math.Exp(-p.FloatAt(1)*t) - obs[i]
|
|||
|
|
}
|
|||
|
|
return tensor.FromFloats(out, len(out))
|
|||
|
|
}
|
|||
|
|
p0, _ := tensor.FromFloats([]float64{1, 0.5}, 2)
|
|||
|
|
|
|||
|
|
p, ss, err := tensor.LevenbergMarquardt(residual, p0, tensor.LMOptions{})
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
fmt.Printf("A = %.4f, k = %.4f, sum of squares %.3e\n",
|
|||
|
|
p.FloatAt(0), p.FloatAt(1), ss)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`MinimiseLBFGS` with optional box walls, `Minimise` (Nelder-Mead),
|
|||
|
|
`MinimiseConstrained` for the linear rows, `MinimiseNonlinearConstrained`
|
|||
|
|
for constraint functions, `MinimiseDifferentialEvolution`,
|
|||
|
|
`MinimiseCMAES` and `MinimiseSimulatedAnnealing` for the multimodal
|
|||
|
|
landscapes, `MinimiseLinear` and `MinimiseQP` for the programs, and
|
|||
|
|
`FindRoot`, `FindRootNewton` and `FindRootSystem` for the roots. A
|
|||
|
|
solver that runs out of budget refuses rather than reporting a
|
|||
|
|
converged answer, and `AllowBudgetExit` opts into the best point
|
|||
|
|
instead; `MinimiseDifferentialEvolution` is the exception, returning
|
|||
|
|
its best generation without an error.
|
|||
|
|
|
|||
|
|
### Data in and out
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
dir, err := os.MkdirTemp("", "tensor-readme")
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
defer os.RemoveAll(dir)
|
|||
|
|
|
|||
|
|
scan, _ := tensor.FromFloats([]float64{1, 2, 3, 4}, 2, 2)
|
|||
|
|
path := filepath.Join(dir, "field.h5")
|
|||
|
|
err = tensor.SaveHDF5(path, []tensor.HDF5Dataset{{
|
|||
|
|
Path: "/scan/temperature",
|
|||
|
|
Values: scan,
|
|||
|
|
Attrs: map[string]string{"units": "K"},
|
|||
|
|
}}, nil, tensor.HDF5WriteOptions{Gzip: 6, Shuffle: true})
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sets, err := tensor.LoadHDF5(path)
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
fmt.Println(sets[0].Path, sets[0].Shape, sets[0].Attrs["units"], sets[0].Values)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`LoadCSV`/`SaveCSV` handle the tabular case, `LoadFITS`/`SaveFITS`
|
|||
|
|
images and `LoadFITSTable`/`SaveFITSTable` the table extensions,
|
|||
|
|
`LoadNetCDF`/`SaveNetCDF` the classic model, and `MapFloats`,
|
|||
|
|
`MapFloat32s` and `MapInts` open a native-endian file as a read-only
|
|||
|
|
array without reading it; `SaveNativeFloats` writes the float64 form
|
|||
|
|
`MapFloats` reads.
|
|||
|
|
|
|||
|
|
### Charts
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
wavelengths := make([]float64, 200)
|
|||
|
|
intensity := make([]float64, 200)
|
|||
|
|
for i := range wavelengths {
|
|||
|
|
wavelengths[i] = 400 + 2*float64(i)
|
|||
|
|
intensity[i] = 100 + 40*math.Exp(-math.Pow(wavelengths[i]-589, 2)/25)
|
|||
|
|
}
|
|||
|
|
xs, _ := tensor.FromFloats(wavelengths, 200)
|
|||
|
|
ys, _ := tensor.FromFloats(intensity, 200)
|
|||
|
|
series, _ := tensor.Line("sodium D line", xs, ys)
|
|||
|
|
chart := tensor.Chart{
|
|||
|
|
Title: "Absorption spectrum",
|
|||
|
|
XLabel: "wavelength [nm]", YLabel: "intensity",
|
|||
|
|
Series: []tensor.Series{series},
|
|||
|
|
}
|
|||
|
|
if err := chart.WriteSVG("spectrum.svg"); err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
fmt.Println("spectrum.svg written")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Deterministic SVG line charts: linear axes, five ticks each, one legend
|
|||
|
|
line per series, and a byte-identical file on every run, so a figure in
|
|||
|
|
a paper is compared exactly like any other computed number. The package
|
|||
|
|
is small by intent; it draws the figures, it does not stage a cinema.
|
|||
|
|
|
|||
|
|
### Automatic differentiation
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/grad"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
x, _ := grad.FromFloat64s([]float64{1, 2, 3}, true, 3)
|
|||
|
|
w, _ := grad.FromFloat64s([]float64{0.5, -1, 2}, true, 3)
|
|||
|
|
|
|||
|
|
prod, _ := x.Mul(w)
|
|||
|
|
sq, _ := prod.Pow(2)
|
|||
|
|
loss, _ := sq.Sum()
|
|||
|
|
if err := loss.Backward(); err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
// dL/dx = 2·x·w² and dL/dw = 2·x²·w, exact to rounding.
|
|||
|
|
fmt.Println(x.Grad(), w.Grad())
|
|||
|
|
|
|||
|
|
// Second-order questions come from the same graph: H·v in two
|
|||
|
|
// gradient evaluations, the tool Newton-CG scales on. The Hessian
|
|||
|
|
// of Σz² is 2·I, so H·x is 2·x.
|
|||
|
|
quadratic := func(z *grad.Tensor) (*grad.Tensor, error) {
|
|||
|
|
sq, err := z.Pow(2)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return sq.Sum()
|
|||
|
|
}
|
|||
|
|
hv, err := grad.HessianVectorProduct(quadratic, x, x, grad.HessianOptions{})
|
|||
|
|
if err != nil {
|
|||
|
|
panic(err)
|
|||
|
|
}
|
|||
|
|
fmt.Println(hv)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`MinimiseNewtonCG` minimises a graph function with truncated-CG Newton
|
|||
|
|
steps, `SampleHMC` runs Hamiltonian Monte Carlo on any differentiable
|
|||
|
|
unnormalised density, and `AdjointODE` differentiates an ODE solution
|
|||
|
|
at the cost of one extra solve. Complex graphs follow the Wirtinger
|
|||
|
|
convention, with `Real`, `Imag`, `Conj` and `Abs2` bridging into a real
|
|||
|
|
loss.
|
|||
|
|
|
|||
|
|
at the cost of one extra solve. Complex graphs follow the Wirtinger
|
|||
|
|
convention, with `Real`, `Imag`, `Conj` and `Abs2` bridging into a real
|
|||
|
|
loss.
|
|||
|
|
|
|||
|
|
### Where to look next
|
|||
|
|
|
|||
|
|
- The complete surface, package by package: [docs/API.md](docs/API.md).
|
|||
|
|
- Runnable end-to-end workflows, one directory each:
|
|||
|
|
[`examples/`](examples).
|
|||
|
|
- The package map and the data flow:
|
|||
|
|
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|||
|
|
- Building, testing, benchmarks and releases:
|
|||
|
|
[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) and
|
|||
|
|
[docs/BENCHMARKING.md](docs/BENCHMARKING.md).
|
|||
|
|
- The godoc comments in the source are the authority on signatures:
|
|||
|
|
`go doc -all sourcedock.dev/petrbalvin/tensor`.
|
|||
|
|
|
|||
|
|
## Development
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
just build # compile everything, examples included
|
|||
|
|
just test # the suite with the coverage floor
|
|||
|
|
just gates # the definition of done: build, fmt-check, vet, test, race
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
CI (Gitea Actions) enforces the same gates on every push to
|
|||
|
|
`development`, race excepted: the race detector is dispatched by hand.
|
|||
|
|
See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for the full workflow
|
|||
|
|
and [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute.
|
|||
|
|
|
|||
|
|
## Documentation
|
|||
|
|
|
|||
|
|
- [docs/API.md](docs/API.md): the exported API reference, per package
|
|||
|
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): the package map,
|
|||
|
|
data flow and design
|
|||
|
|
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): building, testing and
|
|||
|
|
releasing
|
|||
|
|
- [docs/BENCHMARKING.md](docs/BENCHMARKING.md): how performance is
|
|||
|
|
measured, and the reports
|
|||
|
|
- [CHANGELOG.md](CHANGELOG.md): release history
|
|||
|
|
|
|||
|
|
## Licence
|
|||
|
|
|
|||
|
|
MIT. See [LICENSE](LICENSE) for the text.
|
|||
|
|
|
|||
|
|
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|