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