Chapter 01Before you start
cnumpy is a native Windows x64 DLL with a public C ABI and an AutoHotkey v2 facade. Everything in this tutorial runs through two files from the repository:
| Component | Path |
|---|---|
| AutoHotkey facade | ahk\numpy.ahk |
| Qualified release DLL | build\x64\Release\cnumpy_ahk.dll |
Use every component at the same architecture: Windows x64, a
64-bit AutoHotkey v2 interpreter (the release was
qualified with 2.1-alpha.30 x64), and the x64 DLL. Set
Numpy.DllPath before the first library call — array
factories call Numpy.Init() on demand, and changing the path
after the DLL is loaded does not replace the loaded module.
#Requires AutoHotkey v2.0
#Include ahk\numpy.ahk
; For a script in the repository root:
Numpy.DllPath := A_ScriptDir "\build\x64\Release\cnumpy_ahk.dll"
Numpy.Init()
MsgBox "cnumpy " Numpy.Version()
cnumpy 1.21.0-cnumpyEvery NdArray owns a native handle. Release arrays by
dropping every AHK reference (assign 0), and call
Numpy.Cleanup() only after the last array is gone.
Chapter 10 covers the full lifecycle discipline; the short examples
in between omit it for readability.
Chapter 02The basics
cnumpy's main object is the homogeneous multidimensional array: a table of elements, all of the same type, indexed by non-negative integers. In cnumpy dimensions are called axes, exactly as in NumPy.
The array class is Numpy.NdArray. Its most important
properties mirror NumPy's:
| Property | Meaning |
|---|---|
Ndim | the number of axes (dimensions) |
Shape | an AHK Array of dimension sizes (returned as a clone) |
Size | the total number of elements |
Dtype | the element type id, e.g. Numpy.DT_FLOAT64 = 13 |
ItemSize | the size in bytes of each element |
Nbytes | total bytes of element data |
Strides, CContiguous, FContiguous | memory layout |
An example
a := Numpy.Arange(0, 15).Reshape([3, 5])
MsgBox a.ToString()
MsgBox "shape: [" a.Shape[1] ", " a.Shape[2] "]`n"
. "ndim: " a.Ndim "`n"
. "dtype: " Numpy.Typename(a.Dtype) "`n"
. "itemsize: " a.ItemSize "`n"
. "size: " a.Size
[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]
shape: [3, 5]
ndim: 2
dtype: double precision
itemsize: 8
size: 15Numpy.Arange produces float64 elements by
default, so Numpy.Typename(a.Dtype) reports the native name
double precision. Shape returns a cloned AHK
Array — mutating it does not reshape the native owner.
Array creation
Numpy.Array accepts a flat AHK array of numbers plus an
optional row-major shape. The data length must equal the product of the
shape dimensions — there is no silent truncation or padding:
vector := Numpy.Array([6, 7, 8]) ; 1-D float64, shape [3]
matrix := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
ints := Numpy.IntArray([1, 2, 3]) ; int64 elements
booleans := Numpy.Array([1, 0, 1], [3], Numpy.DT_BOOL)
MsgBox matrix.ToString()
MsgBox ints.ToString()
MsgBox booleans.ToString()
[[1, 2, 3],
[4, 5, 6]]
[1, 2, 3]
[True, False, True]The frequently used dtype constants:
| AHK constant | Native dtype |
|---|---|
Numpy.DT_BOOL | bool |
Numpy.DT_INT32 | signed 32-bit integer |
Numpy.DT_LONGLONG | signed 64-bit integer |
Numpy.DT_FLOAT32 | 32-bit float |
Numpy.DT_FLOAT64 | 64-bit float — the default |
Numpy.DT_COMPLEX128 | two 64-bit floating components |
Often the elements of an array are unknown but its size is known. cnumpy offers the same placeholder factories as NumPy, all taking a shape first:
z := Numpy.Zeros([3, 4]) ; float64 zeros
o := Numpy.Ones([2, 3], Numpy.DT_INT32) ; int32 ones
e := Numpy.Empty([2, 3]) ; uninitialized
f := Numpy.Full([2, 2], 3.14) ; constant fill
r := Numpy.Arange(10, 30, 5) ; [10, 15, 20, 25]
x := Numpy.Linspace(0, 2, 9) ; 9 points from 0 to 2
MsgBox r.ToString()
MsgBox x.ToString()
[10, 15, 20, 25]
[0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]When Arange is used with floating point steps, prefer
Linspace: it takes the count of elements instead of
the step, so it is immune to floating point step accumulation.
Printing arrays
NdArray.ToString() renders the array as nested brackets:
the last axis is printed left to right, and the remaining axes are
separated by newlines, exactly like NumPy's layout. Booleans render as
True/False, floats use up to 8 significant
digits by default.
c := Numpy.Arange(0, 24).Reshape([2, 3, 4]) ; 3-D array
MsgBox c.ToString()
; Array2String honors the print options and can summarize large arrays:
previous := Numpy.SetPrintOptions(, 6) ; threshold := 6
big := Numpy.Arange(0, 10000)
MsgBox Numpy.Array2String(big, 4096)
Numpy.SetPrintOptions(, previous["threshold"]) ; restore
[[[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]]
[0, 1, 2, ..., 9997, 9998, 9999]ToString() always prints every element.
Numpy.Array2String(source, bufferSize) applies the print
options set through Numpy.SetPrintOptions(precision, threshold,
edgeitems, linewidth, suppress), which returns the previous settings
so you can restore them; any omitted parameter keeps its current value. To
move data back into plain AHK, ToArray() returns a flat AHK
Array of numbers.
Basic operations
Arithmetic operators apply elementwise and allocate a new array.
One deliberate difference from NumPy: cnumpy operations take
two NdArray operands — an AHK number is not promoted
automatically. Wrap scalars with Numpy.Full (or a 1-element
array, which broadcasts):
a := Numpy.Array([20, 30, 40, 50])
b := Numpy.Arange(0, 4) ; [0, 1, 2, 3]
c := Numpy.Subtract(a, b)
squares := Numpy.Multiply(b, b)
tenSin := Numpy.Multiply(Numpy.Full([1], 10.0), Numpy.Sin(a))
mask := Numpy.Less(a, Numpy.Full([1], 35.0)) ; comparison -> bool array
MsgBox c.ToString()
MsgBox squares.ToString()
MsgBox tenSin.ToString()
MsgBox mask.ToString()
[20, 29, 38, 47]
[0, 1, 4, 9]
[9.1294525, -9.8803162, 7.4511316, -2.6237485]
[True, True, False, False]The product operator works elementwise; the matrix product is
Matmul (or Dot), available both as a static
method and as an instance method:
A := Numpy.Array([1, 1, 0, 1], [2, 2])
B := Numpy.Array([2, 0, 3, 4], [2, 2])
elementwise := Numpy.Multiply(A, B)
product := A.Matmul(B) ; same as Numpy.Matmul(A, B)
MsgBox elementwise.ToString()
MsgBox product.ToString()
[[2, 0],
[0, 4]]
[[5, 4],
[3, 4]]Reductions live on the array. With no axis argument they reduce over
every element and return an AHK number; with an axis they
return a new array. -1 means the last axis (the facade routes
through the v2 exports, so axis=None and “last axis” stay
distinct):
rg := Numpy.Arange(0, 6).Reshape([2, 3]) ; [[0,1,2],[3,4,5]]
total := rg.Sum() ; AHK number: 15.0
colSums := rg.Sum(0) ; NdArray, one sum per column
rowSums := rg.Sum(1) ; NdArray, one sum per row
running := rg.Cumsum(1) ; cumulative sum along each row
MsgBox total "`n" colSums.ToString() "`n" rowSums.ToString()
MsgBox running.ToString()
15.0
[3, 5, 7]
[3, 12]
[[0, 1, 3],
[3, 7, 12]]The same pattern covers Min, Max,
Mean, Std, Var, Prod,
Argmax, Argmin, Any,
All, Median, Percentile and their
NaN-ignoring Nan* variants.
Universal functions
Familiar mathematical functions operate elementwise and produce a new
array. They exist both as Numpy.* statics and as
NdArray methods:
b := Numpy.Arange(0, 3) ; [0, 1, 2]
MsgBox b.Exp().ToString()
MsgBox b.Sqrt().ToString()
c := Numpy.Array([2, -1, 4])
MsgBox Numpy.Add(b, c).ToString()
[1, 2.7182818, 7.3890561]
[0, 1, 1.4142136]
[2, 0, 6]Available families include trigonometry (Sin,
Cos, Arctan2, Hypot…), exponentials
and logarithms (Exp, Log, Log2,
Log1p…), rounding (Floor, Ceil,
Rint, Trunc, Around), comparisons
and extrema (Maximum, Minimum, Fmax),
logic and bitwise operators, and special functions (Erf,
Gamma, I0…).
Operations that support an explicit destination avoid the result allocation entirely — the destination's shape, dtype and layout are validated, never replaced:
out := Numpy.Empty([3])
Numpy.Add(b, c, out) ; writes into out, returns it
Numpy.Sqrt(b, out) ; same for Sqrt
Indexing, slicing and iterating
Element access is explicit in cnumpy, and the two conventions are worth memorizing once:
arr.GetItem(i)/arr.SetItem(i, v)use a 0-based flat index, like NumPy'sarr.item(i).- The bracket sugar
arr[i]is 1-based, to match AutoHotkey convention —arr[1]is the first element. Numpy.ArrayGetItem(arr, [i, j, …])takes one 0-based index per dimension (negative indices count from the end) and returns a 0-d array.
cubes := Numpy.Power(Numpy.Arange(0, 10), Numpy.Full([1], 3.0))
MsgBox cubes.ToString()
MsgBox cubes.GetItem(2) ; 0-based flat access -> 8.0
MsgBox cubes[3] ; 1-based AHK sugar -> 8.0
part := Numpy.Slice(cubes, 2, 5) ; elements 2..4, like a[2:5]
MsgBox part.ToString()
everyOther := Numpy.Slice(cubes, 0, 10, 2) ; like a[0:10:2]
MsgBox everyOther.ToString()
reversed := Numpy.Flip(cubes) ; like a[::-1]
MsgBox reversed.ToString()
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
8.0
8.0
[8, 27, 64]
[0, 8, 64, 216, 512]
[729, 512, 343, 216, 125, 64, 27, 8, 1, 0]Numpy.Slice(source, start, stop, step := 1, axis := 0)
slices one axis and returns a view — no element
data is copied (chapter 4). Multidimensional access combines
ArrayGetItem for points and Slice per axis for
ranges:
; Build a 5x4 array from a coordinate function: b[x, y] = 10x + y
b := Numpy.FromFunction((x, y) => 10 * x + y, [5, 4])
MsgBox b.ToString()
point := Numpy.ArrayGetItem(b, [2, 3]) ; b[2, 3]
MsgBox point.ToArray()[1]
lastRow := Numpy.ArrayGetItem(b, [-1, 1]) ; b[-1, 1]
MsgBox lastRow.ToArray()[1]
rows := Numpy.Slice(b, 1, 3) ; b[1:3, :]
MsgBox rows.ToString()
column := Numpy.Slice(b, 1, 2, 1, 1) ; b[:, 1:2]
MsgBox column.ToString()
[[0, 1, 2, 3],
[10, 11, 12, 13],
[20, 21, 22, 23],
[30, 31, 32, 33],
[40, 41, 42, 43]]
23.0
41.0
[[10, 11, 12, 13],
[20, 21, 22, 23]]
[[1],
[11],
[21],
[31],
[41]]Iterating uses ordinary AHK loops. ToArray() yields the
flat elements; Numpy.Ndenumerate pairs every value with its
coordinates, and Numpy.Ndindex walks a shape without an
array:
small := Numpy.Array([1, 2, 3, 4], [2, 2])
text := ""
lines := ""
for value in small.ToArray()
text .= value " "
for pair in Numpy.Ndenumerate(small)
lines .= "(" pair[1][1] ", " pair[1][2] ") -> " pair[2] "`n"
MsgBox text
MsgBox lines
1.0 2.0 3.0 4.0
(0, 0) -> 1.0
(0, 1) -> 2.0
(1, 0) -> 3.0
(1, 1) -> 4.0Chapter 03Shape manipulation
Changing the shape of an array
An array has a shape given by the number of elements along each axis. The shape can be changed with various commands — all of the following return a result without touching the original array:
a := Numpy.Array([2, 8, 0, 6, 4, 5, 1, 1, 8, 9, 3, 6], [3, 4])
MsgBox a.Ravel().ToString() ; flattened (view when contiguous)
MsgBox a.Reshape([6, 2]).ToString() ; new shape (view when contiguous)
MsgBox a.Transpose().ToString() ; transposed view, shape [4, 3]
MsgBox a.Flatten().ToString() ; flattened, always a copy
[2, 8, 0, 6, 4, 5, 1, 1, 8, 9, 3, 6]
[[2, 8],
[0, 6],
[4, 5],
[1, 1],
[8, 9],
[3, 6]]
[[2, 4, 8],
[8, 5, 9],
[0, 1, 3],
[6, 1, 6]]
[2, 8, 0, 6, 4, 5, 1, 1, 8, 9, 3, 6]As in NumPy, a dimension given as -1 is computed
automatically:
MsgBox a.Reshape([2, -1]).ToString() ; -1 -> 6
[[2, 8, 0, 6, 4, 5],
[1, 1, 8, 9, 3, 6]]An incompatible target shape is a real native error, never a silent fallback:
source := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
try
invalid := source.Reshape([4, 2])
catch Error as err
MsgBox err.Message
NdArray.Reshape failed with status -4:
Cannot reshape array of size 6 into shape (8 elements)Stacking together different arrays
Several arrays can be stacked together along different axes. The stacking functions take an AHK Array of NdArray values:
a := Numpy.Array([9, 7, 5, 2], [2, 2])
b := Numpy.Array([1, 9, 5, 1], [2, 2])
MsgBox Numpy.Vstack([a, b]).ToString() ; stack rows -> [4, 2]
MsgBox Numpy.Hstack([a, b]).ToString() ; stack columns -> [2, 4]
x := Numpy.Array([4.0, 2.0])
y := Numpy.Array([3.0, 8.0])
MsgBox Numpy.ColumnStack([x, y]).ToString() ; 1-D arrays as columns
[[9, 7],
[5, 2],
[1, 9],
[5, 1]]
[[9, 7, 1, 9],
[5, 2, 5, 1]]
[[4, 3],
[2, 8]]Numpy.Concatenate(arrays, axis) generalizes both, and
Numpy.Stack(arrays, axis) joins along a new axis.
Dstack, RowStack and the block assembler
Numpy.Block are also available.
Splitting one array into several smaller ones
Hsplit splits along the horizontal axis: pass either the
number of equal sections, or an AHK Array of column boundaries. The result
is an AHK Array of NdArray parts:
a := Numpy.Arange(0, 12).Reshape([2, 6])
MsgBox a.ToString()
parts := Numpy.Hsplit(a, 3) ; three equal [2, 2] blocks
MsgBox parts[1].ToString() "`n---`n" parts[2].ToString()
uneven := Numpy.Hsplit(a, [3, 4]) ; split after columns 3 and 4
MsgBox uneven[1].ToString() "`n---`n" uneven[2].ToString()
. "`n---`n" uneven[3].ToString()
[[0, 1, 2, 3, 4, 5],
[6, 7, 8, 9, 10, 11]]
[[0, 1],
[6, 7]]
---
[[2, 3],
[8, 9]]
[[0, 1, 2],
[6, 7, 8]]
---
[[3],
[9]]
---
[[4, 5],
[10, 11]]Vsplit splits along the vertical axis,
Split(source, sections, axis) along any given axis, and
ArraySplit allows sections that do not divide the axis
equally.
Chapter 04Copies and views
When operating on arrays, element data is sometimes copied into a new array and sometimes not. There are three cases, exactly as in NumPy:
No copy at all
Simple AHK assignment never copies — both names refer to the same
NdArray object and the same native owner:
a := Numpy.Arange(0, 12).Reshape([3, 4])
b := a ; same object, no new native array
MsgBox (b = a) ; 1 (identical references)
View: looking at the same data
View(), Slice, Transpose(), and
Reshape/Ravel on a contiguous array all return
views: new array objects that share the underlying element
buffer. Numpy.SharesMemory proves it:
a := Numpy.Arange(0, 12).Reshape([3, 4])
v := a.View()
r := a.Reshape([6, 2]) ; view: a is C-contiguous
t := a.Transpose() ; view with swapped strides
c := a.Copy() ; deep copy
MsgBox Numpy.SharesMemory(a, v) ; 1
MsgBox Numpy.SharesMemory(a, r) ; 1
MsgBox Numpy.SharesMemory(a, t) ; 1
MsgBox Numpy.SharesMemory(a, c) ; 0
; Writing through the base array is visible in every view:
a.SetItem(0, 99)
MsgBox r.GetItem(0) ; 99.0
1
1
1
0
99.0A view retains its owner internally, so releasing the source before the view is safe. For readable code, release derived arrays before their sources anyway (chapter 10).
Deep copy
Copy() makes a complete copy of the array and its data.
The idiom from the NumPy quickstart — copying a slice so a huge
intermediate can be released — works the same way:
huge := Numpy.Arange(0, 100000000)
head := Numpy.Slice(huge, 0, 100).Copy() ; own the 100 elements
huge := 0 ; native buffer can be freed now
Flatten() and TransposeCopy() are the
always-copy variants of Ravel() and
Transpose(); AsContiguousArray() materializes a
C-contiguous copy of any strided view.
Chapter 05Broadcasting
Broadcasting lets operations work on arrays of different shapes, under the qualified NumPy 1.25 rules: shapes are compared from the trailing axis backwards, and two dimensions are compatible when they are equal or one of them is 1.
source := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
offsets := Numpy.Array([10, 20, 30], [1, 3])
shifted := Numpy.Add(source, offsets)
MsgBox shifted.ToString()
; A 1-element array broadcasts against anything — the scalar recipe:
doubled := Numpy.Multiply(source, Numpy.Full([1], 2.0))
MsgBox doubled.ToString()
; Ask before you leap:
MsgBox Numpy.CanBroadcast(source, offsets) ; 1
resultShape := Numpy.BroadcastShapes([[3, 1], [1, 4]])
MsgBox "[" resultShape[1] ", " resultShape[2] "]" ; [3, 4]
[[11, 22, 33],
[14, 25, 36]]
[[2, 4, 6],
[8, 10, 12]]
1
[3, 4]Numpy.BroadcastTo(source, shape) materializes the broadcast
view explicitly, and Numpy.BroadcastArrays expands a whole
list against each other.
Chapter 06Advanced indexing and index tricks
cnumpy offers the NumPy family of fancy-indexing tools; the selectors are explicit functions rather than bracket syntax.
Indexing with arrays of indices
squares := Numpy.Multiply(Numpy.Arange(0, 12), Numpy.Arange(0, 12))
MsgBox squares.ToString()
picked := Numpy.Take(squares, [1, 1, 3, 8, 5]) ; indices may repeat
MsgBox picked.ToString()
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
[1, 1, 9, 64, 25]Take accepts a plain AHK Array, an integer, or an
NdArray of indices; with an axis argument it
selects along that axis. Numpy.FancyIndex(source, indices,
axis) is the strided equivalent, and
Numpy.Put(destination, indices, values) is the write-side
counterpart.
Indexing with boolean arrays
Comparison operators produce DT_BOOL arrays, which select
elements through BooleanIndex. A non-bool mask is rejected
with a real error — build masks from comparisons:
a := Numpy.Arange(0, 12).Reshape([3, 4])
mask := Numpy.Greater(a, Numpy.Full([1], 4.0)) ; bool array
MsgBox mask.ToString()
MsgBox Numpy.BooleanIndex(a, mask).ToString() ; 1-D selection
[[False, False, False, False],
[False, True, True, True],
[True, True, True, True]]
[5, 6, 7, 8, 9, 10, 11]The Where family
; Two-branch selection: where(condition, x, y)
x := Numpy.Arange(0, 6)
isBig := Numpy.GreaterEqual(x, Numpy.Full([1], 3.0))
capped := Numpy.Where(isBig, Numpy.Full([6], 3.0), x)
MsgBox capped.ToString()
; Coordinates of nonzero elements: one index array per dimension
grid := Numpy.Array([1, 0, 0, 0, 2, 0], [2, 3])
locations := Numpy.Where(Numpy.NotEqual(grid, Numpy.Zeros([1])))
MsgBox locations[1].ToString() " " locations[2].ToString()
[0, 1, 2, 3, 3, 3]
[0, 1] [0, 1]Related tools: ArgWhere (coordinates as rows),
FlatNonzero, CountNonzero, Extract,
Compress, and Select for multi-condition
choices.
Searching and sorting
source := Numpy.IntArray([3, 1, 2, 2, 5, 4], [2, 3])
MsgBox source.Sort(-1, "stable").ToString() ; per row
MsgBox source.Sort("none", "stable").ToString() ; flattened
MsgBox source.Argsort(-1, "heapsort").ToString() ; sorting indices
parts := Numpy.Unique(source, true, true, true)
MsgBox parts[1].ToString() ; unique values
[[1, 2, 3],
[2, 4, 5]]
[1, 2, 2, 3, 4, 5]
[[1, 2, 0],
[0, 2, 1]]
[1, 2, 3, 4, 5]Unique optionally returns first-occurrence indices, the
inverse mapping, and counts. The set operations
(Intersect1d, Union1d, Setdiff1d,
In1d/Isin) and the searching pair
(Searchsorted, Digitize) round out the
family.
Chapter 07Linear algebra
The core linear-algebra surface follows numpy.linalg.
Simple array operations sit on the array itself; solvers and
decompositions live on Numpy and Numpy.Linalg:
a := Numpy.Array([1.0, 2.0, 3.0, 4.0], [2, 2])
MsgBox a.Transpose().ToString()
MsgBox Numpy.Linalg.Inv(a).ToString()
eye := Numpy.Eye(2) ; 2x2 identity
MsgBox Numpy.TraceExt(eye) ; 2.0 (AHK number)
det := a.Det() ; 1-element NdArray
MsgBox det.GetItem(0) ; -2.0000000000000004 (LU roundoff,
; same as np.linalg.det)
; Solve a @ x = y
y := Numpy.Array([5.0, 7.0], [2, 1])
x := Numpy.Solve(a, y)
MsgBox x.ToString()
[[1, 3],
[2, 4]]
[[-2, 1],
[1.5, -0.5]]
2.0
-2.0000000000000004
[[-3],
[4]]Decompositions return their natural multiple results as an AHK Array
of arrays: Numpy.Eig(a) yields
[eigenvalues, eigenvectors], Numpy.Svd(a) yields
[U, S, Vh], and Numpy.Lstsq(a, b) yields
[x, residuals, rank, singularValues]. Also available:
Cholesky, Eigh/Eigvalsh,
Pinv, MatrixPower, MatrixRank,
Norm, Cond, Kron,
Tensordot, and the Einsum* fixed patterns.
Numpy.SetNumThreads(n) configures the GEMM thread pool
(0 restores the automatic count). Large
Dot/Matmul calls are where it pays off.
Chapter 08Random numbers
Numpy.Random generates arrays from common distributions.
Note the signature difference from NumPy: the shape comes
first, then the distribution parameters:
Numpy.Random.Seed(42) ; deterministic sequence
u := Numpy.Random.Random([2, 3]) ; uniform [0, 1)
n := Numpy.Random.Normal([1000], 2.0, 0.5) ; mean 2.0, std 0.5
i := Numpy.Random.Randint([5], 0, 10) ; integers in [0, 10)
MsgBox "u shape: [" u.Shape[1] ", " u.Shape[2] "]"
MsgBox "sample mean: " Format("{:.2f}", n.Mean())
deck := Numpy.Arange(0, 10)
shuffled := Numpy.Random.Permutation(deck) ; new permuted array
Numpy.Random.Shuffle(deck) ; in-place
choice := Numpy.Random.Choice(deck, 3, false) ; 3 draws, no replacement
u shape: [2, 3]
sample mean: 2.00 (approximately; engine-specific values)The generator is the characterized xoshiro256** / SplitMix64
sequence, not NumPy's bit generator. Distribution semantics are
qualified, but a seeded run does not reproduce NumPy's
element-for-element stream. Choice supports weighted draws
with a probability array and validates that the weights are a proper
distribution.
Chapter 09Callbacks & vectorization
Where NumPy passes Python callables, cnumpy passes AHK functions across
the native boundary through a batched callback ABI. Callback values are
real double scalars; exceptions raised inside your callback
abort the operation atomically and are rethrown to your script.
; Build from coordinates (0-based), like np.fromfunction:
grid := Numpy.FromFunction((x, y) => 10 * x + y, [2, 3])
MsgBox grid.ToString()
; Apply a scalar function elementwise, like np.vectorize:
source := Numpy.Array([1.0, 2.0, 3.0])
tripled := Numpy.Vectorize(value => value * 2 + 1, source)
MsgBox tripled.ToString()
; Reduce each line along an axis to one scalar:
SumLine(values) {
total := 0.0
for value in values
total += value
return total
}
m := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
columnTotals := Numpy.ApplyAlongAxis(SumLine, 0, m)
MsgBox columnTotals.ToString()
; Pull values from an iterator, like np.fromiter:
MakeCounter() {
i := 0
return (*) => (i += 1, i * i)
}
squares := Numpy.FromIter(MakeCounter(), 5)
MsgBox squares.ToString()
[[0, 1, 2],
[10, 11, 12]]
[3, 5, 7]
[5, 7, 9]
[1, 4, 9, 16, 25]Callbacks batch the boundary crossings, not the scalar work:
every element still runs one AHK function call. The qualified benchmark
ratios for callback-heavy operations (e.g. FromFunction)
are far above native kernels. Prefer built-in array operations wherever
one exists; reach for callbacks when the logic genuinely is not
expressible otherwise.
A statistics staple to close the chapter — Histogram
returns int64 bin counts (range auto-detected when the endpoints are left
equal):
Numpy.Random.Seed(7)
data := Numpy.Random.Normal([10000], 2.0, 0.5)
counts := Numpy.Histogram(data, 10)
MsgBox counts.ToString() ; 10 bins over the sample range
MsgBox counts.Sum() ; 10000.0
Chapter 10Native array conversion
cnumpy results are usually consumed through the facade. When you need a
real AutoHotkey Array — for another AHK library, a UI, or a
nested value — NdArray.ToNativeArray() converts without an
element-level AHK loop. The interpreter's Array layout is
discovered at runtime and cross-validated; the DLL fills the pre-built
tree through cnp_ahk_fill_array_flat and
cnp_ahk_fill_array_nd. No interpreter offsets are hardcoded
and no machine code is embedded.
#Requires AutoHotkey v2.0
#Include ahk\numpy.ahk
Numpy.DllPath := A_ScriptDir "\build\x64\Release\cnumpy_ahk.dll"
Numpy.Init()
matrix := Numpy.Arange(0, 12).Reshape([3, 4])
native := matrix.ToNativeArray()
MsgBox native[2][3] ; 6.0
MsgBox native.Length ; 3
MsgBox Type(native) ; Array
6.0
3
ArrayThe conversion requires a numeric, C-contiguous array. Strided views
raise ValueError; non-numeric dtypes raise
TypeError. The result is a deep copy owned by AHK, so the
source array stays writable and independent. On 1,000,000 float64
elements the native path is about 39x faster than the AHK-loop
ToArray(); a 1000x1000 matrix converts in about 12 ms.
Run benchmark\native_conversion_benchmark.ahk for the full
table.
Chapter 11Ownership & cleanup
This is the one chapter with no NumPy counterpart, and the most
important one for a long-running AutoHotkey process. Every
NdArray owns a native handle; its destructor releases the
native reference when the last AHK reference disappears. The discipline
for a well-behaved script:
- Take a baseline with
Numpy.AllocatedMemory()afterInit(). - Do the work inside
try; on the way out, drop every array reference by assigning0, derived arrays first. - Call
Numpy.Cleanup()last, never while an array or callback result is live. - Assert that retained bytes returned to the baseline.
#Requires AutoHotkey v2.0
#Include ahk\numpy.ahk
Numpy.DllPath := A_ScriptDir "\build\x64\Release\cnumpy_ahk.dll"
Numpy.Init()
baseline := Numpy.AllocatedMemory()
source := 0
result := 0
try {
source := Numpy.Arange(0, 1024)
result := Numpy.Sqrt(source)
MsgBox "mean sqrt: " Format("{:.2f}", result.Mean())
} finally {
result := 0 ; derived first
source := 0
retained := Numpy.AllocatedMemory()
Numpy.Cleanup() ; always last
}
if retained != baseline
throw Error("retained native bytes: " (retained - baseline))
mean sqrt: 21.32
(script exits with zero retained bytes)AllocatedMemory() is the library's tracked native
allocation total — a lifecycle assertion, not the Windows working set.
Native failures surface as AHK exceptions carrying the native status and
message; they are never converted into empty arrays or substitute
results. Common symptoms:
| Symptom | Meaning and action |
|---|---|
GetLastError 193 on load | Architecture mismatch — run 64-bit AutoHotkey with the x64 DLL. |
GetLastError 126 on load | Path or native dependency missing — verify the absolute Numpy.DllPath. |
missing native export … | Wrapper and DLL from different builds — deploy them together. |
failed with status -4 | A shape contract failed — read the full native message. |
failed with status -6 | Invalid axis for the array rank or projected API. |
| Retained bytes nonzero | Some owner, view, result, or callback context is still live — release the concrete reference and rerun. |
Further reading
- Getting started with cnumpy in AutoHotkey v2 — the prose companion to chapters 1–2 and 10
- Practical, complete examples — CSV sales analysis, least-squares regression, signal smoothing, a preallocated C pipeline
- Bulk callbacks from AutoHotkey and C — the full callback ABI behind chapter 9
- NumPy 1.25 compatibility statement — every intentional difference, qualified and tested
- Repository README — build, test, and benchmark instructions