|
1 | 1 | # ferrix |
2 | 2 |
|
3 | | -A fast, memory-safe tensor library in Rust — exposed as a Python package. |
| 3 | +A fast, memory-safe tensor library implemented in Rust and exposed as a Python package. |
4 | 4 |
|
5 | | -## Install |
| 5 | +`ferrix` provides an `NDArray` core with stride-based indexing, vectorized math operations, slicing/reshaping utilities, and Python bindings via `pyo3`. |
| 6 | + |
| 7 | +## Why ferrix |
| 8 | + |
| 9 | +- Rust core for predictable performance and memory safety. |
| 10 | +- Python-friendly API for quick experimentation. |
| 11 | +- Multi-dimensional arrays with row-major strides. |
| 12 | +- Useful tensor operations for ML and numerical workloads. |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +Install from PyPI: |
6 | 17 |
|
7 | 18 | ```bash |
8 | | -pip install ferrix |
| 19 | +python -m pip install ferrix |
9 | 20 | ``` |
10 | 21 |
|
11 | | -## Usage |
| 22 | +## Quick Start |
12 | 23 |
|
13 | 24 | ```python |
14 | 25 | import ferrix |
15 | 26 |
|
16 | | -a = ferrix.PyNDArray([1.0,2.0,3.0,4.0], [2,2]) |
17 | | -b = ferrix.PyNDArray([5.0,6.0,7.0,8.0], [2,2]) |
| 27 | +# Create two 2x2 arrays |
| 28 | +a = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 29 | +b = ferrix.PyNDArray([5.0, 6.0, 7.0, 8.0], [2, 2]) |
| 30 | + |
| 31 | +# Core arithmetic |
| 32 | +print(a.add(b).get([0, 0])) # 6.0 |
| 33 | +print(a.mul(b).get([1, 1])) # 32.0 |
| 34 | +print(a.scale(0.5).get([1, 0])) # 1.5 |
| 35 | + |
| 36 | +# Matrix multiplication |
| 37 | +print(a.matmul(b).get([0, 0])) # 19.0 |
| 38 | +print(a.matmul_blas(b).get([0, 0])) # Compatibility API; currently same as matmul |
| 39 | + |
| 40 | +# Activations and reductions |
| 41 | +print(a.relu().get([0, 0])) |
| 42 | +print(a.softmax().sum()) |
| 43 | +print(a.sum(), a.mean(), a.argmax()) |
| 44 | + |
| 45 | +# Shape transforms |
| 46 | +print(a.transpose().shape()) |
| 47 | +print(a.reshape([4]).shape()) |
| 48 | +``` |
| 49 | + |
| 50 | +## Complete API Reference (Python) |
| 51 | + |
| 52 | +`ferrix` exposes two classes: |
| 53 | + |
| 54 | +- `ferrix.PyNDArray` for numeric tensors (`f64`) |
| 55 | +- `ferrix.PyBoolArray` for boolean masks |
| 56 | + |
| 57 | +### `PyNDArray` |
| 58 | + |
| 59 | +Constructor: |
| 60 | + |
| 61 | +- `PyNDArray(data: list[float], shape: list[int]) -> PyNDArray` |
| 62 | + |
| 63 | +```python |
| 64 | +x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 65 | +``` |
| 66 | + |
| 67 | +Introspection and element access: |
| 68 | + |
| 69 | +- `shape() -> list[int]` |
| 70 | +- `get(index: list[int]) -> float` |
| 71 | + |
| 72 | +```python |
| 73 | +x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 74 | +print(x.shape()) # [2, 2] |
| 75 | +print(x.get([1, 0])) # 3.0 |
| 76 | +``` |
| 77 | + |
| 78 | +Reductions: |
| 79 | + |
| 80 | +- `sum() -> float` |
| 81 | +- `mean() -> float` |
| 82 | +- `argmax() -> int` (index in flattened row-major order) |
18 | 83 |
|
19 | | -c = a.matmul_blas(b) |
20 | | -print(c.get([0,0])) # 19.0 |
| 84 | +```python |
| 85 | +x = ferrix.PyNDArray([1.0, 5.0, 3.0, 4.0], [2, 2]) |
| 86 | +print(x.sum()) # 13.0 |
| 87 | +print(x.mean()) # 3.25 |
| 88 | +print(x.argmax()) # 1 |
| 89 | +``` |
| 90 | + |
| 91 | +Element-wise math: |
| 92 | + |
| 93 | +- `add(other: PyNDArray) -> PyNDArray` |
| 94 | +- `mul(other: PyNDArray) -> PyNDArray` |
| 95 | +- `scale(scalar: float) -> PyNDArray` |
21 | 96 |
|
22 | | -print(a.relu().get([0,0])) |
23 | | -print(a.softmax().sum()) # 1.0 |
| 97 | +```python |
| 98 | +a = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 99 | +b = ferrix.PyNDArray([5.0, 6.0, 7.0, 8.0], [2, 2]) |
| 100 | + |
| 101 | +print(a.add(b).get([0, 1])) # 8.0 |
| 102 | +print(a.mul(b).get([1, 0])) # 21.0 |
| 103 | +print(a.scale(10).get([1, 1])) # 40.0 |
24 | 104 | ``` |
25 | 105 |
|
26 | | -## Benchmarks |
| 106 | +Activation functions: |
| 107 | + |
| 108 | +- `relu() -> PyNDArray` |
| 109 | +- `sigmoid() -> PyNDArray` |
| 110 | +- `softmax() -> PyNDArray` |
| 111 | + |
| 112 | +```python |
| 113 | +x = ferrix.PyNDArray([-1.0, 0.0, 1.0], [1, 3]) |
| 114 | +print(x.relu().get([0, 0])) |
| 115 | +print(x.sigmoid().get([0, 2])) |
| 116 | +print(x.softmax().sum()) |
| 117 | +``` |
| 118 | + |
| 119 | +Matrix operations: |
| 120 | + |
| 121 | +- `matmul(other: PyNDArray) -> PyNDArray` |
| 122 | +- `matmul_blas(other: PyNDArray) -> PyNDArray` |
| 123 | + |
| 124 | +```python |
| 125 | +a = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 126 | +b = ferrix.PyNDArray([5.0, 6.0, 7.0, 8.0], [2, 2]) |
| 127 | + |
| 128 | +print(a.matmul(b).get([0, 0])) |
| 129 | +print(a.matmul_blas(b).get([0, 0])) |
| 130 | +``` |
| 131 | + |
| 132 | +Note: `matmul_blas` is currently a compatibility API that uses the same backend behavior as `matmul`. |
| 133 | + |
| 134 | +Reshape and transpose: |
| 135 | + |
| 136 | +- `reshape(new_shape: list[int]) -> PyNDArray` |
| 137 | +- `transpose() -> PyNDArray` (2D transpose) |
| 138 | + |
| 139 | +```python |
| 140 | +x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 141 | +print(x.reshape([4]).shape()) |
| 142 | +print(x.transpose().shape()) |
| 143 | +``` |
| 144 | + |
| 145 | +Slicing and indexing: |
| 146 | + |
| 147 | +- `slice_row(row: int) -> PyNDArray` (2D) |
| 148 | +- `slice_col(col: int) -> PyNDArray` (2D) |
| 149 | +- `slice_range(axis: int, start: int, end: int) -> PyNDArray` |
| 150 | +- `fancy_index(indices: list[int]) -> PyNDArray` (1D input) |
| 151 | +- `gather(axis: int, indices: list[int]) -> PyNDArray` |
| 152 | + |
| 153 | +```python |
| 154 | +m = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]) |
| 155 | +v = ferrix.PyNDArray([10.0, 20.0, 30.0, 40.0], [4]) |
| 156 | + |
| 157 | +print(m.slice_row(1).shape()) |
| 158 | +print(m.slice_col(2).shape()) |
| 159 | +print(m.slice_range(1, 0, 2).shape()) |
| 160 | +print(v.fancy_index([3, 1, 1]).shape()) |
| 161 | +print(m.gather(1, [2, 0]).shape()) |
| 162 | +``` |
| 163 | + |
| 164 | +Masking and conditional operations: |
| 165 | + |
| 166 | +- `boolean_mask(mask: PyBoolArray) -> PyNDArray` |
| 167 | +- `masked_fill(mask: PyBoolArray, value: float) -> None` (in-place) |
| 168 | +- `where_(condition: PyBoolArray, other: PyNDArray) -> PyNDArray` |
| 169 | + |
| 170 | +```python |
| 171 | +x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 172 | +y = ferrix.PyNDArray([9.0, 9.0, 9.0, 9.0], [2, 2]) |
| 173 | +mask = ferrix.PyBoolArray([True, False, True, False], [2, 2]) |
| 174 | + |
| 175 | +print(x.boolean_mask(mask).shape()) |
| 176 | +x.masked_fill(mask, -1.0) |
| 177 | +print(x.where_(mask, y).shape()) |
| 178 | +``` |
| 179 | + |
| 180 | +Mutation and cumulative operations: |
| 181 | + |
| 182 | +- `set_slice(axis: int, start: int, end: int, value: float) -> None` (in-place) |
| 183 | +- `cumsum() -> PyNDArray` (flattened cumulative sum) |
| 184 | + |
| 185 | +```python |
| 186 | +x = ferrix.PyNDArray([1.0, 2.0, 3.0, 4.0], [2, 2]) |
| 187 | +x.set_slice(0, 0, 1, 0.0) |
| 188 | +print(x.get([0, 1])) |
| 189 | +print(x.cumsum().shape()) |
| 190 | +``` |
| 191 | + |
| 192 | +### `PyBoolArray` |
| 193 | + |
| 194 | +Constructor and methods: |
| 195 | + |
| 196 | +- `PyBoolArray(data: list[bool], shape: list[int]) -> PyBoolArray` |
| 197 | +- `shape() -> list[int]` |
| 198 | + |
| 199 | +```python |
| 200 | +mask = ferrix.PyBoolArray([True, False, True, False], [2, 2]) |
| 201 | +print(mask.shape()) |
| 202 | +``` |
| 203 | + |
| 204 | +## Error Behavior |
| 205 | + |
| 206 | +- Invalid shapes, indices, or axis values raise Python exceptions backed by Rust panics. |
| 207 | +- Most binary operations require shape compatibility. |
| 208 | +- `fancy_index` is for 1D arrays. |
| 209 | +- `transpose` and `slice_row`/`slice_col` require 2D arrays. |
| 210 | + |
| 211 | +## Feature Notes |
| 212 | + |
| 213 | +- Arrays are row-major and stride-aware. |
| 214 | +- Shape checks and index checks panic on invalid inputs in the Rust core. |
| 215 | +- `matmul_blas` is currently a compatibility method that falls back to the same implementation as `matmul`. |
| 216 | +- Parallel execution is used for selected element-wise operations through `rayon`. |
| 217 | + |
| 218 | +## Benchmarks (Indicative) |
| 219 | + |
| 220 | +The following numbers are from repository examples and should be treated as indicative (hardware and build mode dependent): |
27 | 221 |
|
28 | 222 | | Operation | ferrix | NumPy | |
29 | | -|-----------|--------|-------| |
30 | | -| matmul 512×512 (BLAS) | 6.5ms | 2.1ms | |
31 | | -| relu 1M elements | 0.52ms | 0.76ms | |
32 | | - |
33 | | -## Features |
34 | | - |
35 | | -- N-dimensional arrays with stride-based indexing |
36 | | -- Zero-copy slicing, reshape, transpose |
37 | | -- Element-wise ops: add, mul, scale, relu, sigmoid, softmax |
38 | | -- Matrix multiply via OpenBLAS FFI |
39 | | -- Parallel ops via rayon |
40 | | -- Boolean masking, fancy indexing, gather |
| 223 | +|---|---:|---:| |
| 224 | +| matmul 512x512 | 6.5 ms | 2.1 ms | |
| 225 | +| relu 1M elements | 0.52 ms | 0.76 ms | |
| 226 | + |
| 227 | +## Build From Source (Optional) |
| 228 | + |
| 229 | +This project uses `maturin` to build the Python extension from Rust. |
| 230 | + |
| 231 | +### Prerequisites |
| 232 | + |
| 233 | +- Rust toolchain (`cargo`, `rustc`) |
| 234 | +- Python `>=3.8` |
| 235 | +- `maturin` |
| 236 | + |
| 237 | +### Local development build |
| 238 | + |
| 239 | +```bash |
| 240 | +python -m pip install --upgrade pip maturin |
| 241 | +maturin develop |
| 242 | +``` |
| 243 | + |
| 244 | +After this, `import ferrix` uses your local build in the active virtual environment. |
| 245 | + |
| 246 | +### Build wheel/sdist |
| 247 | + |
| 248 | +```bash |
| 249 | +rm -rf dist |
| 250 | +maturin build --release --out dist |
| 251 | +maturin sdist --out dist |
| 252 | +``` |
| 253 | + |
| 254 | +## Release and Publishing |
| 255 | + |
| 256 | +For a complete PyPI release runbook, see: |
| 257 | + |
| 258 | +- `docs/pypi-guide.md` |
| 259 | + |
| 260 | +It includes versioning rules, artifact validation, upload options, and post-release checks. |
| 261 | + |
| 262 | +## Repository Layout |
| 263 | + |
| 264 | +Key files and directories: |
| 265 | + |
| 266 | +- `src/lib.rs`: Rust tensor core (`NDArray` and `NDArrayView`) |
| 267 | +- `src/python.rs`: Python bindings (`PyNDArray`, `PyBoolArray`) |
| 268 | +- `src/tests/`: Rust unit tests |
| 269 | +- `test_ferrix.py`: Python API tests |
| 270 | +- `pyproject.toml`: Python packaging metadata and build backend |
| 271 | +- `Cargo.toml`: Rust crate configuration |
| 272 | + |
| 273 | +## Contributing |
| 274 | + |
| 275 | +Contributions are welcome. |
| 276 | + |
| 277 | +For contributions and release process details, start from `docs/pypi-guide.md` and open a PR with clear change notes. |
0 commit comments