From 9776821f16beb5b29e75a6c9171c26e353ba006b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 09:53:22 +0000 Subject: [PATCH 1/8] Initial plan From 2260e0888ae539d630b285f1b6d4c88ae14f6589 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:05:48 +0000 Subject: [PATCH 2/8] Symmetrize Hessian in-place during setup (0.5*(H+H^T)) Co-authored-by: darnstrom <55484604+darnstrom@users.noreply.github.com> Agent-Logs-Url: https://github.com/darnstrom/daqp/sessions/64ce043c-45a2-4415-af04-33c3502e4028 --- interfaces/daqp-eigen/CMakeLists.txt | 2 + .../tests/07_symmetrize_hessian.cpp | 102 ++++++++++++++++++ src/utils.c | 13 +++ 3 files changed, 117 insertions(+) create mode 100644 interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp diff --git a/interfaces/daqp-eigen/CMakeLists.txt b/interfaces/daqp-eigen/CMakeLists.txt index 756b471..db9e189 100644 --- a/interfaces/daqp-eigen/CMakeLists.txt +++ b/interfaces/daqp-eigen/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(03_update tests/03_update.cpp) add_executable(04_slack_sign tests/04_slack_sign.cpp) add_executable(05_warmstart tests/05_warmstart.cpp) add_executable(06_general_hessian tests/06_general_hessian.cpp) +add_executable(07_symmetrize_hessian tests/07_symmetrize_hessian.cpp) set(TARGETS 00_basic_qp @@ -27,6 +28,7 @@ set(TARGETS 04_slack_sign 05_warmstart 06_general_hessian + 07_symmetrize_hessian ) foreach(TARGET ${TARGETS}) diff --git a/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp b/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp new file mode 100644 index 0000000..44d60e6 --- /dev/null +++ b/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp @@ -0,0 +1,102 @@ +#include +#include +#include + +// Verify that DAQP symmetrizes H before factorization. +// +// Key properties tested: +// 1. A fully symmetric H gives the correct reference solution (regression). +// 2. A lower-triangular H is no longer silently treated as a diagonal matrix +// (the pre-fix bug: DAQP read only the upper triangle which was all zero +// except the diagonal, so it ignored the off-diagonal structure). +// 3. A nearly-symmetric H (tiny numerical noise in one triangle) gives the +// same solution as the perfectly symmetric reference. +int main() { + double precision = 1e-6; + + // Symmetric positive-definite Hessian (full storage) + Eigen::MatrixXd H_full(2, 2); + H_full << 4, 2, + 2, 3; + + Eigen::VectorXd f = (Eigen::VectorXd(2) << 1, -1).finished(); + + // Simple bounds only (no general constraints), so ms == m. + Eigen::Matrix A(0, 2); + Eigen::VectorXd bu = (Eigen::VectorXd(2) << 5, 5).finished(); + Eigen::VectorXd bl = (Eigen::VectorXd(2) << -5, -5).finished(); + Eigen::VectorXi sense = Eigen::VectorXi::Zero(2); + Eigen::VectorXi break_points = Eigen::VectorXi::Zero(0); + + // 1. Reference: solve with full symmetric H. + // Unconstrained optimum: x* = -H^{-1}*f = [-0.625, 0.75] + EigenDAQPResult ref = daqp_solve(H_full, f, A, bu, bl, sense, break_points); + if (ref.exitflag <= 0) { + std::cerr << "Reference solve failed with exitflag " << ref.exitflag << std::endl; + return 1; + } + + // 2. Diagonal-only H – used as the "wrong" baseline. + // Before the fix, providing a lower-triangular H would produce exactly + // the same result as this because the upper triangle was read as zeros. + Eigen::MatrixXd H_diag = Eigen::MatrixXd::Zero(2, 2); + H_diag(0, 0) = H_full(0, 0); + H_diag(1, 1) = H_full(1, 1); + EigenDAQPResult res_diag = daqp_solve(H_diag, f, A, bu, bl, sense, break_points); + if (res_diag.exitflag <= 0) { + std::cerr << "Diagonal solve failed with exitflag " << res_diag.exitflag << std::endl; + return 1; + } + + // 3. Lower-triangular H (upper triangle set to zero). + // Before the fix: DAQP reads only the upper triangle, sees all-zero + // off-diagonals, and treats the matrix as diagonal → wrong solution. + // After the fix: DAQP symmetrizes first, so the off-diagonal structure + // is preserved (at 0.5× the original value) → solution differs from diagonal. + Eigen::MatrixXd H_lower = H_full.triangularView(); + EigenDAQPResult res_lower = daqp_solve(H_lower, f, A, bu, bl, sense, break_points); + if (res_lower.exitflag <= 0) { + std::cerr << "Lower-triangle solve failed with exitflag " << res_lower.exitflag + << std::endl; + return 1; + } + + // 4. Nearly-symmetric H: full H with tiny noise in one off-diagonal entry. + // Symmetrization should average the two triangles and give the same + // solution as the perfectly symmetric reference. + Eigen::MatrixXd H_noisy = H_full; + H_noisy(0, 1) += 1e-8; // tiny asymmetry + EigenDAQPResult res_noisy = daqp_solve(H_noisy, f, A, bu, bl, sense, break_points); + if (res_noisy.exitflag <= 0) { + std::cerr << "Noisy-H solve failed with exitflag " << res_noisy.exitflag << std::endl; + return 1; + } + + // --- Checks --- + + // Full symmetric H gives the reference solution. + bool ref_ok = ref.get_primal().isApprox( + (Eigen::VectorXd(2) << -0.625, 0.75).finished(), precision); + + // After symmetrization, lower-triangular H must NOT equal the diagonal result. + // (Before the fix they would be identical.) + bool lower_not_diagonal = !res_lower.get_primal().isApprox( + res_diag.get_primal(), precision); + + // Nearly-symmetric H: symmetrized result matches the reference. + bool noisy_ok = res_noisy.get_primal().isApprox(ref.get_primal(), 1e-5); + + if (!ref_ok) + std::cerr << "Reference solution incorrect: " + << ref.get_primal().transpose() << std::endl; + if (!lower_not_diagonal) + std::cerr << "Lower-triangle H was incorrectly treated as diagonal!\n" + << "lower: " << res_lower.get_primal().transpose() << "\n" + << "diag: " << res_diag.get_primal().transpose() << std::endl; + if (!noisy_ok) + std::cerr << "Noisy-H result differs from reference:\n" + << "noisy: " << res_noisy.get_primal().transpose() << "\n" + << "ref: " << ref.get_primal().transpose() << std::endl; + + return (ref_ok && lower_not_diagonal && noisy_ok) ? 0 : 1; +} diff --git a/src/utils.c b/src/utils.c index f65d206..0f6682a 100644 --- a/src/utils.c +++ b/src/utils.c @@ -187,6 +187,19 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } work->n_prox = 0; + // Symmetrize H in-place: H[i,j] <- 0.5*(H[i,j] + H[j,i]) for i < j. + // This ensures DAQP produces correct results whether the caller supplies a + // full symmetric matrix, only the upper triangle, or only the lower triangle. + if(!is_factored){ + c_float val; + for(i = 0; i < n; i++) + for(j = i+1; j < n; j++){ + val = 0.5*(H[i*n+j] + H[j*n+i]); + H[i*n+j] = val; + H[j*n+i] = val; + } + } + // Check if Diagonal int is_diagonal = 1; for (i=0, disp=1; i Date: Sun, 22 Mar 2026 11:34:39 +0100 Subject: [PATCH 3/8] Make sure Rinv always point to allocated data at the start of Rinv --- src/utils.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/utils.c b/src/utils.c index 0f6682a..cc2ec0d 100644 --- a/src/utils.c +++ b/src/utils.c @@ -181,6 +181,9 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ c_float eps = work->settings->eps_prox; c_float zero_tol = work->settings->zero_tol; + + // Make sure Rinv points to allocated data + if(work->RinvD != NULL){ work->Rinv = work->RinvD; work->RinvD = NULL; } // Reset the semi-proximal mask for this factorization if(work->prox_mask != NULL){ for(i = 0; i < n; i++) work->prox_mask[i] = 0; @@ -213,8 +216,7 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ // Diagonal Case if(is_diagonal){ - if(work->Rinv != NULL){ work->RinvD = work->Rinv; work->Rinv = NULL; } - + work->RinvD = work->Rinv; work->Rinv = NULL; disp = 0; for(i=0; iRinvD != NULL){ work->Rinv = work->RinvD; work->RinvD = NULL; } - // Cholesky if (is_factored) { for(i=0, disp=0; i Date: Sun, 22 Mar 2026 10:59:38 +0000 Subject: [PATCH 4/8] Symmetrize H lazily via Rinv buffer without modifying user's H Co-authored-by: darnstrom <55484604+darnstrom@users.noreply.github.com> Agent-Logs-Url: https://github.com/darnstrom/daqp/sessions/2c2e641d-2aac-4845-9acd-8471eba8ac5a --- include/constants.h | 3 + .../tests/07_symmetrize_hessian.cpp | 7 +- src/utils.c | 113 +++++++++++++----- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/include/constants.h b/include/constants.h index 0107be4..7303533 100644 --- a/include/constants.h +++ b/include/constants.h @@ -27,6 +27,9 @@ extern "C" { #define DAQP_DEFAULT_SING_TOL (3.7e-11) #define DAQP_DEFAULT_REFACTOR_TOL 1e-9 #define DAQP_DEFAULT_EPS_PROX 1e-6 +// Tolerance for detecting that H is not symmetric: if |H[0,1] - H[1,0]| exceeds +// this value, DAQP symmetrizes H via its internal buffer before factorization. +#define DAQP_SYMMETRY_TOL 1e-9 // MACROS #define DAQP_ARSUM(x) ((x)*(x+1)/2) diff --git a/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp b/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp index 44d60e6..4a87acf 100644 --- a/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp +++ b/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp @@ -54,12 +54,14 @@ int main() { // After the fix: DAQP symmetrizes first, so the off-diagonal structure // is preserved (at 0.5× the original value) → solution differs from diagonal. Eigen::MatrixXd H_lower = H_full.triangularView(); + Eigen::MatrixXd H_lower_copy = H_lower; // snapshot to check H is not mutated EigenDAQPResult res_lower = daqp_solve(H_lower, f, A, bu, bl, sense, break_points); if (res_lower.exitflag <= 0) { std::cerr << "Lower-triangle solve failed with exitflag " << res_lower.exitflag << std::endl; return 1; } + bool lower_not_mutated = H_lower.isApprox(H_lower_copy); // 4. Nearly-symmetric H: full H with tiny noise in one off-diagonal entry. // Symmetrization should average the two triangles and give the same @@ -83,6 +85,9 @@ int main() { bool lower_not_diagonal = !res_lower.get_primal().isApprox( res_diag.get_primal(), precision); + if (!lower_not_mutated) + std::cerr << "H was mutated by daqp_solve (should be read-only)!" << std::endl; + // Nearly-symmetric H: symmetrized result matches the reference. bool noisy_ok = res_noisy.get_primal().isApprox(ref.get_primal(), 1e-5); @@ -98,5 +103,5 @@ int main() { << "noisy: " << res_noisy.get_primal().transpose() << "\n" << "ref: " << ref.get_primal().transpose() << std::endl; - return (ref_ok && lower_not_diagonal && noisy_ok) ? 0 : 1; + return (ref_ok && lower_not_diagonal && lower_not_mutated && noisy_ok) ? 0 : 1; } diff --git a/src/utils.c b/src/utils.c index cc2ec0d..f5855b8 100644 --- a/src/utils.c +++ b/src/utils.c @@ -190,28 +190,48 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } work->n_prox = 0; - // Symmetrize H in-place: H[i,j] <- 0.5*(H[i,j] + H[j,i]) for i < j. - // This ensures DAQP produces correct results whether the caller supplies a - // full symmetric matrix, only the upper triangle, or only the lower triangle. - if(!is_factored){ - c_float val; - for(i = 0; i < n; i++) - for(j = i+1; j < n; j++){ - val = 0.5*(H[i*n+j] + H[j*n+i]); - H[i*n+j] = val; - H[j*n+i] = val; - } + // Check whether symmetrization is needed by comparing the first + // off-diagonal pair H[0,1] vs H[1,0] (only meaningful when n > 1). + // If they differ by more than 1e-9, the caller supplied only one triangle + // (or a genuinely asymmetric matrix) and we must symmetrize before + // factorization. H is user-owned so we must not modify it; instead we + // pack the symmetrized upper triangle directly into the pre-allocated + // Rinv buffer (which uses the same packed row-major layout the Cholesky + // expects) and run the factorization in-place there. + int needs_sym = (!is_factored && n > 1 && + (H[1] - H[n] > (c_float)DAQP_SYMMETRY_TOL || + H[n] - H[1] > (c_float)DAQP_SYMMETRY_TOL)); + if(needs_sym){ + // Pack 0.5*(H[i,j]+H[j,i]) for j>=i into Rinv. + // Row i occupies positions disp .. disp+n-i-1 in the packed buffer. + for(i = 0, disp = 0; i < n; i++){ + work->Rinv[disp++] = H[i*n+i]; + for(j = i+1; j < n; j++) + work->Rinv[disp++] = (c_float)0.5*(H[i*n+j] + H[j*n+i]); + } } // Check if Diagonal int is_diagonal = 1; - for (i=0, disp=1; i 1e-12 || H[disp] < -1e-12){ - is_diagonal = 0; break; + if(needs_sym){ + // Off-diagonals of row i are at packed positions disp+1 .. disp+n-i-1. + for(i = 0, disp = 0; i < n; disp += n-i, i++){ + for(j = 1; j < n-i; j++){ + if(work->Rinv[disp+j] > 1e-12 || work->Rinv[disp+j] < -1e-12){ + is_diagonal = 0; break; + } + } + if(!is_diagonal) break; + } + } else { + for(i = 0, disp = 1; i < n; i++, disp += (is_factored ? 1 : (i+1))){ + for(j = 1; j < n-i; j++, disp++){ + if(H[disp] > 1e-12 || H[disp] < -1e-12){ + is_diagonal = 0; break; + } } + if(!is_diagonal) break; } - if(!is_diagonal) break; } // Diagonal Case @@ -219,47 +239,78 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ work->RinvD = work->Rinv; work->Rinv = NULL; disp = 0; for(i=0; iRinvD[disp]; + disp += n-i; + } else if(!is_factored){ + Hi = H[disp]; + disp += n+1; + } else { + Hi = H[disp]; + disp += n-i; + } + // Apply regularization / sqrt (same logic for all non-factored cases). if(!is_factored){ - // Only add eps if this direction would be singular without it if(Hi <= zero_tol){ if(work->prox_mask != NULL) work->prox_mask[i] = 1; work->n_prox++; Hi += eps; } - if (Hi <= zero_tol) return DAQP_EXIT_NONCONVEX; + if(Hi <= zero_tol) return DAQP_EXIT_NONCONVEX; Hi = sqrt(Hi); - disp += n+1; } else { if(Hi <= zero_tol){ if(work->prox_mask != NULL) work->prox_mask[i] = 1; work->n_prox++; Hi = sqrt(Hi*Hi + eps); // Regularization for factors } - disp += n-i; } - work->RinvD[i] = 1/Hi; if(work->scaling != NULL && i < work->ms) work->scaling[i] = Hi; } return 1; } - // Cholesky - if (is_factored) { + // Cholesky + if(is_factored){ for(i=0, disp=0; iRinv[disp] = 1/H[disp]; // Store 1/rii - for (j=1, disp++; jRinv[disp] = H[disp]; } + } else if(needs_sym){ + // Hs is already packed in Rinv at the same positions the Cholesky + // writes R. Row i starts at disp; Rinv[disp+j] = Hs[i,i+j]. + // Reading and writing the same location is safe because the value is + // consumed (into diag_i or the inner-k update) before being overwritten. + for(i = 0, disp = 0; i < n; disp += n-i, i++){ + c_float diag_i = work->Rinv[disp]; // read Hs[i,i] before overwrite + for(k = 0, disp2 = i; k < i; k++, disp2 += n-k) + diag_i -= work->Rinv[disp2] * work->Rinv[disp2]; + if(diag_i <= zero_tol){ + if(work->prox_mask != NULL) work->prox_mask[i] = 1; + work->n_prox++; + diag_i += eps; + } + if(diag_i <= zero_tol) return DAQP_EXIT_NONCONVEX; + work->Rinv[disp] = sqrt(diag_i); + for(j = 1; j < n-i; j++){ + // Rinv[disp+j] holds Hs[i,i+j]; subtract accumulated correction. + for(k = 0, disp2 = i; k < i; k++, disp2 += n-k) + work->Rinv[disp+j] -= work->Rinv[disp2] * work->Rinv[disp2+j]; + work->Rinv[disp+j] /= work->Rinv[disp]; + } + work->Rinv[disp] = 1/work->Rinv[disp]; + } } else { // Standard Cholesky (H -> R), adding eps only where the diagonal // of the factor would otherwise be non-positive (semi-proximal). - for (i=0, disp=0, disp3=0; iRinv[disp2] * work->Rinv[disp2]; // Only regularize if this direction is singular @@ -269,12 +320,12 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ diag_i += eps; } - if (diag_i <= zero_tol) return DAQP_EXIT_NONCONVEX; + if(diag_i <= zero_tol) return DAQP_EXIT_NONCONVEX; work->Rinv[disp] = sqrt(diag_i); - for (j=1; jRinv[disp+j] = H[disp3++]; - for (k=0, disp2=i; kRinv[disp+j] -= work->Rinv[disp2] * work->Rinv[disp2+j]; work->Rinv[disp+j] /= work->Rinv[disp]; } From 78fcd8e71909b1a680ef78b7a35aa0d3e0e7a05a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:16:21 +0000 Subject: [PATCH 5/8] =?UTF-8?q?Always=20symmetrize=20H=20via=20Rinv=20buff?= =?UTF-8?q?er=20=E2=80=94=20remove=20lazy-check=20branching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: darnstrom <55484604+darnstrom@users.noreply.github.com> Agent-Logs-Url: https://github.com/darnstrom/daqp/sessions/cc0f5907-2fed-436a-a621-7fbd2d290f86 --- include/constants.h | 4 +- .../tests/07_symmetrize_hessian.cpp | 15 +-- src/utils.c | 107 +++++------------- 3 files changed, 37 insertions(+), 89 deletions(-) diff --git a/include/constants.h b/include/constants.h index 7303533..80cdade 100644 --- a/include/constants.h +++ b/include/constants.h @@ -27,9 +27,7 @@ extern "C" { #define DAQP_DEFAULT_SING_TOL (3.7e-11) #define DAQP_DEFAULT_REFACTOR_TOL 1e-9 #define DAQP_DEFAULT_EPS_PROX 1e-6 -// Tolerance for detecting that H is not symmetric: if |H[0,1] - H[1,0]| exceeds -// this value, DAQP symmetrizes H via its internal buffer before factorization. -#define DAQP_SYMMETRY_TOL 1e-9 + // MACROS #define DAQP_ARSUM(x) ((x)*(x+1)/2) diff --git a/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp b/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp index 4a87acf..1420a80 100644 --- a/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp +++ b/interfaces/daqp-eigen/tests/07_symmetrize_hessian.cpp @@ -6,11 +6,13 @@ // // Key properties tested: // 1. A fully symmetric H gives the correct reference solution (regression). -// 2. A lower-triangular H is no longer silently treated as a diagonal matrix -// (the pre-fix bug: DAQP read only the upper triangle which was all zero -// except the diagonal, so it ignored the off-diagonal structure). +// 2. A lower-triangular H is no longer silently treated as a diagonal matrix. +// DAQP always packs 0.5*(H+H^T) into its internal Rinv buffer before +// factorization, so the off-diagonal structure is preserved regardless of +// which triangle the caller fills in. // 3. A nearly-symmetric H (tiny numerical noise in one triangle) gives the // same solution as the perfectly symmetric reference. +// 4. H is never mutated — it is user-owned and strictly read-only. int main() { double precision = 1e-6; @@ -49,10 +51,9 @@ int main() { } // 3. Lower-triangular H (upper triangle set to zero). - // Before the fix: DAQP reads only the upper triangle, sees all-zero - // off-diagonals, and treats the matrix as diagonal → wrong solution. - // After the fix: DAQP symmetrizes first, so the off-diagonal structure - // is preserved (at 0.5× the original value) → solution differs from diagonal. + // DAQP always packs 0.5*(H+H^T) before factorization, so the + // off-diagonal entries (0.5*lower value) are correctly included. + // Result must differ from the diagonal-only solve. Eigen::MatrixXd H_lower = H_full.triangularView(); Eigen::MatrixXd H_lower_copy = H_lower; // snapshot to check H is not mutated EigenDAQPResult res_lower = daqp_solve(H_lower, f, A, bu, bl, sense, break_points); diff --git a/src/utils.c b/src/utils.c index f5855b8..6ac4d3d 100644 --- a/src/utils.c +++ b/src/utils.c @@ -176,7 +176,7 @@ int daqp_update_ldp(const int mask, DAQPWorkspace *work, DAQPProblem* qp){ } int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ - int i, j, k, disp, disp2, disp3; + int i, j, k, disp, disp2; const int n = work->n; c_float eps = work->settings->eps_prox; c_float zero_tol = work->settings->zero_tol; @@ -190,20 +190,14 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } work->n_prox = 0; - // Check whether symmetrization is needed by comparing the first - // off-diagonal pair H[0,1] vs H[1,0] (only meaningful when n > 1). - // If they differ by more than 1e-9, the caller supplied only one triangle - // (or a genuinely asymmetric matrix) and we must symmetrize before - // factorization. H is user-owned so we must not modify it; instead we - // pack the symmetrized upper triangle directly into the pre-allocated - // Rinv buffer (which uses the same packed row-major layout the Cholesky - // expects) and run the factorization in-place there. - int needs_sym = (!is_factored && n > 1 && - (H[1] - H[n] > (c_float)DAQP_SYMMETRY_TOL || - H[n] - H[1] > (c_float)DAQP_SYMMETRY_TOL)); - if(needs_sym){ - // Pack 0.5*(H[i,j]+H[j,i]) for j>=i into Rinv. - // Row i occupies positions disp .. disp+n-i-1 in the packed buffer. + // When H is not yet factored, always pack its upper triangle — symmetrized + // as 0.5*(H[i,j]+H[j,i]) — into the pre-allocated Rinv buffer before + // factorization. This keeps H read-only (H is user-owned), produces a + // uniform packed layout that the Cholesky and diagonal checks can read + // without further branching, and handles symmetric, upper-triangular, and + // lower-triangular inputs identically. The packing cost is O(n^2/2), + // which is negligible compared to the O(n^3/6) Cholesky. + if(!is_factored){ for(i = 0, disp = 0; i < n; i++){ work->Rinv[disp++] = H[i*n+i]; for(j = i+1; j < n; j++) @@ -211,22 +205,15 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } } - // Check if Diagonal + // Check if Diagonal. + // For !is_factored the packed upper triangle is now in Rinv. + // For is_factored H is already in packed upper-triangular layout. int is_diagonal = 1; - if(needs_sym){ - // Off-diagonals of row i are at packed positions disp+1 .. disp+n-i-1. + { + c_float *src = is_factored ? H : work->Rinv; for(i = 0, disp = 0; i < n; disp += n-i, i++){ for(j = 1; j < n-i; j++){ - if(work->Rinv[disp+j] > 1e-12 || work->Rinv[disp+j] < -1e-12){ - is_diagonal = 0; break; - } - } - if(!is_diagonal) break; - } - } else { - for(i = 0, disp = 1; i < n; i++, disp += (is_factored ? 1 : (i+1))){ - for(j = 1; j < n-i; j++, disp++){ - if(H[disp] > 1e-12 || H[disp] < -1e-12){ + if(src[disp+j] > 1e-12 || src[disp+j] < -1e-12){ is_diagonal = 0; break; } } @@ -234,24 +221,13 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } } - // Diagonal Case + // Diagonal Case — read diagonal entries from the same source as above. if(is_diagonal){ work->RinvD = work->Rinv; work->Rinv = NULL; - disp = 0; - for(i=0; iRinvD[disp]; - disp += n-i; - } else if(!is_factored){ - Hi = H[disp]; - disp += n+1; - } else { - Hi = H[disp]; - disp += n-i; - } - // Apply regularization / sqrt (same logic for all non-factored cases). + c_float *src = is_factored ? H : work->RinvD; + for(i = 0, disp = 0; i < n; i++){ + c_float Hi = src[disp]; + disp += n-i; if(!is_factored){ if(Hi <= zero_tol){ if(work->prox_mask != NULL) work->prox_mask[i] = 1; @@ -273,20 +249,21 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ return 1; } - // Cholesky + // Cholesky. + // For is_factored: copy H (already packed R) into Rinv so the R->Rinv + // inversion below can work in-place. + // For !is_factored: the packed upper triangle is already in Rinv; run + // Cholesky in-place. Each Rinv[disp] (diagonal) is read into a local + // before being overwritten, so no aliasing issues arise. if(is_factored){ for(i=0, disp=0; iRinv[disp] = 1/H[disp]; // Store 1/rii for(j=1, disp++; jRinv[disp] = H[disp]; } - } else if(needs_sym){ - // Hs is already packed in Rinv at the same positions the Cholesky - // writes R. Row i starts at disp; Rinv[disp+j] = Hs[i,i+j]. - // Reading and writing the same location is safe because the value is - // consumed (into diag_i or the inner-k update) before being overwritten. + } else { for(i = 0, disp = 0; i < n; disp += n-i, i++){ - c_float diag_i = work->Rinv[disp]; // read Hs[i,i] before overwrite + c_float diag_i = work->Rinv[disp]; // read before overwrite for(k = 0, disp2 = i; k < i; k++, disp2 += n-k) diag_i -= work->Rinv[disp2] * work->Rinv[disp2]; if(diag_i <= zero_tol){ @@ -297,40 +274,12 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ if(diag_i <= zero_tol) return DAQP_EXIT_NONCONVEX; work->Rinv[disp] = sqrt(diag_i); for(j = 1; j < n-i; j++){ - // Rinv[disp+j] holds Hs[i,i+j]; subtract accumulated correction. for(k = 0, disp2 = i; k < i; k++, disp2 += n-k) work->Rinv[disp+j] -= work->Rinv[disp2] * work->Rinv[disp2+j]; work->Rinv[disp+j] /= work->Rinv[disp]; } work->Rinv[disp] = 1/work->Rinv[disp]; } - } else { - // Standard Cholesky (H -> R), adding eps only where the diagonal - // of the factor would otherwise be non-positive (semi-proximal). - for(i=0, disp=0, disp3=0; iRinv[disp2] * work->Rinv[disp2]; - - // Only regularize if this direction is singular - if(diag_i <= zero_tol){ - if(work->prox_mask != NULL) work->prox_mask[i] = 1; - work->n_prox++; - diag_i += eps; - } - - if(diag_i <= zero_tol) return DAQP_EXIT_NONCONVEX; - work->Rinv[disp] = sqrt(diag_i); - - for(j=1; jRinv[disp+j] = H[disp3++]; - for(k=0, disp2=i; kRinv[disp+j] -= work->Rinv[disp2] * work->Rinv[disp2+j]; - work->Rinv[disp+j] /= work->Rinv[disp]; - } - work->Rinv[disp] = 1/work->Rinv[disp]; - } } // R -> Rinv From 65130545cd773106b8c79d3a038b3689214142f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Arnstr=C3=B6m?= Date: Sun, 22 Mar 2026 13:11:28 +0100 Subject: [PATCH 6/8] Save some divisions --- src/utils.c | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/utils.c b/src/utils.c index 6ac4d3d..39ec22c 100644 --- a/src/utils.c +++ b/src/utils.c @@ -190,13 +190,7 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } work->n_prox = 0; - // When H is not yet factored, always pack its upper triangle — symmetrized - // as 0.5*(H[i,j]+H[j,i]) — into the pre-allocated Rinv buffer before - // factorization. This keeps H read-only (H is user-owned), produces a - // uniform packed layout that the Cholesky and diagonal checks can read - // without further branching, and handles symmetric, upper-triangular, and - // lower-triangular inputs identically. The packing cost is O(n^2/2), - // which is negligible compared to the O(n^3/6) Cholesky. + // When H is not yet factored, symmetrize if(!is_factored){ for(i = 0, disp = 0; i < n; i++){ work->Rinv[disp++] = H[i*n+i]; @@ -206,8 +200,6 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } // Check if Diagonal. - // For !is_factored the packed upper triangle is now in Rinv. - // For is_factored H is already in packed upper-triangular layout. int is_diagonal = 1; { c_float *src = is_factored ? H : work->Rinv; @@ -250,11 +242,6 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } // Cholesky. - // For is_factored: copy H (already packed R) into Rinv so the R->Rinv - // inversion below can work in-place. - // For !is_factored: the packed upper triangle is already in Rinv; run - // Cholesky in-place. Each Rinv[disp] (diagonal) is read into a local - // before being overwritten, so no aliasing issues arise. if(is_factored){ for(i=0, disp=0; iRinv[disp] = 1/H[disp]; // Store 1/rii @@ -272,13 +259,13 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ diag_i += eps; } if(diag_i <= zero_tol) return DAQP_EXIT_NONCONVEX; - work->Rinv[disp] = sqrt(diag_i); + diag_i = 1/sqrt(diag_i); for(j = 1; j < n-i; j++){ for(k = 0, disp2 = i; k < i; k++, disp2 += n-k) work->Rinv[disp+j] -= work->Rinv[disp2] * work->Rinv[disp2+j]; - work->Rinv[disp+j] /= work->Rinv[disp]; + work->Rinv[disp+j] *= diag_i; } - work->Rinv[disp] = 1/work->Rinv[disp]; + work->Rinv[disp] = diag_i; } } @@ -289,7 +276,8 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ for(j=k+1; jRinv[disp2++] *= -work->Rinv[disp]; for(i=k+1, disp++; iRinv[disp] *= work->Rinv[disp2++]; - for(j=1; jRinv[disp+j] -= work->Rinv[disp2++] * work->Rinv[disp]; + for(j=1; jRinv[disp+j] -= work->Rinv[disp2++] * work->Rinv[disp]; } } return 1; From e95b6256664858f62eb770d99e4e4c2bd8caab12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:04:18 +0000 Subject: [PATCH 7/8] Check diagonality before packing to restore O(n) fast path for diagonal H Co-authored-by: darnstrom <55484604+darnstrom@users.noreply.github.com> Agent-Logs-Url: https://github.com/darnstrom/daqp/sessions/1ae17631-1e7d-47aa-b9e4-d1d9583eb84d --- src/utils.c | 45 ++++++++++++++++++++++++++------------------- utils_new.o | Bin 0 -> 21016 bytes 2 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 utils_new.o diff --git a/src/utils.c b/src/utils.c index 39ec22c..7a3b515 100644 --- a/src/utils.c +++ b/src/utils.c @@ -182,30 +182,26 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ c_float zero_tol = work->settings->zero_tol; - // Make sure Rinv points to allocated data - if(work->RinvD != NULL){ work->Rinv = work->RinvD; work->RinvD = NULL; } // Reset the semi-proximal mask for this factorization if(work->prox_mask != NULL){ for(i = 0; i < n; i++) work->prox_mask[i] = 0; } work->n_prox = 0; - // When H is not yet factored, symmetrize + // Check if Diagonal — for unfactored H scan only the upper-triangle + // off-diagonals of the n×n row-major matrix (O(n²/2) reads), skipping + // the packing step entirely when H is diagonal. + int is_diagonal = 1; if(!is_factored){ - for(i = 0, disp = 0; i < n; i++){ - work->Rinv[disp++] = H[i*n+i]; - for(j = i+1; j < n; j++) - work->Rinv[disp++] = (c_float)0.5*(H[i*n+j] + H[j*n+i]); + for(i = 0, disp = 1; i < n && is_diagonal; i++, disp += i+1){ + for(j = 1; j < n-i; j++, disp++){ + if(H[disp] > zero_tol || H[disp] < -zero_tol){ is_diagonal = 0; break; } + } } - } - - // Check if Diagonal. - int is_diagonal = 1; - { - c_float *src = is_factored ? H : work->Rinv; + } else { for(i = 0, disp = 0; i < n; disp += n-i, i++){ for(j = 1; j < n-i; j++){ - if(src[disp+j] > 1e-12 || src[disp+j] < -1e-12){ + if(H[disp+j] > zero_tol || H[disp+j] < -zero_tol){ is_diagonal = 0; break; } } @@ -213,13 +209,13 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ } } - // Diagonal Case — read diagonal entries from the same source as above. + // Diagonal Case — for unfactored H read diagonals directly (no packing needed). if(is_diagonal){ - work->RinvD = work->Rinv; work->Rinv = NULL; - c_float *src = is_factored ? H : work->RinvD; + if(work->Rinv != NULL){ work->RinvD = work->Rinv; work->Rinv = NULL; } for(i = 0, disp = 0; i < n; i++){ - c_float Hi = src[disp]; - disp += n-i; + c_float Hi; + if(is_factored){ Hi = H[disp]; disp += n-i; } + else { Hi = H[i*n+i]; } if(!is_factored){ if(Hi <= zero_tol){ if(work->prox_mask != NULL) work->prox_mask[i] = 1; @@ -241,6 +237,17 @@ int daqp_update_Rinv(DAQPWorkspace *work, c_float* H, int is_factored){ return 1; } + // Not diagonal: ensure Rinv points to allocated data, then pack + // (symmetrize) H into Rinv before Cholesky. + if(work->RinvD != NULL){ work->Rinv = work->RinvD; work->RinvD = NULL; } + if(!is_factored){ + for(i = 0, disp = 0; i < n; i++){ + work->Rinv[disp++] = H[i*n+i]; + for(j = i+1; j < n; j++) + work->Rinv[disp++] = (c_float)0.5*(H[i*n+j] + H[j*n+i]); + } + } + // Cholesky. if(is_factored){ for(i=0, disp=0; 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 c@pAnAAc7653Y<6U-xM Date: Sun, 22 Mar 2026 13:04:27 +0000 Subject: [PATCH 8/8] Remove accidentally committed object file Co-authored-by: darnstrom <55484604+darnstrom@users.noreply.github.com> Agent-Logs-Url: https://github.com/darnstrom/daqp/sessions/1ae17631-1e7d-47aa-b9e4-d1d9583eb84d --- utils_new.o | Bin 21016 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 utils_new.o diff --git a/utils_new.o b/utils_new.o deleted file mode 100644 index 28abf921906ae10ee2f9b5255dcef55bb91383b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21016 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 c@pAnAAc7653Y<6U-xM