From b51a85ba75fbb728fab5fb3caa5cbdc8e74c560d Mon Sep 17 00:00:00 2001 From: jajhall Date: Fri, 23 Oct 2026 15:00:26 +0100 Subject: [PATCH 01/14] Set up issue-3459 unit test --- check/TestLpSolvers.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/check/TestLpSolvers.cpp b/check/TestLpSolvers.cpp index 65f1f58bf3..1039f21925 100644 --- a/check/TestLpSolvers.cpp +++ b/check/TestLpSolvers.cpp @@ -602,3 +602,42 @@ TEST_CASE("issue-2300", "[highs_lp_solver]") { h.resetThreadScheduler(true); } + +TEST_CASE("issue-3459", "[highs_lp_solver]") { + Highs h; + // h.setOptionValue("output_flag", dev_run); + h.setOptionValue(kSolverString, kPdlpString); + h.setOptionValue(kPresolveString, kHighsOffString); + const bool use_3459_model = true; + if (use_3459_model) { + std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/3459.mps"; + h.readModel(model_file); + } else { + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 3; + lp.col_cost_ = {1, -1}; + lp.col_lower_ = {0, 0}; + lp.col_upper_ = {kHighsInf, kHighsInf}; + lp.row_lower_ = {1, 2, 3}; // Infeasible or unbounded + lp.row_upper_ = {2, 3, 4}; // Infeasible or unbounded + lp.row_lower_ = {2, 0, 0}; + lp.row_upper_ = {3, 1, 1}; + lp.a_matrix_.start_ = {0, 3, 6}; + lp.a_matrix_.index_ = {0, 1, 2, 0, 1, 2}; + lp.a_matrix_.value_ = {1, -1, 2, 2, 1, -1}; + h.passModel(lp); + } + HighsStatus return_status = h.run(); + h.writeSolution("", 1); + REQUIRE(return_status == HighsStatus::kOk); + + HighsSolution solution = h.getSolution(); + + h.crossover(solution); + h.writeSolution("", 1); + + h.resetGlobalScheduler(true); +} + From 6b919bea7a7122019bfba0d39869b744c5cd3169 Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 15:25:18 +0000 Subject: [PATCH 02/14] Add 3459.mps test instance --- check/instances/3459.mps | 279 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 check/instances/3459.mps diff --git a/check/instances/3459.mps b/check/instances/3459.mps new file mode 100644 index 0000000000..f0d33951de --- /dev/null +++ b/check/instances/3459.mps @@ -0,0 +1,279 @@ +NAME +ROWS + N Obj + L r0 + L r1 + L r2 + L r3 + L r4 + L r5 + L r6 + L r7 + L r8 + L r9 + L r10 + L r11 + L r12 + L r13 + L r14 + L r15 + L r16 + L r17 + L r18 + L r19 +COLUMNS + c0 Obj 2 + c0 r0 3 + c0 r1 3 + c0 r2 3 + c0 r3 1 + c0 r7 1 + c0 r8 2 + c0 r12 2 + c1 Obj 8 + c1 r3 3 + c1 r5 1 + c1 r11 1 + c1 r15 2 + c1 r19 2 + c2 Obj 7 + c2 r3 1 + c2 r4 3 + c2 r5 3 + c2 r7 2 + c2 r12 3 + c2 r15 3 + c2 r16 1 + c3 Obj 5 + c3 r0 1 + c3 r7 1 + c3 r10 1 + c3 r15 1 + c4 Obj 4 + c4 r3 2 + c4 r6 3 + c4 r8 3 + c4 r13 2 + c4 r19 1 + c5 Obj 8 + c5 r1 2 + c5 r7 3 + c5 r11 3 + c6 Obj 4 + c6 r4 2 + c6 r5 2 + c6 r9 2 + c6 r13 2 + c6 r14 2 + c6 r16 2 + c6 r18 1 + c6 r19 2 + c7 Obj 3 + c7 r4 2 + c7 r8 2 + c7 r10 2 + c7 r17 2 + c7 r18 2 + c7 r19 2 + c8 Obj 4 + c8 r2 3 + c8 r13 2 + c8 r15 1 + c9 Obj 7 + c9 r4 1 + c9 r5 1 + c9 r8 1 + c9 r9 1 + c9 r11 2 + c9 r15 1 + c9 r17 3 + c9 r18 3 + c10 Obj 5 + c10 r5 3 + c10 r12 3 + c10 r13 3 + c10 r14 1 + c10 r16 1 + c10 r19 1 + c11 Obj 7 + c11 r3 3 + c11 r4 2 + c11 r9 2 + c11 r18 3 + c11 r19 1 + c12 Obj 4 + c12 r0 2 + c12 r4 3 + c12 r11 2 + c12 r12 2 + c12 r13 3 + c12 r14 3 + c12 r19 3 + c13 Obj 5 + c13 r2 1 + c13 r5 1 + c13 r15 1 + c13 r16 2 + c13 r17 1 + c14 Obj 3 + c14 r2 2 + c14 r4 3 + c14 r5 1 + c14 r6 2 + c14 r10 3 + c14 r11 2 + c14 r16 1 + c14 r17 2 + c15 Obj 2 + c15 r2 3 + c15 r3 3 + c15 r5 2 + c15 r7 2 + c15 r9 3 + c15 r11 1 + c15 r15 2 + c16 Obj 4 + c16 r0 2 + c16 r7 1 + c16 r12 2 + c16 r13 2 + c16 r14 2 + c16 r18 1 + c17 Obj 4 + c17 r6 3 + c17 r15 3 + c17 r19 1 + c18 Obj 8 + c18 r8 3 + c18 r17 1 + c18 r18 2 + c19 Obj 3 + c19 r0 3 + c19 r1 2 + c19 r5 1 + c19 r6 3 + c19 r12 2 + c19 r14 3 + c19 r16 3 + c19 r19 1 + c20 Obj 8 + c20 r1 3 + c20 r3 2 + c20 r4 2 + c20 r8 3 + c20 r10 1 + c20 r13 3 + c20 r17 2 + c21 Obj 8 + c21 r6 1 + c21 r7 2 + c21 r8 2 + c21 r16 1 + c21 r17 1 + c21 r18 2 + c22 Obj 2 + c22 r0 2 + c22 r2 1 + c22 r3 3 + c22 r4 3 + c22 r6 1 + c22 r7 3 + c22 r9 3 + c22 r11 1 + c22 r12 3 + c22 r13 3 + c22 r14 1 + c23 Obj 6 + c23 r1 3 + c23 r4 3 + c23 r10 1 + c23 r16 1 + c24 Obj 6 + c24 r0 1 + c24 r2 2 + c24 r3 3 + c24 r9 2 + c24 r10 1 + c24 r11 2 + c24 r16 2 + c24 r18 2 + c25 Obj 8 + c25 r2 2 + c25 r3 3 + c25 r4 1 + c25 r5 3 + c25 r11 3 + c25 r18 3 + c25 r19 3 + c26 Obj 8 + c26 r1 1 + c26 r3 2 + c26 r9 3 + c26 r10 1 + c26 r12 1 + c26 r13 2 + c26 r15 3 + c27 Obj 3 + c27 r1 2 + c27 r6 3 + c27 r13 1 + c27 r18 1 + c28 Obj 5 + c28 r2 3 + c28 r3 3 + c28 r6 3 + c28 r8 1 + c28 r9 3 + c28 r10 2 + c28 r12 2 + c28 r13 2 + c29 Obj 4 + c29 r2 1 + c29 r6 1 + c29 r10 3 + c29 r16 1 + c29 r18 3 + c29 r19 3 +RHS + RHS_V r0 6.269489302794252 + RHS_V r1 5.4189127335612373 + RHS_V r2 5.825998508615795 + RHS_V r3 6.3972282288045239 + RHS_V r4 4.1131438974023098 + RHS_V r5 5.3592446195886989 + RHS_V r6 4.0008867988411927 + RHS_V r7 5.9301504880066922 + RHS_V r8 6.4320026603217606 + RHS_V r9 4.3719618402626708 + RHS_V r10 4.9683776085791873 + RHS_V r11 7.2159672842360543 + RHS_V r12 7.3611262393983914 + RHS_V r13 5.5509330170814586 + RHS_V r14 7.2568949221649071 + RHS_V r15 5.1085610121285709 + RHS_V r16 6.8244328892539272 + RHS_V r17 6.1818264946284582 + RHS_V r18 5.7603963156106497 + RHS_V r19 6.6257691046951059 +RANGES + RANGE r0 3.0000000000000004 + RANGE r1 3 + RANGE r2 3 + RANGE r3 3 + RANGE r4 3.0000000000000004 + RANGE r5 3.0000000000000004 + RANGE r6 3 + RANGE r7 3.0000000000000004 + RANGE r8 2.9999999999999996 + RANGE r9 3.0000000000000004 + RANGE r10 3.0000000000000004 + RANGE r11 3 + RANGE r12 3 + RANGE r13 3 + RANGE r14 3 + RANGE r15 3 + RANGE r16 3.0000000000000004 + RANGE r17 3.0000000000000004 + RANGE r18 3 + RANGE r19 2.9999999999999996 +ENDATA From 8740abacd796ae9560b337b44d19fb1c77fe5311 Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 15:25:26 +0000 Subject: [PATCH 03/14] HiPDLP: handle maximization (#3467) --- check/TestPdlpHi.cpp | 45 +++++++++++++++++++++++++++++++++++++++ highs/pdlp/hipdlp/pdhg.cc | 9 ++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/check/TestPdlpHi.cpp b/check/TestPdlpHi.cpp index d38dde9248..fdd3b6fabc 100644 --- a/check/TestPdlpHi.cpp +++ b/check/TestPdlpHi.cpp @@ -1,4 +1,5 @@ #include +#include #include "HCheckConfig.h" #include "HConfig.h" @@ -38,3 +39,47 @@ TEST_CASE("hi-pdlp", "[pdlp]") { } h.resetThreadScheduler(true); } + +TEST_CASE("hi-pdlp-maximize", "[pdlp]") { + const std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/3459.mps"; + + // Reference: minimize with simplex + Highs ref; + ref.setOptionValue("output_flag", dev_run); + REQUIRE(ref.readModel(model_file) != HighsStatus::kError); + ref.setOptionValue("solver", kSimplexString); + ref.setOptionValue("presolve", kHighsOffString); + REQUIRE(ref.run() == HighsStatus::kOk); + const double min_objective = ref.getInfo().objective_function_value; + + // Negate all costs and maximize: the optimal objective is -min_objective + HighsLp lp = ref.getLp(); + for (double& cost : lp.col_cost_) cost = -cost; + lp.sense_ = ObjSense::kMaximize; + + Highs h; + h.setOptionValue("output_flag", dev_run); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.setOptionValue("solver", kHiPdlpString); + h.setOptionValue("presolve", kHighsOffString); + h.setOptionValue("kkt_tolerance", kkt_tolerance); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value + min_objective) < + 1e-3 * (1.0 + std::fabs(min_objective))); + + // The objective offset must also be reported on the original (maximize) + // scale + const double offset = 5.0; + lp.offset_ = offset; + h.clearSolver(); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value - + (-min_objective + offset)) < + 1e-3 * (1.0 + std::fabs(min_objective))); + + h.resetGlobalScheduler(true); +} diff --git a/highs/pdlp/hipdlp/pdhg.cc b/highs/pdlp/hipdlp/pdhg.cc index 338978738f..fd9f72653a 100644 --- a/highs/pdlp/hipdlp/pdhg.cc +++ b/highs/pdlp/hipdlp/pdhg.cc @@ -162,11 +162,10 @@ void PDLPSolver::preprocessLp() { logger_.info("Original LP matrix must be in column-wise format,"); } - lp_.offset_ = original_lp_->offset_; - HighsInt num_new_cols = 0; HighsInt nEqs = 0; sense_origin_ = (original_lp_->sense_ == ObjSense::kMinimize) ? 1 : -1; + lp_.offset_ = original_lp_->offset_ * sense_origin_; constraint_types_.resize(nRows_orig); // 1. First pass: Classify constraints and count slack variables needed @@ -230,7 +229,7 @@ void PDLPSolver::preprocessLp() { // 5. Populate costs and bounds for original and new slack variables for (HighsInt i = 0; i < nCols_orig; ++i) { - processed_lp.col_cost_[i] = original_lp_->col_cost_[i]; + processed_lp.col_cost_[i] = original_lp_->col_cost_[i] * sense_origin_; processed_lp.col_lower_[i] = original_lp_->col_lower_[i]; processed_lp.col_upper_[i] = original_lp_->col_upper_[i]; } @@ -421,9 +420,9 @@ PostSolveRetcode PDLPSolver::postprocess(HighsSolution& solution) { // Apply sign correction for LEQ constraints if (constraint_types_[orig_row] == LEQ) { - solution.row_dual[orig_row] = -dual_value; + solution.row_dual[orig_row] = -dual_value * sense_origin_; } else { - solution.row_dual[orig_row] = dual_value; + solution.row_dual[orig_row] = dual_value * sense_origin_; } } From 652f449f94d59b9fa3b9c0154dd0515b14790b9c Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 17:43:35 +0000 Subject: [PATCH 04/14] HiPDLP: remove free rows instead of adding free slacks (#3467) --- check/TestPdlpHi.cpp | 49 +++++++++++++++++++++++++++++++++++++++ highs/pdlp/hipdlp/pdhg.cc | 36 +++++++++++++++++++--------- 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/check/TestPdlpHi.cpp b/check/TestPdlpHi.cpp index fdd3b6fabc..734a00c854 100644 --- a/check/TestPdlpHi.cpp +++ b/check/TestPdlpHi.cpp @@ -83,3 +83,52 @@ TEST_CASE("hi-pdlp-maximize", "[pdlp]") { h.resetGlobalScheduler(true); } + +TEST_CASE("hi-pdlp-free-rows", "[pdlp]") { + // min x0 + 2 x1 + 3 x2 + // row 0: x0 + x1 >= 1 + // row 1: free (x0 - x2) + // row 2: x1 + x2 >= 2 + // row 3: x0 + x1 + x2 = 4 + HighsLp lp; + lp.num_col_ = 3; + lp.num_row_ = 4; + lp.col_cost_ = {1.0, 2.0, 3.0}; + lp.col_lower_ = {0.0, 0.0, 0.0}; + lp.col_upper_ = {kHighsInf, kHighsInf, kHighsInf}; + lp.row_lower_ = {1.0, -kHighsInf, 2.0, 4.0}; + lp.row_upper_ = {kHighsInf, kHighsInf, kHighsInf, 4.0}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.start_ = {0, 3, 6, 9}; + lp.a_matrix_.index_ = {0, 1, 3, 0, 2, 3, 1, 2, 3}; + lp.a_matrix_.value_ = {1, 1, 1, 1, 1, 1, -1, 1, 1}; + + for (const ObjSense sense : {ObjSense::kMinimize, ObjSense::kMaximize}) { + lp.sense_ = sense; + + Highs ref; + ref.setOptionValue("output_flag", dev_run); + REQUIRE(ref.passModel(lp) == HighsStatus::kOk); + ref.setOptionValue("solver", kSimplexString); + ref.setOptionValue("presolve", kHighsOffString); + REQUIRE(ref.run() == HighsStatus::kOk); + const double ref_objective = ref.getInfo().objective_function_value; + + Highs h; + h.setOptionValue("output_flag", dev_run); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.setOptionValue("solver", kHiPdlpString); + h.setOptionValue("presolve", kHighsOffString); + h.setOptionValue("kkt_tolerance", kkt_tolerance); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value - ref_objective) < + 1e-3 * (1.0 + std::fabs(ref_objective))); + + const HighsSolution& solution = h.getSolution(); + REQUIRE(solution.row_dual.size() == 4); + REQUIRE(solution.row_value.size() == 4); + REQUIRE(solution.row_dual[1] == 0.0); // free row + h.resetGlobalScheduler(true); + } +} diff --git a/highs/pdlp/hipdlp/pdhg.cc b/highs/pdlp/hipdlp/pdhg.cc index fd9f72653a..8a5b0bf50f 100644 --- a/highs/pdlp/hipdlp/pdhg.cc +++ b/highs/pdlp/hipdlp/pdhg.cc @@ -164,6 +164,7 @@ void PDLPSolver::preprocessLp() { HighsInt num_new_cols = 0; HighsInt nEqs = 0; + HighsInt num_free_rows = 0; sense_origin_ = (original_lp_->sense_ == ObjSense::kMinimize) ? 1 : -1; lp_.offset_ = original_lp_->offset_ * sense_origin_; constraint_types_.resize(nRows_orig); @@ -188,15 +189,14 @@ void PDLPSolver::preprocessLp() { constraint_types_[i] = LEQ; } else { constraint_types_[i] = FREE; - num_new_cols++; - nEqs++; + num_free_rows++; } } // 2. Set new dimensions HighsLp& processed_lp = lp_; processed_lp.num_col_ = nCols_orig + num_new_cols; - processed_lp.num_row_ = nRows_orig; + processed_lp.num_row_ = nRows_orig - num_free_rows; // Free rows are removed original_num_col_ = nCols_orig; constraint_new_idx_.resize(nRows_orig); @@ -210,21 +210,22 @@ void PDLPSolver::preprocessLp() { HighsInt eq_idx = 0; HighsInt ineq_idx = nEqs; for (HighsInt i = 0; i < nRows_orig; ++i) { - if (constraint_types_[i] == EQ || constraint_types_[i] == BOUND || - constraint_types_[i] == FREE) { + if (constraint_types_[i] == EQ || constraint_types_[i] == BOUND) { constraint_new_idx_[i] = eq_idx++; + } else if (constraint_types_[i] == FREE) { + constraint_new_idx_[i] = -1; // Mark free rows with -1 } else { constraint_new_idx_[i] = ineq_idx++; } } // 4. Create is_equality_row_ vector based on NEW ordering - is_equality_row_.resize(nRows_orig, false); + is_equality_row_.resize(processed_lp.num_row_, false); for (HighsInt i = 0; i < nRows_orig; ++i) { + if (constraint_types_[i] == FREE) continue; // Skip free rows HighsInt new_row_idx = constraint_new_idx_[i]; is_equality_row_[new_row_idx] = - (constraint_types_[i] == EQ || constraint_types_[i] == BOUND || - constraint_types_[i] == FREE); + (constraint_types_[i] == EQ || constraint_types_[i] == BOUND); } // 5. Populate costs and bounds for original and new slack variables @@ -236,7 +237,7 @@ void PDLPSolver::preprocessLp() { HighsInt current_slack_col = nCols_orig; for (HighsInt i = 0; i < nRows_orig; ++i) { - if (constraint_types_[i] == BOUND || constraint_types_[i] == FREE) { + if (constraint_types_[i] == BOUND) { processed_lp.col_cost_[current_slack_col] = 0.0; processed_lp.col_lower_[current_slack_col] = original_lp_->row_lower_[i]; processed_lp.col_upper_[current_slack_col] = original_lp_->row_upper_[i]; @@ -246,6 +247,7 @@ void PDLPSolver::preprocessLp() { // 6. Set the new RHS (row bounds) in PERMUTED order for (HighsInt i = 0; i < nRows_orig; ++i) { + if (constraint_types_[i] == FREE) continue; // Skip free rows HighsInt new_row_idx = constraint_new_idx_[i]; switch (constraint_types_[i]) { case EQ: @@ -262,11 +264,12 @@ void PDLPSolver::preprocessLp() { processed_lp.row_upper_[new_row_idx] = kHighsInf; break; case BOUND: - case FREE: // Becomes ax - z = 0 processed_lp.row_lower_[new_row_idx] = 0.0; processed_lp.row_upper_[new_row_idx] = 0.0; break; + case FREE: + break; // Removed above; never reaches here } } @@ -297,6 +300,12 @@ void PDLPSolver::preprocessLp() { for (HighsInt el = original_matrix.start_[col]; el < original_matrix.start_[col + 1]; ++el) { HighsInt old_row = original_matrix.index_[el]; + + // Skip free rows + if (constraint_types_[old_row] == FREE) { + continue; + } + HighsInt new_row = constraint_new_idx_[old_row]; double val = original_matrix.value_[el]; @@ -323,7 +332,7 @@ void PDLPSolver::preprocessLp() { // Add slack variable columns current_slack_col = nCols_orig; for (HighsInt i = 0; i < nRows_orig; ++i) { - if (constraint_types_[i] == BOUND || constraint_types_[i] == FREE) { + if (constraint_types_[i] == BOUND) { HighsInt row_idx = constraint_new_idx_[i]; processed_matrix.index_.push_back(row_idx); processed_matrix.value_.push_back(-1.0); @@ -415,6 +424,11 @@ PostSolveRetcode PDLPSolver::postprocess(HighsSolution& solution) { for (HighsInt orig_row = 0; orig_row < original_lp_->num_row_; ++orig_row) { HighsInt reordered_row = constraint_new_idx_[orig_row]; + if (constraint_types_[orig_row] == FREE) { + solution.row_dual[orig_row] = 0.0; // Free rows have zero duals + continue; + } + // Get the dual value from the reordered position double dual_value = y_reordered[reordered_row]; From b3dda78397fef7a7e26c9192748ff923f5bf1f88 Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 17:51:07 +0000 Subject: [PATCH 05/14] HiPDLP: accept an LP with no rows after free rows are removed (#3467) --- check/TestPdlpHi.cpp | 36 ++++++++++++++++++++++++++++++++++++ highs/pdlp/hipdlp/pdhg.cc | 5 +++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/check/TestPdlpHi.cpp b/check/TestPdlpHi.cpp index 734a00c854..36d761c5af 100644 --- a/check/TestPdlpHi.cpp +++ b/check/TestPdlpHi.cpp @@ -132,3 +132,39 @@ TEST_CASE("hi-pdlp-free-rows", "[pdlp]") { h.resetGlobalScheduler(true); } } + +TEST_CASE("hi-pdlp-all-free-rows", "[pdlp]") { + // min x0 + 2 x1 + // 0 <= x0 <= 3, 1 <= x1 <= 2 + // two free rows, so there are no constraints left after they are removed: + // the optimum is x = (0, 1), objective 2 + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 2; + lp.col_cost_ = {1.0, 2.0}; + lp.col_lower_ = {0.0, 1.0}; + lp.col_upper_ = {3.0, 2.0}; + lp.row_lower_ = {-kHighsInf, -kHighsInf}; + lp.row_upper_ = {kHighsInf, kHighsInf}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.start_ = {0, 2, 4}; + lp.a_matrix_.index_ = {0, 1, 0, 1}; + lp.a_matrix_.value_ = {1.0, 1.0, 1.0, -1.0}; + + Highs h; + h.setOptionValue("output_flag", dev_run); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.setOptionValue("solver", kHiPdlpString); + h.setOptionValue("presolve", kHighsOffString); + h.setOptionValue("kkt_tolerance", kkt_tolerance); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value - 2.0) < 1e-3); + + const HighsSolution& solution = h.getSolution(); + REQUIRE(solution.row_dual.size() == 2); + REQUIRE(solution.row_dual[0] == 0.0); + REQUIRE(solution.row_dual[1] == 0.0); + + h.resetGlobalScheduler(true); +} \ No newline at end of file diff --git a/highs/pdlp/hipdlp/pdhg.cc b/highs/pdlp/hipdlp/pdhg.cc index 8a5b0bf50f..70c0e29193 100644 --- a/highs/pdlp/hipdlp/pdhg.cc +++ b/highs/pdlp/hipdlp/pdhg.cc @@ -206,7 +206,8 @@ void PDLPSolver::preprocessLp() { processed_lp.row_lower_.resize(processed_lp.num_row_); processed_lp.row_upper_.resize(processed_lp.num_row_); - // 3. Determine row permutation: EQ/BOUND/FREE first, then LEQ/GEQ + // 3. Determine row permutation: EQ/BOUND first, then LEQ/GEQ; free rows are + // removed HighsInt eq_idx = 0; HighsInt ineq_idx = nEqs; for (HighsInt i = 0; i < nRows_orig; ++i) { @@ -373,7 +374,7 @@ PostSolveRetcode PDLPSolver::postprocess(HighsSolution& solution) { return PostSolveRetcode::DIMENSION_MISMATCH; } - if (x_current_.empty() || y_current_.empty()) { + if (x_current_.empty() || (y_current_.empty() && lp_.num_row_ > 0)) { return PostSolveRetcode::INVALID_SOLUTION; } From 21f363fcb5928d92bc53fb9319913caba1b3e4f9 Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 15:25:18 +0000 Subject: [PATCH 06/14] Add 3459.mps test instance --- check/instances/3459.mps | 279 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 check/instances/3459.mps diff --git a/check/instances/3459.mps b/check/instances/3459.mps new file mode 100644 index 0000000000..f0d33951de --- /dev/null +++ b/check/instances/3459.mps @@ -0,0 +1,279 @@ +NAME +ROWS + N Obj + L r0 + L r1 + L r2 + L r3 + L r4 + L r5 + L r6 + L r7 + L r8 + L r9 + L r10 + L r11 + L r12 + L r13 + L r14 + L r15 + L r16 + L r17 + L r18 + L r19 +COLUMNS + c0 Obj 2 + c0 r0 3 + c0 r1 3 + c0 r2 3 + c0 r3 1 + c0 r7 1 + c0 r8 2 + c0 r12 2 + c1 Obj 8 + c1 r3 3 + c1 r5 1 + c1 r11 1 + c1 r15 2 + c1 r19 2 + c2 Obj 7 + c2 r3 1 + c2 r4 3 + c2 r5 3 + c2 r7 2 + c2 r12 3 + c2 r15 3 + c2 r16 1 + c3 Obj 5 + c3 r0 1 + c3 r7 1 + c3 r10 1 + c3 r15 1 + c4 Obj 4 + c4 r3 2 + c4 r6 3 + c4 r8 3 + c4 r13 2 + c4 r19 1 + c5 Obj 8 + c5 r1 2 + c5 r7 3 + c5 r11 3 + c6 Obj 4 + c6 r4 2 + c6 r5 2 + c6 r9 2 + c6 r13 2 + c6 r14 2 + c6 r16 2 + c6 r18 1 + c6 r19 2 + c7 Obj 3 + c7 r4 2 + c7 r8 2 + c7 r10 2 + c7 r17 2 + c7 r18 2 + c7 r19 2 + c8 Obj 4 + c8 r2 3 + c8 r13 2 + c8 r15 1 + c9 Obj 7 + c9 r4 1 + c9 r5 1 + c9 r8 1 + c9 r9 1 + c9 r11 2 + c9 r15 1 + c9 r17 3 + c9 r18 3 + c10 Obj 5 + c10 r5 3 + c10 r12 3 + c10 r13 3 + c10 r14 1 + c10 r16 1 + c10 r19 1 + c11 Obj 7 + c11 r3 3 + c11 r4 2 + c11 r9 2 + c11 r18 3 + c11 r19 1 + c12 Obj 4 + c12 r0 2 + c12 r4 3 + c12 r11 2 + c12 r12 2 + c12 r13 3 + c12 r14 3 + c12 r19 3 + c13 Obj 5 + c13 r2 1 + c13 r5 1 + c13 r15 1 + c13 r16 2 + c13 r17 1 + c14 Obj 3 + c14 r2 2 + c14 r4 3 + c14 r5 1 + c14 r6 2 + c14 r10 3 + c14 r11 2 + c14 r16 1 + c14 r17 2 + c15 Obj 2 + c15 r2 3 + c15 r3 3 + c15 r5 2 + c15 r7 2 + c15 r9 3 + c15 r11 1 + c15 r15 2 + c16 Obj 4 + c16 r0 2 + c16 r7 1 + c16 r12 2 + c16 r13 2 + c16 r14 2 + c16 r18 1 + c17 Obj 4 + c17 r6 3 + c17 r15 3 + c17 r19 1 + c18 Obj 8 + c18 r8 3 + c18 r17 1 + c18 r18 2 + c19 Obj 3 + c19 r0 3 + c19 r1 2 + c19 r5 1 + c19 r6 3 + c19 r12 2 + c19 r14 3 + c19 r16 3 + c19 r19 1 + c20 Obj 8 + c20 r1 3 + c20 r3 2 + c20 r4 2 + c20 r8 3 + c20 r10 1 + c20 r13 3 + c20 r17 2 + c21 Obj 8 + c21 r6 1 + c21 r7 2 + c21 r8 2 + c21 r16 1 + c21 r17 1 + c21 r18 2 + c22 Obj 2 + c22 r0 2 + c22 r2 1 + c22 r3 3 + c22 r4 3 + c22 r6 1 + c22 r7 3 + c22 r9 3 + c22 r11 1 + c22 r12 3 + c22 r13 3 + c22 r14 1 + c23 Obj 6 + c23 r1 3 + c23 r4 3 + c23 r10 1 + c23 r16 1 + c24 Obj 6 + c24 r0 1 + c24 r2 2 + c24 r3 3 + c24 r9 2 + c24 r10 1 + c24 r11 2 + c24 r16 2 + c24 r18 2 + c25 Obj 8 + c25 r2 2 + c25 r3 3 + c25 r4 1 + c25 r5 3 + c25 r11 3 + c25 r18 3 + c25 r19 3 + c26 Obj 8 + c26 r1 1 + c26 r3 2 + c26 r9 3 + c26 r10 1 + c26 r12 1 + c26 r13 2 + c26 r15 3 + c27 Obj 3 + c27 r1 2 + c27 r6 3 + c27 r13 1 + c27 r18 1 + c28 Obj 5 + c28 r2 3 + c28 r3 3 + c28 r6 3 + c28 r8 1 + c28 r9 3 + c28 r10 2 + c28 r12 2 + c28 r13 2 + c29 Obj 4 + c29 r2 1 + c29 r6 1 + c29 r10 3 + c29 r16 1 + c29 r18 3 + c29 r19 3 +RHS + RHS_V r0 6.269489302794252 + RHS_V r1 5.4189127335612373 + RHS_V r2 5.825998508615795 + RHS_V r3 6.3972282288045239 + RHS_V r4 4.1131438974023098 + RHS_V r5 5.3592446195886989 + RHS_V r6 4.0008867988411927 + RHS_V r7 5.9301504880066922 + RHS_V r8 6.4320026603217606 + RHS_V r9 4.3719618402626708 + RHS_V r10 4.9683776085791873 + RHS_V r11 7.2159672842360543 + RHS_V r12 7.3611262393983914 + RHS_V r13 5.5509330170814586 + RHS_V r14 7.2568949221649071 + RHS_V r15 5.1085610121285709 + RHS_V r16 6.8244328892539272 + RHS_V r17 6.1818264946284582 + RHS_V r18 5.7603963156106497 + RHS_V r19 6.6257691046951059 +RANGES + RANGE r0 3.0000000000000004 + RANGE r1 3 + RANGE r2 3 + RANGE r3 3 + RANGE r4 3.0000000000000004 + RANGE r5 3.0000000000000004 + RANGE r6 3 + RANGE r7 3.0000000000000004 + RANGE r8 2.9999999999999996 + RANGE r9 3.0000000000000004 + RANGE r10 3.0000000000000004 + RANGE r11 3 + RANGE r12 3 + RANGE r13 3 + RANGE r14 3 + RANGE r15 3 + RANGE r16 3.0000000000000004 + RANGE r17 3.0000000000000004 + RANGE r18 3 + RANGE r19 2.9999999999999996 +ENDATA From 78b1cf63b999f13a09319aa9ceb810a8aa289114 Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 15:25:26 +0000 Subject: [PATCH 07/14] HiPDLP: handle maximization (#3467) --- check/TestPdlpHi.cpp | 45 +++++++++++++++++++++++++++++++++++++++ highs/pdlp/hipdlp/pdhg.cc | 9 ++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/check/TestPdlpHi.cpp b/check/TestPdlpHi.cpp index d38dde9248..fdd3b6fabc 100644 --- a/check/TestPdlpHi.cpp +++ b/check/TestPdlpHi.cpp @@ -1,4 +1,5 @@ #include +#include #include "HCheckConfig.h" #include "HConfig.h" @@ -38,3 +39,47 @@ TEST_CASE("hi-pdlp", "[pdlp]") { } h.resetThreadScheduler(true); } + +TEST_CASE("hi-pdlp-maximize", "[pdlp]") { + const std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/3459.mps"; + + // Reference: minimize with simplex + Highs ref; + ref.setOptionValue("output_flag", dev_run); + REQUIRE(ref.readModel(model_file) != HighsStatus::kError); + ref.setOptionValue("solver", kSimplexString); + ref.setOptionValue("presolve", kHighsOffString); + REQUIRE(ref.run() == HighsStatus::kOk); + const double min_objective = ref.getInfo().objective_function_value; + + // Negate all costs and maximize: the optimal objective is -min_objective + HighsLp lp = ref.getLp(); + for (double& cost : lp.col_cost_) cost = -cost; + lp.sense_ = ObjSense::kMaximize; + + Highs h; + h.setOptionValue("output_flag", dev_run); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.setOptionValue("solver", kHiPdlpString); + h.setOptionValue("presolve", kHighsOffString); + h.setOptionValue("kkt_tolerance", kkt_tolerance); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value + min_objective) < + 1e-3 * (1.0 + std::fabs(min_objective))); + + // The objective offset must also be reported on the original (maximize) + // scale + const double offset = 5.0; + lp.offset_ = offset; + h.clearSolver(); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value - + (-min_objective + offset)) < + 1e-3 * (1.0 + std::fabs(min_objective))); + + h.resetGlobalScheduler(true); +} diff --git a/highs/pdlp/hipdlp/pdhg.cc b/highs/pdlp/hipdlp/pdhg.cc index 338978738f..fd9f72653a 100644 --- a/highs/pdlp/hipdlp/pdhg.cc +++ b/highs/pdlp/hipdlp/pdhg.cc @@ -162,11 +162,10 @@ void PDLPSolver::preprocessLp() { logger_.info("Original LP matrix must be in column-wise format,"); } - lp_.offset_ = original_lp_->offset_; - HighsInt num_new_cols = 0; HighsInt nEqs = 0; sense_origin_ = (original_lp_->sense_ == ObjSense::kMinimize) ? 1 : -1; + lp_.offset_ = original_lp_->offset_ * sense_origin_; constraint_types_.resize(nRows_orig); // 1. First pass: Classify constraints and count slack variables needed @@ -230,7 +229,7 @@ void PDLPSolver::preprocessLp() { // 5. Populate costs and bounds for original and new slack variables for (HighsInt i = 0; i < nCols_orig; ++i) { - processed_lp.col_cost_[i] = original_lp_->col_cost_[i]; + processed_lp.col_cost_[i] = original_lp_->col_cost_[i] * sense_origin_; processed_lp.col_lower_[i] = original_lp_->col_lower_[i]; processed_lp.col_upper_[i] = original_lp_->col_upper_[i]; } @@ -421,9 +420,9 @@ PostSolveRetcode PDLPSolver::postprocess(HighsSolution& solution) { // Apply sign correction for LEQ constraints if (constraint_types_[orig_row] == LEQ) { - solution.row_dual[orig_row] = -dual_value; + solution.row_dual[orig_row] = -dual_value * sense_origin_; } else { - solution.row_dual[orig_row] = dual_value; + solution.row_dual[orig_row] = dual_value * sense_origin_; } } From 38fd39d4a69b9e7f40bcfd6c1497ece53e01432d Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 17:43:35 +0000 Subject: [PATCH 08/14] HiPDLP: remove free rows instead of adding free slacks (#3467) --- check/TestPdlpHi.cpp | 49 +++++++++++++++++++++++++++++++++++++++ highs/pdlp/hipdlp/pdhg.cc | 36 +++++++++++++++++++--------- 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/check/TestPdlpHi.cpp b/check/TestPdlpHi.cpp index fdd3b6fabc..734a00c854 100644 --- a/check/TestPdlpHi.cpp +++ b/check/TestPdlpHi.cpp @@ -83,3 +83,52 @@ TEST_CASE("hi-pdlp-maximize", "[pdlp]") { h.resetGlobalScheduler(true); } + +TEST_CASE("hi-pdlp-free-rows", "[pdlp]") { + // min x0 + 2 x1 + 3 x2 + // row 0: x0 + x1 >= 1 + // row 1: free (x0 - x2) + // row 2: x1 + x2 >= 2 + // row 3: x0 + x1 + x2 = 4 + HighsLp lp; + lp.num_col_ = 3; + lp.num_row_ = 4; + lp.col_cost_ = {1.0, 2.0, 3.0}; + lp.col_lower_ = {0.0, 0.0, 0.0}; + lp.col_upper_ = {kHighsInf, kHighsInf, kHighsInf}; + lp.row_lower_ = {1.0, -kHighsInf, 2.0, 4.0}; + lp.row_upper_ = {kHighsInf, kHighsInf, kHighsInf, 4.0}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.start_ = {0, 3, 6, 9}; + lp.a_matrix_.index_ = {0, 1, 3, 0, 2, 3, 1, 2, 3}; + lp.a_matrix_.value_ = {1, 1, 1, 1, 1, 1, -1, 1, 1}; + + for (const ObjSense sense : {ObjSense::kMinimize, ObjSense::kMaximize}) { + lp.sense_ = sense; + + Highs ref; + ref.setOptionValue("output_flag", dev_run); + REQUIRE(ref.passModel(lp) == HighsStatus::kOk); + ref.setOptionValue("solver", kSimplexString); + ref.setOptionValue("presolve", kHighsOffString); + REQUIRE(ref.run() == HighsStatus::kOk); + const double ref_objective = ref.getInfo().objective_function_value; + + Highs h; + h.setOptionValue("output_flag", dev_run); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.setOptionValue("solver", kHiPdlpString); + h.setOptionValue("presolve", kHighsOffString); + h.setOptionValue("kkt_tolerance", kkt_tolerance); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value - ref_objective) < + 1e-3 * (1.0 + std::fabs(ref_objective))); + + const HighsSolution& solution = h.getSolution(); + REQUIRE(solution.row_dual.size() == 4); + REQUIRE(solution.row_value.size() == 4); + REQUIRE(solution.row_dual[1] == 0.0); // free row + h.resetGlobalScheduler(true); + } +} diff --git a/highs/pdlp/hipdlp/pdhg.cc b/highs/pdlp/hipdlp/pdhg.cc index fd9f72653a..8a5b0bf50f 100644 --- a/highs/pdlp/hipdlp/pdhg.cc +++ b/highs/pdlp/hipdlp/pdhg.cc @@ -164,6 +164,7 @@ void PDLPSolver::preprocessLp() { HighsInt num_new_cols = 0; HighsInt nEqs = 0; + HighsInt num_free_rows = 0; sense_origin_ = (original_lp_->sense_ == ObjSense::kMinimize) ? 1 : -1; lp_.offset_ = original_lp_->offset_ * sense_origin_; constraint_types_.resize(nRows_orig); @@ -188,15 +189,14 @@ void PDLPSolver::preprocessLp() { constraint_types_[i] = LEQ; } else { constraint_types_[i] = FREE; - num_new_cols++; - nEqs++; + num_free_rows++; } } // 2. Set new dimensions HighsLp& processed_lp = lp_; processed_lp.num_col_ = nCols_orig + num_new_cols; - processed_lp.num_row_ = nRows_orig; + processed_lp.num_row_ = nRows_orig - num_free_rows; // Free rows are removed original_num_col_ = nCols_orig; constraint_new_idx_.resize(nRows_orig); @@ -210,21 +210,22 @@ void PDLPSolver::preprocessLp() { HighsInt eq_idx = 0; HighsInt ineq_idx = nEqs; for (HighsInt i = 0; i < nRows_orig; ++i) { - if (constraint_types_[i] == EQ || constraint_types_[i] == BOUND || - constraint_types_[i] == FREE) { + if (constraint_types_[i] == EQ || constraint_types_[i] == BOUND) { constraint_new_idx_[i] = eq_idx++; + } else if (constraint_types_[i] == FREE) { + constraint_new_idx_[i] = -1; // Mark free rows with -1 } else { constraint_new_idx_[i] = ineq_idx++; } } // 4. Create is_equality_row_ vector based on NEW ordering - is_equality_row_.resize(nRows_orig, false); + is_equality_row_.resize(processed_lp.num_row_, false); for (HighsInt i = 0; i < nRows_orig; ++i) { + if (constraint_types_[i] == FREE) continue; // Skip free rows HighsInt new_row_idx = constraint_new_idx_[i]; is_equality_row_[new_row_idx] = - (constraint_types_[i] == EQ || constraint_types_[i] == BOUND || - constraint_types_[i] == FREE); + (constraint_types_[i] == EQ || constraint_types_[i] == BOUND); } // 5. Populate costs and bounds for original and new slack variables @@ -236,7 +237,7 @@ void PDLPSolver::preprocessLp() { HighsInt current_slack_col = nCols_orig; for (HighsInt i = 0; i < nRows_orig; ++i) { - if (constraint_types_[i] == BOUND || constraint_types_[i] == FREE) { + if (constraint_types_[i] == BOUND) { processed_lp.col_cost_[current_slack_col] = 0.0; processed_lp.col_lower_[current_slack_col] = original_lp_->row_lower_[i]; processed_lp.col_upper_[current_slack_col] = original_lp_->row_upper_[i]; @@ -246,6 +247,7 @@ void PDLPSolver::preprocessLp() { // 6. Set the new RHS (row bounds) in PERMUTED order for (HighsInt i = 0; i < nRows_orig; ++i) { + if (constraint_types_[i] == FREE) continue; // Skip free rows HighsInt new_row_idx = constraint_new_idx_[i]; switch (constraint_types_[i]) { case EQ: @@ -262,11 +264,12 @@ void PDLPSolver::preprocessLp() { processed_lp.row_upper_[new_row_idx] = kHighsInf; break; case BOUND: - case FREE: // Becomes ax - z = 0 processed_lp.row_lower_[new_row_idx] = 0.0; processed_lp.row_upper_[new_row_idx] = 0.0; break; + case FREE: + break; // Removed above; never reaches here } } @@ -297,6 +300,12 @@ void PDLPSolver::preprocessLp() { for (HighsInt el = original_matrix.start_[col]; el < original_matrix.start_[col + 1]; ++el) { HighsInt old_row = original_matrix.index_[el]; + + // Skip free rows + if (constraint_types_[old_row] == FREE) { + continue; + } + HighsInt new_row = constraint_new_idx_[old_row]; double val = original_matrix.value_[el]; @@ -323,7 +332,7 @@ void PDLPSolver::preprocessLp() { // Add slack variable columns current_slack_col = nCols_orig; for (HighsInt i = 0; i < nRows_orig; ++i) { - if (constraint_types_[i] == BOUND || constraint_types_[i] == FREE) { + if (constraint_types_[i] == BOUND) { HighsInt row_idx = constraint_new_idx_[i]; processed_matrix.index_.push_back(row_idx); processed_matrix.value_.push_back(-1.0); @@ -415,6 +424,11 @@ PostSolveRetcode PDLPSolver::postprocess(HighsSolution& solution) { for (HighsInt orig_row = 0; orig_row < original_lp_->num_row_; ++orig_row) { HighsInt reordered_row = constraint_new_idx_[orig_row]; + if (constraint_types_[orig_row] == FREE) { + solution.row_dual[orig_row] = 0.0; // Free rows have zero duals + continue; + } + // Get the dual value from the reordered position double dual_value = y_reordered[reordered_row]; From df98398acb254ba655ca7100e2abcbf6862b5ae2 Mon Sep 17 00:00:00 2001 From: Yanyu Zhou Date: Mon, 26 Oct 2026 17:51:07 +0000 Subject: [PATCH 09/14] HiPDLP: accept an LP with no rows after free rows are removed (#3467) --- check/TestPdlpHi.cpp | 36 ++++++++++++++++++++++++++++++++++++ highs/pdlp/hipdlp/pdhg.cc | 5 +++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/check/TestPdlpHi.cpp b/check/TestPdlpHi.cpp index 734a00c854..36d761c5af 100644 --- a/check/TestPdlpHi.cpp +++ b/check/TestPdlpHi.cpp @@ -132,3 +132,39 @@ TEST_CASE("hi-pdlp-free-rows", "[pdlp]") { h.resetGlobalScheduler(true); } } + +TEST_CASE("hi-pdlp-all-free-rows", "[pdlp]") { + // min x0 + 2 x1 + // 0 <= x0 <= 3, 1 <= x1 <= 2 + // two free rows, so there are no constraints left after they are removed: + // the optimum is x = (0, 1), objective 2 + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 2; + lp.col_cost_ = {1.0, 2.0}; + lp.col_lower_ = {0.0, 1.0}; + lp.col_upper_ = {3.0, 2.0}; + lp.row_lower_ = {-kHighsInf, -kHighsInf}; + lp.row_upper_ = {kHighsInf, kHighsInf}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.start_ = {0, 2, 4}; + lp.a_matrix_.index_ = {0, 1, 0, 1}; + lp.a_matrix_.value_ = {1.0, 1.0, 1.0, -1.0}; + + Highs h; + h.setOptionValue("output_flag", dev_run); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.setOptionValue("solver", kHiPdlpString); + h.setOptionValue("presolve", kHighsOffString); + h.setOptionValue("kkt_tolerance", kkt_tolerance); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(std::fabs(h.getInfo().objective_function_value - 2.0) < 1e-3); + + const HighsSolution& solution = h.getSolution(); + REQUIRE(solution.row_dual.size() == 2); + REQUIRE(solution.row_dual[0] == 0.0); + REQUIRE(solution.row_dual[1] == 0.0); + + h.resetGlobalScheduler(true); +} \ No newline at end of file diff --git a/highs/pdlp/hipdlp/pdhg.cc b/highs/pdlp/hipdlp/pdhg.cc index 8a5b0bf50f..70c0e29193 100644 --- a/highs/pdlp/hipdlp/pdhg.cc +++ b/highs/pdlp/hipdlp/pdhg.cc @@ -206,7 +206,8 @@ void PDLPSolver::preprocessLp() { processed_lp.row_lower_.resize(processed_lp.num_row_); processed_lp.row_upper_.resize(processed_lp.num_row_); - // 3. Determine row permutation: EQ/BOUND/FREE first, then LEQ/GEQ + // 3. Determine row permutation: EQ/BOUND first, then LEQ/GEQ; free rows are + // removed HighsInt eq_idx = 0; HighsInt ineq_idx = nEqs; for (HighsInt i = 0; i < nRows_orig; ++i) { @@ -373,7 +374,7 @@ PostSolveRetcode PDLPSolver::postprocess(HighsSolution& solution) { return PostSolveRetcode::DIMENSION_MISMATCH; } - if (x_current_.empty() || y_current_.empty()) { + if (x_current_.empty() || (y_current_.empty() && lp_.num_row_ > 0)) { return PostSolveRetcode::INVALID_SOLUTION; } From f56359e4ad3633e9c1d28b22ee061a38a638279a Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Tue, 27 Oct 2026 12:20:55 +0000 Subject: [PATCH 10/14] Merged latest into this branch --- check/TestLpSolvers.cpp | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/check/TestLpSolvers.cpp b/check/TestLpSolvers.cpp index 1039f21925..70242012a5 100644 --- a/check/TestLpSolvers.cpp +++ b/check/TestLpSolvers.cpp @@ -603,41 +603,3 @@ TEST_CASE("issue-2300", "[highs_lp_solver]") { h.resetThreadScheduler(true); } -TEST_CASE("issue-3459", "[highs_lp_solver]") { - Highs h; - // h.setOptionValue("output_flag", dev_run); - h.setOptionValue(kSolverString, kPdlpString); - h.setOptionValue(kPresolveString, kHighsOffString); - const bool use_3459_model = true; - if (use_3459_model) { - std::string model_file = - std::string(HIGHS_DIR) + "/check/instances/3459.mps"; - h.readModel(model_file); - } else { - HighsLp lp; - lp.num_col_ = 2; - lp.num_row_ = 3; - lp.col_cost_ = {1, -1}; - lp.col_lower_ = {0, 0}; - lp.col_upper_ = {kHighsInf, kHighsInf}; - lp.row_lower_ = {1, 2, 3}; // Infeasible or unbounded - lp.row_upper_ = {2, 3, 4}; // Infeasible or unbounded - lp.row_lower_ = {2, 0, 0}; - lp.row_upper_ = {3, 1, 1}; - lp.a_matrix_.start_ = {0, 3, 6}; - lp.a_matrix_.index_ = {0, 1, 2, 0, 1, 2}; - lp.a_matrix_.value_ = {1, -1, 2, 2, 1, -1}; - h.passModel(lp); - } - HighsStatus return_status = h.run(); - h.writeSolution("", 1); - REQUIRE(return_status == HighsStatus::kOk); - - HighsSolution solution = h.getSolution(); - - h.crossover(solution); - h.writeSolution("", 1); - - h.resetGlobalScheduler(true); -} - From f684cd3e2f957af7437f6ae2798f4facec86a40f Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Tue, 27 Oct 2026 12:25:44 +0000 Subject: [PATCH 11/14] Removed unnecessary reference to resetGlobalScheduler from latest --- check/TestCrossover.cpp | 2 +- check/TestMipSolver.cpp | 4 ++-- check/TestPresolve.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/check/TestCrossover.cpp b/check/TestCrossover.cpp index dde7065891..6ed97c3507 100644 --- a/check/TestCrossover.cpp +++ b/check/TestCrossover.cpp @@ -227,5 +227,5 @@ TEST_CASE("issue-3459", "[highs_crossover]") { testSolvers("with free row"); } - h.resetGlobalScheduler(true); + h.resetThreadScheduler(true); } diff --git a/check/TestMipSolver.cpp b/check/TestMipSolver.cpp index b2f29fdb6c..c617570f48 100644 --- a/check/TestMipSolver.cpp +++ b/check/TestMipSolver.cpp @@ -2538,7 +2538,7 @@ TEST_CASE("issue-3433", "[highs_test_mip_solver]") { highs.clearSolver(); solve(highs, kHighsOnString, require_model_status, optimal_objective); } - highs.resetGlobalScheduler(true); + highs.resetThreadScheduler(true); } TEST_CASE("issue-3451", "[highs_test_mip_solver]") { @@ -2556,7 +2556,7 @@ TEST_CASE("issue-3451", "[highs_test_mip_solver]") { const HighsModelStatus require_model_status = HighsModelStatus::kOptimal; const double optimal_objective = -135.865250363; solve(highs, kHighsOnString, require_model_status, optimal_objective); - highs.resetGlobalScheduler(true); + highs.resetThreadScheduler(true); } TEST_CASE("conflict-pool-sync-empty-conflict", "[highs_test_mip_solver]") { diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index cc1380b20d..3f8dcab58a 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -1979,5 +1979,5 @@ TEST_CASE("initial-sweep-empty-fixed-col", "[highs_test_presolve]") { REQUIRE(h.getInfo().objective_function_value == 7); REQUIRE(h.getSolution().col_value[1] == 2); - h.resetGlobalScheduler(true); + h.resetThreadScheduler(true); } From e192d15377df5cdcb7f950db6dc7f11e90fe6cc1 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Tue, 27 Oct 2026 12:38:43 +0000 Subject: [PATCH 12/14] Renamed TestPdlpHi.cpp to TestHiPdlp.cpp and added TestHiPdlp.cpp to meson.build and BUILD.bazel --- BUILD.bazel | 1 + check/CMakeLists.txt | 2 +- check/{TestPdlpHi.cpp => TestHiPdlp.cpp} | 10 +++++----- check/TestLpSolvers.cpp | 1 - check/meson.build | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) rename check/{TestPdlpHi.cpp => TestHiPdlp.cpp} (97%) diff --git a/BUILD.bazel b/BUILD.bazel index 05df1f13a8..2d6439a7d7 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -212,6 +212,7 @@ TEST_NAMES = [ "TestHighsModel", "TestHighsParallel", "TestHighsRbTree", + "TestHiPdlp", "TestHSet", "TestICrash", "TestInfo", diff --git a/check/CMakeLists.txt b/check/CMakeLists.txt index 65446a7a1f..6dba61a391 100644 --- a/check/CMakeLists.txt +++ b/check/CMakeLists.txt @@ -49,6 +49,7 @@ if ((NOT FAST_BUILD OR ALL_TESTS) AND NOT (BUILD_EXTRA_UNIT_ONLY)) TestHighsRbTree.cpp TestHighsSparseMatrix.cpp TestHighsVersion.cpp + TestHiPdlp.cpp TestHSet.cpp TestICrash.cpp TestIis.cpp @@ -69,7 +70,6 @@ if ((NOT FAST_BUILD OR ALL_TESTS) AND NOT (BUILD_EXTRA_UNIT_ONLY)) TestNames.cpp TestOptions.cpp TestPdlp.cpp - TestPdlpHi.cpp TestPresolve.cpp TestPresolveRules.cpp TestQpOracle.cpp diff --git a/check/TestPdlpHi.cpp b/check/TestHiPdlp.cpp similarity index 97% rename from check/TestPdlpHi.cpp rename to check/TestHiPdlp.cpp index 36d761c5af..7f39d72235 100644 --- a/check/TestPdlpHi.cpp +++ b/check/TestHiPdlp.cpp @@ -16,7 +16,7 @@ const bool dev_run = false; const double kkt_tolerance = 1e-4; -TEST_CASE("hi-pdlp", "[pdlp]") { +TEST_CASE("hi-pdlp", "[hipdlp]") { std::string model = "afiro"; std::string model_file = std::string(HIGHS_DIR) + "/check/instances/" + model + ".mps"; @@ -40,7 +40,7 @@ TEST_CASE("hi-pdlp", "[pdlp]") { h.resetThreadScheduler(true); } -TEST_CASE("hi-pdlp-maximize", "[pdlp]") { +TEST_CASE("hi-pdlp-maximize", "[hipdlp]") { const std::string model_file = std::string(HIGHS_DIR) + "/check/instances/3459.mps"; @@ -84,7 +84,7 @@ TEST_CASE("hi-pdlp-maximize", "[pdlp]") { h.resetGlobalScheduler(true); } -TEST_CASE("hi-pdlp-free-rows", "[pdlp]") { +TEST_CASE("hi-pdlp-free-rows", "[hipdlp]") { // min x0 + 2 x1 + 3 x2 // row 0: x0 + x1 >= 1 // row 1: free (x0 - x2) @@ -133,7 +133,7 @@ TEST_CASE("hi-pdlp-free-rows", "[pdlp]") { } } -TEST_CASE("hi-pdlp-all-free-rows", "[pdlp]") { +TEST_CASE("hi-pdlp-all-free-rows", "[hipdlp]") { // min x0 + 2 x1 // 0 <= x0 <= 3, 1 <= x1 <= 2 // two free rows, so there are no constraints left after they are removed: @@ -167,4 +167,4 @@ TEST_CASE("hi-pdlp-all-free-rows", "[pdlp]") { REQUIRE(solution.row_dual[1] == 0.0); h.resetGlobalScheduler(true); -} \ No newline at end of file +} diff --git a/check/TestLpSolvers.cpp b/check/TestLpSolvers.cpp index 70242012a5..65f1f58bf3 100644 --- a/check/TestLpSolvers.cpp +++ b/check/TestLpSolvers.cpp @@ -602,4 +602,3 @@ TEST_CASE("issue-2300", "[highs_lp_solver]") { h.resetThreadScheduler(true); } - diff --git a/check/meson.build b/check/meson.build index 3050d164de..d335541cf9 100644 --- a/check/meson.build +++ b/check/meson.build @@ -33,6 +33,7 @@ test_array = [ ['test_TestHighsRbTree', 'TestHighsRbTree.cpp'], ['test_TestHighsSparseMatrix', 'TestHighsSparseMatrix.cpp'], ['test_TestHighsVersion', 'TestHighsVersion.cpp'], + ['test_TestHiPdlp', 'TestHiPdlp.cpp'], # ['test_TestHipo', 'TestHipo.cpp'], ['test_TestHSet', 'TestHSet.cpp'], ['test_TestICrash', 'TestICrash.cpp'], From 46c056aae496a141491a3aabefa1436c47b44fed Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Tue, 27 Oct 2026 12:40:03 +0000 Subject: [PATCH 13/14] Removed further unnecessary references to resetGlobalScheduler --- check/TestHiPdlp.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/check/TestHiPdlp.cpp b/check/TestHiPdlp.cpp index 7f39d72235..bb211cb37e 100644 --- a/check/TestHiPdlp.cpp +++ b/check/TestHiPdlp.cpp @@ -81,7 +81,7 @@ TEST_CASE("hi-pdlp-maximize", "[hipdlp]") { (-min_objective + offset)) < 1e-3 * (1.0 + std::fabs(min_objective))); - h.resetGlobalScheduler(true); + h.resetThreadScheduler(true); } TEST_CASE("hi-pdlp-free-rows", "[hipdlp]") { @@ -129,7 +129,7 @@ TEST_CASE("hi-pdlp-free-rows", "[hipdlp]") { REQUIRE(solution.row_dual.size() == 4); REQUIRE(solution.row_value.size() == 4); REQUIRE(solution.row_dual[1] == 0.0); // free row - h.resetGlobalScheduler(true); + h.resetThreadScheduler(true); } } @@ -166,5 +166,5 @@ TEST_CASE("hi-pdlp-all-free-rows", "[hipdlp]") { REQUIRE(solution.row_dual[0] == 0.0); REQUIRE(solution.row_dual[1] == 0.0); - h.resetGlobalScheduler(true); + h.resetThreadScheduler(true); } From 6792299f75eb1a66f0fc03376da47e7fa2895f6c Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Tue, 27 Oct 2026 14:32:08 +0000 Subject: [PATCH 14/14] Added HiPdlp to solvers tested in issue-3459 --- check/TestCrossover.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/check/TestCrossover.cpp b/check/TestCrossover.cpp index 6ed97c3507..7bc9863552 100644 --- a/check/TestCrossover.cpp +++ b/check/TestCrossover.cpp @@ -111,9 +111,7 @@ void report(const std::string message, const HighsLp& lp, printf(" Obj %11.4g\n", objective); } -const std::vector solvers{kPdlpString, - // kHiPdlpString, - kIpxString +const std::vector solvers{kPdlpString, kHiPdlpString, kIpxString #ifdef HIPO , kHipoString