Standard library Numerics NDArray

Math and Scientific Computing

klyn.math combines scalar mathematics, typed multidimensional arrays, random sampling, complex numbers, and exact rational values. Generic numeric types remain visible to the compiler; NDArray<Double> uses contiguous native storage for hot kernels, while other numeric specializations use compact primitive list storage where available, with boxed fallbacks.

Scalar Mathematics

import klyn.math

radius = 4.0
area = Math.PI * radius ** 2

assert Math.gcd(84, 30) == 6
assert Math.fact(5) == 120
assert Math.sqrt(81) == 9.0
assert Math.isClose(Math.sin(Math.PI / 2.0), 1.0)

print(area)

Math exposes E, PI, and TAU, plus roots, trigonometric and hyperbolic functions, logarithms, powers, rounding, angle conversion, and helpers such as gcd() and fact(). Integer inputs return a Double when the mathematical result is not constrained to an integer.

Compare Floating-Point Results

Use Math.isClose() for computed floating-point values. Exact equality remains appropriate for values that are expected to have identical representations.

assert Math.isClose(0.1 + 0.2, 0.3)
assert Math.isClose(0.0, 1e-12, abs_tol=1e-9)
assert not Math.isClose(1.0, 1.0001)

assert not Math.isClose(nan, nan)
assert Math.isClose(inf, inf)
assert not Math.isClose(inf, -inf)

Defaults are rel_tol=1e-9 and abs_tol=0.0. Both tolerances must be non-negative. NaN is close to no value, and an infinity is close only to the same infinity.

Create Typed NDArrays

The constructor copies values: a flat list creates a vector, and a rectangular list of rows creates a matrix. To allocate from dimensions, use zeros(), ones(), full(), or a random factory instead.

import klyn.math

vector = NDArray<Int>([2, 3])        # Values 2 and 3; shape [2].
grid = NDArray<Int>.zeros([2, 3])    # Six zeros; shape [2, 3].

matrix = NDArray<Double>([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0]
])

assert matrix.ndim == 2ul
assert matrix.size == 6ul
assert matrix[1, 2] == 6.0

zeros = NDArray<Double>.zeros([2, 3])
ones = NDArray<Double>.ones([2, 3])
filled = NDArray<Int>.full((2, 3), 7)
identity = NDArray<Double>.eye(3)

Constructors reject null lists, null rows, and ragged rows. Empty inputs are valid. The result owns its storage: changing the source list does not change the array. Factory dimensions must be non-negative; shapes and total-size overflow are validated before allocation. Use reshape(), flatten(), and t to obtain independent arrays without weakening the element type. These operations copy values; they are not views.

matrix = NDArray<Int>([[1, 2], [3, 4]])
value as Int = matrix.flat[2]       # 3: the last axis varies fastest.
matrix.flat[-1] = 42               # Updates matrix[1, 1], without copying.
snapshot = matrix.flatten()        # Independent NDArray<Int> of shape [4].
matrix.flat[0] = 99
assert snapshot.flat[0] == 1

flat[index] is a named indexed property returning the element type T. It reads or writes the original array in row-major order, at any rank, without allocating an intermediate view. Negative indices count backwards from the end; an invalid index raises IndexException. flat alone is not a first-class value or an iterator. By contrast, flatten() always returns an independent one-dimensional copy, including for an empty array; primitive values are copied in bulk without conversion. getFlat(index) and setFlat(index, value) remain available for compatibility.

Dimensions, strides and element counts use ULong. shape exposes an IList<ULong>; indices and axes use Long, including negative indices. Shape factories accept lists of Int, UInt, Long or ULong dimensions. Allocation is limited by the addressable storage and available memory, not a 32-bit element-count ceiling.

The constructor signatures accept IList<T> and IList<IList<T>>. When storing rows in a variable before construction, declare that nested interface type explicitly. Generic collections are invariant: a variable inferred as ArrayList<ArrayList<T>> is not interchangeable with IList<IList<T>>.

import klyn.collections
import klyn.math

rows as IList<IList<Int>> = [[1, 2], [3, 4]]
matrix = NDArray<Int>(rows)

Calculate with Arrays

left = NDArray<Double>([
    [1.0, 2.0],
    [3.0, 4.0]
])
right = NDArray<Double>.eye(2)

sum = left + right
product = left @ right

assert product.toString() == left.toString()
assert Math.isClose(left.mean(), 2.5)
assert left.mean(axis=0).toString() == "[2.0, 3.0]"
assert left.argMin() == 0
assert left.argMax() == 3

Arithmetic operators are element-wise. @ performs matrix multiplication. Reductions include sum(), min(), max(), mean(), argMin(), and argMax(); axis-aware operations validate dimensions and axis bounds explicitly.

argMin() and argMax() return a Long index. Their axis overloads return NDArray<Long>, not an array of values having the input element type.

In-place sorting

sort() and sort(axis) modify the array and return the same instance. Arithmetic and mathematical transforms return new arrays.

Multidimensional Slicing

matrix = NDArray<Float>([[1f, 2f, 3f], [4f, 5f, 6f]])
column = matrix[:, 1]       # [2, 5], shape [2].
row = matrix[-1, :]         # [4, 5, 6], shape [3].
reversed = matrix[::-1, ::-1]
empty = matrix[0:0, :]      # Shape [0, 3], size zero.
saved = matrix.copy()

Each axis accepts an integer or start:stop:step. Stops are exclusive, bounds clip to the axis length, and negative indices/steps count backwards. A zero step is an error. Integer indices remove their axis; slice axes keep it, including axes of length zero. Omitted trailing axes select their full extent.

Slices are independent, contiguous copies with the same numeric type, not shared views. Only the selected elements are copied. Changing a slice never changes its source, and changing the source never changes previously extracted columns. Complete integer coordinates such as cube[1, 2, 3] return a scalar.

Dimensions may be zero, but not negative. Empty tensors keep their shape and dtype; their sum is zero. Reductions requiring observations, such as a global mean or an extremum, raise ValueException when no observations are selected.

Centering and Scaling

import klyn.math.preprocessing

scaler = StandardScaler<Float>()
xTrainScaled = scaler.fitTransform(xTrain)
xTestScaled = scaler.transform(xTest)

# Alternatively, modify the original buffers without allocating another tensor.
scaler.fitTransform(xTrain, inPlace=true)
scaler.transform(xTest, inPlace=true)

original = scaler.inverseTransform(xTestScaled)

fit() learns one mean and population standard deviation per column from a finite, non-empty matrix. It keeps only the statistics, not the dataset. Always fit on training data and reuse transform() for validation/test data. Constant features use a divisor of one.

transform(), fitTransform() and inverseTransform() preserve the tensor precision and return a new independent tensor by default. With inPlace=true they modify and return exactly the input instance. In-place validation happens before the first write, including overflow checks; no full-size temporary tensor is used.

Use withMean=false to disable centering or withStd=false to disable scaling. Statistics use Double even for Float16 inputs; tensor storage remains in its declared precision. mean and scale expose immutable cached snapshots.

Array Statistics

Statistics are methods of NDArray; there is no competing set of global functions. mean(), variance(), standardDeviation(), median(), quantile(), percentile() and peakToPeak() return Double scalars for a complete array. Calculations use Double accumulation without changing Float16/Float/integer source storage. Conversion to Double can round large integer averages and interpolated quantiles; integer differences for moments and ranges are formed before that conversion.

import klyn.math

data = NDArray<Double>([1.0, 2.0, 3.0, 4.0])
assert data.mean() == 2.5
assert data.variance() == 1.25
assert Math.isClose(data.variance(ddof=1l), 5.0 / 3.0)
assert Math.isClose(data.standardDeviation(), Math.sqrt(1.25))
assert data.median() == 2.5
assert data.percentile(50.0) == 2.5
quartiles = data.quantile([0.25, 0.5, 0.75])

weights = NDArray<Double>([1.0, 1.0, 2.0, 2.0])
weightedMean = data.mean(weights=weights)

Variance divides by N - ddof: the default 0 describes a population; 1 estimates sample variance. Negative corrections, insufficient selected samples, and empty selections raise ValueException. Mean weights must be finite, non-negative Double values with positive total weight. They must match the input shape, or be a 1D vector with one weight per reduced position when axes are supplied.

Dimensions, Selection and Reusable Output

cube = NDArray<Double>([1, 2, 3, 4, 5, 6, 7, 8]).reshape([2, 2, 2])
means = cube.mean(axes=[0l, 2l])  # [3.5, 5.5]
lastAxis = cube.mean(axis=-1l)
destination = NDArray<Double>.zeros([1, 2, 1])
assert cube.mean(axes=[0l, 2l], keepDims=true, result=destination) is destination

missing = NDArray<Double>([nan, 1.0, 3.0])
assert missing.mean(ignoreNaN=true) == 2.0
mask = Boolean[3l]
mask[1l] = true
assert missing.mean(where=mask) == 1.0

Reductions support arbitrary array ranks, one axis or distinct axes, and negative axes counted from the end. Axes are interpreted in source order, regardless of the order supplied. keepDims=true retains reduced dimensions with length one. A tensor with all dimensions reduced has shape [1], not rank zero. sum, min and max retain the element type; argMin/argMax return Long indices flattening the reduced axes only. Masks are flat Boolean[] values with exactly one entry per input element; they are not implicitly broadcast. A supplied destination must have the exact result shape and may not alias the source or weights. Data-dependent failures can leave it partially written.

New statistical reductions propagate NaN unless ignoreNaN=true is supplied. Existing extrema retain their original first-value comparison behavior by default; use ignoreNaN=true to skip NaNs consistently. A sum with no selected elements is zero; other empty statistics raise ValueException. Infinite values are not treated as missing. Source arrays must not be mutated concurrently during statistics. A device-resident input is synchronized for CPU statistics; this is not a GPU statistics backend.

Exact Quantiles

Quantiles accept probabilities in [0,1]; percentiles accept percentages in [0,100]. QuantileMethod offers Linear (default), Lower, Higher, Nearest (ties to even index), and Midpoint, at rank q * (N - 1). A list of probabilities creates a leading output dimension. Exact selection never modifies the source and uses scratch proportional to the largest reduced slice, reused across output lanes. Selection is expected linear time with a bounded O(N log N) fallback, not a hidden full sort of the source.

Relationships and Histograms

x = NDArray<Double>([1, 2, 3])
y = NDArray<Double>([2, 4, 6])
assert x.covariance(y) == 2.0
assert Math.isClose(x.correlation(y), 1.0)
lags = x.correlate(y, CorrelationMode.Full)

counts, edges = x.histogram(bins=2l)
onlyEdges = x.histogramBinEdges(bins=2l)
joint, xEdges, yEdges = x.histogram2D(y, bins=2l)
points = NDArray<Double>([[0, 0], [0, 1], [1, 0], [1, 1]])
grid, coordinateEdges = points.histogramDD([2l, 2l])
frequencies = NDArray<Int>([0, 2, 2]).binCount()
classes = x.digitize(NDArray<Double>([0, 2, 4]))

Paired covariance/correlation use equally-sized 1D arrays of the same numeric type. Their matrix overloads accept 1D/2D arrays: rowVariables=true means rows are variables; false means columns are variables. Covariance defaults to ddof=1. Correlation rejects constant series and requires two observations. NaN removal is pairwise and does not guarantee a positive-semidefinite covariance matrix. correlate is direct 1D cross-correlation, O(N*M), with Valid (default), Same, or Full output; it does not silently switch to FFTs.

Histograms accept positive uniform bin counts or explicit strictly increasing finite edges. The final bin includes its upper endpoint; values outside the edges are excluded. Counts and weighted counts use Double; density=true normalizes the bin-width/volume integral, not the sum of heights. Histogram weights are finite Double values, with signed weights permitted. NaN observations raise unless explicitly ignored. histogramDD accepts a matrix shaped [observations, coordinates] without copying its columns. binCount requires non-negative integer 1D input, returns exact ULong counts (Double for its weighted overload), and allocates up to max(value)+1 bins. digitize accepts strictly ascending or descending boundaries and preserves the source shape in a Long index tensor.

Random Sampling

import klyn.math

roll = Random.randInt(1, 6)       # 1 through 6, both bounds included
temperature = Random.randRange(-5.0, 35.0)

names = ["Ada", "Grace", "Linus"]
selected = Random.choice(names)
Random.shuffle(names)

uniform = NDArray<Double>.random([4, 4])
normal = NDArray<Double>.randn([4, 4])

Integer ranges include both bounds. Floating-point randRange(min, max) uses a finite, half-open interval: min <= value < max, or returns min when both finite bounds are equal. NaN and infinite bounds are rejected. Large opposite-sign bounds are supported without overflowing their difference. choice() rejects an empty list, and shuffle() changes a mutable List in place.

Not for secrets

Random is for simulations, sampling, and application behavior. Use SecureRandom from klyn.cryptography for keys, tokens, salts, or any security-sensitive value.

Complex and Rational Values

Complex numbers
import klyn.math

z = Complex(3.0, 4.0)
conjugate = z.conjugate()

assert z.magnitude() == 5.0
assert conjugate.real == 3.0
assert conjugate.imag == -4.0
assert z.toString() == "3.0 + 4.0j"
print(z.phase())
Exact rational numbers
import klyn.math

oneThird = Rational(1, 3)
oneSixth = Rational(1, 6)
result = oneThird + oneSixth

assert result == Rational(1, 2)
assert result.hash() == Rational(2, 4).hash()

Rational compares equivalent fractions by value, rejects a zero denominator, and implements a hash consistent with equality. Call simplify() when the stored numerator and denominator must be reduced. Complex<Float> can be used when float-sized components are required; Complex<Double> is the default. Each component follows the Float or Double display rules, with the shortest round-trip digits and .0 on integral mantissas, for example 3.0 - 4.0j. Small and large components use scientific notation independently: Complex(3_000_000_000.0, 1.0) displays as 3.0e+9 + 1.0j, without narrowing large values to an integer type. Signed zero is preserved in both components: Complex(0.0, -0.0) displays as 0.0 - 0.0j.

Deep Learning

Neural networks and their activation functions are covered in Deep Learning. That chapter also explains training, numeric precision and execution devices.

Numerical Performance Guidelines

  • Choose the element type once and keep it stable through a calculation.
  • Prefer NDArray operators and transforms over repeatedly converting values to Object.
  • Allocate reusable arrays outside hot loops when the algorithm permits it.
  • Use toList() only at an API boundary; it copies every element.
  • Benchmark optimized Klyn execution rather than drawing conclusions from a --clean compile.