Skip to content

API reference

solve

Solve A x = right_hand_side for a sparse matrix A given in CSR format.

Solves A^T x = right_hand_side instead when transpose is set, using the same factorization Pardiso would use for A: no separate factorization of A^T is needed. Runs analysis, factorization, and solve in a single call, and does not keep the factorization around afterward: use PardisoSolver instead if the same pattern will be solved again. Works under jit and vmap, batching over values, right_hand_side, or both.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/primitive.py
def solve(
    indptr, indices, values, right_hand_side, *, matrix_type: MatrixType, transpose: bool = False
):
    """Solve A x = right_hand_side for a sparse matrix A given in CSR format.

    Solves A^T x = right_hand_side instead when transpose is set, using the
    same factorization Pardiso would use for A: no separate factorization of
    A^T is needed. Runs analysis, factorization, and solve in a single call,
    and does not keep the factorization around afterward: use PardisoSolver
    instead if the same pattern will be solved again. Works under jit and
    vmap, batching over values, right_hand_side, or both.
    """
    check_matrix_type_supported(matrix_type)
    check_csr_arrays(indptr, indices, values)
    # check_upper_triangular returns indices threaded through a runtime
    # check (see its docstring): the returned value, not the original
    # indices, must be what actually reaches the solve below, or the check
    # is dead-code-eliminated whenever indptr/indices are traced.
    indices = check_upper_triangular(indptr, indices, matrix_type)
    solve_core = _make_solve_core(MatrixType(matrix_type), transpose)
    return solve_core(indptr, indices, values, right_hand_side)

PardisoSolver

Reuses a single Pardiso factorization across many solves.

The sparsity pattern (indptr and indices) is fixed for the solver's lifetime. The three Pardiso stages are kept as separate calls so callers control exactly what work happens on each one:

  • analyze() runs the symbolic phase once for the pattern.
  • factorize() runs the first numeric factorization, and requires a prior analyze().
  • refactorize() updates the numeric factorization for new values on the same pattern, and requires a prior factorize(). It runs the same Pardiso phase as factorize(); the separate name and precondition make the reuse explicit at the call site.
  • solve() solves against whatever factorization is currently stored, and requires a prior factorize().
  • refactor_and_solve() factorizes for new values and solves in one call, reusing the analysis and requiring only a prior analyze(). It keeps no reference to the values, so unlike factorize() plus solve() it is safe to call from inside a jitted function where the values and right-hand side are tracers.

PardisoSolver must be used as a context manager. Its native memory is released in exit, not in a destructor: Python does not guarantee when or whether del runs, so relying on it could leave the native factorization alive far longer than intended, or leak it entirely if the interpreter is shutting down.

with PardisoSolver(indptr, indices, matrix_type=MatrixType.REAL_NONSYMMETRIC) as solver:
    solver.analyze(values)
    solver.factorize(values)
    x = solver.solve(b)
Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/solver.py
class PardisoSolver:
    """Reuses a single Pardiso factorization across many solves.

    The sparsity pattern (indptr and indices) is fixed for the solver's
    lifetime. The three Pardiso stages are kept as separate calls so callers
    control exactly what work happens on each one:

    - analyze() runs the symbolic phase once for the pattern.
    - factorize() runs the first numeric factorization, and requires a prior
      analyze().
    - refactorize() updates the numeric factorization for new values on the
      same pattern, and requires a prior factorize(). It runs the same
      Pardiso phase as factorize(); the separate name and precondition make
      the reuse explicit at the call site.
    - solve() solves against whatever factorization is currently stored, and
      requires a prior factorize().
    - refactor_and_solve() factorizes for new values and solves in one call,
      reusing the analysis and requiring only a prior analyze(). It keeps no
      reference to the values, so unlike factorize() plus solve() it is safe
      to call from inside a jitted function where the values and right-hand
      side are tracers.

    PardisoSolver must be used as a context manager. Its native memory is
    released in __exit__, not in a destructor: Python does not guarantee when
    or whether __del__ runs, so relying on it could leave the native
    factorization alive far longer than intended, or leak it entirely if the
    interpreter is shutting down.

        with PardisoSolver(indptr, indices, matrix_type=MatrixType.REAL_NONSYMMETRIC) as solver:
            solver.analyze(values)
            solver.factorize(values)
            x = solver.solve(b)
    """

    def __init__(self, indptr, indices, *, matrix_type: MatrixType):
        check_matrix_type_supported(matrix_type)
        if indptr.dtype.name != "int32" or indices.dtype.name != "int32":
            raise TypeError("indptr and indices must have dtype int32.")
        self._indptr = indptr
        self._indices = check_upper_triangular(indptr, indices, matrix_type)
        self._matrix_type = matrix_type
        self._dimension = matrix_dimension(indptr)
        # The handle is only obtained from analyze(), which allocates it on
        # the native side, so there is nothing to hold until then.
        self._handle = None
        self._values = None
        self._entered = False
        self._closed = False
        self._analyzed = False
        self._factorized = False

    def __enter__(self) -> PardisoSolver:
        self._entered = True
        return self

    def __exit__(self, exc_type, exc_value, traceback) -> None:
        self.close()

    def close(self) -> None:
        """Release the native factorization. Called automatically by __exit__."""
        if not self._closed:
            if self._handle is not None:
                primitive.release(self._handle)
            self._closed = True

    def _check_usable(self) -> None:
        if not self._entered:
            raise RuntimeError(
                "PardisoSolver must be used as a context manager: "
                "'with PardisoSolver(...) as solver: ...'."
            )
        if self._closed:
            raise RuntimeError("PardisoSolver is closed and can no longer be used.")

    def analyze(self, values) -> None:
        """Run the symbolic analysis (fill-reducing ordering) for the stored pattern.

        Takes a representative values array because Pardiso's default
        heuristics for non-symmetric matrices, scaling and matching, look at
        the numeric values during analysis. The permutation and scaling this
        produces stay valid for a later factorize() call with different
        values on the same pattern, so this only needs to run once.
        """
        self._check_usable()
        check_csr_arrays(self._indptr, self._indices, values)
        self._handle = primitive.analyze(
            self._indptr,
            self._indices,
            values,
            matrix_type=self._matrix_type,
        )
        self._analyzed = True

    def factorize(self, values) -> None:
        """Run the first numeric factorization for values. Requires a prior analyze()."""
        self._check_usable()
        if not self._analyzed:
            raise RuntimeError("factorize() requires analyze() to have been called first.")
        self._run_numeric_factorization(values)
        self._factorized = True

    def refactorize(self, values) -> None:
        """Update the numeric factorization with new values on the same pattern.

        Requires a prior factorize(). Skips the analysis phase, which is the
        cheap path when only the matrix values change between solves.
        """
        self._check_usable()
        if not self._factorized:
            raise RuntimeError("refactorize() requires factorize() to have been called first.")
        self._run_numeric_factorization(values)

    def _run_numeric_factorization(self, values) -> None:
        check_csr_arrays(self._indptr, self._indices, values)
        self._handle = primitive.factor(
            self._handle,
            self._indptr,
            self._indices,
            values,
            matrix_type=self._matrix_type,
        )
        self._values = values

    def solve(self, right_hand_side, *, transpose: bool = False):
        """Solve against the current factorization. Requires a prior factorize().

        Solves against A^T instead of A when transpose is set, reusing the
        same factorization: no extra factorize() call is needed to switch
        between the two, and consecutive calls with different transpose
        values are safe.
        """
        self._check_usable()
        if not self._factorized:
            raise RuntimeError("solve() requires factorize() to have been called first.")
        stacked_right_hand_side = right_hand_side[None, :]
        solution = primitive.solve_stateful(
            self._handle,
            self._indptr,
            self._indices,
            self._values,
            stacked_right_hand_side,
            matrix_type=self._matrix_type,
            transpose=transpose,
        )
        return solution[0]

    def refactor_and_solve(self, values, right_hand_side, *, transpose: bool = False):
        """Factorize for values and solve in one call, reusing the analysis.

        Requires a prior analyze(). Runs the numeric factorization and the
        solve as one combined Pardiso step (phase 23), reusing the symbolic
        analysis rather than re-running it. Because it is a single FFI call it
        stays correct inside a jitted function, where a separate factorize()
        and solve() would not be ordered, and it keeps no reference to values
        on the solver, so values and right_hand_side may be tracers.

        Solves against A^T instead of A when transpose is set.
        """
        self._check_usable()
        if not self._analyzed:
            raise RuntimeError("refactor_and_solve() requires analyze() to have been called first.")
        check_csr_arrays(self._indptr, self._indices, values)
        stacked_right_hand_side = right_hand_side[None, :]
        solution = primitive.factor_and_solve_stateful(
            self._handle,
            self._indptr,
            self._indices,
            values,
            stacked_right_hand_side,
            matrix_type=self._matrix_type,
            transpose=transpose,
        )
        return solution[0]

analyze(values)

Run the symbolic analysis (fill-reducing ordering) for the stored pattern.

Takes a representative values array because Pardiso's default heuristics for non-symmetric matrices, scaling and matching, look at the numeric values during analysis. The permutation and scaling this produces stay valid for a later factorize() call with different values on the same pattern, so this only needs to run once.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/solver.py
def analyze(self, values) -> None:
    """Run the symbolic analysis (fill-reducing ordering) for the stored pattern.

    Takes a representative values array because Pardiso's default
    heuristics for non-symmetric matrices, scaling and matching, look at
    the numeric values during analysis. The permutation and scaling this
    produces stay valid for a later factorize() call with different
    values on the same pattern, so this only needs to run once.
    """
    self._check_usable()
    check_csr_arrays(self._indptr, self._indices, values)
    self._handle = primitive.analyze(
        self._indptr,
        self._indices,
        values,
        matrix_type=self._matrix_type,
    )
    self._analyzed = True

close()

Release the native factorization. Called automatically by exit.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/solver.py
def close(self) -> None:
    """Release the native factorization. Called automatically by __exit__."""
    if not self._closed:
        if self._handle is not None:
            primitive.release(self._handle)
        self._closed = True

factorize(values)

Run the first numeric factorization for values. Requires a prior analyze().

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/solver.py
def factorize(self, values) -> None:
    """Run the first numeric factorization for values. Requires a prior analyze()."""
    self._check_usable()
    if not self._analyzed:
        raise RuntimeError("factorize() requires analyze() to have been called first.")
    self._run_numeric_factorization(values)
    self._factorized = True

refactor_and_solve(values, right_hand_side, *, transpose=False)

Factorize for values and solve in one call, reusing the analysis.

Requires a prior analyze(). Runs the numeric factorization and the solve as one combined Pardiso step (phase 23), reusing the symbolic analysis rather than re-running it. Because it is a single FFI call it stays correct inside a jitted function, where a separate factorize() and solve() would not be ordered, and it keeps no reference to values on the solver, so values and right_hand_side may be tracers.

Solves against A^T instead of A when transpose is set.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/solver.py
def refactor_and_solve(self, values, right_hand_side, *, transpose: bool = False):
    """Factorize for values and solve in one call, reusing the analysis.

    Requires a prior analyze(). Runs the numeric factorization and the
    solve as one combined Pardiso step (phase 23), reusing the symbolic
    analysis rather than re-running it. Because it is a single FFI call it
    stays correct inside a jitted function, where a separate factorize()
    and solve() would not be ordered, and it keeps no reference to values
    on the solver, so values and right_hand_side may be tracers.

    Solves against A^T instead of A when transpose is set.
    """
    self._check_usable()
    if not self._analyzed:
        raise RuntimeError("refactor_and_solve() requires analyze() to have been called first.")
    check_csr_arrays(self._indptr, self._indices, values)
    stacked_right_hand_side = right_hand_side[None, :]
    solution = primitive.factor_and_solve_stateful(
        self._handle,
        self._indptr,
        self._indices,
        values,
        stacked_right_hand_side,
        matrix_type=self._matrix_type,
        transpose=transpose,
    )
    return solution[0]

refactorize(values)

Update the numeric factorization with new values on the same pattern.

Requires a prior factorize(). Skips the analysis phase, which is the cheap path when only the matrix values change between solves.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/solver.py
def refactorize(self, values) -> None:
    """Update the numeric factorization with new values on the same pattern.

    Requires a prior factorize(). Skips the analysis phase, which is the
    cheap path when only the matrix values change between solves.
    """
    self._check_usable()
    if not self._factorized:
        raise RuntimeError("refactorize() requires factorize() to have been called first.")
    self._run_numeric_factorization(values)

solve(right_hand_side, *, transpose=False)

Solve against the current factorization. Requires a prior factorize().

Solves against A^T instead of A when transpose is set, reusing the same factorization: no extra factorize() call is needed to switch between the two, and consecutive calls with different transpose values are safe.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/solver.py
def solve(self, right_hand_side, *, transpose: bool = False):
    """Solve against the current factorization. Requires a prior factorize().

    Solves against A^T instead of A when transpose is set, reusing the
    same factorization: no extra factorize() call is needed to switch
    between the two, and consecutive calls with different transpose
    values are safe.
    """
    self._check_usable()
    if not self._factorized:
        raise RuntimeError("solve() requires factorize() to have been called first.")
    stacked_right_hand_side = right_hand_side[None, :]
    solution = primitive.solve_stateful(
        self._handle,
        self._indptr,
        self._indices,
        self._values,
        stacked_right_hand_side,
        matrix_type=self._matrix_type,
        transpose=transpose,
    )
    return solution[0]

MatrixType

Bases: IntEnum

Pardiso matrix type codes, matching the mtype parameter.

Only the real-valued members can be used in this version of the package, since it works with float64 values throughout. The complex members are included so the full set of matrix types Pardiso itself supports is documented here, and raise NotImplementedError if selected.

The members whose values are mathematically symmetric or Hermitian (REAL_SYMMETRIC_POSITIVE_DEFINITE, REAL_SYMMETRIC_INDEFINITE, COMPLEX_HERMITIAN_POSITIVE_DEFINITE, COMPLEX_HERMITIAN_INDEFINITE, and COMPLEX_SYMMETRIC) require the CSR arrays to hold only the upper triangle, including the diagonal, not the full matrix: passing the full matrix corrupts Pardiso's factorization instead of raising a clear error. check_upper_triangular enforces this.

The structurally symmetric members (REAL_STRUCTURALLY_SYMMETRIC and COMPLEX_STRUCTURALLY_SYMMETRIC) only assume a symmetric sparsity pattern, not symmetric values, so they need the full matrix like the nonsymmetric members do.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/matrix.py
class MatrixType(enum.IntEnum):
    """Pardiso matrix type codes, matching the `mtype` parameter.

    Only the real-valued members can be used in this version of the package,
    since it works with float64 values throughout. The complex members are
    included so the full set of matrix types Pardiso itself supports is
    documented here, and raise NotImplementedError if selected.

    The members whose values are mathematically symmetric or Hermitian
    (REAL_SYMMETRIC_POSITIVE_DEFINITE, REAL_SYMMETRIC_INDEFINITE,
    COMPLEX_HERMITIAN_POSITIVE_DEFINITE, COMPLEX_HERMITIAN_INDEFINITE, and
    COMPLEX_SYMMETRIC) require the CSR arrays to hold only the upper
    triangle, including the diagonal, not the full matrix: passing the full
    matrix corrupts Pardiso's factorization instead of raising a clear
    error. check_upper_triangular enforces this.

    The structurally symmetric members (REAL_STRUCTURALLY_SYMMETRIC and
    COMPLEX_STRUCTURALLY_SYMMETRIC) only assume a symmetric sparsity
    pattern, not symmetric values, so they need the full matrix like the
    nonsymmetric members do.
    """

    REAL_STRUCTURALLY_SYMMETRIC = 1
    """Real values, symmetric sparsity pattern, no assumption on the values themselves."""

    REAL_SYMMETRIC_POSITIVE_DEFINITE = 2
    """Real, symmetric, and positive definite. The cheapest and most stable case to factor."""

    REAL_SYMMETRIC_INDEFINITE = -2
    """Real and symmetric, but not guaranteed positive definite."""

    COMPLEX_STRUCTURALLY_SYMMETRIC = 3
    """Complex values, symmetric sparsity pattern. Not yet supported: requires complex values."""

    COMPLEX_HERMITIAN_POSITIVE_DEFINITE = 4
    """Complex and Hermitian positive definite. Not yet supported: requires complex values."""

    COMPLEX_HERMITIAN_INDEFINITE = -4
    """Complex and Hermitian, not guaranteed positive definite. Not yet supported."""

    COMPLEX_SYMMETRIC = 6
    """Complex and symmetric. Not yet supported: requires complex values."""

    REAL_NONSYMMETRIC = 11
    """Real, with no assumed symmetry. The general-purpose default matrix type."""

    COMPLEX_NONSYMMETRIC = 13
    """Complex, with no assumed symmetry. Not yet supported: requires complex values."""

COMPLEX_HERMITIAN_INDEFINITE = -4 class-attribute instance-attribute

Complex and Hermitian, not guaranteed positive definite. Not yet supported.

COMPLEX_HERMITIAN_POSITIVE_DEFINITE = 4 class-attribute instance-attribute

Complex and Hermitian positive definite. Not yet supported: requires complex values.

COMPLEX_NONSYMMETRIC = 13 class-attribute instance-attribute

Complex, with no assumed symmetry. Not yet supported: requires complex values.

COMPLEX_STRUCTURALLY_SYMMETRIC = 3 class-attribute instance-attribute

Complex values, symmetric sparsity pattern. Not yet supported: requires complex values.

COMPLEX_SYMMETRIC = 6 class-attribute instance-attribute

Complex and symmetric. Not yet supported: requires complex values.

REAL_NONSYMMETRIC = 11 class-attribute instance-attribute

Real, with no assumed symmetry. The general-purpose default matrix type.

REAL_STRUCTURALLY_SYMMETRIC = 1 class-attribute instance-attribute

Real values, symmetric sparsity pattern, no assumption on the values themselves.

REAL_SYMMETRIC_INDEFINITE = -2 class-attribute instance-attribute

Real and symmetric, but not guaranteed positive definite.

REAL_SYMMETRIC_POSITIVE_DEFINITE = 2 class-attribute instance-attribute

Real, symmetric, and positive definite. The cheapest and most stable case to factor.

primitive

The low-level building blocks PardisoSolver is built from, for callers who want to manage a factorization's handle explicitly. See Building on the low-level primitives.

Run the analyze (phase 11) step and allocate a fresh native factorization.

Returns the handle for the new factorization, an int64 array value that every later call (factor, solve_stateful, factor_and_solve_stateful, release) takes as an input. Threading the handle as data, rather than addressing the native state by a Python-side id, is what lets XLA order the whole analyze-factor-solve-release lifecycle and lets it run inside a jitted function.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/primitive.py
def analyze(indptr, indices, values, *, matrix_type: MatrixType):
    """Run the analyze (phase 11) step and allocate a fresh native factorization.

    Returns the handle for the new factorization, an int64 array value that
    every later call (factor, solve_stateful, factor_and_solve_stateful,
    release) takes as an input. Threading the handle as data, rather than
    addressing the native state by a Python-side id, is what lets XLA order
    the whole analyze-factor-solve-release lifecycle and lets it run inside a
    jitted function.
    """
    dimension = indptr.shape[0] - 1
    handle, _status = jax.ffi.ffi_call(
        "pardiso_mkl_jax_analyze",
        (
            jax.ShapeDtypeStruct((), jnp.int64),
            jax.ShapeDtypeStruct((), jnp.int32),
        ),
        has_side_effect=True,
    )(
        indptr,
        indices,
        values,
        matrix_type=np.int64(matrix_type),
        dimension=np.int64(dimension),
    )
    return handle

Run the numeric factorization (phase 22) step against handle.

Returns handle unchanged, so a later call that consumes this function's return value is ordered after the factorization it performed.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/primitive.py
def factor(handle, indptr, indices, values, *, matrix_type: MatrixType):
    """Run the numeric factorization (phase 22) step against handle.

    Returns handle unchanged, so a later call that consumes this function's
    return value is ordered after the factorization it performed.
    """
    dimension = indptr.shape[0] - 1
    handle_out, _status = jax.ffi.ffi_call(
        "pardiso_mkl_jax_factor",
        (
            jax.ShapeDtypeStruct((), jnp.int64),
            jax.ShapeDtypeStruct((), jnp.int32),
        ),
        has_side_effect=True,
    )(
        handle,
        indptr,
        indices,
        values,
        matrix_type=np.int64(matrix_type),
        dimension=np.int64(dimension),
    )
    return handle_out

Solve (phase 33) against the factorization already produced for handle.

transpose solves A^T x = right_hand_side instead of A x = right_hand_side, reusing the same factorization: no call to factor() is needed to switch between the two for a given handle.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/primitive.py
def solve_stateful(
    handle,
    indptr,
    indices,
    values,
    right_hand_side,
    *,
    matrix_type: MatrixType,
    transpose: bool = False,
):
    """Solve (phase 33) against the factorization already produced for handle.

    transpose solves A^T x = right_hand_side instead of A x = right_hand_side,
    reusing the same factorization: no call to factor() is needed to switch
    between the two for a given handle.
    """
    dimension = indptr.shape[0] - 1
    number_of_right_hand_sides = right_hand_side.shape[0]
    return jax.ffi.ffi_call(
        "pardiso_mkl_jax_solve",
        jax.ShapeDtypeStruct(right_hand_side.shape, jnp.float64),
        has_side_effect=True,
    )(
        handle,
        indptr,
        indices,
        values,
        right_hand_side,
        matrix_type=np.int64(matrix_type),
        dimension=np.int64(dimension),
        number_of_right_hand_sides=np.int64(number_of_right_hand_sides),
        transpose_mode=_transpose_mode(transpose),
    )

Refactor and solve in one call, reusing the analysis produced for handle.

Runs Pardiso's combined phase 23 (numeric factorization then solve) for the given values against the stored analysis. This is a single FFI call, so the factorization and the solve stay ordered under jit, unlike a factor() followed by a separate solve_stateful(): those share no data dependency XLA must honor, so the solve could otherwise run before the factor.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/primitive.py
def factor_and_solve_stateful(
    handle,
    indptr,
    indices,
    values,
    right_hand_side,
    *,
    matrix_type: MatrixType,
    transpose: bool = False,
):
    """Refactor and solve in one call, reusing the analysis produced for handle.

    Runs Pardiso's combined phase 23 (numeric factorization then solve) for the
    given values against the stored analysis. This is a single FFI call, so the
    factorization and the solve stay ordered under jit, unlike a factor()
    followed by a separate solve_stateful(): those share no data dependency XLA
    must honor, so the solve could otherwise run before the factor.
    """
    dimension = indptr.shape[0] - 1
    number_of_right_hand_sides = right_hand_side.shape[0]
    return jax.ffi.ffi_call(
        "pardiso_mkl_jax_factor_solve",
        jax.ShapeDtypeStruct(right_hand_side.shape, jnp.float64),
        has_side_effect=True,
    )(
        handle,
        indptr,
        indices,
        values,
        right_hand_side,
        matrix_type=np.int64(matrix_type),
        dimension=np.int64(dimension),
        number_of_right_hand_sides=np.int64(number_of_right_hand_sides),
        transpose_mode=_transpose_mode(transpose),
    )

Free the native factorization state for handle.

Source code in .venv/lib/python3.12/site-packages/pardiso_mkl_jax/primitive.py
def release(handle):
    """Free the native factorization state for handle."""
    return jax.ffi.ffi_call(
        "pardiso_mkl_jax_release",
        jax.ShapeDtypeStruct((), jnp.int32),
        has_side_effect=True,
    )(handle)