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.
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.
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
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())
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
--cleancompile.