Skip to content

Commit 90e424e

Browse files
authored
Fix the bug detecting nonconvex quadratic constraints in the general … (#1438)
Cross-only indefinite quadratic constraints (e.g. `2·x₀·x₁ ≤ 0.5` with `H = [[0,2],[2,0]]`) previously passed convexity checks on the general SOC conversion path. Diagonal LDLT returned `rank = 0` without `INDEFINITE_MATRIX_RETURN`, and a degenerate `r = 0` SOC lift silently dropped the quadratic term, allowing wrong solutions to be reported as Optimal (e.g. obj=2 at (1,1) instead of rejecting the problem). Fixes: - `right_looking_ldlt`: return `INDEFINITE_MATRIX_RETURN` when factorization stalls at `rank = 0` on a nonzero matrix. - `translate_soc.hpp` (general path): `cuopt_expects(rank >= 1, …)` before building the SOC lift. Adds LDLT and SOC-conversion unit tests for the cross-only indefinite case. ## Issue closes #1434 Authors: - Yuwen Chen (https://github.com/yuwenchen95) Approvers: - Rajesh Gandham (https://github.com/rg20) - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) - Miles Lubin (https://github.com/mlubin) URL: #1438
1 parent dd85925 commit 90e424e

9 files changed

Lines changed: 169 additions & 9 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ __pycache__
1212
*.dylib
1313
.cache
1414
.vscode
15+
.cursor/
1516
*.swp
1617
*.pytest_cache
1718
*.manifest

cpp/src/barrier/translate_soc.hpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -659,9 +659,15 @@ void convert_quadratic_constraints_to_second_order_cones(
659659
"Quadratic constraint '%s' is non-convex (Q matrix is indefinite)",
660660
qc.constraint_row_name.c_str());
661661

662-
// Since q_nnz >= 1 is enforced above, Q is nonzero and rank must be >= 1.
663-
// (A nonzero Q entry produces a nonzero H diagonal or off-diagonal, guaranteeing rank > 0.)
664-
assert(rank >= 1);
662+
// q_nnz >= 1 implies H is nonzero, but diagonal LDLT may still return rank 0 (e.g. cross-only
663+
// indefinite H with zero diagonals). Reject before building a degenerate r=0 SOC lift.
664+
cuopt_expects(rank >= 1,
665+
error_type_t::ValidationError,
666+
"Quadratic constraint '%s' is non-convex or could not be converted to a "
667+
"second-order cone (LDLT rank %d; Q may be indefinite or have zero diagonal "
668+
"with cross terms)",
669+
qc.constraint_row_name.c_str(),
670+
static_cast<int>(rank));
665671

666672
// Step 4: Build standard SOC of dimension rank + 2.
667673
// New variables: y_0,...,y_{r-1}, s_0 (head), s_{r+1} (tail)

cpp/src/dual_simplex/right_looking_lu.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
#include <cassert>
1515
#include <cmath>
16-
#include <cstdio>
1716

1817
using cuopt::ins_vector;
1918

@@ -1684,7 +1683,8 @@ i_t right_looking_ldlt(const csc_matrix_t<i_t, f_t>& A,
16841683
f_t& work_estimate)
16851684
{
16861685
raft::common::nvtx::range scope("LU::right_looking_ldlt");
1687-
const i_t n = A.n;
1686+
const i_t n = A.n;
1687+
const i_t input_nnz = A.nnz();
16881688
assert(A.m == n);
16891689

16901690
symmetric_trailing_matrix_t<i_t, f_t> trailing_matrix(A);
@@ -1720,7 +1720,12 @@ i_t right_looking_ldlt(const csc_matrix_t<i_t, f_t>& A,
17201720
f_t pivot_val = 0;
17211721
trailing_matrix.symmetric_markowitz_search(pivot_tol, pivot_p, pivot_val);
17221722

1723-
if (pivot_p == -1) { break; } // No acceptable pivot found (remaining diagonals are zero/tiny)
1723+
if (pivot_p == -1) {
1724+
// No acceptable diagonal pivot. Zero matrix is rank 0; nonzero cross-only indefinite
1725+
// matrices (e.g. [[0, a]; [a, 0]]) also stall here and must not be treated as PSD.
1726+
if (pivots == 0 && input_nnz > 0) { return INDEFINITE_MATRIX_RETURN; }
1727+
break;
1728+
}
17241729

17251730
// Check for indefiniteness: a negative pivot means the matrix is not PSD
17261731
if (pivot_val < 0) { return INDEFINITE_MATRIX_RETURN; }

cpp/src/dual_simplex/right_looking_lu.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ i_t right_looking_lu_row_permutation_only(const csc_matrix_t<i_t, f_t>& A,
4343
// D = diagonal factor (length = rank)
4444
// Returns:
4545
// rank >= 0: number of successful pivots with D(k,k) >= pivot_tol (PSD case).
46-
// INDEFINITE_MATRIX_RETURN (-4): a negative pivot was encountered (matrix is not PSD).
46+
// INDEFINITE_MATRIX_RETURN (-4): a negative pivot was encountered, or the matrix is nonzero
47+
// but no acceptable diagonal pivot was found (matrix is not PSD).
4748
// CONCURRENT_HALT_RETURN (-2): concurrent halt requested.
4849
// TIME_LIMIT_RETURN (-3): time limit exceeded.
4950
template <typename i_t, typename f_t>

cpp/src/pdlp/solve.cu

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,13 @@ mps_data_model_to_optimization_problem(
21932193
Q_offsets.size());
21942194
}
21952195

2196+
// Preserve quadratic constraints.
2197+
if (data_model.has_quadratic_constraints()) {
2198+
static_cast<cuopt::mathematical_optimization::optimization_problem_interface_t<i_t, f_t>&>(
2199+
op_problem)
2200+
.set_quadratic_constraints(data_model.get_quadratic_constraints());
2201+
}
2202+
21962203
return op_problem;
21972204
}
21982205

cpp/src/pdlp/translate.hpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ static simplex::user_problem_t<i_t, f_t> cuopt_problem_to_user_problem(
4444
csr_A.x = std::move(A_values);
4545
csr_A.j = std::move(A_indices);
4646
csr_A.row_start = std::move(A_offsets);
47-
48-
csr_A.to_compressed_col(user_problem.A);
47+
if (m == 0) {
48+
csr_A.row_start.resize(1);
49+
csr_A.row_start[0] = 0;
50+
}
4951

5052
user_problem.rhs.resize(m);
5153
user_problem.row_sense.resize(m);
@@ -96,6 +98,13 @@ static simplex::user_problem_t<i_t, f_t> cuopt_problem_to_user_problem(
9698
user_problem.Q_indices = problem.get_quadratic_objective_indices();
9799
user_problem.Q_values = problem.get_quadratic_objective_values();
98100

101+
if (problem.has_quadratic_constraints()) {
102+
barrier::convert_quadratic_constraints_to_second_order_cones<i_t, f_t>(
103+
n, problem.get_quadratic_constraints(), csr_A, user_problem);
104+
}
105+
106+
csr_A.to_compressed_col(user_problem.A);
107+
99108
return user_problem;
100109
}
101110

cpp/tests/dual_simplex/unit_tests/right_looking_ldlt.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,26 @@ TEST(right_looking_ldlt, indefinite_2x2)
512512
EXPECT_EQ(result, INDEFINITE_MATRIX_RETURN);
513513
}
514514

515+
// Test 14b: Cross-only indefinite matrix (zero diagonals, no 1x1 pivot).
516+
// A = [0, 2; 2, 0] has eigenvalues +2 and -2 (indefinite).
517+
TEST(right_looking_ldlt, indefinite_cross_only_2x2)
518+
{
519+
const int n = 2;
520+
std::vector<double> dense = {0.0, 2.0, 2.0, 0.0};
521+
auto A = dense_to_lower_csc(n, dense);
522+
523+
simplex_solver_settings_t<int, double> settings;
524+
std::vector<int> perm;
525+
csc_matrix_t<int, double> L(n, n, 1);
526+
std::vector<double> D;
527+
double work_estimate = 0;
528+
double start_time = tic();
529+
530+
int result = right_looking_ldlt(A, settings, 1e-12, start_time, perm, L, D, work_estimate);
531+
532+
EXPECT_EQ(result, INDEFINITE_MATRIX_RETURN);
533+
}
534+
515535
// Test 15: Larger indefinite matrix (4x4 with mixed eigenvalues).
516536
// A = [2, 1, 0, 1; 1, 0, 1, 0; 0, 1, 2, -1; 1, 0, -1, -1]
517537
TEST(right_looking_ldlt, indefinite_4x4)

cpp/tests/linear_programming/unit_tests/solution_interface_test.cu

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,30 @@ TEST_F(SolutionInterfaceTest, mps_data_model_to_optimization_problem)
400400
}
401401
}
402402

403+
TEST_F(SolutionInterfaceTest, mps_data_model_to_optimization_problem_quadratic_constraints)
404+
{
405+
auto mps_data = cuopt::mathematical_optimization::io::read_lp_from_string<int, double>(R"LP(
406+
Minimize
407+
obj: x + y
408+
Subject To
409+
q0: [ 4 x * y ] <= 0.5
410+
Bounds
411+
-1 <= x <= 1
412+
-1 <= y <= 1
413+
End
414+
)LP");
415+
ASSERT_TRUE(mps_data.has_quadratic_constraints());
416+
ASSERT_EQ(mps_data.get_quadratic_constraints().size(), 1u);
417+
418+
raft::handle_t handle;
419+
auto problem = mps_data_model_to_optimization_problem(&handle, mps_data);
420+
421+
ASSERT_TRUE(problem.has_quadratic_constraints());
422+
ASSERT_EQ(problem.get_quadratic_constraints().size(), 1u);
423+
EXPECT_EQ(problem.get_quadratic_constraints()[0].constraint_row_name, "q0");
424+
EXPECT_NEAR(problem.get_quadratic_constraints()[0].rhs_value, 0.5, 1e-9);
425+
}
426+
403427
// =============================================================================
404428
// Solution conversion tests (hand-constructed, known values)
405429
// =============================================================================

cpp/tests/socp/general_quadratic_test.cu

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
*/
66
/* clang-format on */
77

8+
#include <gmock/gmock.h>
89
#include <gtest/gtest.h>
910

1011
#include <barrier/translate_soc.hpp>
12+
#include <cuopt/error.hpp>
13+
#include <cuopt/mathematical_optimization/io/parser.hpp>
1114
#include <cuopt/mathematical_optimization/optimization_problem_interface.hpp>
1215
#include <dual_simplex/solve.hpp>
1316
#include <dual_simplex/sparse_matrix.hpp>
@@ -214,6 +217,90 @@ TEST(general_quadratic, rejects_non_convex)
214217
cuopt::logic_error);
215218
}
216219

220+
// End-to-end: cross-only indefinite Q (issue #1434). H = [[0, 2]; [2, 0]] has zero diagonals so
221+
// LDLT returns rank 0 without a negative pivot; must still be rejected as non-convex via
222+
// solve_qcqp.
223+
TEST(general_quadratic, rejects_cross_only_indefinite)
224+
{
225+
raft::handle_t handle{};
226+
init_handler(&handle);
227+
228+
// H = [[0, 2]; [2, 0]] from [ 4 x * y ] in LP bracket notation.
229+
auto lp = io::read_lp_from_string<i_t, f_t>(R"LP(
230+
Minimize
231+
obj: x + y
232+
Subject To
233+
q0: [ 4 x * y ] <= 0.5
234+
Bounds
235+
-1 <= x <= 1
236+
-1 <= y <= 1
237+
End
238+
)LP");
239+
240+
ASSERT_TRUE(lp.has_quadratic_constraints());
241+
ASSERT_EQ(lp.get_quadratic_constraints().size(), 1u);
242+
243+
const i_t n = lp.get_n_variables();
244+
const i_t m = lp.get_n_constraints();
245+
EXPECT_EQ(n, 2);
246+
EXPECT_EQ(m, 0);
247+
248+
user_problem_t<i_t, f_t> user_problem(&handle);
249+
user_problem.num_rows = m;
250+
user_problem.num_cols = n;
251+
user_problem.objective = lp.get_objective_coefficients();
252+
253+
// Initialize the empty A matrix
254+
user_problem.A.m = m;
255+
user_problem.A.n = n;
256+
user_problem.A.nz_max = 0;
257+
user_problem.A.reallocate(0);
258+
user_problem.A.col_start.assign(n + 1, 0);
259+
260+
user_problem.rhs.clear();
261+
user_problem.row_sense.clear();
262+
user_problem.lower = lp.get_variable_lower_bounds();
263+
user_problem.upper = lp.get_variable_upper_bounds();
264+
user_problem.num_range_rows = 0;
265+
user_problem.var_types.assign(n, variable_type_t::CONTINUOUS);
266+
267+
const auto& src_qc = lp.get_quadratic_constraints()[0];
268+
qc_t qc;
269+
qc.constraint_row_index = src_qc.constraint_row_index;
270+
qc.constraint_row_name = src_qc.constraint_row_name;
271+
qc.constraint_row_type = src_qc.constraint_row_type;
272+
qc.linear_values = src_qc.linear_values;
273+
qc.linear_indices = src_qc.linear_indices;
274+
qc.rhs_value = src_qc.rhs_value;
275+
qc.rows = src_qc.rows;
276+
qc.cols = src_qc.cols;
277+
qc.vals = src_qc.vals;
278+
279+
csr_matrix_t<i_t, f_t> csr_A(m, n, 0);
280+
csr_A.m = m;
281+
csr_A.n = n;
282+
csr_A.row_start = {0};
283+
284+
std::vector<qc_t> qcs = {qc};
285+
286+
simplex_solver_settings_t<i_t, f_t> settings;
287+
settings.barrier = true;
288+
settings.barrier_presolve = true;
289+
settings.dualize = 0;
290+
291+
try {
292+
convert_quadratic_constraints_to_second_order_cones<i_t, f_t>(n, qcs, csr_A, user_problem);
293+
csr_A.to_compressed_col(user_problem.A);
294+
lp_solution_t<i_t, f_t> solution(user_problem.num_rows, user_problem.num_cols);
295+
(void)solve_linear_program_with_barrier(user_problem, settings, solution);
296+
FAIL() << "Expected ValidationError for cross-only indefinite Q";
297+
} catch (const cuopt::logic_error& e) {
298+
EXPECT_EQ(e.get_error_type(), cuopt::error_type_t::ValidationError);
299+
EXPECT_THAT(e.what(), testing::HasSubstr("non-convex"));
300+
EXPECT_THAT(e.what(), testing::HasSubstr("q0"));
301+
}
302+
}
303+
217304
// Test: rank-deficient PSD Q (e.g., Q = v*v^T with v = [1, 1])
218305
// minimize x0 + x1
219306
// subject to (x0 + x1)^2 <= 4 (i.e., |x0 + x1| <= 2)

0 commit comments

Comments
 (0)