A fast, memory-safe tensor library implemented in Rust and exposed as a Python package.
ferrix provides an NDArray core with stride-based indexing, vectorized math operations, slicing/reshaping utilities, and Python bindings via pyo3.
- Rust core for predictable performance and memory safety.
- Python-friendly API for quick experimentation.
- Multi-dimensional arrays with row-major strides.
- Useful tensor operations for ML and numerical workloads.
Install from PyPI:
python -m pip install ferriximport ferrix
# Create two 2x2 arrays
a = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])
b = ferrix.PyNDArray([5.0, 6.0, 7.0, 8.0], [2, 2])
# Core arithmetic
print(a.add(b).get([0, 0])) # 6.0
print(a.mul(b).get([1, 1])) # 32.0
print(a.scale(0.5).get([1, 0])) # 1.5
# Matrix multiplication
print(a.matmul(b).get([0, 0])) # 19.0
print(a.matmul_blas(b).get([0, 0])) # Compatibility API; currently same as matmul
# Activations and reductions
print(a.relu().get([0, 0]))
print(a.softmax().sum())
print(a.sum(), a.mean(), a.argmax())
# Shape transforms
print(a.transpose().shape())
print(a.reshape([4]).shape())ferrix exposes two classes:
ferrix.PyNDArrayfor numeric tensors (f64)ferrix.PyBoolArrayfor boolean masks
Constructor:
PyNDArray(data: list[float], shape: list[int]) -> PyNDArray
x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])Introspection and element access:
shape() -> list[int]get(index: list[int]) -> float
x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])
print(x.shape()) # [2, 2]
print(x.get([1, 0])) # 3.0Reductions:
sum() -> floatmean() -> floatargmax() -> int(index in flattened row-major order)
x = ferrix.PyNDArray([1.0, 5.0, 3.0, 4.0], [2, 2])
print(x.sum()) # 13.0
print(x.mean()) # 3.25
print(x.argmax()) # 1Element-wise math:
add(other: PyNDArray) -> PyNDArraymul(other: PyNDArray) -> PyNDArrayscale(scalar: float) -> PyNDArray
a = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])
b = ferrix.PyNDArray([5.0, 6.0, 7.0, 8.0], [2, 2])
print(a.add(b).get([0, 1])) # 8.0
print(a.mul(b).get([1, 0])) # 21.0
print(a.scale(10).get([1, 1])) # 40.0Activation functions:
relu() -> PyNDArraysigmoid() -> PyNDArraysoftmax() -> PyNDArray
x = ferrix.PyNDArray([-1.0, 0.0, 1.0], [1, 3])
print(x.relu().get([0, 0]))
print(x.sigmoid().get([0, 2]))
print(x.softmax().sum())Matrix operations:
matmul(other: PyNDArray) -> PyNDArraymatmul_blas(other: PyNDArray) -> PyNDArray
a = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])
b = ferrix.PyNDArray([5.0, 6.0, 7.0, 8.0], [2, 2])
print(a.matmul(b).get([0, 0]))
print(a.matmul_blas(b).get([0, 0]))Note: matmul_blas is currently a compatibility API that uses the same backend behavior as matmul.
Reshape and transpose:
reshape(new_shape: list[int]) -> PyNDArraytranspose() -> PyNDArray(2D transpose)
x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])
print(x.reshape([4]).shape())
print(x.transpose().shape())Slicing and indexing:
slice_row(row: int) -> PyNDArray(2D)slice_col(col: int) -> PyNDArray(2D)slice_range(axis: int, start: int, end: int) -> PyNDArrayfancy_index(indices: list[int]) -> PyNDArray(1D input)gather(axis: int, indices: list[int]) -> PyNDArray
m = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3])
v = ferrix.PyNDArray([10.0, 20.0, 30.0, 40.0], [4])
print(m.slice_row(1).shape())
print(m.slice_col(2).shape())
print(m.slice_range(1, 0, 2).shape())
print(v.fancy_index([3, 1, 1]).shape())
print(m.gather(1, [2, 0]).shape())Masking and conditional operations:
boolean_mask(mask: PyBoolArray) -> PyNDArraymasked_fill(mask: PyBoolArray, value: float) -> None(in-place)where_(condition: PyBoolArray, other: PyNDArray) -> PyNDArray
x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])
y = ferrix.PyNDArray([9.0, 9.0, 9.0, 9.0], [2, 2])
mask = ferrix.PyBoolArray([True, False, True, False], [2, 2])
print(x.boolean_mask(mask).shape())
x.masked_fill(mask, -1.0)
print(x.where_(mask, y).shape())Mutation and cumulative operations:
set_slice(axis: int, start: int, end: int, value: float) -> None(in-place)cumsum() -> PyNDArray(flattened cumulative sum)
x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2])
x.set_slice(0, 0, 1, 0.0)
print(x.get([0, 1]))
print(x.cumsum().shape())Constructor and methods:
PyBoolArray(data: list[bool], shape: list[int]) -> PyBoolArrayshape() -> list[int]
mask = ferrix.PyBoolArray([True, False, True, False], [2, 2])
print(mask.shape())- Invalid shapes, indices, or axis values raise Python exceptions backed by Rust panics.
- Most binary operations require shape compatibility.
fancy_indexis for 1D arrays.transposeandslice_row/slice_colrequire 2D arrays.
- Arrays are row-major and stride-aware.
- Shape checks and index checks panic on invalid inputs in the Rust core.
matmul_blasis currently a compatibility method that falls back to the same implementation asmatmul.- Parallel execution is used for selected element-wise operations through
rayon.
The following numbers are from repository examples and should be treated as indicative (hardware and build mode dependent):
| Operation | ferrix | NumPy |
|---|---|---|
| matmul 512x512 | 6.5 ms | 2.1 ms |
| relu 1M elements | 0.52 ms | 0.76 ms |
This project uses maturin to build the Python extension from Rust.
- Rust toolchain (
cargo,rustc) - Python
>=3.8 maturin
python -m pip install --upgrade pip maturin
maturin developAfter this, import ferrix uses your local build in the active virtual environment.
rm -rf dist
maturin build --release --out dist
maturin sdist --out distFor a complete PyPI release runbook, see:
docs/pypi-guide.md
It includes versioning rules, artifact validation, upload options, and post-release checks.
Key files and directories:
src/lib.rs: Rust tensor core (NDArrayandNDArrayView)src/python.rs: Python bindings (PyNDArray,PyBoolArray)src/tests/: Rust unit teststest_ferrix.py: Python API testspyproject.toml: Python packaging metadata and build backendCargo.toml: Rust crate configuration
Contributions are welcome.
For contributions and release process details, start from docs/pypi-guide.md and open a PR with clear change notes.