ndarray
Our own numpy-flavored N-dimensional array, generic over its element type, with full NumPy broadcasting.
The array is basic_ndarray<T> for any Field element type T — a real arithmetic type (int or float family) or a std::complex of a floating type, so complex matrices/vectors (and the complex eigenvalues a real matrix can have) are first-class. The element type is deduced from the literals — array([1, 2, 3]) is an integer array, array([1.0, …]) is a double array. NDArray is the default basic_ndarray<double>. A complex array prints element-wise Python-style, e.g. [0+1j, 0-1j]. (Ordering-dependent ops like arange, and mean which returns a double, stay real-only.)
Elements live in a shared buffer (shared_ptr<vector<T>>); an array is a VIEW into it — {shape, strides, offset} — so reshape and broadcast are zero-copy (a stretched dimension just gets stride 0). Shared ownership keeps it memory-safe.
Element-wise ops vectorize declaratively: a contiguous fast path uses std::transform(std::execution::unseq, …) (and sum uses std::reduce(unseq)), so the compiler emits SIMD for any T; broadcast/strided views fall back to a correct C-order walk.
Usage
import io
import ndarray
let a = ndarray.array([1.0, 2.0, 3.0]) # 1-D
let m = ndarray.array([[1.0, 2.0], [3.0, 4.0]]) # 2-D, shape read off the nesting
let t = ndarray.array([[[1.0], [2.0]], [[3.0], [4.0]]]) # 3-D — nests to any depth
let c = ndarray.add(m, ndarray.scalar(10.0)) # broadcasts the scalar
io.print(ndarray.to_string(c))An ndarray is genuinely N-dimensional: array(...) infers the shape from a nested list to any rank (a ragged list is rejected, as in numpy), and broadcasting/reductions work at every rank — reshape is the other way to set a shape.
import ndarray includes ndarray.hpp and links libcheatah_ndarray.
API
Factories
array(values)— array from a list; a nested list builds an N-D array (array([[1,2],[3,4]])is 2-D), with the shape inferred from the nesting.scalar(value)— 0-D array (broadcasts to anything).zeros(shape)/ones(shape)/full(shape, value)— filled arrays.zeros_like(a)/ones_like(a)/full_like(a, value)— filled arrays with the same shape and element type asa(numpy's*_likefamily).arange(start, stop, step)— 1-D range, like Pythonrange.reshape(a, shape)— same data, new shape (C-order).a.astype(dtype)— convert the element type (numpy'sa.astype), e.g.array([1,2,3]).astype(i16). This is how you build a narrow-element array for a smaller memory footprint (i8…u64/f32/f64): the result is a realbasic_ndarray<int16_t>(2 bytes/element, not 8). Widening is exact; narrowing truncates at the target width (like a numpy fixed dtype). A declared narrow type drives it too —let a: ndarray<i8> = array([…])converts for you.
Broadcasting
broadcast_shapes(a, b)— the NumPy result shape of two shapes.broadcast_to(a, target)— a zero-copy view ofastretched totarget.
Element-wise ops (broadcasting)
add/sub/mul/divide—aopbover the common shape.
Hot-loop notes (all selected automatically, nothing to call): infix a + b etc. are the same functions; an expiring operand's buffer is reused in place (a + b + c allocates once, not twice); and each op also has an allocation-free out-parameter overload (add(out, a, b)) in ndarray.hpp for buffer-reuse loops.
Element-wise math (numpy-style ufuncs)
The array forms of the scalar math module: where math.sqrt(x) takes one number, ndarray.sqrt(a) applies the function to every element of an array, SIMD-vectorized on the contiguous fast path:
sqrt/cbrt/exp/log— roots, exponential, natural log.sin/cos/tan— trigonometric (radians).abs— absolute value.
io.print(ndarray.to_string(ndarray.sqrt(ndarray.array([1.0, 4.0, 9.0])))) # [1, 2, 3]Complex
complex(re, im)— build a complex array from real & imaginary parts (broadcasts).real(a)/imag(a)— the real / imaginary parts as a real array.conj(a)— element-wise complex conjugate (identity on a real array).
let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))
io.print(ndarray.to_string(z)) # [0+1j, 2-3j]
io.print(ndarray.to_string(ndarray.conj(z))) # [0-1j, 2+3j]Reductions, access, display
sum(a)/mean(a)— reduce all elements.get(a, index)— read one element (bounds-checked).shape_of(a)/size_of(a)— query dimensions / element count.to_string(a)— nested-bracket text, e.g."[[1, 2], [3, 4]]".
The NDArray class exposes shape(), strides(), ndim(), size(), at(index), buffer(), and offset().
Negative dims/indices and size-overflowing shapes throw rather than corrupting memory.
Performance vs NumPy
The element-wise math ufuncs are benchmarked against NumPy's vectorized equivalents (same fixed-seed array to both, op run many times, results cross-checked) by scripts/numpy_compare.py. Each function's Performance row above carries its own number. The table below is generated by that harness — see docs/performance.md for the methodology (striated rounds, medians, paired ratios) and the reference machine. The band column is the range of the per-round ratios, which on the large-array rows is wide enough to matter: a bare headline would hide that sqrt at 16384 lands anywhere from 0.6× to 1.1× depending on the round.
op | operand dimensions | cheatah | NumPy | winner | band |
|---|---|---|---|---|---|
| 64 | 0.09 | 0.35 | cheatah 3.9x | 3.48-4.12 |
| 16384 | 11.85 | 11.89 | cheatah 1.0x | 0.83-1.03 |
| 16384 | 10.66 | 46.94 | cheatah 4.4x | 3.61-4.66 |
| 16384 | 11.45 | 95.75 | cheatah 8.5x | 6.31-9.39 |
| 16384 | 2.85 | 3.07 | cheatah 1.1x | 0.94-1.40 |
| 16384 | 3.43 | 9.62 | cheatah 2.6x | 1.71-2.97 |
exp/sin route their contiguous-double case through glibc's libmvec vector math (_ZGVdN4v_exp, …) compiled with -fveclib=libmvec -fno-math-errno — without -ffast-math, so results stay strictly IEEE — and so beat NumPy ≈4–7× at 16384 elements. sqrt is memory-bandwidth-bound (wins small, ties large). The plain element-wise ops like add are bandwidth-bound too: their result buffer is allocated uninitialized (no throwaway zero-fill before the overwrite), so they read once and write once — matching or beating NumPy. See the Performance guide for the single-core-by-design rationale.
Per-function docs (parameters, complexity, heap behavior) are in ndarray.hpp. Tested in ../tests/ndarray_test.cpp; ASan + Valgrind clean via the QA gate (security/run-valgrind.sh).
Classes
basic_ndarray— An N-dimensional array ofT(a Field element type — real or complex): a view ({shape, strides, offset}) over a shared element buffer.
Functions
std::vector< std::size_t > broadcast_shapes(const std::vector< std::size_t > &a, const std::vector< std::size_t > &b)
source#
The broadcast result shape of two shapes (NumPy rules).
Aligns the shapes from the trailing (rightmost) dimension, treating missing leading dims as 1; each output dim is the non-1 input dim, and two unequal dims that are both not 1 are incompatible and throw.
a | first shape. |
b | second shape. |
the broadcast shape (trailing-aligned).
O(max(ndim)).
allocates the small result vector; throws if the shapes are incompatible.
CheatahNDArray.BroadcastShapeRulesStdlibE2E.NdarrayThe integer position an Subscript addresses.
For a scoped enum this is its underlying ordinal; this static_cast is the whole of the enum-to-index conversion the language sanctions.
Ix | the subscript type: an integer, or a scoped |
i | the subscript to resolve. |
the integer position it names (a scoped enum's underlying ordinal).
O(1).
none.
Whether a is a contiguous C-order block (no broadcast/stride-0/permuted view), so its elements live consecutively from offset() and can be walked flatly.
a | the array (or view) to test. |
true if a's strides are the C-order strides for its shape.
O(ndim).
none.
CheatahNDArray.BroadcastingAddStdlibE2E.Ndarraybasic_ndarray< T > broadcast_to(const basic_ndarray< T > &a, const std::vector< std::size_t > &target)
source#
A zero-copy view of a stretched to target (size-1 / missing dims get stride 0).
a | source array. |
target | the shape to stretch to. |
a VIEW sharing a's buffer (no element copy).
O(rank of target).
no element copy — only the view's stride vector; throws if the shapes are not broadcast-compatible.
CheatahNDArray.BroadcastToStdlibE2E.Ndarrayarray · 4 overloads
basic_ndarray< T > array(const std::vector< T > &values)source#basic_ndarray< T > array(std::vector< T > &&values)source#basic_ndarray< detail::nested_scalar_t< V > > array(const std::vector< V > &values)source#basic_ndarray< T > array(std::initializer_list< T > values)source#1-D array from a list of values; the element type is the list's element type (array([1,2,3]) is integer, array([1.0,…]) is double).
values | the elements, copied into a fresh contiguous buffer. |
a contiguous 1-D basic_ndarray<T>.
O(n).
allocates a new buffer of values.size() elements.
NdarrayCompileRun.ArrayStdlibE2E.Ndarray SystemApps.LinearSolve0-D scalar array (broadcasts to anything); element type deduced from value.
value | the single element. |
a 0-d basic_ndarray<T>.
O(1).
allocates a one-element buffer.
NdarrayCompileRun.ScalarStdlibE2E.NdarrayArray of shape filled with 0 (a double array by default; rejects negatives).
shape | the dimensions (signed; throws on a negative). |
a zero-filled NDArray.
O(size).
allocates a new buffer; throws on negative/overflowing dims.
NdarrayCompileRun.ZerosStdlibE2E.NdarrayArray of shape filled with 1 (a double array by default; rejects negatives).
shape | the dimensions (signed; throws on a negative). |
a one-filled NDArray.
O(size).
allocates a new buffer; throws on negative/overflowing dims.
NdarrayCompileRun.OnesStdlibE2E.NdarrayArray of shape filled with value; element type deduced from value.
shape | the dimensions (signed; throws on a negative). |
value | the fill value (its type is the array's element type). |
a filled basic_ndarray<T>.
O(size).
allocates a new buffer; throws on negative/overflowing dims.
NdarrayCompileRun.FullStdlibE2E.NdarrayA fresh array with the SAME shape and element type as a, filled with value (≈ numpy.full_like).
The companion zeros_like / ones_like default the fill.
a | the array whose shape and element type to mirror. |
value | the fill value. |
a same-shape basic_ndarray<T> filled with value.
O(size).
allocates a new buffer.
CheatahNDArray.LikeFactoriesA zero-filled array with the SAME shape and element type as a (≈ numpy.zeros_like) — the idiomatic way to allocate a matching gradient/velocity/scratch buffer for an existing array.
a | the array whose shape and element type to mirror. |
a same-shape basic_ndarray<T> of zeros.
O(size).
allocates a new buffer.
CheatahNDArray.LikeFactoriesA one-filled array with the SAME shape and element type as a (≈ numpy.ones_like).
a | the array whose shape and element type to mirror. |
a same-shape basic_ndarray<T> of ones.
O(size).
allocates a new buffer.
CheatahNDArray.LikeFactories1-D range [start, stop) stepping by step; element type deduced from the args.
start | first value. |
stop | exclusive bound. |
step | increment (throws if zero); a step pointing away from |
a 1-D basic_ndarray<T> of the generated values.
O(count).
allocates a new buffer (built via a growing temporary vector, then copied); throws if step is zero.
CheatahNDArray.ArangeNdarrayCompileRun.ArangeStdlibE2E.Ndarraybasic_ndarray< T > reshape(const basic_ndarray< T > &a, const std::vector< long long > &shape)
source#
Reshape a to shape (same element count); reads in C-order so views/broadcasts are flattened into a fresh contiguous buffer (a copy, not an alias).
a | source array. |
shape | the new dimensions (signed; throws on a negative). |
a new contiguous basic_ndarray<T> with the data in C-order.
O(size).
allocates a new buffer; throws on size mismatch or negative dims.
NdarrayCompileRun.ReshapeStdlibE2E.Ndarray SystemApps.LinearSolveConvert a to a new array with element type U — numpy's a.astype(dtype).
Every element is static_cast into U, so this is the way to build a NARROW-element array (a smaller memory footprint): array([1,2,3]).astype(i16) is a basic_ndarray<std::int16_t> — 2 bytes/element, not 8. Reads a in C-order (a view/broadcast is flattened into a fresh contiguous buffer — a copy, never an alias), same shape out as in. Widening is exact; narrowing truncates/wraps at the target width (as in C / a numpy fixed dtype). Constrained to conversions that actually exist (convertible_to), so e.g. complex→real fails with a clear concept error, not template spam.
U | the destination element type (the only type spelled at the call site). |
a | source array (any Field element type convertible to |
a fresh contiguous basic_ndarray<U> of a's shape.
O(size).
allocates the result buffer.
NdarrayCompileRun.AstypeStdlibE2E.NdarrayElement-wise a + b with broadcasting.
a | first operand. |
b | second operand. |
a + b broadcast to the common shape.
O(size of result).
allocates the result.
CheatahNDArray.BroadcastingAddNdarrayCompileRun.AddStdlibE2E.NdarrayElement-wise a - b with broadcasting.
a | first operand. |
b | second operand. |
a - b broadcast to the common shape.
O(size of result).
allocates the result.
NdarrayCompileRun.SubStdlibE2E.Ndarray SystemApps.LinearSolveElement-wise a * b with broadcasting.
a | first operand. |
b | second operand. |
a * b broadcast to the common shape.
O(size of result).
allocates the result.
NdarrayCompileRun.MulStdlibE2E.NdarrayElement-wise a / b with broadcasting (an integer element type does integer division).
a | numerator. |
b | denominator (float division follows IEEE-754: /0 yields inf/nan, no throw). |
a / b broadcast to the common shape.
O(size of result).
allocates the result.
NdarrayCompileRun.DivideStdlibE2E.Ndarrayoperator+ · 3 overloads
basic_ndarray< T > operator+(const basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > operator+(const basic_ndarray< T > &a, S s)source#basic_ndarray< T > operator+(S s, const basic_ndarray< T > &a)source#Elementwise infix forms: a + b, a - b, a * b, a / b (broadcasting).
Elementwise a + b with broadcasting (infix form of add()).
a | first operand. |
b | second operand. |
the broadcast sum (a fresh array).
O(size of result).
the result.
operator- · 3 overloads
basic_ndarray< T > operator-(const basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > operator-(const basic_ndarray< T > &a, S s)source#basic_ndarray< T > operator-(S s, const basic_ndarray< T > &a)source#Elementwise a - b with broadcasting (infix form of sub()).
a | first operand. |
b | second operand. |
the broadcast difference (a fresh array).
O(size of result).
the result.
operator* · 3 overloads
basic_ndarray< T > operator*(const basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > operator*(const basic_ndarray< T > &a, S s)source#basic_ndarray< T > operator*(S s, const basic_ndarray< T > &a)source#Elementwise a * b with broadcasting (infix form of mul()).
a | first operand. |
b | second operand. |
the broadcast product (a fresh array).
O(size of result).
the result.
operator/ · 3 overloads
basic_ndarray< T > operator/(const basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > operator/(const basic_ndarray< T > &a, S s)source#basic_ndarray< T > operator/(S s, const basic_ndarray< T > &a)source#Elementwise a / b with broadcasting (infix form of divide()).
a | numerator. |
b | denominator. |
the broadcast quotient (a fresh array).
O(size of result).
the result.
CheatahNDArray.DivideInfixLvalueFormoperator+= · 2 overloads
basic_ndarray< T > & operator+=(basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > & operator+=(basic_ndarray< T > &a, S s)source#In-place compound assignment: a += b, a -= b, a *= b, a /= b (array or arithmetic-scalar right operand).
See compound_apply. In-place a += b, updating a's buffer (see compound_apply).
a | the array to update in place. |
b | the right operand (same shape or single-element). |
reference to a.
O(size of a).
none on the contiguous fast path.
operator-= · 2 overloads
basic_ndarray< T > & operator-=(basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > & operator-=(basic_ndarray< T > &a, S s)source#In-place a -= b, updating a's buffer (see compound_apply).
a | the array to update in place. |
b | the right operand (same shape or single-element). |
reference to a.
O(size of a).
none on the contiguous fast path.
operator*= · 2 overloads
basic_ndarray< T > & operator*=(basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > & operator*=(basic_ndarray< T > &a, S s)source#In-place a *= b, updating a's buffer (see compound_apply).
a | the array to update in place. |
b | the right operand (same shape or single-element). |
reference to a.
O(size of a).
none on the contiguous fast path.
operator/= · 2 overloads
basic_ndarray< T > & operator/=(basic_ndarray< T > &a, const basic_ndarray< T > &b)source#basic_ndarray< T > & operator/=(basic_ndarray< T > &a, S s)source#In-place a /= b, updating a's buffer (see compound_apply).
a | the array to update in place. |
b | the right operand (same shape or single-element). |
reference to a.
O(size of a).
none on the contiguous fast path.
basic_ndarray< std::complex< T > > complex(const basic_ndarray< T > &re, const basic_ndarray< T > &im)
source#
Build a complex array from real and imaginary parts (element-wise re + im·j), broadcasting the two together — the way to construct a complex matrix/vector (a wavefunction, a Hermitian operator) since cheatah literals are real.
re | the real parts (a real floating array). |
im | the imaginary parts (a real floating array, broadcast against |
a basic_ndarray<std::complex<T>> of re + im·j; throws if the shapes don't broadcast.
O(size of result).
allocates the result buffer.
NdarrayCompileRun.ComplexStdlibE2E.NdarrayComplexElement-wise complex conjugate (a − b·j for each a + b·j); on a real array it is the identity (a copy).
Type-preserving. Used to form Hermitian adjoints and conjugate-linear inner products.
a | the array. |
a fresh array of the same element type with each element conjugated.
O(size).
allocates the result buffer.
NdarrayCompileRun.ConjStdlibE2E.NdarrayComplexThe real parts as a real array (the identity on a real array).
For a + b·j it returns a.
a | the array. |
a basic_ndarray<real_base_t<T>> of the real parts.
O(size).
allocates the result buffer.
NdarrayCompileRun.RealStdlibE2E.NdarrayComplexThe imaginary parts as a real array (all zeros for a real array).
For a + b·j it returns b.
a | the array. |
a basic_ndarray<real_base_t<T>> of the imaginary parts.
O(size).
allocates the result buffer.
NdarrayCompileRun.ImagStdlibE2E.NdarrayComplexElement-wise square root (the array form of math.sqrt; ≈ numpy.sqrt).
a | a floating-point array. |
a fresh same-shape array with √x for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathNdarrayCompileRun.SqrtStdlibE2E.NdarrayMathElement-wise cube root (the array form of math.cbrt; ≈ numpy.cbrt).
a | a floating-point array. |
a fresh same-shape array with ∛x for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathStdlibE2E.NdarrayMathElement-wise eˣ (the array form of math.exp; ≈ numpy.exp).
a | a floating-point array. |
a fresh same-shape array with exp(x) for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathNdarrayCompileRun.ExpStdlibE2E.NdarrayMathElement-wise natural log (the array form of math.log; ≈ numpy.log).
a | a floating-point array. |
a fresh same-shape array with ln(x) for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathStdlibE2E.NdarrayMathElement-wise sine (the array form of math.sin; ≈ numpy.sin).
a | a floating-point array (radians). |
a fresh same-shape array with sin(x) for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathNdarrayCompileRun.SinStdlibE2E.NdarrayMathElement-wise cosine (the array form of math.cos; ≈ numpy.cos).
a | a floating-point array (radians). |
a fresh same-shape array with cos(x) for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathStdlibE2E.NdarrayMathElement-wise tangent (the array form of math.tan; ≈ numpy.tan).
a | a floating-point array (radians). |
a fresh same-shape array with tan(x) for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathStdlibE2E.NdarrayMathElement-wise absolute value (the array form of math.abs; ≈ numpy.abs).
a | a floating-point array. |
a fresh same-shape array with |x| for each element.
O(size).
allocates the result buffer.
CheatahNDArray.ElementwiseMathStdlibE2E.NdarrayMathSum of all elements — a full reduction across every axis (a contiguous array goes through the shared multi-accumulator SIMD reduction detail::reduce_lanes, else a C-order walk); empty sums to 0.
a | the array. |
the total, as the element type T.
O(size).
none.
NdarrayCompileRun.SumStdlibE2E.NdarrayMean of all elements, always as a double (0.0 for an empty array — no divide-by-zero).
a | the array. |
the average as a double.
O(size).
none.
NdarrayCompileRun.MeanStdlibE2E.NdarrayRead one element by signed multi-index (the cheatah-facing wrapper over basic_ndarray::at; rejects negative coordinates).
a | the array. |
index | one coordinate per dimension (signed; throws on a negative). |
the element value (type T); throws on a wrong-rank/out-of-range index.
O(ndim).
none.
NdarrayCompileRun.GetStdlibE2E.NdarrayThe shape as signed dims (cheatah integers are signed; a 0-d array yields an empty list).
a | the array. |
the dimensions as a long long vector.
O(ndim).
allocates the result vector.
NdarrayCompileRun.ShapeOfStdlibE2E.NdarrayThe element count as a signed value (1 for a 0-d array).
a | the array. |
the number of elements as a long long.
O(ndim).
none.
NdarrayCompileRun.SizeOfStdlibE2E.NdarrayRender as a nested-bracket string, e.g.
"[[1, 2], [3, 4]]" (a 0-d scalar renders as the bare number). Each element is formatted with the default ostream precision.
a | the array. |
the textual representation.
O(size).
allocates the result string.
NdarrayCompileRun.ToStringStdlibE2E.Ndarray SystemApps.LinearSolveStream an array to a std::ostream (the FULL nested-bracket form) — so an NDArray is directly Streamable, like a primitive or a cheatah struct, without going through to_string/str().
io.rprint, str(), and a struct that holds an array all stream it this way; io.print instead uses cheatah_pretty_print to abbreviate large arrays.
os | the stream. |
a | the array. |
os.
O(size).
allocates the intermediate string.
CheatahNDArray.StreamableOperatorStdlibE2E.NdarrayConstants & variables
Whether T is a std::complex of a floating type — the trait behind Field.
Types
The real type underlying a Field T (double for both double and complex<double>).
complex_of_t<T>: the complex type over T's real base.
eig/eigvals return an array of these, because a real matrix can have complex eigenvalues (conjugate pairs) — e.g. the rotation matrix [[0,-1],[1,0]] has eigenvalues ±i.
The backing store of an ndarray: a flat, contiguous, shared element buffer.
It uses detail::default_init_allocator so a freshly-sized result buffer that an op is about to overwrite in full is not needlessly zero-filled first. zeros/full/scalar, which value-fill, are unaffected — only the no-value sizing path skips initialization.
The default ndarray element type is double — NDArray names that instantiation (the std::string ↔ std::basic_string<char> pattern), so existing code and the linalg routines keep working unchanged.
