Skip to content

Commit 8c47410

Browse files
PTNobelclaude
andcommitted
Add QDLDL_symbolic and QDLDL_numeric for separate factorization phases
Split QDLDL_factor into QDLDL_symbolic (computes Lp/Li sparsity pattern without numeric values) and QDLDL_numeric (computes Lx/D/Dinv given pre-computed pattern). QDLDL_factor now calls both internally, preserving backward compatibility. This enables symbolic-only use and efficient refactorization when the sparsity pattern is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent af62a6f commit 8c47410

4 files changed

Lines changed: 330 additions & 15 deletions

File tree

include/qdldl.h

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,79 @@ QDLDL_API QDLDL_int QDLDL_etree(const QDLDL_int n, const QDLDL_int* Ap, const QD
9494
QDLDL_int* work, QDLDL_int* Lnz, QDLDL_int* etree);
9595

9696

97+
/**
98+
* Compute the symbolic factorization (sparsity pattern) of the L factor
99+
* for a quasidefinite matrix in compressed sparse column form, where
100+
* the input matrix is assumed to contain data for the upper triangular
101+
* part of A only, and there are no duplicate indices.
102+
*
103+
* Returns the column pointers Lp and row indices Li of L, which together
104+
* define the sparsity pattern of L in CSC format. This function uses
105+
* only the sparsity structure of A (not the numeric values), so the
106+
* result can be reused across multiple numeric factorizations with
107+
* QDLDL_numeric when the sparsity pattern of A remains the same.
108+
*
109+
* Does not use MALLOC. It is assumed that Lp is allocated with n+1
110+
* elements, Li is allocated with sumLnz elements (as returned by
111+
* QDLDL_etree), and iwork is allocated with 3*n elements.
112+
*
113+
* @param n number of columns in L and A (both square)
114+
* @param Ap column pointers (size n+1) for columns of A (not modified)
115+
* @param Ai row indices of A. Has Ap[n] elements (not modified)
116+
* @param Lp column pointers (size n+1) for columns of L
117+
* @param Li row indices of L. Has sumLnz elements
118+
* @param Lnz count of nonzeros in each column of L below diagonal,
119+
* as given by QDLDL_etree (not modified)
120+
* @param etree elimination tree as given by QDLDL_etree (not modified)
121+
* @param bwork working array of bools. Length is n
122+
* @param iwork working array of integers. Length is 3*n
123+
*
124+
*/
125+
QDLDL_API void QDLDL_symbolic(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int* Ai,
126+
QDLDL_int* Lp, QDLDL_int* Li, const QDLDL_int* Lnz,
127+
const QDLDL_int* etree, QDLDL_bool* bwork, QDLDL_int* iwork);
128+
129+
130+
/**
131+
* Compute the numeric LDL decomposition for a quasidefinite matrix,
132+
* given a pre-computed symbolic factorization (sparsity pattern of L).
133+
*
134+
* This function computes the numeric values Lx, D, and Dinv using
135+
* the sparsity pattern (Lp, Li) previously computed by QDLDL_symbolic.
136+
* It can be called repeatedly with different Ax values (but the same
137+
* sparsity pattern) to efficiently refactorize.
138+
*
139+
* Does not use MALLOC.
140+
*
141+
* @param n number of columns in L and A (both square)
142+
* @param Ap column pointers (size n+1) for columns of A (not modified)
143+
* @param Ai row indices of A. Has Ap[n] elements (not modified)
144+
* @param Ax data of A. Has Ap[n] elements (not modified)
145+
* @param Lp column pointers (size n+1) for columns of L (not modified,
146+
* as computed by QDLDL_symbolic)
147+
* @param Li row indices of L (not modified, as computed by QDLDL_symbolic)
148+
* @param Lx data of L. Has Lp[n] elements
149+
* @param D vectorized factor D. Length is n
150+
* @param Dinv reciprocal of D. Length is n
151+
* @param Lnz count of nonzeros in each column of L below diagonal,
152+
* as given by QDLDL_etree (not modified)
153+
* @param etree elimination tree as given by QDLDL_etree (not modified)
154+
* @param bwork working array of bools. Length is n
155+
* @param iwork working array of integers. Length is 3*n
156+
* @param fwork working array of floats. Length is n
157+
* @return Returns a count of the number of positive elements
158+
* in D. Returns -1 and exits immediately if any element
159+
* of D evaluates exactly to zero (matrix is not quasidefinite
160+
* or otherwise LDL factorisable)
161+
*
162+
*/
163+
QDLDL_API QDLDL_int QDLDL_numeric(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int* Ai,
164+
const QDLDL_float* Ax, const QDLDL_int* Lp, const QDLDL_int* Li,
165+
QDLDL_float* Lx, QDLDL_float* D, QDLDL_float* Dinv,
166+
const QDLDL_int* Lnz, const QDLDL_int* etree, QDLDL_bool* bwork,
167+
QDLDL_int* iwork, QDLDL_float* fwork);
168+
169+
97170
/**
98171
* Compute an LDL decomposition for a quasidefinite matrix
99172
* in compressed sparse column form, where the input matrix is
@@ -102,6 +175,10 @@ QDLDL_API QDLDL_int QDLDL_etree(const QDLDL_int n, const QDLDL_int* Ap, const QD
102175
*
103176
* Returns factors L, D and Dinv = 1./D.
104177
*
178+
* This is equivalent to calling QDLDL_symbolic followed by QDLDL_numeric.
179+
* If you need to refactorize with different numeric values but the same
180+
* sparsity pattern, use QDLDL_symbolic once and QDLDL_numeric repeatedly.
181+
*
105182
* Does not use MALLOC. It is assumed that L will be a compressed
106183
* sparse column matrix with data (n,Lp,Li,Lx) with sufficient space
107184
* allocated, with a number of nonzeros equal to the count given

src/qdldl.c

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,99 @@ QDLDL_int QDLDL_etree(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int* A
9191
}
9292

9393

94-
QDLDL_int QDLDL_factor(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int* Ai,
95-
const QDLDL_float* Ax, QDLDL_int* Lp, QDLDL_int* Li, QDLDL_float* Lx,
96-
QDLDL_float* D, QDLDL_float* Dinv, const QDLDL_int* Lnz,
97-
const QDLDL_int* etree, QDLDL_bool* bwork, QDLDL_int* iwork,
98-
QDLDL_float* fwork) {
94+
void QDLDL_symbolic(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int* Ai, QDLDL_int* Lp,
95+
QDLDL_int* Li, const QDLDL_int* Lnz, const QDLDL_int* etree,
96+
QDLDL_bool* bwork, QDLDL_int* iwork) {
97+
QDLDL_int i = 0;
98+
QDLDL_int k = 0;
99+
QDLDL_int bidx = 0;
100+
QDLDL_int nextIdx = 0;
101+
QDLDL_int nnzE = 0;
102+
QDLDL_int nnzY = 0;
103+
QDLDL_int tmpIdx = 0;
104+
QDLDL_int* yIdx;
105+
QDLDL_int* elimBuffer;
106+
QDLDL_int* LNextSpaceInCol;
107+
QDLDL_bool* yMarkers;
108+
109+
// Partition working memory into pieces
110+
yMarkers = bwork;
111+
yIdx = iwork;
112+
elimBuffer = iwork + n;
113+
LNextSpaceInCol = iwork + n * 2;
114+
115+
Lp[0] = 0; // First column starts at index zero
116+
117+
for(i = 0; i < n; i++) {
118+
// Compute L column pointers (cumulative sum of Lnz)
119+
Lp[i + 1] = Lp[i] + Lnz[i];
120+
121+
// Initialize markers and next-space trackers
122+
yMarkers[i] = QDLDL_UNUSED;
123+
LNextSpaceInCol[i] = Lp[i];
124+
}
125+
126+
// Start from 1: column 0 of L has no subdiagonal entries
127+
for(k = 1; k < n; k++) {
128+
nnzY = 0;
129+
130+
tmpIdx = Ap[k + 1];
131+
132+
for(i = Ap[k]; i < tmpIdx; i++) {
133+
bidx = Ai[i];
134+
135+
// Skip the diagonal entry
136+
if(bidx == k) {
137+
continue;
138+
}
139+
140+
// Walk the elimination tree to find the nonzero pattern
141+
nextIdx = bidx;
142+
143+
if(yMarkers[nextIdx] == QDLDL_UNUSED) {
144+
145+
yMarkers[nextIdx] = QDLDL_USED;
146+
elimBuffer[0] = nextIdx;
147+
nnzE = 1;
148+
149+
nextIdx = etree[bidx];
150+
151+
while(nextIdx != QDLDL_UNKNOWN && nextIdx < k) {
152+
if(yMarkers[nextIdx] == QDLDL_USED)
153+
break;
154+
155+
yMarkers[nextIdx] = QDLDL_USED;
156+
elimBuffer[nnzE] = nextIdx;
157+
nnzE++;
158+
nextIdx = etree[nextIdx];
159+
}
160+
161+
while(nnzE) {
162+
yIdx[nnzY++] = elimBuffer[--nnzE];
163+
}
164+
}
165+
}
166+
167+
// Place the row indices into Li
168+
for(i = (nnzY - 1); i >= 0; i--) {
169+
QDLDL_int cidx = yIdx[i];
170+
tmpIdx = LNextSpaceInCol[cidx];
171+
172+
Li[tmpIdx] = k;
173+
LNextSpaceInCol[cidx]++;
174+
175+
// Reset marker
176+
yMarkers[cidx] = QDLDL_UNUSED;
177+
}
178+
}
179+
}
180+
181+
182+
QDLDL_int QDLDL_numeric(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int* Ai,
183+
const QDLDL_float* Ax, const QDLDL_int* Lp, const QDLDL_int* Li,
184+
QDLDL_float* Lx, QDLDL_float* D, QDLDL_float* Dinv, const QDLDL_int* Lnz,
185+
const QDLDL_int* etree, QDLDL_bool* bwork, QDLDL_int* iwork,
186+
QDLDL_float* fwork) {
99187
QDLDL_int i = 0;
100188
QDLDL_int j = 0;
101189
QDLDL_int k = 0;
@@ -120,16 +208,7 @@ QDLDL_int QDLDL_factor(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int*
120208
LNextSpaceInCol = iwork + n * 2;
121209
yVals = fwork;
122210

123-
124-
Lp[0] = 0; // First column starts at index zero
125-
126211
for(i = 0; i < n; i++) {
127-
// Compute L column indices
128-
Lp[i + 1] = Lp[i] + Lnz[i]; // cumsum, total at the end
129-
130-
// Set all Yidx to be 'unused' initially
131-
// in each column of L, the next available space
132-
// to start is just the first space in the column
133212
yMarkers[i] = QDLDL_UNUSED;
134213
yVals[i] = 0.0;
135214
D[i] = 0.0;
@@ -228,7 +307,6 @@ QDLDL_int QDLDL_factor(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int*
228307
// Now I have the cidx^th element of y = L\b.
229308
// so compute the corresponding element of
230309
// this row of L and put it into the right place
231-
Li[tmpIdx] = k;
232310
Lx[tmpIdx] = yVals_cidx * Dinv[cidx];
233311

234312
// D[k] -= yVals[cidx]*yVals[cidx]*Dinv[cidx];
@@ -261,6 +339,18 @@ QDLDL_int QDLDL_factor(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int*
261339
return positiveValuesInD;
262340
}
263341

342+
343+
QDLDL_int QDLDL_factor(const QDLDL_int n, const QDLDL_int* Ap, const QDLDL_int* Ai,
344+
const QDLDL_float* Ax, QDLDL_int* Lp, QDLDL_int* Li, QDLDL_float* Lx,
345+
QDLDL_float* D, QDLDL_float* Dinv, const QDLDL_int* Lnz,
346+
const QDLDL_int* etree, QDLDL_bool* bwork, QDLDL_int* iwork,
347+
QDLDL_float* fwork) {
348+
349+
QDLDL_symbolic(n, Ap, Ai, Lp, Li, Lnz, etree, bwork, iwork);
350+
351+
return QDLDL_numeric(n, Ap, Ai, Ax, Lp, Li, Lx, D, Dinv, Lnz, etree, bwork, iwork, fwork);
352+
}
353+
264354
// Solves (L+I)x = b
265355
void QDLDL_Lsolve(const QDLDL_int n, const QDLDL_int* Lp, const QDLDL_int* Li,
266356
const QDLDL_float* Lx, QDLDL_float* x) {

tests/qdldl_tester.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ int ldl_factor_solve(QDLDL_int An, QDLDL_int* Ap, QDLDL_int* Ai, QDLDL_float* Ax
4545
#include "test_two_by_two.h"
4646
#include "test_zero_on_diag.h"
4747
#include "test_osqp_kkt.h"
48+
#include "test_symbolic.h"
4849

4950

5051
int tests_run = 0;
@@ -60,6 +61,8 @@ static char* all_tests() {
6061
mu_run_test(test_two_by_two);
6162
mu_run_test(test_zero_on_diag);
6263
mu_run_test(test_osqp_kkt);
64+
mu_run_test(test_symbolic);
65+
mu_run_test(test_symbolic_refactor);
6366

6467
return 0;
6568
}

0 commit comments

Comments
 (0)