88#include " gf2_presolve.hpp"
99
1010#include < mip_heuristics/mip_constants.hpp>
11+ #include < utilities/macros.cuh>
1112
1213#include < cmath>
14+ #include < cstdint>
1315#include < unordered_map>
1416
1517#if GF2_PRESOLVE_DEBUG
@@ -33,55 +35,103 @@ static inline i_t positive_modulo(i_t i, i_t n)
3335 return (i % n + n) % n;
3436}
3537
38+ static constexpr int GF2_WORD_BITS = 64 ;
39+
40+ static inline int gf2_nwords (int N) { return (N + GF2_WORD_BITS - 1 ) / GF2_WORD_BITS ; }
41+
42+ static inline bool gf2_test_bit (const std::vector<uint64_t >& row, int col)
43+ {
44+ return (row[col / GF2_WORD_BITS ] >> (col % GF2_WORD_BITS )) & uint64_t {1 };
45+ }
46+
47+ static inline void gf2_set_bit (std::vector<uint64_t >& row, int col)
48+ {
49+ row[col / GF2_WORD_BITS ] |= (uint64_t {1 } << (col % GF2_WORD_BITS ));
50+ }
51+
3652// this is kind-of a stopgap implementation (as in practice MIPLIB2017 only contains a couple of GF2
3753// problems and they're small) but cuDSS could be used for this since A is likely to be sparse and
3854// low-bandwidth (i think?) unlikely to occur in real-world problems however. doubt it'd be worth
39- // the effort trashes A and b, return true if solved
40- static bool gf2_solve (std::vector<std::vector<int >>& A, std::vector<int >& b, std::vector<int >& x)
55+ // the effort
56+ gf2_status_t gf2_solve (std::vector<std::vector<uint64_t >>& A,
57+ int n_cols,
58+ std::vector<int >& b,
59+ std::vector<int >& x,
60+ std::vector<uint8_t >& determined)
4161{
42- int i, j, k;
43- const int N = A.size ();
44- for (i = 0 ; i < N; i++) {
45- // Find pivot
62+ const int m = (int )A.size ();
63+ const int n = n_cols;
64+ const int nwords = gf2_nwords (n);
65+ cuopt_assert (m > 0 , " " );
66+ cuopt_assert (n >= 0 , " " );
67+ cuopt_assert ((int )b.size () == m, " " );
68+ cuopt_assert ((int )A[0 ].size () == nwords, " " );
69+
70+ // pivot_row_of_col[c] = row holding the pivot for column c, or -1 if free
71+ std::vector<int > pivot_row_of_col (n, -1 );
72+ int next_pivot_row = 0 ;
73+
74+ for (int col = 0 ; col < n; col++) {
4675 int pivot = -1 ;
47- for (j = i; j < N; j ++) {
48- if (A[j][i] ) {
49- pivot = j ;
76+ for (int r = next_pivot_row; r < m; r ++) {
77+ if (gf2_test_bit (A[r], col) ) {
78+ pivot = r ;
5079 break ;
5180 }
5281 }
53- if (pivot == -1 ) return false ; // No solution
54-
55- // Swap current row with pivot row if needed
56- if (pivot != i) {
57- for (k = 0 ; k < N; k++) {
58- int temp = A[i][k];
59- A[i][k] = A[pivot][k];
60- A[pivot][k] = temp;
61- }
62- int temp = b[i];
63- b[i] = b[pivot];
64- b[pivot] = temp;
82+ if (pivot == -1 ) continue ; // free column
83+
84+ if (pivot != next_pivot_row) {
85+ std::swap (A[next_pivot_row], A[pivot]);
86+ std::swap (b[next_pivot_row], b[pivot]);
6587 }
6688
67- // Eliminate downwards
68- for (j = i + 1 ; j < N; j ++) {
69- if (A[j][i] ) {
70- for (k = i; k < N; k ++)
71- A[j][k ] ^= A[i][k ];
72- b[j ] ^= b[i ];
89+ // Eliminate column from all other rows (RREF)
90+ for (int r = 0 ; r < m; r ++) {
91+ if (r != next_pivot_row && gf2_test_bit (A[r], col) ) {
92+ for (int w = 0 ; w < nwords; w ++)
93+ A[r][w ] ^= A[next_pivot_row][w ];
94+ b[r ] ^= b[next_pivot_row ];
7395 }
7496 }
97+
98+ pivot_row_of_col[col] = next_pivot_row;
99+ next_pivot_row++;
75100 }
76101
77- // Back-substitution
78- for (i = N - 1 ; i >= 0 ; i-- ) {
79- x[i] = b[i];
80- for (j = i + 1 ; j < N; j++)
81- x[i] ^= (A[i][j] & x[j]);
82- if (!A[i][i] && x[i] ) return false ; // No solution
102+ const int rank = next_pivot_row;
103+ for (int r = rank; r < m; r++ ) {
104+ for ( int w = 0 ; w < nwords; w++) {
105+ cuopt_assert (A[r][w] == 0 , " RREF unused row must be zero " );
106+ }
107+ if (b[r] ) return gf2_status_t ::Infeasible;
83108 }
84- return true ; // Success
109+
110+ std::vector<uint64_t > free_mask (nwords, 0 );
111+ for (int c = 0 ; c < n; c++) {
112+ if (pivot_row_of_col[c] == -1 ) gf2_set_bit (free_mask, c);
113+ }
114+
115+ determined.assign (n, 0 );
116+ x.assign (n, 0 );
117+
118+ for (int col = 0 ; col < n; col++) {
119+ int row = pivot_row_of_col[col];
120+ if (row == -1 ) continue ; // free: x=0, determined=false
121+
122+ bool has_free_support = false ;
123+ for (int w = 0 ; w < nwords; w++) {
124+ if (A[row][w] & free_mask[w]) {
125+ has_free_support = true ;
126+ break ;
127+ }
128+ }
129+ // Particular solution with free vars = 0: x[pivot] = b[row]
130+ x[col] = b[row];
131+ determined[col] = !has_free_support;
132+ }
133+
134+ return gf2_status_t ::Feasible;
85135}
86136
87137template <typename f_t >
@@ -161,16 +211,20 @@ papilo::PresolveStatus GF2Presolve<f_t>::execute(const papilo::Problem<f_t>& pro
161211 if (key_var_idx != -1 ) { NOT_GF2 (" multiple key variables" , var_idx); }
162212 key_var_idx = var_idx;
163213 key_var_coeff = coeff;
164- gf2_key_vars.insert ({var_idx, gf2_key_vars.size ()});
165214 } else {
166215 // Binary variable
167216 constraint_bin_vars.push_back ({var_idx, coeff});
168- gf2_bin_vars.insert ({var_idx, gf2_bin_vars.size ()});
169217 }
170218 }
171219
172220 if (key_var_idx == -1 ) NOT_GF2 (" missing key variable" );
173221
222+ // Commit to global maps only after the row is fully accepted
223+ gf2_key_vars.insert ({(size_t )key_var_idx, gf2_key_vars.size ()});
224+ for (auto [bin_var, _] : constraint_bin_vars) {
225+ gf2_bin_vars.insert ({bin_var, gf2_bin_vars.size ()});
226+ }
227+
174228 gf2_constraints.emplace_back ((size_t )cstr_idx,
175229 std::move (constraint_bin_vars),
176230 std::pair<size_t , f_t >{key_var_idx, key_var_coeff},
@@ -183,12 +237,11 @@ papilo::PresolveStatus GF2Presolve<f_t>::execute(const papilo::Problem<f_t>& pro
183237 // If no GF2 constraints found, return unchanged
184238 if (gf2_constraints.empty ()) { return papilo::PresolveStatus::kUnchanged ; }
185239
186- // Skip if that would cause computational explosion (O(n^3) with simple gaussian elimination)
187- if (gf2_constraints .size () > 1000 ) { return papilo::PresolveStatus::kUnchanged ; }
240+ // one unique key per GF2 row. #bins may differ from #rows.
241+ if (gf2_key_vars .size () != gf2_constraints. size () ) { return papilo::PresolveStatus::kUnchanged ; }
188242
189- // Validate structure
190- if (gf2_key_vars.size () != gf2_constraints.size () ||
191- gf2_bin_vars.size () != gf2_constraints.size ()) {
243+ // Skip if that would cause computational explosion (dense GE ~ O(m * n * min(m,n)))
244+ if (gf2_constraints.size () > 1000 || gf2_bin_vars.size () > 1000 ) {
192245 return papilo::PresolveStatus::kUnchanged ;
193246 }
194247
@@ -198,40 +251,76 @@ papilo::PresolveStatus GF2Presolve<f_t>::execute(const papilo::Problem<f_t>& pro
198251 gf2_bin_vars_invmap.insert ({gf2_idx, var_idx});
199252 }
200253
201- // Build binary matrix
202- // Could be a flat vector but. oh well. in practice N is small
203- std::vector<std::vector<int >> A (gf2_constraints.size (),
204- std::vector<int >(gf2_constraints.size (), 0 ));
205- std::vector<int > b (gf2_constraints.size ());
206- for (size_t gf2_cstr_idx = 0 ; gf2_cstr_idx < gf2_constraints.size (); ++gf2_cstr_idx) {
254+ // Build binary matrix as packed uint64_t words
255+ const int m = (int )gf2_constraints.size ();
256+ const int n = (int )gf2_bin_vars.size ();
257+ const int nwords = gf2_nwords (n);
258+ std::vector<std::vector<uint64_t >> A (m, std::vector<uint64_t >(nwords, 0 ));
259+ std::vector<int > b (m);
260+ for (int gf2_cstr_idx = 0 ; gf2_cstr_idx < m; ++gf2_cstr_idx) {
207261 const auto & cons = gf2_constraints[gf2_cstr_idx];
208262 for (auto [bin_var, _] : cons.bin_vars ) {
209- A[gf2_cstr_idx][ gf2_bin_vars[bin_var]] = 1 ;
263+ gf2_set_bit ( A[gf2_cstr_idx], ( int ) gf2_bin_vars[bin_var]) ;
210264 }
211265 b[gf2_cstr_idx] = cons.rhs ;
212266 }
213267
214- std::vector<int > solution (gf2_constraints.size ());
215- bool feasible = gf2_solve (A, b, solution);
216- if (!feasible) { return papilo::PresolveStatus::kInfeasible ; }
268+ std::vector<int > solution (n);
269+ std::vector<uint8_t > determined (n);
270+ gf2_status_t gf2_status = gf2_solve (A, n, b, solution, determined);
271+ if (gf2_status == gf2_status_t ::Infeasible) { return papilo::PresolveStatus::kInfeasible ; }
217272
218273 std::unordered_map<size_t , f_t > fixings;
219- // Fix binary variables
220- for (size_t sol_idx = 0 ; sol_idx < gf2_constraints.size (); ++sol_idx) {
221- fixings[gf2_bin_vars_invmap[sol_idx]] = solution[sol_idx];
274+
275+ // Fix only uniquely determined binaries
276+ for (int sol_idx = 0 ; sol_idx < n; ++sol_idx) {
277+ if (determined[sol_idx]) { fixings[gf2_bin_vars_invmap[sol_idx]] = solution[sol_idx]; }
222278 }
223279
224- // Compute fixings for key variables by solving for the constraint
280+ // Fix key only when every binary in that constraint is uniquely determined
225281 for (const auto & cons : gf2_constraints) {
282+ bool all_bins_determined = true ;
283+ for (auto [bin_var, _] : cons.bin_vars ) {
284+ cuopt_assert (gf2_bin_vars.count (bin_var), " " );
285+ if (!determined[gf2_bin_vars[bin_var]]) {
286+ all_bins_determined = false ;
287+ break ;
288+ }
289+ }
290+ if (!all_bins_determined) continue ;
291+
226292 auto [key_var_idx, key_var_coeff] = cons.key_var ;
227- f_t constraint_rhs = lhs_values[cons.cstr_idx ]; // equality constraint
293+ const f_t constraint_rhs = std::round ( lhs_values[cons.cstr_idx ]);
228294 f_t lhs = -constraint_rhs;
229295 for (auto [bin_var, coeff] : cons.bin_vars ) {
296+ cuopt_assert (fixings.count (bin_var), " " );
230297 lhs += fixings[bin_var] * coeff;
231298 }
232- fixings[key_var_idx] = std::round (-lhs / key_var_coeff);
299+ const f_t key_val = std::round (-lhs / key_var_coeff);
300+
301+ // Residual must be exactly 0 after rounding (rejects half-integer / inconsistent carry)
302+ if (!num.isEq (lhs + key_val * key_var_coeff, f_t {0 })) {
303+ return papilo::PresolveStatus::kInfeasible ;
304+ }
305+ // Dual-role: same var already fixed as a GF(2) binary
306+ if (fixings.count (key_var_idx) && !num.isEq (fixings[key_var_idx], key_val)) {
307+ return papilo::PresolveStatus::kInfeasible ;
308+ }
309+ if (!col_flags[key_var_idx].test (papilo::ColFlag::kLbInf ) &&
310+ key_val < lower_bounds[key_var_idx] - integrality_tolerance) {
311+ return papilo::PresolveStatus::kInfeasible ;
312+ }
313+ if (!col_flags[key_var_idx].test (papilo::ColFlag::kUbInf ) &&
314+ key_val > upper_bounds[key_var_idx] + integrality_tolerance) {
315+ return papilo::PresolveStatus::kInfeasible ;
316+ }
317+
318+ fixings[key_var_idx] = key_val;
233319 }
234320
321+ // necessary because Papilo asserts on empty TransactionGuard
322+ if (fixings.empty ()) { return papilo::PresolveStatus::kUnchanged ; }
323+
235324 papilo::PresolveStatus status = papilo::PresolveStatus::kUnchanged ;
236325 papilo::TransactionGuard rg{reductions};
237326 for (const auto & [var_idx, fixing] : fixings) {
0 commit comments