Indexing that tracks shapes¶
a[0] # first axis dropped
a[:, 1] # mixed int and slice
a[1:3] # slice bounds compute the length (via folding: H*2 works too)
a[::-1] # reversed, same shape
a[None, :] # new axis of size 1
a[..., 1] # ellipsis fills the middle
a[5] # error: index 5 is out of range for axis 0 with size 2
a[::0] # error: slice step is zero
a[..., Slots.X] # IntEnum members index by their value
a[:, COL] # a Final slice(Slots.X, Slots.Y + 1) object, same thing
a[Slots.VX] # error: index 2 is out of range for axis 0 with size 2
Slicing past the end clips like NumPy (a[1:99] on a size-2 axis is
size 1); a 1-D boolean mask turns its axis into a fresh unknown
(a[np.ones(3, dtype=bool)] on (3, 4) is (?U0, 4), see unknown dims), and unknown bounds bail out. An n-D mask consumes n axes for
one count (a[a > 0] on (3, 4) is (?U0,), numpy returns 1-D), and a
mask whose length provably mismatches its axis errors with NumPy's
wording. A full slice on a
symbolic axis keeps the TypeVar (a[:, :] on (N, K) stays (N, K));
a slice bounded by a literal on one (a[:, :3]) bails out, since numpy
clips a bound past the end. Indexing every axis
with integers yields a scalar, which the plugin leaves to the stubs.
Iterating and unpacking¶
for row in m and xs, ys = points.T give items with the first axis
removed: a row of F64[3, 2] is (2,), and the two halves of a
(2, ?U) array are (?U,) each. With a literal first axis, an
unpacking into another number of names is an error (a, b =
np.zeros((3, 2)), tests/fixtures/array_unpack.py). A rank-1 array
gives scalars, which stay with the stubs.
Bounds over int variables¶
A bound that is a formula over int variables is read as one: a
variable bound once is ?name, a variable assigned once from such a
formula reads it, len(x) and x.shape[i] read the size of x, and
+, - and a literal factor combine them. With out: F64[N] and
k: int:
n_windows = len(values) - k + 1
values[:n_windows] # (N-?k+1,) printed -?k+N+1
values[shift : shift + n_windows] # (N-?k+1,)
out[k - 1 :] # (N-?k+1,)
out[k:] # (N-?k,)
This is the size when the bounds are inside the axis; numpy clips them
otherwise (x[i : i + bs] on the last batch). So it is given when an
int variable is left in it, and such a size is an unknown: nothing
is proved from it with numty-names off. Under the numty-names code
(on by default) two
of them a constant apart are reported, out[k:] = mins as -?k+N+1 and
-?k+N are 1 apart (tests/fixtures/symbolic_slice.py).
A bound over names alone is given when it is inside the axis (issue
333): with n = X.shape[1] of size M and coef: F64["M+1"],
coef[:n] is (M,) (the intercept split of a linear model), and so is
coef[: coef.shape[0] - 1]. As for x[1:], the axis is assumed long
enough for a constant (x[: len(x) - 1] is (N-1,)), but one name is
never assumed larger than another: x[:n] with n of size N on an
M axis is unknown. A step that is a size gives the exact count when it
divides the axis (r[:, ::f] of (c, f*f) is (c, f)), or when the
axis is the square of the step less one: the flat diagonal
m.reshape(c, -1)[:, :: f + 1] of an (f, f) block is (c, f)
(tests/fixtures/index_kept_names_ok.py).
A slice object held in a variable is read like the slice it was built
from when the variable is bound once to slice(...)
(head = slice(0, X.shape[0] - 1) makes X[head] (N-1, M)); any
other slice object (a parameter, rows = slice(bs * i, bs * (i + 1)))
keeps its axis with an unknown size, (?U, M). Like a mask variable, a
slice variable bound once takes one count from every axis of one size:
with sl = slice(first, last), fps[sl] and fns[sl] of two (N,)
arrays share it, so both fit one free name K in a returned tuple.
Fancy indexing¶
Integer arrays index fancy-style: the index shape replaces the axis,
broadcast when there are several, and an advanced block split by a
slice moves to the front as NumPy does (x[0, :, idx] on (2, 3, 4)
with idx: (5,) is (5, 3)); take_along_axis broadcasts the two
shapes, with the index winning along the axis (even a size-1 index:
take_along_axis(a, idx, axis=1) with a: (3, 4), idx: (3, 1) is
typed (3, 1), as numpy returns):
table[ids] # (5, 4): index shape + trailing dims
b[np.arange(5), actor] # (5,): broadcast (5,) + (5,)
np.take_along_axis(a, idx, axis=1) # broadcast shape, arr's dtype
b[np.zeros(3)] # error: integer (or boolean) type required
np.take_along_axis(a, idx2d, axis=1) # error: same number of dimensions
range(k) indexes like np.arange(k) (issue 333): u[range(u.shape[0]),
j] is (M,), its positions are on M, and u[range(u.shape[0] + 1)]
is the same certain overrun as the arange spelling.
Index positions: Index[P, N]¶
Integer arrays can also carry which axis their values index into,
not just how many entries they hold. Index[P, N] spells positions on
size P, N of them (Index[P] leaves the count dynamic, Index["P N"]
splits a string the same way); argmin/argmax, argsort,
arange, flatnonzero, nonzero and searchsorted record the same
fact themselves when the values are certainly positions. A recorded
size above the axis is an error by default only when the overrun is
certain: the index holds every position below its size, as arange(n)
and argsort do. For searchsorted, argmax, flatnonzero and an
Index[...] annotation the size is an upper bound on the values, so
the overrun is only possible, and that report is under the
[numty-names] code (on by default; disable_error_code = numty-names
turns it off), like a different named size. A variable rebound between uses loses its
record (idx = searchsorted(...); idx = clip(idx, ...) checks clean),
and so does a call wrapped in another one where it is assigned
(idx = np.clip(np.searchsorted(a, v), 0, len(a) - 1),
tests/fixtures/index_clip_ok.py). An index assigned in both branches
of an if is checked for each branch
(tests/fixtures/union_index_optin.py),
and a same-module plain call checks the actual's record against the
formal's Index under the same code:
table[row, col] # clean: positions on the right axes
table[col, row] # error (numty-names): Q on P, P on Q
small[np.arange(len(small) + 1)] # error: every position 0..N occurs
plain[np.searchsorted(a, v)] # error (numty-names): they may run past the axis
padded[np.searchsorted(a, v)] # clean: the axis spells N+1
a[np.searchsorted(a, v)] is wrong only when a value is above a[-1].
Code that fixes the positions in place (idx[idx == len(a)] = len(a) - 1,
np.clip(idx, 0, len(a) - 1, out=idx)), guards them, or looks up
values that are in a is right, which is why this is not a default
error (tests/fixtures/bad_index_overrun.py).
An Index spelled through an alias (R = Index[P, N]), a call from
another module, a method call, or keyword arguments keeps today's
behavior (silent); like every other record, value-preserving reshapes
and arithmetic do not carry it. A selection does carry it: rows[known],
rows[1:] and rows[order] hold fewer positions, or the same ones in
another order, on the size rows holds them on, so
table[cols[known], rows[known]] is reported like table[cols, rows]
(tests/fixtures/index_selection.py). A tuple target gives each name
the record of its own item: after r, c = np.array([...]),
np.arange(5) only c holds positions on 5, although both are (5,)
and the records are kept per line (issue 333).
Indexed assignment¶
Indexed assignment checks the write: the target solves like a read
(out-of-range and mask-length errors fire), and the value must
broadcast one-directionally into it (m[:, 0] = col with (5,) <- (4,)
errors; a[:] = v with (5, 3) <- (3,) stays clean, scalars always
fit). Symbolic sizes are compared by the rule of a broadcast: out[i] =
v[:-1] with out: F64[S, C] and v: F64[C] is cannot assign value of
shape (C-1,) into target of shape (C,). Two different names (S into
C) and a mask's count against a name are not provable either: they
are [numty-names] reports (on by default), silent with that code off.
Other unknowns and an int size stay silent
(tests/fixtures/bad_setitem_symbolic.py). An int axis is skipped on
its own, not the whole write: in a buffer a = np.empty((x.shape[0], n))
of shape (N, int), a[:, -1] = np.ones(x.shape[0] + 1) is cannot
assign value of shape (N+1,) into target of shape (N,)
(tests/fixtures/bad_setitem_int_axis.py).
The dtype of the value is checked under the opt-in numty-cast code
(enable_error_code = numty-cast). numpy casts an indexed assignment
with casting="unsafe", so rows[:] = times with rows: I64[N] and
times: F32[N] truncates the floats with no error at run time; with
the code on it is assignment casts float32 into int64 (values are
truncated); use .astype(...) if this is meant. Integer into float,
Python scalars that fit and unknown dtypes stay silent, and
.astype(...) on the value says the cast is meant (see
Error codes).