Supported operations¶
What the solver follows, family by family. Anything not listed keeps NumPy's own stub type: unknown is never an error.
Construction¶
zeros_like keeps shape and dtype, full((2, 3), 5.0)
is a known 2x3 float64, plus eye(3), arange(6), and atleast_2d
promoting (N,) to (1, N). np.arange with a step of 1 has
stop - start entries for a literal start, with the stop folded like a
builder's size: np.arange(m + 1) is (M+1,) and np.arange(1, m + 1)
is (M,), as x[1:] is (N-1,). A builder keeps its shape when the
dtype= it is given is Any (np.full((n, k), np.nan,
dtype=other.dtype) with a bare np.ndarray is (int, int) with dtype
Any). np.eye(n) and np.identity(n) read their size like a
builder: with n = X.shape[1] both are (D, D), np.eye(k + 1) is
(D+1, D+1), and np.eye(n, m, k=1) is (n, m) (the offset never
resizes).
Joining¶
concatenate adds dims on the axis, stack adds the
stacked axis, vstack/hstack/dstack/column_stack promote then
concatenate — including enum-driven stacks; mismatches error. Lists
built at runtime whose items share one shape (typed lists, list
comprehensions) leave only the joined axis unknown. Items typed as a
union of arrays of one rank join too: np.hstack([l.ravel() for l in
(W, b)]) is (?U,). An axis the join does not grow keeps a name one
item has there against an int or unknown size, since the items must
agree: np.hstack of two (N, N, ?U) arrays is (N, 2*N, ?U). Two
unknowns, or two different names (a [numty-names] report), make it
unknown.
Reshape¶
Conserves the total with symbolic sizes and infers -1
((N, 10, 2) to (N, 20) is fine, (N, 10, 2) to (N, 21) errors,
(N, 10) to (M, 10) bails). Over *Ts the spelled target is the
whole new shape ((*Ts, 2, 3) to (6,); a -1 that would absorb
*Ts, or a total no *Ts can absorb, bails or errors respectively).
A target spelled in literals and names, with no -1, is the result
whatever the input's size, since numpy raises otherwise: from a ?U
slice (np.reshape(p[k:k + n * h], (n, h))), from an int input, and
to a tuple-typed value (x.reshape(W.shape)) it is (N, H) for n: N,
h: H, in the method form (p.reshape(n, h)) as in the function form.
Rank changes¶
transpose/.T (which bail out over *Ts: no
annotation can spell its reversal), squeeze, expand_dims; bad
permutations, repeated axes and non-1 squeeze axes error.
Reductions¶
sum(axis=1) (plus median and every nan* twin:
nansum, nanmean, nanargmin, …), average (1-D weights must match
the averaged axis; weights: F64[N] | None is checked on its array
member, and named sizes that differ are numty-names), diff (concrete lengths subtract, bad axes and
negative orders error), cumsum/cumprod (shape-preserving).
Matmul family¶
matmul solves inner dims and batch-broadcasts
through shared *Ts (a lone concrete 1-D/2-D operand contributes an
empty batch). The rest solves through the same inner-dim machinery:
dot (no batch broadcast past rank 2), tensordot (int or pair-of-lists
axes), inner/outer/vdot (scalars included; outer ravels each
operand, so (N,) by (T,) is (N, T) and an (N, D) operand counts
D*N), and take (the axis is
swapped for the index shape; literal indices past the axis error unless
mode clips or wraps). matmul on a scalar or 0-d operand errors, and
linalg.norm with an explicit ord needs one or two reduced axes.
Diagonals and covariance¶
np.diag of a vector (N,) is the square (N, N) ((N+2, N+2) with
k=2); of a matrix it is the diagonal: (N,) for a square (N, N),
the exact count for literal sizes, and a ?U for two different names
(numpy's min(N, M)) or an offset on a symbolic size. np.diagonal
and .diagonal() drop axis1/axis2 and append the diagonal, so
np.diagonal(P, axis1=1, axis2=2) of (C, D, D) is (C, D); an axis
out of range or the same axis twice is an error. np.cov(m) reads rows
as variables: (D, N) gives (D, D), rowvar=False reads columns
((N, D) gives (D, D)), a vector is one variable (()), and the
dtype is result_type(m, float64) (float32 input gives float64).
Indexing¶
Consumes leading dims without an ellipsis (v[0],
v[None, ...]). Comparisons broadcast and return bool arrays; argmax
drops the axis, sort keeps the shape, and a literal partition kth
past the axis errors.
Shape math¶
flip/fliplr/flipud/roll preserve (with rank floors
and axis checks; flip rejects a repeated axis, roll a shift that does
not match its axes), rot90 swaps dims on odd turns (equal axes error),
gradient keeps the shape for one resolved axis, convolve/correlate
compute full/valid/same lengths (empty operands error). The fft family tracks complex dtypes — rfft halves (N//2+1),
fftn/rfftn apply s over axes with trailing alignment; bad axes and
non-positive sizes error.
Data prep¶
tile multiplies reps, repeat scales the axis
(flattening to one dim for axis=None; a count list of several entries
must have one per element and literal counts sum to the new size; an
array of counts gives a plain int, a size that is not followed, and a
scalar count that is not a literal gives ?U), pad
adds widths per axis, split returns a list of equal parts (?U for
uneven index splits); negative counts and uneven divisions error.
Selection¶
unique keeps the dtype with a ?U length (rank kept for
axis=). With literal return_index/return_inverse/return_counts
it is numpy's tuple: the index and counts arrays are int64 and share
the values' ?U, and the inverse has the input's shape (its axis length
with axis=). count_nonzero(a, axis=...) drops the axes like
a.sum(axis=...) (keeps them as 1 with keepdims=True) in int64; a
full count stays numpy's scalar. nonzero/where with one argument return per-dim ?U tuples
(flatnonzero flattens, argwhere pairs), isin keeps the input shape
with a bool dtype, searchsorted follows the values shape; 0-d nonzero
errors. histogram returns (counts, edges) with one edge more than
counts: bins=16 gives (16,) and (17,), an array of B edges gives
(B-1,) and (B,). The counts are int64, and the edges have the dtype
of the edges array, or of the data for a count of bins (float64 for
integer data). split and array_split at a 1-D array of positions
return a list of pieces with a ?U size on the axis. bincount(x,
weights=w) needs one weight per entry of x: provably different
lengths are an error, two different names a [numty-names] report.
Arrays from lists built at runtime¶
np.array of a list
comprehension or of a list[...] value has one ?U axis in front:
np.array([i == 1 for i in kinds]) is (?U,) bool, and a list of
F64[N] items is (?U, N), as for np.stack. An unfiltered
comprehension with one for over a known size has that many items, in
np.array and np.stack: over range(3) it is 3, over the rows of an
(N, M) array, zip of such arrays or enumerate of one it is N,
over a tuple of two arrays 2. A filter (if), a second for, or an
int length (range(n) with n: int) leave the ?U. np.array([a, b]) of
two arrays of one shape is (2, *shape). The dtype is the one the call
names, else bool for bool items (a display too: np.array([True])
is (1,) bool), else the item arrays' dtype; for
int and float items it is left to numpy's stubs, because a
list[int] may hold bools and a list[float] may hold ints. A builder
whose sizes are all runtime ints keeps the dtype it is given
(np.zeros((n, k), dtype=bool), np.full((n, k), np.nan)), and
np.broadcast_to(x, (n, len(x))) has the shape it is asked for
(tests/fixtures/coverage_batch_ok.py).
A display whose items are numpy scalars has the display's length:
np.array([v[0], v[1], 0.0]) is (3,). numpy promotes the items as
arrays, so a Python float counts as float64 there
(np.array([np.float32(1), 0.0]) is float64). The dtype of a value typed
float or int is claimed only next to a float64 or complex128 item,
where what it really holds cannot change the answer. Rows written as
tuples of scalars give the width: np.array(rows) for rows:
list[tuple[float, float, float]] is (?U, 3), and a display of such
tuples is (len, 3). An item typed Any counts as one value next to
an item that is certainly a scalar: were it an array, numpy would
refuse the ragged display (setting an array element with a sequence).
So np.array([a, v[0]]) with a: Any is (2,), and a row (f, x, y)
with x, y: Any has width 3. An item that indexes every axis of an
array with an integer (ball["x"][row]) is one value whatever the
array's dtype. A display of Any items alone keeps numpy's own answer,
with the dtype the call names (dtype=float as well as
dtype=np.float64), tests/fixtures/array_any_items.py.
Functions held in a variable¶
A numpy function bound to a name is solved by its own rule: op =
np.nansum if flag else np.sum, then op(a, axis=0) is (N,) for an
(M, N) array, as is a module-level alias (total = np.sum). Ufuncs
in a variable (f = np.exp) were already followed.
Masked arrays¶
np.ma.masked_array(data, mask=...) (and np.ma.array) has its data's
shape and dtype. A MaskedArray goes through the ndarray rules and
stays masked: sm.min(axis=1) of an (N, M) masked array is a
MaskedArray of (N,), .reshape((-1, 1)) is (N, 1), .T swaps,
np.sum(sm, axis=0) and a comparison with a plain array broadcast and
check like ndarray's. .filled() keeps numpy's own answer, a plain
array of the same shape.
Ufunc methods¶
reduce drops the axis, accumulate keeps the shape, outer joins the
two shapes, and reduceat has one entry per index, whatever kind of size
the index array has (I64[K], I64[int]). maximum, minimum, fmax
and fmin keep the operand's dtype; add and multiply keep a float or
complex operand and widen a bool or signed integer to int64 and an
unsigned one to uint64; the logical_* ufuncs give bool
(tests/fixtures/coverage_batch_2.py).
Symbolic arithmetic¶
Dims stay exact through arithmetic (N*2, N+2, N//2+1, C*H*W).
See Symbolic arithmetic.
polars and numba¶
See polars and numba.
Elementwise, einsum, linalg, random¶
Operators and bare np.*
ufuncs alike — broadcast shapes and promote dtypes. In-place ops
(+=, -=, …) reuse the broadcast check and additionally require the
result to fit the target ((3,) += (2, 3) errors). With symbolic sizes
the result must be the target too: a size over names that grows a
1 axis ((N, 1, M) *= (N, N, 1)) and literal sizes that set one name
to two values ((d, d) += (2, 3)) are errors, while a literal against
one name ((d, d) += (3, 3)) and an int or unknown axis stay
silent, and two different names are a [numty-names] report. Promotion matches
NumPy (np.result_type over the full dtype matrix, plus weak Python
and numpy scalars; a size taken from a shape, X.shape[0] or
len(X), is a Python int, so X / len(X) keeps X's shape): float ufuncs pick the smallest float (sqrt(uint8)
is float16), true division of ints is float64 (f32 / u8 stays
float32), rng(..., dtype=np.float32) is honored, clip promotes
across all operands, linalg.solve maps ints to float64 and rejects
float16, and reductions keep NumPy dtypes (sum of ints is int64,
mean is float64, complex std is real).
einsum parses the subscript string (explicit, implicit,
ellipsis, diagonals) and linalg checks squareness and inner dims while
tracking dtypes (eigh stays real, svd shrinks with
full_matrices=False). svd and qr of a symbolic (M, N) matrix
(stacked ones too) give K = min(M, N) as one fresh unknown per call,
shared by the factors: qr (reduced) is (M, ?K) and (?K, N), svd
is (M, ?K), (?K,), (?K, N) with full_matrices=False and (M, M),
(?K,), (N, N) with the default; a square (D, D) input is exact.
The result keeps its NamedTuple fields (.U, .S, .Q), typed by the
stubs (tests/fixtures/svd_qr_symbolic_ok.py). linalg.cross takes batches of vectors of any
rank, where NumPy's stubs stop at 2-D. np.round and np.around keep
the shape and dtype; a bool array comes back float16, and rounding
one to decimals other than 0, which NumPy refuses, is typed Any
(numty reports shape errors, not dtype errors). Generator random methods solve from size=. Tuple and list displays keep per-item precision. An explicit
dtype= you name is used as-is when it resolves; anything exotic bails
out instead of guessing.