Skip to content

Numba JIT Acceleration for uLib Recipes

uLib provides first-class support for high-performance JIT compilation and parallelization in Python recipes via Numba.


1. Architectural Model: Orchestration vs Computation

When writing recipes in gcompose, Python scripts follow a clean two-tier architecture:

┌────────────────────────────────────────────────────────┐
│  Python Orchestration Layer: def run(ctx):            │
│  - Reads inputs / outputs (e.g. img = Input0)          │
│  - Coordinates scene graph and objects context         │
│  - Calls the compiled @njit kernel                    │
│  - Emits Qt/C++ signals (img.Updated())                │
└──────────────────────────┬─────────────────────────────┘
                           │ Dispatches raw buffers & domain objects
                           ▼
┌────────────────────────────────────────────────────────┐
│  High-Performance Numba Layer: @njit(parallel=True)    │
│  - Native LLVM compilation without GIL overhead        │
│  - Direct zero-copy C++ buffer access                  │
│  - Multi-threaded loop execution (OpenMP / TBB)        │
│  - Vector and Matrix linear algebra                    │
└────────────────────────────────────────────────────────┘

Why run(ctx) is not @njit: ctx is an ObjectsContext / Scene instance that manages C++ object lifecycles, Qt signals, and dynamic node connections. These framework reflection mechanisms require Python bytecode execution. Numerical computations (voxel processing, linear transformations, filters) are separated into @njit kernels.


2. Using Images (VoxImage) in Numba Kernels

uLib.numba_ext registers Math.VoxImage directly with Numba. You can pass img as an argument to any @njit function without manual conversion or copying.

Supported Operations inside @njit

Operation Syntax Description
Get Dimensions u, v, w = img.GetDims() Returns a 3-element tuple (dim_x, dim_y, dim_z)
Get Strides sx, sy, sz = img.GetIncrements() Returns memory increments
3D Voxel Read val = img[i, j, k] Direct 3D float voxel access
3D Voxel Write img[i, j, k] = val Direct 3D float voxel write
Method Read val = img.GetValue([i, j, k]) Supports lists, tuples, or flat index
Method Write img.SetValue([i, j, k], val) Supports lists, tuples, or flat index
Voxel Count Read cnt = img.GetCount([i, j, k]) Reads integer hit count
Voxel Count Write img.SetCount([i, j, k], cnt) Writes integer hit count
Voxel Total Count n = len(img) Total voxel count u * v * w
Bounds Check is_valid = img.IsInsideGrid([i, j, k]) Fast boundary check

Parallel Multi-Threading Pattern

Use prange from numba to distribute outer loops across all available CPU threads:

import numpy as np
from numba import njit, prange

Input0 = InputPort("Image")

@njit(fastmath=True, parallel=True)
def apply_voxel_filter(img, scale_factor):
    u, v, w = img.GetDims()
    for i in prange(u):
        for j in range(v):
            for k in range(w):
                val = img[i, j, k]
                img[i, j, k] = val * scale_factor

def run(ctx):
    img = Input0
    apply_voxel_filter(img, 1.5)
    img.Updated()

3. Using Vectors and Matrices (Vector3f, Vector3i, Matrix3f, Matrix4f)

Vectors (Vector3f / Vector3i)

In Python, vectors can be created via Math.Vector3f([x, y, z]), Math.Vector3i([x, y, z]), tuples, or 1D NumPy arrays.

Inside @njit kernels: - Tuples / Coordinates: (x, y, z) are lightweight, zero-overhead, and unbox automatically. - 1D NumPy Arrays: np.array([x, y, z], dtype=np.float32) allow standard vector algebra (np.dot, np.cross, np.linalg.norm).

@njit(fastmath=True)
def distance_to_center(point, center):
    # point and center can be 3-tuples or 1D float arrays
    dx = point[0] - center[0]
    dy = point[1] - center[1]
    dz = point[2] - center[2]
    return np.sqrt(dx*dx + dy*dy + dz*dz)

Matrices (Matrix3f / Matrix4f / Transforms)

In Python and Numba, matrices are represented as 2D NumPy float arrays (np.ndarray of shape (3, 3) or (4, 4)):

@njit(fastmath=True)
def transform_point(mat4x4, point3):
    # point3: (x, y, z)
    # mat4x4: 4x4 float array
    x = mat4x4[0, 0]*point3[0] + mat4x4[0, 1]*point3[1] + mat4x4[0, 2]*point3[2] + mat4x4[0, 3]
    y = mat4x4[1, 0]*point3[0] + mat4x4[1, 1]*point3[1] + mat4x4[1, 2]*point3[2] + mat4x4[1, 3]
    z = mat4x4[2, 0]*point3[0] + mat4x4[2, 1]*point3[1] + mat4x4[2, 2]*point3[2] + mat4x4[2, 3]
    return (x, y, z)

4. UI Integration in gcompose

The gcompose Recipe editor provides instant shortcuts for inserting Numba acceleration templates:

  1. New Recipes: Automatically generated with import numpy as np.
  2. ⚡ Numba Menu:
  3. ⚡ Parallel Kernel: Adds required from numba import njit, prange imports and inserts an optimized @njit(fastmath=True, parallel=True) 3D voxel loop.
  4. ⚡ Compiled Function: Inserts a standard @njit(fastmath=True) kernel template.