Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
487 commits
Select commit Hold shift + click to select a range
512a960
Revert "stripped down ins_wrapper"
aliceb-nv Jan 18, 2026
fb45144
merge attempt 1, needs cleanup
aliceb-nv Jan 18, 2026
61c1563
initial detemrinistic diving impl
aliceb-nv Jan 18, 2026
bfb6611
separate time limit and work unit parameters
aliceb-nv Jan 18, 2026
2f673e7
fix issue on presolved to optimality instances
aliceb-nv Jan 18, 2026
cfbb99b
restore gpu heurs
aliceb-nv Jan 18, 2026
01747f4
fix BSP b&b getting starved too early
aliceb-nv Jan 19, 2026
e689154
same diving ratio as base solver
aliceb-nv Jan 19, 2026
14eb9a7
cleanup work
aliceb-nv Jan 19, 2026
7755bd7
further cleanup work
aliceb-nv Jan 19, 2026
a63f032
incorporating cpufj into the deterministic framework
aliceb-nv Jan 19, 2026
bc3bfde
fjcpu cleanup
aliceb-nv Jan 20, 2026
50c574e
update terminology
aliceb-nv Jan 20, 2026
2947a23
unify pseudocost computations
aliceb-nv Jan 20, 2026
5f58f6d
parallelized the trial branching
nguidotti Jan 20, 2026
fbc17c9
added debug log
nguidotti Jan 20, 2026
a1eb6b8
solved early termination in CMS750_4. fixed hard coded number of thre…
nguidotti Jan 20, 2026
8c5e9f6
policy system for solve_lp_
aliceb-nv Jan 20, 2026
8116fea
Merge branch 'main' into fix-bugs
nguidotti Jan 20, 2026
c2bab57
Revert policy system to move it to a later PR
aliceb-nv Jan 20, 2026
f16db00
restore fixes
aliceb-nv Jan 20, 2026
2b7859e
log ds features and bounds strenghtening
aliceb-nv Jan 20, 2026
af31388
fix logs
aliceb-nv Jan 20, 2026
f102139
timing stuff
aliceb-nv Jan 20, 2026
5a62393
bump 1
aliceb-nv Jan 20, 2026
fc82a41
bump 2
aliceb-nv Jan 20, 2026
115a0b1
silenced logs from the concurrent mode when running inside MIP. ignor…
nguidotti Jan 21, 2026
d50c064
Merge branch 'main' into fix-bugs
nguidotti Jan 21, 2026
27d0d39
Merge branch 'fix-bugs' into reliability-branching
nguidotti Jan 21, 2026
c47adda
fixed merge errors
nguidotti Jan 21, 2026
261bfc8
add bounds strenghtening predictor (unused yet)
aliceb-nv Jan 21, 2026
c0422ed
fixed crash
nguidotti Jan 21, 2026
a0a1d93
better num thread initialization
nguidotti Jan 21, 2026
9f0fe29
fix compilation
nguidotti Jan 21, 2026
0dcb5ff
Merge branch 'fix-bugs' into reliability-branching
nguidotti Jan 21, 2026
298c68c
moved parameters to simplex_settings. added command line option
nguidotti Jan 21, 2026
8a890fd
handle cli arguments
nguidotti Jan 21, 2026
2c2c515
set additional openmp flags
nguidotti Jan 21, 2026
1aca951
Fix issue with work limits that aren't multiples of the horizon steps
aliceb-nv Jan 21, 2026
e593d36
set the number of tasks for strong branching.
nguidotti Jan 21, 2026
19210f0
propagate solutions to the solver in determinsitic mode
aliceb-nv Jan 21, 2026
92a8705
fix envvar
aliceb-nv Jan 21, 2026
bbc966b
fix some issues with hashes
aliceb-nv Jan 21, 2026
cff46f3
bump1
aliceb-nv Jan 21, 2026
254be07
bump2
aliceb-nv Jan 21, 2026
aa15d8e
added additional information in the logs when solving the root relaxa…
nguidotti Jan 22, 2026
ae98cbd
fix tie-breaking and ins_vector counters not being reset appropriately
aliceb-nv Jan 22, 2026
83e0b37
bump1
aliceb-nv Jan 22, 2026
f6a908d
no CPUFJ
aliceb-nv Jan 22, 2026
5ed3732
bump1
aliceb-nv Jan 22, 2026
ce0586c
Merge branch 'fix-bugs' into reliability-branching
nguidotti Jan 22, 2026
d604cb6
renamed macro
nguidotti Jan 22, 2026
4a5270f
Refactoring, fix incorrect optimality, add tests
aliceb-nv Jan 22, 2026
781dd36
Merge branch 'fix-bugs' into reliability-branching
nguidotti Jan 22, 2026
2ed103c
add work limit as a parameter
aliceb-nv Jan 22, 2026
16dcfa8
Merge branch 'main' into determinism
aliceb-nv Jan 22, 2026
628c22b
changed the number of threads in probing cache
nguidotti Jan 22, 2026
0d2226c
add parameter for MIP seed
aliceb-nv Jan 22, 2026
6956bbc
fix type
nguidotti Jan 22, 2026
409b1ee
restore probing cache
aliceb-nv Jan 22, 2026
addae13
bump1
aliceb-nv Jan 22, 2026
e9a5fac
bump2
aliceb-nv Jan 22, 2026
4f75132
fix compute_hash using the defautl stream and breaking graph capture
aliceb-nv Jan 22, 2026
2fd6859
bump1
aliceb-nv Jan 22, 2026
6bdd587
changed the logs for the root relaxation
nguidotti Jan 23, 2026
6d7007c
Merge branch 'fix-bugs' into reliability-branching
nguidotti Jan 23, 2026
a10ac30
fix concurrent LP solve and probing cache in deterministic mode
aliceb-nv Jan 23, 2026
5000cda
more stats logging
aliceb-nv Jan 23, 2026
fab2ffe
horizon 0.15
aliceb-nv Jan 23, 2026
183e2ce
horizon 0.25
aliceb-nv Jan 23, 2026
24e4192
horizon 0.5
aliceb-nv Jan 23, 2026
48b74c8
horizon step 1.00
aliceb-nv Jan 23, 2026
12f7b87
restore
aliceb-nv Jan 23, 2026
fc1c60a
limited the number of candidates for strong branching. refactoring to…
nguidotti Jan 23, 2026
444b95b
removed try_lock
nguidotti Jan 23, 2026
61c5f77
fix incorrect optimal report
aliceb-nv Jan 23, 2026
a0137d1
Merge branch 'release/26.02' into determinism
aliceb-nv Jan 23, 2026
90dcdc9
removed unused settings
nguidotti Jan 23, 2026
5260b51
adjusted number of workers for rb
nguidotti Jan 23, 2026
87098e3
adjusting number of workers for rb
nguidotti Jan 24, 2026
b66ebae
fix incorrect max tasks
nguidotti Jan 24, 2026
9e8488c
fix pseudocost updates
aliceb-nv Jan 24, 2026
30eb52e
w/ bounds strenght
aliceb-nv Jan 25, 2026
9817131
fix holes in implementation
aliceb-nv Jan 25, 2026
383e69a
no BS
aliceb-nv Jan 25, 2026
585bdf0
no BS typo
aliceb-nv Jan 25, 2026
dcf0542
greater horizon
aliceb-nv Jan 25, 2026
84d0567
removed ramp-up phase
nguidotti Jan 25, 2026
175ffc8
BS back
aliceb-nv Jan 25, 2026
0e89356
with logging
aliceb-nv Jan 25, 2026
15dd371
add numericla restart to diving and lower bound ceiling updates in BSP
aliceb-nv Jan 26, 2026
30a147a
cleanup
aliceb-nv Jan 26, 2026
dc2fd90
Merge branch 'release/26.02' into determinism
aliceb-nv Jan 26, 2026
91c95e2
heap instead of rebuild
aliceb-nv Jan 26, 2026
1fdd13e
fix root relaxation message when the solution is not optimal
nguidotti Jan 26, 2026
5ea1621
cleanup, fix loss of determinism
aliceb-nv Jan 26, 2026
b9960c0
more cleanup
aliceb-nv Jan 26, 2026
6b1bee8
Merge branch 'fix-bugs' into reliability-branching
nguidotti Jan 26, 2026
66f6cfd
increase max_lookahead
nguidotti Jan 26, 2026
2c6e12e
fix determinism test seed
aliceb-nv Jan 26, 2026
be15f63
fix fjcpu bug
aliceb-nv Jan 26, 2026
3b93226
bump
aliceb-nv Jan 26, 2026
9e4b2ee
add comment description
aliceb-nv Jan 26, 2026
1b35ac9
Merge branch 'release/26.02' into determinism
aliceb-nv Jan 26, 2026
deff899
copyright fixes
aliceb-nv Jan 26, 2026
e46eba6
Merge branch 'release/26.02' into reliability-branching
nguidotti Jan 26, 2026
0b0e65b
review comments
aliceb-nv Jan 26, 2026
c5438a4
fix wait_for_producers target
aliceb-nv Jan 26, 2026
31b5285
set the reliable threshold dynamically
nguidotti Jan 26, 2026
433ae0e
no cpufj for bench
aliceb-nv Jan 26, 2026
afd24ad
Merge branch 'release/26.02' into determinism
aliceb-nv Jan 27, 2026
9448dd7
Revert "no cpufj for bench"
aliceb-nv Jan 27, 2026
7bac62d
fixed incorrect threshold formula. fixed time limit violation.
nguidotti Jan 27, 2026
a6e055c
simplified parallel loop
nguidotti Jan 27, 2026
de4389b
added single-threaded mode for rins and submip
nguidotti Jan 27, 2026
799f9f5
more logging for bounds strength
aliceb-nv Jan 27, 2026
eb51080
added missing mutexes
nguidotti Jan 27, 2026
f3567cb
fixed empty vector in shuffle
nguidotti Jan 27, 2026
233933f
reverted some code changes
nguidotti Jan 27, 2026
527754c
replaced with lock_guards
nguidotti Jan 27, 2026
2251b87
fixed crash
nguidotti Jan 27, 2026
7919288
fixed number of threads set to 0
nguidotti Jan 28, 2026
cec6f38
enable reliablity branching by default
nguidotti Jan 28, 2026
6c408f2
fix logging
aliceb-nv Jan 28, 2026
27ee927
set the solve mode based on the number of threads
nguidotti Jan 28, 2026
4d1f684
disable RUNPATH
aliceb-nv Jan 28, 2026
8810401
Revert "fix logging"
aliceb-nv Jan 28, 2026
a811e86
Revert "more logging for bounds strength"
aliceb-nv Jan 28, 2026
b761b22
naive prediction
aliceb-nv Jan 29, 2026
8dfb7c3
scaled mem prediction
aliceb-nv Jan 29, 2026
d550547
no cpufj
aliceb-nv Jan 29, 2026
4d75fb0
Revert "no cpufj"
aliceb-nv Jan 30, 2026
da6dfe0
Revert "disable RUNPATH"
aliceb-nv Jan 30, 2026
25388d8
replace work predictor machinery with mem ops only
aliceb-nv Jan 30, 2026
e88d5ef
remove debug log machinery
aliceb-nv Jan 30, 2026
6202972
Merge branch 'release/26.02' into determinism
aliceb-nv Jan 30, 2026
c91127b
refactoring and simplification work
aliceb-nv Jan 30, 2026
12e095c
switch to openmp for syncs, add infeas test
aliceb-nv Jan 30, 2026
e68941e
minor touchups
aliceb-nv Jan 30, 2026
00158e0
unify update_tree, fix timing issue
aliceb-nv Jan 30, 2026
5f929f4
unify b&b worker struct
aliceb-nv Jan 30, 2026
8c4f417
Revert "unify b&b worker struct"
aliceb-nv Jan 30, 2026
a23d8af
Merge branch 'release/26.02' into determinism
aliceb-nv Feb 2, 2026
f7d0c93
Merge branch 'reliability-branching' into determinism
aliceb-nv Feb 3, 2026
71d1272
replaced locks with atomics
nguidotti Feb 3, 2026
cc14fee
cleanup
aliceb-nv Feb 3, 2026
326037c
Merge branch 'reliability-branching' into determinism
aliceb-nv Feb 3, 2026
70e6c73
cleanup
aliceb-nv Feb 3, 2026
faa4952
fix bsp deadlock on timelimit
aliceb-nv Feb 3, 2026
e23c16c
unify update_tree
aliceb-nv Feb 3, 2026
222cda0
fix pseudocost update
nguidotti Feb 3, 2026
649e295
Merge branch 'release/26.02' into reliability-branching
nguidotti Feb 3, 2026
cf2f577
more cleanup
aliceb-nv Feb 3, 2026
3aab472
Merge branch 'reliability-branching' into determinism
aliceb-nv Feb 3, 2026
ff1aaf4
more cleanup
aliceb-nv Feb 4, 2026
a53b38a
deleted assignment in omp_mutex_t to avoid double destruction.
nguidotti Feb 4, 2026
0826aba
addressing reviewer comments
nguidotti Feb 4, 2026
929e0b5
fix compilation
nguidotti Feb 4, 2026
af718df
update naming
aliceb-nv Feb 4, 2026
36b211b
additional refactoring
nguidotti Feb 4, 2026
5edbfbf
variable renaming
nguidotti Feb 4, 2026
2b9da53
fix compilation
nguidotti Feb 4, 2026
62a058b
coderabbit suggestions
nguidotti Feb 4, 2026
5a54f85
more cleanup
aliceb-nv Feb 4, 2026
0f6df39
fix negative pseudocost
nguidotti Feb 4, 2026
56d5ead
changed initial score
nguidotti Feb 4, 2026
a72c85c
adding more safeguards
nguidotti Feb 4, 2026
0b14897
fix initial value
nguidotti Feb 4, 2026
ebe7aa3
Merge remote-tracking branch 'cuopt/release/26.02' into reliability-b…
nguidotti Feb 5, 2026
d65f258
renaming variables
nguidotti Feb 5, 2026
725363d
split locks in pseudocost
nguidotti Feb 5, 2026
4879d1f
progress
aliceb-nv Feb 5, 2026
46d59b3
fix build
aliceb-nv Feb 5, 2026
d1989cc
fix crash in timtab1 due to double infinite pseudocost
nguidotti Feb 5, 2026
f6d2a94
fix race condition
aliceb-nv Feb 6, 2026
6417074
small tweaks
nguidotti Feb 6, 2026
e5fdae1
Merge branch 'release/26.02' into reliability-branching
nguidotti Feb 6, 2026
9e4b855
Merge branch 'reliability-branching' into determinism
aliceb-nv Feb 6, 2026
d5a7149
Merge branch 'reliability-branching' into determinism
aliceb-nv Feb 6, 2026
91e0168
renamed variable
nguidotti Feb 6, 2026
fd4f5a8
Merge branch 'reliability-branching' into determinism
aliceb-nv Feb 6, 2026
515d17c
fix incorrect lower bounds during the cut passes
nguidotti Feb 6, 2026
d8cde66
cleanup, fix race
aliceb-nv Feb 6, 2026
084072a
Merge branch 'reliability-branching' into determinism
aliceb-nv Feb 6, 2026
1df294b
more cleanup
aliceb-nv Feb 6, 2026
0deb0fd
Merge branch 'release/26.02' into determinism
aliceb-nv Feb 6, 2026
a05d4c2
more cleanup
aliceb-nv Feb 6, 2026
91bb13f
Merge branch 'release/26.02' into determinism
aliceb-nv Feb 6, 2026
6914c8a
bump1
aliceb-nv Feb 6, 2026
ff47875
bump2
aliceb-nv Feb 6, 2026
57129b7
address review comments
aliceb-nv Feb 6, 2026
1332fbd
review comments
aliceb-nv Feb 6, 2026
da6fd11
some attempts at optimizing
aliceb-nv Feb 7, 2026
d801c9e
disable phase2 nvtx
aliceb-nv Feb 7, 2026
b4a8934
more microoptimizing
aliceb-nv Feb 8, 2026
7d2edbd
Merge branch 'release/26.02' into determinism
aliceb-nv Feb 8, 2026
80037ef
fix nvcc build
aliceb-nv Feb 8, 2026
f8021de
address review comments
aliceb-nv Feb 9, 2026
7c92978
address AI comments
aliceb-nv Feb 9, 2026
b099d81
test no sync pruning
aliceb-nv Feb 9, 2026
3cfc0c7
remove redundant bounds strenghtening call in diving
aliceb-nv Feb 9, 2026
a6f98b3
centralize pseudocost logic
aliceb-nv Feb 9, 2026
90ffa81
bump1
aliceb-nv Feb 9, 2026
ac0ca00
bump2
aliceb-nv Feb 9, 2026
c86d753
update naming
aliceb-nv Feb 9, 2026
1cca483
address review comments
aliceb-nv Feb 10, 2026
dec2185
added BB determinism glossary
aliceb-nv Feb 10, 2026
98b8ddd
Merge branch 'release/26.02' into determinism
aliceb-nv Feb 10, 2026
57ad57a
integer objective pruning
aliceb-nv Feb 10, 2026
7fc7c57
test for rational objective function
aliceb-nv Feb 10, 2026
8543b7b
support rational objective functions
aliceb-nv Feb 10, 2026
57ab935
fix rel mip gap output
aliceb-nv Feb 10, 2026
3ded90a
fewer CPUFJ threads
aliceb-nv Feb 10, 2026
92d30c6
bump
aliceb-nv Feb 10, 2026
cfdc63e
experimental presolve reduction
aliceb-nv Feb 10, 2026
1b4fc77
bump
aliceb-nv Feb 10, 2026
4e9dccd
more printouts
aliceb-nv Feb 10, 2026
88e076b
fix bugs on many duplicates in substritute vars
aliceb-nv Feb 11, 2026
9ea8e7a
fix repair queue crushing bug
aliceb-nv Feb 11, 2026
83e1eed
Merge branch 'release/26.02' into integer-objective
aliceb-nv Feb 11, 2026
4cdd204
Revert "fewer CPUFJ threads"
aliceb-nv Feb 11, 2026
0bbe832
Merge branch 'sub-duplicate-fix' into integer-objective
aliceb-nv Feb 11, 2026
ecdad5d
Merge branch 'integer-objective' into setppc-presolve
aliceb-nv Feb 11, 2026
f64fada
Merge branch 'main' into integer-objective
aliceb-nv Feb 11, 2026
ef4b090
refactor
aliceb-nv Feb 11, 2026
f90929f
rework objective integer scaling
aliceb-nv Feb 11, 2026
16fadf9
Merge branch 'main' into integer-objective
aliceb-nv Feb 12, 2026
e766e86
stricter tolerances
aliceb-nv Feb 12, 2026
019878b
remove data_ptr
aliceb-nv Feb 12, 2026
7e3f2b0
further cleanup
aliceb-nv Feb 12, 2026
f57cef4
minor cleanup
aliceb-nv Feb 12, 2026
8f17bd4
Merge branch 'cleanup-memins' into integer-objective
aliceb-nv Feb 12, 2026
41dca67
restore deleted comments
aliceb-nv Feb 12, 2026
97958dc
address ai review
aliceb-nv Feb 12, 2026
400bbec
Merge branch 'integer-objective' into setppc-presolve
aliceb-nv Feb 12, 2026
df49625
merge
aliceb-nv Feb 12, 2026
9bc45cd
more tweaks
aliceb-nv Feb 12, 2026
265e4dc
more reductions
aliceb-nv Feb 13, 2026
a1afa66
fix invalid presolve reduction
aliceb-nv Feb 13, 2026
8b6e221
better aggregation
aliceb-nv Feb 19, 2026
a5cbf70
Merge branch 'main' into setppc-presolve
aliceb-nv Feb 19, 2026
834af37
optimizations
aliceb-nv Feb 19, 2026
c1956c2
shadow locks
aliceb-nv Feb 19, 2026
fc1287f
fixes
aliceb-nv Feb 19, 2026
f0188c9
no extension 1 or shadow locks
aliceb-nv Feb 20, 2026
d3b93be
more cleanup
aliceb-nv Feb 20, 2026
535c2cd
bug fixes
aliceb-nv Feb 20, 2026
cc3ab70
code simplification and test
aliceb-nv Feb 20, 2026
d4b2aa8
Merge branch 'main' into setppc-presolve
aliceb-nv Feb 23, 2026
7761da8
Merge branch 'main' into setppc-presolve
aliceb-nv Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cpp/src/mip_heuristics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ set(MIP_LP_NECESSARY_FILES
${CMAKE_CURRENT_SOURCE_DIR}/solver_solution.cu
${CMAKE_CURRENT_SOURCE_DIR}/local_search/rounding/simple_rounding.cu
${CMAKE_CURRENT_SOURCE_DIR}/presolve/third_party_presolve.cpp
${CMAKE_CURRENT_SOURCE_DIR}/presolve/single_lock_dual_aggregation.cpp
${CMAKE_CURRENT_SOURCE_DIR}/presolve/gf2_presolve.cpp
${CMAKE_CURRENT_SOURCE_DIR}/solution/solution.cu
)
Expand Down
359 changes: 359 additions & 0 deletions cpp/src/mip_heuristics/presolve/single_lock_dual_aggregation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */

#include "single_lock_dual_aggregation.hpp"

#include <mip_heuristics/mip_constants.hpp>
#include <utilities/logger.hpp>

#include <algorithm>
#include <vector>

namespace cuopt::linear_programming::detail {

// Single-Lock Dual Aggregation
//
// For a binary variable x with exactly one "up-lock" (one constraint preventing
// it from increasing), we try to prove an implication y=0 => x=0 via activity
// bounds on the locking row. If additionally the row is non-binding when y=1
// (no capacity competition), we can substitute x = y, eliminating a variable.
//
// Symmetric logic applies for "down-lock" candidates (one constraint preventing
// decrease), proving y=1 => x=1.

template <typename f_t>
papilo::PresolveStatus SingleLockDualAggregation<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 auto& objective = problem.getObjective().coefficients;

const int nrows = constraint_matrix.getNRows();
const int ncols = problem.getNCols();
const double tlim = problemUpdate.getPresolveOptions().tlim;
const f_t tol = num.getFeasTol();

// =========================================================================
// Step 1: Lock Counting — O(nnz)
//
// An "up-lock" on column j means a constraint prevents j from increasing:
// - a_j > 0 in a <= row, or a_j < 0 in a >= row.
// "Down-lock" is the reverse. Equality rows lock both directions.
// We record the row index of the first lock; a second lock invalidates it.
// =========================================================================

enum lock_dir { UP = 0, DOWN = 1 };
enum bound_side { LOWER = 0, UPPER = 1 };
std::vector<int> locks[2] = {std::vector<int>(ncols, 0), std::vector<int>(ncols, 0)};
std::vector<int> lock_row[2] = {std::vector<int>(ncols, -1), std::vector<int>(ncols, -1)};

for (int row = 0; row < nrows; ++row) {
if (this->is_time_exceeded(timer, tlim)) return papilo::PresolveStatus::kUnchanged;
if (row_flags[row].test(papilo::RowFlag::kRedundant)) continue;

// Row direction: 'E' = equality, 'L' = <=, 'G' = >=, 'R' = ranged/free (skip)
bool lhs_inf = row_flags[row].test(papilo::RowFlag::kLhsInf);
bool rhs_inf = row_flags[row].test(papilo::RowFlag::kRhsInf);
char row_dir = (!lhs_inf && !rhs_inf) ? 'E' : (lhs_inf ? 'L' : (rhs_inf ? 'G' : 'R'));
if (row_dir == 'R') continue;

auto row_coeff = constraint_matrix.getRowCoefficients(row);
const int* cols = row_coeff.getIndices();
const f_t* coefs = row_coeff.getValues();
const int length = row_coeff.getLength();

// Record the index of the locking row.
// If more than one lock exists, mark the col as excluded from the search.
auto record_lock = [&](lock_dir dir, int col) {
if (locks[dir][col]++ == 0)
lock_row[dir][col] = row;
else
lock_row[dir][col] = -1;
};

if (row_dir == 'E') {
// Equality: locks both directions
for (int j = 0; j < length; ++j) {
record_lock(UP, cols[j]);
record_lock(DOWN, cols[j]);
}
} else {
// One-sided: directions swap between L (<=) and G (>=)
lock_dir pos_dir = (row_dir == 'L') ? UP : DOWN;
lock_dir neg_dir = (row_dir == 'L') ? DOWN : UP;
for (int j = 0; j < length; ++j) {
if (coefs[j] > 0)
record_lock(pos_dir, cols[j]);
else if (coefs[j] < 0)
record_lock(neg_dir, cols[j]);
}
}
}

// =========================================================================
// Step 2: Candidate Identification — O(ncols)
//
// Upward candidates: binary, single up-lock, c <= 0 (objective doesn't
// penalize increase — needed so x pushes against the lock or is indifferent).
// Downward: symmetric with single down-lock, c >= 0.
// =========================================================================

struct candidate_t {
int col;
int locking_row;
lock_dir dir;
};
std::vector<candidate_t> candidates;
candidates.reserve(std::min(ncols, nrows));

for (int col = 0; col < ncols; ++col) {
if (col_flags[col].test(papilo::ColFlag::kFixed, papilo::ColFlag::kSubstituted)) continue;
if (!is_binary_or_implied(col, col_flags.data(), lower_bounds.data(), upper_bounds.data()))
continue;
// Skip singletons: PaPILO's stuffing presolver handles these.
if (constraint_matrix.getColumnCoefficients(col).getLength() <= 1) continue;

// can be turned into strict checks if we need to guarantee
// that we never cut off any optimal solution
if (locks[UP][col] == 1 && objective[col] <= 0)
candidates.push_back({col, lock_row[UP][col], UP});
else if (locks[DOWN][col] == 1 && objective[col] >= 0)
candidates.push_back({col, lock_row[DOWN][col], DOWN});
}

if (this->is_time_exceeded(timer, tlim) || candidates.empty())
return papilo::PresolveStatus::kUnchanged;

// =========================================================================
// Step 3: Mini-Probing — O(L + K) per row
//
// For each locking row (L nonzeros, K candidates), we prove implications by
// fixing two variables and checking if the row's activity bounds are violated:
// - Fix candidate x to its "bad" bound (ub for upward, lb for downward)
// - Fix master y to its "unfavorable" bound (0 for upward, 1 for downward)
// - If the resulting minimum (LEQ) or maximum (GEQ) activity exceeds the
// row's bound, the combination is infeasible, proving y_unfav => x_safe.
//
// The master y is the binary variable in the row whose coefficient best
// amplifies the violation. We track the top-2 most extreme coefficients
// (neg_y for most negative, pos_y for most positive) so that if the
// candidate itself is the top-1 extremum, we can fall back to top-2.
// This keeps master selection O(1) per candidate instead of O(L).
//
// Candidates are sorted by lock_row so all K candidates sharing a row are
// processed together in a single O(L) scan, yielding O(L+K) per row group.
//
// dense_row_coefs[] is an ncols-sized scratch array giving O(1) coefficient
// lookup by column index; populated and cleaned per row in O(L).
// =========================================================================

std::sort(candidates.begin(), candidates.end(), [](const candidate_t& a, const candidate_t& b) {
return a.locking_row < b.locking_row;
});

struct top2_t {
std::pair<int, f_t> top1{-1, 0}, top2{-1, 0};

void update(int idx, f_t val, bound_side side)
{
auto better = [side](f_t a, f_t b) { return side == LOWER ? a < b : a > b; };
if (top1.first == -1 || better(val, top1.second)) {
top2 = top1;
top1 = {idx, val};
} else if (top2.first == -1 || better(val, top2.second)) {
top2 = {idx, val};
}
}
};

int n_substitutions = 0;
std::vector<f_t> dense_row_coefs(ncols, f_t{0});
std::vector<bool> substituted(ncols, false);

auto cand_it = candidates.begin();
while (cand_it != candidates.end()) {
if (this->is_time_exceeded(timer, tlim)) break;

int r = cand_it->locking_row;
if (r < 0) {
++cand_it;
continue;
}

// advance row_end to the first candidate with a different locking_row
auto row_end = std::find_if(
cand_it, candidates.end(), [r](const candidate_t& c) { return c.locking_row != r; });

auto row_coeff = constraint_matrix.getRowCoefficients(r);
const int* cols = row_coeff.getIndices();
const f_t* coefs = row_coeff.getValues();
const int length = row_coeff.getLength();

bool has_lhs = !row_flags[r].test(papilo::RowFlag::kLhsInf);
bool has_rhs = !row_flags[r].test(papilo::RowFlag::kRhsInf);

// A_min / A_max: tightest possible activity of the row over all variable bounds
f_t A_min = 0, A_max = 0;
bool can_reach_neg_inf = false, can_reach_pos_inf = false;
top2_t neg_y, pos_y;

for (int j = 0; j < length; ++j) {
int col = cols[j];
f_t coef = coefs[j];
bool lb_inf = col_flags[col].test(papilo::ColFlag::kLbInf);
bool ub_inf = col_flags[col].test(papilo::ColFlag::kUbInf);

dense_row_coefs[col] = coef;

// coef > 0: min activity uses lb, max uses ub; coef < 0: swapped
bool min_inf = (coef > 0) ? lb_inf : ub_inf;
bool max_inf = (coef > 0) ? ub_inf : lb_inf;
f_t min_bound = (coef > 0) ? lower_bounds[col] : upper_bounds[col];
f_t max_bound = (coef > 0) ? upper_bounds[col] : lower_bounds[col];

if (min_inf)
can_reach_neg_inf = true;
else
A_min += coef * min_bound;
if (max_inf)
can_reach_pos_inf = true;
else
A_max += coef * max_bound;

if (col_flags[col].test(papilo::ColFlag::kFixed, papilo::ColFlag::kSubstituted)) continue;
if (!is_binary_or_implied(col, col_flags.data(), lower_bounds.data(), upper_bounds.data()))
continue;
if (lower_bounds[col] == upper_bounds[col]) continue;

neg_y.update(col, coef, LOWER);
pos_y.update(col, coef, UPPER);
}

// LEQ probe needs finite A_min; GEQ probe needs finite A_max
bool use_leq_check = has_rhs && !can_reach_neg_inf;
bool use_geq_check = has_lhs && !can_reach_pos_inf;

// Probe: replace cand and y's min/max contributions with their fixed test
// values, then check if the resulting activity violates the row bound.
// Both candidate and master are binary [0,1], so min/max contributions simplify
auto evaluate = [&](f_t cand_coeff, bool is_upward, int y_col, f_t y_coef) -> bool {
if (y_col < 0) return false;
f_t cand_test = is_upward ? cand_coeff : f_t{0};
f_t y_test = is_upward ? f_t{0} : y_coef;
f_t test = cand_test + y_test;

if (use_leq_check) {
f_t probed_min = A_min - std::min(f_t{0}, cand_coeff) - std::min(f_t{0}, y_coef) + test;
if (probed_min > rhs_values[r] + tol) return true;
}
if (use_geq_check) {
f_t probed_max = A_max - std::max(f_t{0}, cand_coeff) - std::max(f_t{0}, y_coef) + test;
if (probed_max < lhs_values[r] - tol) return true;
}
return false;
};

// Return the best master from the top-2 tracker, skipping excluded columns.
auto pick_master = [&substituted](const top2_t& t, int exclude) -> std::pair<int, f_t> {
if (t.top1.first >= 0 && t.top1.first != exclude && !substituted[t.top1.first]) return t.top1;
if (t.top2.first >= 0 && t.top2.first != exclude && !substituted[t.top2.first]) return t.top2;
return {-1, f_t{0}};
};

for (auto ci = cand_it; ci != row_end; ++ci) {
auto [cand, locking_row, dir] = *ci;
if (substituted[cand]) continue;

bool is_upward = (dir == UP);
f_t cand_coeff = dense_row_coefs[cand];

bool proven = false;
int master_col = -1;

// For LEQ upward: y=0 zeroes out y's contribution, so the best master
// is the one with the most negative coefficient (maximizes probed_min).
// For LEQ downward: y=1 adds y's coefficient, so pick the most positive.
auto try_prove = [&](bool check, const top2_t& tracker) {
if (!check || proven) return;
auto [y, yc] = pick_master(tracker, cand);
if (evaluate(cand_coeff, is_upward, y, yc)) {
proven = true;
master_col = y;
}
};
try_prove(use_leq_check, is_upward ? neg_y : pos_y);
try_prove(use_geq_check, is_upward ? pos_y : neg_y);
if (!proven) continue;

// The probe proves a one-directional implication (e.g. y=0 => x=0).
// The substitution x=y also asserts the reverse (y=1 => x=1), which is
// only safe if forcing x to its bound doesn't starve other variables of
// capacity in the locking row. Verify the row becomes globally non-binding
// when y is in its favorable state.
f_t y_coef_val = dense_row_coefs[master_col];
f_t fav_y_contrib = is_upward ? y_coef_val : 0.0;

auto check_side =
[&](bool active, bool unbounded, f_t activity, f_t orig_y, f_t bound, bound_side side) {
if (!active || !proven) return;
if (unbounded) {
proven = false;
return;
}
f_t fav = activity - orig_y + fav_y_contrib;
if (side == UPPER ? fav > bound + tol : fav < bound - tol) proven = false;
};
check_side(
has_rhs, can_reach_pos_inf, A_max, std::max(0.0, y_coef_val), rhs_values[r], UPPER);
check_side(
has_lhs, can_reach_neg_inf, A_min, std::min(0.0, y_coef_val), lhs_values[r], LOWER);
if (!proven) continue;

substituted[cand] = true;
reductions.replaceCol(cand, master_col, f_t{1}, f_t{0});
++n_substitutions;
}

for (int j = 0; j < length; ++j)
dense_row_coefs[cols[j]] = 0;
cand_it = row_end;
}

if (n_substitutions == 0) return papilo::PresolveStatus::kUnchanged;

CUOPT_LOG_DEBUG("Single-lock dual aggregation: %d candidates, %d substitutions",
(int)candidates.size(),
n_substitutions);

return papilo::PresolveStatus::kReduced;
}

#define INSTANTIATE(F_TYPE) template class SingleLockDualAggregation<F_TYPE>;

#if MIP_INSTANTIATE_FLOAT
INSTANTIATE(float)
#endif

#if MIP_INSTANTIATE_DOUBLE
INSTANTIATE(double)
#endif

#undef INSTANTIATE

} // namespace cuopt::linear_programming::detail
Loading