-
Notifications
You must be signed in to change notification settings - Fork 121
GF(2) presolve reduction for enlight_hard.mps #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a0fffe6
gf2 xor constraint detection
aliceb-nv e16e608
working, b4 moving to papilo presolver
aliceb-nv 9d95014
added as a papilo presolver
aliceb-nv 182ac18
cleanup
aliceb-nv dc61271
switch to an actually hard instance :)
aliceb-nv 3e1fe0c
Update cpp/src/mip/presolve/gf2_presolve.cpp
aliceb-nv 6a955ea
Update cpp/src/mip/presolve/third_party_presolve.cpp
aliceb-nv 201fb80
Merge branch 'branch-25.10' into gf2-pr
aliceb-nv 7b28b24
transaction guard, fix build
aliceb-nv 1588c15
style fix
aliceb-nv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights | ||
| * reserved. SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| #include "gf2_presolve.hpp" | ||
|
|
||
| #include <mip/mip_constants.hpp> | ||
|
|
||
| #include <cmath> | ||
| #include <unordered_map> | ||
|
|
||
| #if GF2_PRESOLVE_DEBUG | ||
| #define NOT_GF2(reason, ...) \ | ||
| do { \ | ||
| printf("NO : Cons %d is not gf2: " reason "\n", cstr_idx, ##__VA_ARGS__); \ | ||
| goto not_valid; \ | ||
| } while (0) | ||
| #else | ||
| #define NOT_GF2(reason, ...) \ | ||
| do { \ | ||
| goto not_valid; \ | ||
| } while (0) | ||
| #endif | ||
|
|
||
| namespace cuopt::linear_programming::detail { | ||
|
|
||
| // this is kind-of a stopgap implementation (as in practice MIPLIB2017 only contains a couple of GF2 | ||
| // problems and they're small) but cuDSS could be used for this since A is likely to be sparse and | ||
| // low-bandwidth (i think?) unlikely to occur in real-world problems however. doubt it'd be worth | ||
| // the effort trashes A and b, return true if solved | ||
| static bool gf2_solve(std::vector<std::vector<int>>& A, std::vector<int>& b, std::vector<int>& x) | ||
| { | ||
| int i, j, k; | ||
| const int N = A.size(); | ||
| for (i = 0; i < N; i++) { | ||
| // Find pivot | ||
| int pivot = -1; | ||
| for (j = i; j < N; j++) { | ||
| if (A[j][i]) { | ||
| pivot = j; | ||
| break; | ||
| } | ||
| } | ||
| if (pivot == -1) return false; // No solution | ||
|
|
||
| // Swap current row with pivot row if needed | ||
| if (pivot != i) { | ||
| for (k = 0; k < N; k++) { | ||
| int temp = A[i][k]; | ||
| A[i][k] = A[pivot][k]; | ||
| A[pivot][k] = temp; | ||
| } | ||
| int temp = b[i]; | ||
| b[i] = b[pivot]; | ||
| b[pivot] = temp; | ||
| } | ||
|
|
||
| // Eliminate downwards | ||
| for (j = i + 1; j < N; j++) { | ||
| if (A[j][i]) { | ||
| for (k = i; k < N; k++) | ||
| A[j][k] ^= A[i][k]; | ||
| b[j] ^= b[i]; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Back-substitution | ||
| for (i = N - 1; i >= 0; i--) { | ||
| x[i] = b[i]; | ||
| for (j = i + 1; j < N; j++) | ||
| x[i] ^= (A[i][j] & x[j]); | ||
| if (!A[i][i] && x[i]) return false; // No solution | ||
| } | ||
| return true; // Success | ||
| } | ||
|
|
||
| template <typename f_t> | ||
| papilo::PresolveStatus GF2Presolve<f_t>::execute(const papilo::Problem<f_t>& problem, | ||
| const papilo::ProblemUpdate<f_t>& problemUpdate, | ||
| const papilo::Num<f_t>& num, | ||
| papilo::Reductions<f_t>& reductions, | ||
| const papilo::Timer& timer, | ||
| int& reason_of_infeasibility) | ||
| { | ||
| const auto& constraint_matrix = problem.getConstraintMatrix(); | ||
| const auto& lhs_values = constraint_matrix.getLeftHandSides(); | ||
| const auto& rhs_values = constraint_matrix.getRightHandSides(); | ||
| const auto& row_flags = constraint_matrix.getRowFlags(); | ||
| const auto& domains = problem.getVariableDomains(); | ||
| const auto& col_flags = domains.flags; | ||
| const auto& lower_bounds = domains.lower_bounds; | ||
| const auto& upper_bounds = domains.upper_bounds; | ||
|
|
||
| const int num_rows = constraint_matrix.getNRows(); | ||
|
|
||
| std::unordered_map<size_t, size_t> gf2_bin_vars; | ||
| std::unordered_map<size_t, size_t> gf2_key_vars; | ||
| std::vector<gf2_constraint_t> gf2_constraints; | ||
|
|
||
| const f_t integrality_tolerance = num.getFeasTol(); | ||
|
|
||
| for (int cstr_idx = 0; cstr_idx < num_rows; ++cstr_idx) { | ||
| int key_var_idx = -1; | ||
| f_t key_var_coeff = 0.0; | ||
|
|
||
| std::vector<std::pair<size_t, f_t>> constraint_bin_vars; | ||
|
|
||
| // Check constraint coefficients | ||
| auto row_coeff = constraint_matrix.getRowCoefficients(cstr_idx); | ||
| const int* row_indices = row_coeff.getIndices(); | ||
| const f_t* row_values = row_coeff.getValues(); | ||
| const int row_length = row_coeff.getLength(); | ||
| f_t rhs = std::round(lhs_values[cstr_idx]); | ||
|
|
||
| // Check if this is an equality constraint | ||
| if (!num.isEq(lhs_values[cstr_idx], rhs_values[cstr_idx])) | ||
| NOT_GF2("not eq", lhs_values[cstr_idx], rhs_values[cstr_idx]); | ||
| if (!std::isfinite(lhs_values[cstr_idx])) NOT_GF2("not finite", lhs_values[cstr_idx]); | ||
| if (!is_integer(lhs_values[cstr_idx], integrality_tolerance)) | ||
| NOT_GF2("not integer", lhs_values[cstr_idx]); | ||
|
|
||
| // Only accept 0, 1, -1 as rhs | ||
| if (rhs != 0.0 && rhs != 1.0 && rhs != -1.0) NOT_GF2("not 0, 1, -1", rhs); | ||
|
|
||
| for (int j = 0; j < row_length; ++j) { | ||
| if (!is_integer(row_values[j], integrality_tolerance)) { | ||
| NOT_GF2("coeff not integer", row_values[j]); | ||
| } | ||
|
|
||
| int var_idx = row_indices[j]; | ||
| f_t coeff = std::round(row_values[j]); | ||
|
|
||
| // Check if variable is integer | ||
| if (!col_flags[var_idx].test(papilo::ColFlag::kIntegral)) { | ||
| NOT_GF2("not integral", var_idx); | ||
| } | ||
|
|
||
| bool is_binary = col_flags[var_idx].test(papilo::ColFlag::kLbInf) ? false | ||
| : col_flags[var_idx].test(papilo::ColFlag::kUbInf) | ||
| ? false | ||
| : (lower_bounds[var_idx] == 0.0 && upper_bounds[var_idx] == 1.0); | ||
|
|
||
| // Check coefficient constraints | ||
| if (is_binary && (std::abs(coeff) != 1.0 && std::abs(coeff) != 2.0)) { | ||
| NOT_GF2("not binary", var_idx); | ||
| } | ||
| if (!is_binary && (std::abs(coeff) != 2.0)) { NOT_GF2("not binary", var_idx); } | ||
|
|
||
| // Key variable (coefficient of 2) | ||
| if (std::abs(coeff) == 2.0) { | ||
| if (key_var_idx != -1) { NOT_GF2("multiple key variables", var_idx); } | ||
| key_var_idx = var_idx; | ||
| key_var_coeff = coeff; | ||
| gf2_key_vars.insert({var_idx, gf2_key_vars.size()}); | ||
| } else { | ||
| // Binary variable | ||
| constraint_bin_vars.push_back({var_idx, coeff}); | ||
| gf2_bin_vars.insert({var_idx, gf2_bin_vars.size()}); | ||
| } | ||
| } | ||
|
|
||
| if (key_var_idx == -1) NOT_GF2("missing key variable"); | ||
|
|
||
| gf2_constraints.emplace_back((size_t)cstr_idx, | ||
| std::move(constraint_bin_vars), | ||
| std::pair<size_t, f_t>{key_var_idx, key_var_coeff}, | ||
| ((size_t)rhs) % 2); | ||
| continue; | ||
| not_valid: | ||
| continue; | ||
| } | ||
|
|
||
| // If no GF2 constraints found, return unchanged | ||
| if (gf2_constraints.empty()) { return papilo::PresolveStatus::kUnchanged; } | ||
|
|
||
| // Skip if that would cause computational explosion (O(n^3) with simple gaussian elimination) | ||
| if (gf2_constraints.size() > 1000) { return papilo::PresolveStatus::kUnchanged; } | ||
|
|
||
| // Validate structure | ||
| if (gf2_key_vars.size() != gf2_constraints.size() || | ||
| gf2_bin_vars.size() != gf2_constraints.size()) { | ||
| return papilo::PresolveStatus::kUnchanged; | ||
| } | ||
|
|
||
| // Create inverse mappings | ||
| std::unordered_map<size_t, size_t> gf2_bin_vars_invmap; | ||
| for (const auto& [var_idx, gf2_idx] : gf2_bin_vars) { | ||
| gf2_bin_vars_invmap.insert({gf2_idx, var_idx}); | ||
| } | ||
|
|
||
| // Build binary matrix | ||
| // Could be a flat vector but. oh well. in practice N is small | ||
| std::vector<std::vector<int>> A(gf2_constraints.size(), | ||
| std::vector<int>(gf2_constraints.size(), 0)); | ||
| std::vector<int> b(gf2_constraints.size()); | ||
| for (const auto& cons : gf2_constraints) { | ||
| for (auto [bin_var, _] : cons.bin_vars) { | ||
| A[cons.cstr_idx][gf2_bin_vars[bin_var]] = 1; | ||
| } | ||
| b[cons.cstr_idx] = cons.rhs; | ||
| } | ||
|
|
||
| std::vector<int> solution(gf2_constraints.size()); | ||
| bool feasible = gf2_solve(A, b, solution); | ||
| if (!feasible) { return papilo::PresolveStatus::kInfeasible; } | ||
|
|
||
| std::unordered_map<size_t, f_t> fixings; | ||
| // Fix binary variables | ||
| for (size_t sol_idx = 0; sol_idx < gf2_constraints.size(); ++sol_idx) { | ||
| fixings[gf2_bin_vars_invmap[sol_idx]] = solution[sol_idx]; | ||
| } | ||
|
|
||
| // Compute fixings for key variables by solving for the constraint | ||
| for (const auto& cons : gf2_constraints) { | ||
| auto [key_var_idx, key_var_coeff] = cons.key_var; | ||
| f_t constraint_rhs = lhs_values[cons.cstr_idx]; // equality constraint | ||
| f_t lhs = -constraint_rhs; | ||
| for (auto [bin_var, coeff] : cons.bin_vars) { | ||
| lhs += fixings[bin_var] * coeff; | ||
| } | ||
| fixings[key_var_idx] = std::round(-lhs / key_var_coeff); | ||
| } | ||
|
|
||
| papilo::PresolveStatus status = papilo::PresolveStatus::kUnchanged; | ||
| papilo::TransactionGuard rg{reductions}; | ||
| for (const auto& [var_idx, fixing] : fixings) { | ||
| if (num.isZero(fixing)) { | ||
| reductions.fixCol(var_idx, 0); | ||
| } else { | ||
| reductions.fixCol(var_idx, fixing); | ||
| } | ||
| status = papilo::PresolveStatus::kReduced; | ||
| } | ||
|
|
||
| return status; | ||
| } | ||
|
|
||
| #define INSTANTIATE(F_TYPE) template class GF2Presolve<F_TYPE>; | ||
|
|
||
| #if MIP_INSTANTIATE_FLOAT | ||
| INSTANTIATE(float) | ||
| #endif | ||
|
|
||
| #if MIP_INSTANTIATE_DOUBLE | ||
| INSTANTIATE(double) | ||
| #endif | ||
|
|
||
| #undef INSTANTIATE | ||
|
|
||
| } // namespace cuopt::linear_programming::detail | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights | ||
| * reserved. SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #pragma GCC diagnostic push | ||
| #pragma GCC diagnostic ignored "-Wstringop-overflow" // ignore boost error for pip wheel build | ||
| #include <papilo/Config.hpp> | ||
| #include <papilo/core/PresolveMethod.hpp> | ||
| #include <papilo/core/Problem.hpp> | ||
| #include <papilo/core/ProblemUpdate.hpp> | ||
| #pragma GCC diagnostic pop | ||
|
|
||
| namespace cuopt::linear_programming::detail { | ||
|
|
||
| template <typename f_t> | ||
| class GF2Presolve : public papilo::PresolveMethod<f_t> { | ||
| public: | ||
| GF2Presolve() : papilo::PresolveMethod<f_t>() | ||
| { | ||
| this->setName("gf2presolve"); | ||
| this->setType(papilo::PresolverType::kIntegralCols); | ||
| this->setTiming(papilo::PresolverTiming::kMedium); | ||
| } | ||
|
|
||
| papilo::PresolveStatus execute(const papilo::Problem<f_t>& problem, | ||
| const papilo::ProblemUpdate<f_t>& problemUpdate, | ||
| const papilo::Num<f_t>& num, | ||
| papilo::Reductions<f_t>& reductions, | ||
| const papilo::Timer& timer, | ||
| int& reason_of_infeasibility) override; | ||
|
|
||
| private: | ||
| struct gf2_constraint_t { | ||
| size_t cstr_idx; | ||
| std::vector<std::pair<size_t, f_t>> bin_vars; | ||
| std::pair<size_t, f_t> key_var; | ||
| size_t rhs; // 0 or 1 | ||
|
|
||
| gf2_constraint_t() = default; | ||
| gf2_constraint_t(size_t cstr_idx, | ||
| std::vector<std::pair<size_t, f_t>> bin_vars, | ||
| std::pair<size_t, f_t> key_var, | ||
| size_t rhs) | ||
| : cstr_idx(cstr_idx), bin_vars(std::move(bin_vars)), key_var(key_var), rhs(rhs) | ||
| { | ||
| } | ||
| gf2_constraint_t(const gf2_constraint_t& other) = default; | ||
| gf2_constraint_t(gf2_constraint_t&& other) noexcept = default; | ||
| gf2_constraint_t& operator=(const gf2_constraint_t& other) = default; | ||
| gf2_constraint_t& operator=(gf2_constraint_t&& other) noexcept = default; | ||
| }; | ||
|
|
||
| inline bool is_integer(f_t value, f_t tolerance) const | ||
| { | ||
| return std::abs(value - std::round(value)) <= tolerance; | ||
| } | ||
| }; | ||
|
|
||
| } // namespace cuopt::linear_programming::detail |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.