Skip to content

Add low-level split-solve API for advanced users#18

Closed
flaport wants to merge 4 commits intomainfrom
issue-16-split-solve
Closed

Add low-level split-solve API for advanced users#18
flaport wants to merge 4 commits intomainfrom
issue-16-split-solve

Conversation

@flaport
Copy link
Owner

@flaport flaport commented Jan 20, 2026

Summary

Addresses #16 by exposing klu_analyze, klu_factor, and klu_solve separately for advanced users who need to optimize Newton-Raphson solvers by reusing symbolic analysis.

New Functions in klujax_cpp

True split-solve API (handle-based):

  • klu_analyze(Bp, Bi) - Symbolic analysis on CSC sparsity pattern. Returns a handle.
  • klu_factor_f64(symbolic_handle, Bp, Bi, Bx) - Numeric factorization (float64). Returns a handle.
  • klu_factor_c128(symbolic_handle, Bp, Bi, Bx) - Numeric factorization (complex128). Returns a handle.
  • klu_solve_f64(symbolic_handle, numeric_handle, b) - Solve using pre-computed factorizations (float64).
  • klu_solve_c128(symbolic_handle, numeric_handle, b) - Solve using pre-computed factorizations (complex128).
  • klu_free_symbolic(handle) - Free a symbolic factorization handle.
  • klu_free_numeric(handle) - Free a numeric factorization handle.

FFI-based helpers:

  • coo_to_csc - Convert COO format (Ai, Aj) to CSC format (Bp, Bi, Bk)
  • solve_csc_f64 / solve_csc_c128 - Solve using pre-computed CSC structure

Usage Example (Newton-Raphson)

import numpy as np
import klujax_cpp

# COO to CSC conversion (one-time)
Bp, Bi, Bx = convert_coo_to_csc(...)  # your helper

# Symbolic analysis (one-time for fixed sparsity pattern)
sym_handle = klujax_cpp.klu_analyze(Bp, Bi)

# Newton-Raphson loop
for iteration in range(max_iterations):
    # Update Jacobian values (same sparsity, new values)
    Bx = compute_jacobian_values(...)
    
    # Numeric factorization (each iteration)
    num_handle = klujax_cpp.klu_factor_f64(sym_handle, Bp, Bi, Bx)
    
    # Solve
    dx = klujax_cpp.klu_solve_f64(sym_handle, num_handle, residual)
    
    # Free numeric handle (symbolic is reused!)
    klujax_cpp.klu_free_numeric(num_handle)
    
    x = x + dx
    if converged:
        break

# Cleanup
klujax_cpp.klu_free_symbolic(sym_handle)

The key benefit is that klu_analyze (the expensive symbolic analysis) only needs to be called once per sparsity pattern, while klu_factor can be called repeatedly with different values.

Note: These are low-level functions accessible via klujax_cpp module. The high-level klujax.solve API remains unchanged.

Test plan

  • All 46 tests pass (including 5 new tests for split-solve API)
  • New functions are exported and accessible via klujax_cpp
  • Pre-commit hooks pass

Closes #16

🤖 Generated with Claude Code

flaport and others added 4 commits January 20, 2026 13:41
Exposes the following new FFI functions via pycapsule:
- coo_to_csc: Convert COO format to CSC format (returns Bp, Bi, Bk)
- solve_csc_f64: Solve using pre-computed CSC structure (float64)
- solve_csc_c128: Solve using pre-computed CSC structure (complex128)

This allows advanced users to reuse the COO->CSC conversion when
solving multiple systems with the same sparsity pattern, which is
useful for Newton-Raphson solvers in circuit simulation.

Closes #16

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements handle-based caching to allow reusing symbolic analysis
across multiple solves. This addresses the core request in issue #16
for Newton-Raphson optimization.

New functions:
- klu_analyze(Bp, Bi) -> symbolic_handle
- klu_factor_f64/c128(sym_handle, Bp, Bi, Bx) -> numeric_handle
- klu_solve_f64/c128(sym_handle, num_handle, b) -> x
- klu_free_symbolic/numeric(handle)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@cdaunt
Copy link
Contributor

cdaunt commented Jan 21, 2026

Hi @flaport

Close, but not quite there yet :)

I will try and find some time to dig through in more detail but the quick feedback is that these do not appear to be jit compatible (and I did not see it in the test suite either)

All the tests use numpy and not jnp.numpy, is this intentional? For a non-linear+transitent solver the goal would be to pass jnp.arrays to a jitted function

`
File ~/code/circulus/circulus/solvers/strategies.py:366, in KLUSplitSolver._solve_impl(self, all_vals, residual)
    364     num_handle = klujax_cpp.klu_factor_c128(self.sym_handle, Bp, Bi, Bx)
    365 else:
--> 366     num_handle = klujax_cpp.klu_factor_f64(self.sym_handle, Bp, Bi, Bx)
    368 # 5. Solve
    369 if is_complex:

TypeError: klu_factor_f64(): incompatible function arguments. The following argument types are supported:
    1. (symbolic_handle: int, Bp: numpy.ndarray[numpy.int32], Bi: numpy.ndarray[numpy.int32], Bx: numpy.ndarray[numpy.float64]) -> int

Invoked with: 1, JitTracer<int64[11]>, JitTracer<int32[46]>, JitTracer<float64[46]>`

@flaport
Copy link
Owner Author

flaport commented Feb 10, 2026

Closing in favor of #19

@flaport flaport closed this Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Possibility of exposing symbolic output

2 participants