cnumpy · a field manual for AutoHotkey v2

The cnumpy
Quickstart Tutorial

Native x64 arrays with NumPy 1.25 semantics, driven entirely from AutoHotkey v2. This tutorial mirrors the classic NumPy Quickstart, chapter for chapter, with every example rewritten for the Numpy facade in ahk/numpy.ahk.

Qualified against
NumPy 1.25.0
752/752 declarations
EN 中文

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:

ComponentPath
AutoHotkey facadeahk\numpy.ahk
Qualified release DLLbuild\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.

setup.ahkAutoHotkey v2
#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()
output
cnumpy 1.21.0-cnumpy
Contract

Every 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:

PropertyMeaning
Ndimthe number of axes (dimensions)
Shapean AHK Array of dimension sizes (returned as a clone)
Sizethe total number of elements
Dtypethe element type id, e.g. Numpy.DT_FLOAT64 = 13
ItemSizethe size in bytes of each element
Nbytestotal bytes of element data
Strides, CContiguous, FContiguousmemory layout

An example

basics_example.ahkAutoHotkey v2
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
output
[[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:     15

Numpy.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:

creation.ahkAutoHotkey v2
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()
output
[[1, 2, 3],
 [4, 5, 6]]
[1, 2, 3]
[True, False, True]

The frequently used dtype constants:

AHK constantNative dtype
Numpy.DT_BOOLbool
Numpy.DT_INT32signed 32-bit integer
Numpy.DT_LONGLONGsigned 64-bit integer
Numpy.DT_FLOAT3232-bit float
Numpy.DT_FLOAT6464-bit float — the default
Numpy.DT_COMPLEX128two 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:

placeholders.ahkAutoHotkey v2
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()
output
[10, 15, 20, 25]
[0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
Same rule as NumPy

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.

printing.ahkAutoHotkey v2
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
output
[[[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):

operations.ahkAutoHotkey v2
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()
output
[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:

matmul.ahkAutoHotkey v2
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()
output
[[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):

reductions.ahkAutoHotkey v2
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()
output
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:

ufuncs.ahkAutoHotkey v2
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()
output
[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:

into.ahkAutoHotkey v2
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's arr.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.
indexing.ahkAutoHotkey v2
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()
output
[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:

indexing_2d.ahkAutoHotkey v2
; 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()
output
[[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:

iterate.ahkAutoHotkey v2
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
output
1.0 2.0 3.0 4.0
(0, 0) -> 1.0
(0, 1) -> 2.0
(1, 0) -> 3.0
(1, 1) -> 4.0

Chapter 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:

reshape.ahkAutoHotkey v2
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
output
[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:

reshape_auto.ahkAutoHotkey v2
MsgBox a.Reshape([2, -1]).ToString()   ; -1 -> 6
output
[[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:

reshape_error.ahkAutoHotkey v2
source := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
try
    invalid := source.Reshape([4, 2])
catch Error as err
    MsgBox err.Message
output
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:

stacking.ahkAutoHotkey v2
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
output
[[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:

splitting.ahkAutoHotkey v2
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()
output
[[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:

no_copy.ahkAutoHotkey v2
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:

views.ahkAutoHotkey v2
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
output
1
1
1
0
99.0
Lifecycle

A 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:

deep_copy.ahkAutoHotkey v2
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.

123 456 shape [2, 3] + 102030 102030 shape [1, 3] — row repeats = 112233 142536 shape [2, 3]
The [1, 3] row is virtually repeated along axis 0 — no copy is made.
broadcasting.ahkAutoHotkey v2
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]
output
[[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

take.ahkAutoHotkey v2
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()
output
[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:

boolean_mask.ahkAutoHotkey v2
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
output
[[False, False, False, False],
 [False, True, True, True],
 [True, True, True, True]]
[5, 6, 7, 8, 9, 10, 11]

The Where family

where.ahkAutoHotkey v2
; 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()
output
[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

sorting.ahkAutoHotkey v2
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
output
[[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:

linalg.ahkAutoHotkey v2
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()
output
[[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.

Performance

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:

random.ahkAutoHotkey v2
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
output
u shape: [2, 3]
sample mean: 2.00   (approximately; engine-specific values)
Compatibility boundary

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.

callbacks.ahkAutoHotkey v2
; 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()
output
[[0, 1, 2],
 [10, 11, 12]]
[3, 5, 7]
[5, 7, 9]
[1, 4, 9, 16, 25]
Performance reality

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):

histogram.ahkAutoHotkey v2
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.

native_conversion.ahkAutoHotkey v2
#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
output
6.0
3
Array

The 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:

  1. Take a baseline with Numpy.AllocatedMemory() after Init().
  2. Do the work inside try; on the way out, drop every array reference by assigning 0, derived arrays first.
  3. Call Numpy.Cleanup() last, never while an array or callback result is live.
  4. Assert that retained bytes returned to the baseline.
lifecycle.ahkAutoHotkey v2
#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))
output
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:

SymptomMeaning and action
GetLastError 193 on loadArchitecture mismatch — run 64-bit AutoHotkey with the x64 DLL.
GetLastError 126 on loadPath or native dependency missing — verify the absolute Numpy.DllPath.
missing native export …Wrapper and DLL from different builds — deploy them together.
failed with status -4A shape contract failed — read the full native message.
failed with status -6Invalid axis for the array rank or projected API.
Retained bytes nonzeroSome owner, view, result, or callback context is still live — release the concrete reference and rerun.
END OF TUTORIAL

Further reading