Skip to content

Feature: Numba/JIT-compiled constraint matrix assembly for 3-4x faster model building #599

@MaykThewessen

Description

@MaykThewessen

Describe the feature you'd like to see

JIT-compile the sparse matrix assembly in matrices.py using Numba to reduce model build time by 3-4x.

Profiling in #271 (PyOptInterface benchmarks) showed that matrix construction — converting labeled xarray variables/constraints into sparse COO matrices for solver handoff — accounts for 55% of total solve time (8s out of 14.5s). The solver itself is often faster than the formulation layer.

For production workloads (120+ buses, 8760h, scenario sweeps), the model building step is the dominant bottleneck. This is especially painful for iterative workflows like MGA, rolling horizon, and sensitivity analysis where the model is rebuilt many times.

Motivation

Model Size Current Build Time Estimated with Numba Improvement
50 buses, 8760h ~5s ~1.5s 3x
120 buses, 8760h ~15s ~4s 3-4x
500 buses (PyPSA-Eur scale), 8760h ~60s+ ~15s 4x

Estimates are based on typical Numba JIT speedups for sparse matrix assembly in similar scientific computing workloads (scipy sparse construction, finite element assembly).

Why this matters more now

Proposed implementation

Target: MatrixAccessor in matrices.py

The core hot path is the loop that constructs COO triplets (row, col, data) from labeled xarray DataArrays. This is pure numerical work operating on integer indices and float coefficients — ideal for Numba.

Steps

  1. Profile matrices.py to isolate the specific functions where time is spent (COO triplet construction, index alignment, coefficient extraction)
  2. Extract the inner numerical loops into standalone functions with pure NumPy array signatures (no xarray/pandas inside the hot path)
  3. Apply @numba.njit(cache=True) to the extracted functions — this compiles them to machine code with zero Python interpreter overhead
  4. Parallel variant using @numba.njit(parallel=True) with numba.prange for the outer loop over constraints (each constraint's matrix contribution is independent)
  5. Fallback path for when Numba is not installed — keep current pure-Python implementation as default, use Numba when available
  6. Benchmark on standard test cases (PyPSA-Eur 37-bus, 128-bus, 256-bus electricity models)

Example pattern

import numba

@numba.njit(cache=True)
def _build_coo_triplets(var_labels, coeff_data, row_offsets, col_offsets):
    """Assemble COO sparse matrix entries from constraint coefficients.
    
    Pure numerical loop — no Python objects, no xarray, no pandas.
    """
    n_entries = len(var_labels)
    rows = np.empty(n_entries, dtype=np.int64)
    cols = np.empty(n_entries, dtype=np.int64)
    data = np.empty(n_entries, dtype=np.float64)
    
    for i in range(n_entries):
        rows[i] = row_offsets[var_labels[i]]
        cols[i] = col_offsets[var_labels[i]]
        data[i] = coeff_data[i]
    
    return rows, cols, data

Dependency consideration

Numba is already standard in the scientific Python ecosystem (depends only on NumPy + LLVM). It could be an optional dependency:

try:
    from linopy._numba_matrices import _build_coo_triplets
except ImportError:
    from linopy._matrices_fallback import _build_coo_triplets

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions