Skip to content

Commit 4caf1f1

Browse files
committed
add tests, refactor for clarity
1 parent 39bc2da commit 4caf1f1

7 files changed

Lines changed: 571 additions & 315 deletions

File tree

cpp/src/mip_heuristics/presolve/single_lock_dual_aggregation.cpp

Lines changed: 314 additions & 235 deletions
Large diffs are not rendered by default.

cpp/src/mip_heuristics/presolve/single_lock_dual_aggregation.hpp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,6 @@ class SingleLockDualAggregation : public papilo::PresolveMethod<f_t> {
3737
papilo::Reductions<f_t>& reductions,
3838
const papilo::Timer& timer,
3939
int& reason_of_infeasibility) override;
40-
41-
private:
42-
bool is_binary_or_implied(int col,
43-
const papilo::Flags<papilo::ColFlag>* col_flags,
44-
const f_t* lower_bounds,
45-
const f_t* upper_bounds) const
46-
{
47-
if (!col_flags[col].test(papilo::ColFlag::kIntegral) &&
48-
!col_flags[col].test(papilo::ColFlag::kImplInt))
49-
return false;
50-
if (col_flags[col].test(papilo::ColFlag::kLbInf)) return false;
51-
if (col_flags[col].test(papilo::ColFlag::kUbInf)) return false;
52-
return lower_bounds[col] == 0.0 && upper_bounds[col] == 1.0;
53-
}
5440
};
5541

5642
} // namespace cuopt::linear_programming::detail

cpp/src/mip_heuristics/presolve/third_party_presolve.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ std::optional<third_party_presolve_result_t<i_t, f_t>> third_party_presolve_t<i_
646646
set_presolve_parameters(
647647
papilo_presolver, category, op_problem.get_n_constraints(), op_problem.get_n_variables());
648648

649+
// Disable papilo logs
649650
papilo_presolver.setVerbosityLevel(papilo::VerbosityLevel::kQuiet);
650651

651652
auto result = papilo_presolver.apply(papilo_problem);

cpp/tests/mip/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ ConfigureTest(EMPTY_FIXED_PROBLEMS_TEST
3737
${CMAKE_CURRENT_SOURCE_DIR}/empty_fixed_problems_test.cu
3838
)
3939
ConfigureTest(PRESOLVE_TEST
40-
${CMAKE_CURRENT_SOURCE_DIR}/presolve_test.cu
40+
${CMAKE_CURRENT_SOURCE_DIR}/presolve_test.cpp
4141
)
4242
# Disable for now
4343
# ConfigureTest(FEASIBILITY_JUMP_TEST

cpp/tests/mip/presolve_test.cpp

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/* clang-format off */
2+
/*
3+
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
/* clang-format on */
7+
8+
#include <mip_heuristics/presolve/single_lock_dual_aggregation.hpp>
9+
10+
#include <papilo/core/ProblemBuilder.hpp>
11+
#include <papilo/core/ProblemUpdate.hpp>
12+
#include <papilo/core/Reductions.hpp>
13+
#include <papilo/core/Statistics.hpp>
14+
#include <papilo/core/postsolve/PostsolveStorage.hpp>
15+
#include <papilo/io/Message.hpp>
16+
#include <papilo/misc/Timer.hpp>
17+
18+
#include <gtest/gtest.h>
19+
20+
#include <cmath>
21+
#include <limits>
22+
#include <tuple>
23+
#include <vector>
24+
25+
namespace cuopt::linear_programming::detail::test {
26+
27+
namespace {
28+
29+
struct papilo_harness_t {
30+
papilo::Num<double> num;
31+
papilo::Reductions<double> reductions;
32+
papilo::Statistics stats;
33+
papilo::PresolveOptions options;
34+
papilo::Message msg;
35+
double timer_acc{0};
36+
37+
papilo_harness_t() { options.tlim = std::numeric_limits<double>::max(); }
38+
39+
papilo::PresolveStatus run(papilo::Problem<double>& problem,
40+
SingleLockDualAggregation<double>& presolver)
41+
{
42+
papilo::PostsolveStorage<double> postsolve(problem.getNRows(), problem.getNCols());
43+
papilo::ProblemUpdate<double> update(problem, postsolve, stats, options, num, msg);
44+
papilo::Timer timer(timer_acc);
45+
int reason = 0;
46+
return presolver.execute(problem, update, num, reductions, timer, reason);
47+
}
48+
49+
bool has_replace(int col1, int col2, double factor, double offset) const
50+
{
51+
const auto& txns = reductions.getTransactions();
52+
const auto& reds = reductions.getReductions();
53+
for (const auto& t : txns) {
54+
if (t.end - t.start != 2) continue;
55+
const auto& r0 = reds[t.start];
56+
const auto& r1 = reds[t.start + 1];
57+
if (r0.row == papilo::ColReduction::REPLACE && r0.col == col1 &&
58+
std::abs(r0.newval - factor) < 1e-9 && r1.col == col2 &&
59+
std::abs(r1.newval - offset) < 1e-9)
60+
return true;
61+
}
62+
return false;
63+
}
64+
};
65+
66+
papilo::Problem<double> build_problem(int nrows,
67+
int ncols,
68+
const std::vector<std::tuple<int, int, double>>& entries,
69+
const std::vector<double>& obj,
70+
const std::vector<double>& lb,
71+
const std::vector<double>& ub,
72+
const std::vector<bool>& integer,
73+
const std::vector<double>& row_lhs,
74+
const std::vector<double>& row_rhs,
75+
const std::vector<bool>& lhs_inf,
76+
const std::vector<bool>& rhs_inf)
77+
{
78+
papilo::ProblemBuilder<double> builder;
79+
builder.setNumRows(nrows);
80+
builder.setNumCols(ncols);
81+
for (auto& [r, c, v] : entries)
82+
builder.addEntry(r, c, v);
83+
for (int c = 0; c < ncols; ++c) {
84+
builder.setObj(c, obj[c]);
85+
builder.setColLb(c, lb[c]);
86+
builder.setColUb(c, ub[c]);
87+
builder.setColIntegral(c, integer[c]);
88+
}
89+
for (int r = 0; r < nrows; ++r) {
90+
builder.setRowLhs(r, row_lhs[r]);
91+
builder.setRowRhs(r, row_rhs[r]);
92+
builder.setRowLhsInf(r, lhs_inf[r]);
93+
builder.setRowRhsInf(r, rhs_inf[r]);
94+
}
95+
return builder.build();
96+
}
97+
98+
} // namespace
99+
100+
// x has one up-lock in a LEQ row. Probe proves y=0 => x=0 via activity.
101+
// Favorable-state check passes. Result: x = y (direct substitution).
102+
//
103+
// min -x
104+
// s.t. 3x - 4y <= 1 (the locking row)
105+
// x + y >= 0 (GEQ slack: positive coeffs add down-locks only)
106+
// x, y in {0,1}
107+
//
108+
// On the locking row:
109+
// A_min=-4, A_max=3. Probe(x=1,y=0): probed_min = -4-0-(-4)+3 = 3 > 1 => proven.
110+
// Favorable(y=1): A_max - max(0,-4) + (-4) = 3-0-4 = -1 <= 1 => safe.
111+
TEST(SingleLockDualAggregation, DirectSubstitution)
112+
{
113+
auto problem = build_problem(2,
114+
2,
115+
{{0, 0, 3.0}, {0, 1, -4.0}, {1, 0, 1.0}, {1, 1, 1.0}},
116+
{-1.0, 0.0},
117+
{0.0, 0.0},
118+
{1.0, 1.0},
119+
{true, true},
120+
{0.0, 0.0},
121+
{1.0, 0.0},
122+
{true, false},
123+
{false, true});
124+
125+
SingleLockDualAggregation<double> presolver;
126+
papilo_harness_t h;
127+
auto status = h.run(problem, presolver);
128+
129+
EXPECT_EQ(status, papilo::PresolveStatus::kReduced);
130+
EXPECT_TRUE(h.has_replace(0, 1, 1.0, 0.0)); // x = y
131+
}
132+
133+
// Direct master has no negative binary coeff, so direct probe fails.
134+
// Anti probe (y=1 unfavorable for upward) succeeds. Result: x = 1 - y.
135+
//
136+
// min -x
137+
// s.t. 3x + 4y <= 5 (the locking row)
138+
// x + y >= 0 (GEQ slack: positive coeffs add down-locks only)
139+
// x, y in {0,1}
140+
//
141+
// On the locking row:
142+
// A_min=0, A_max=7. Direct: no neg binary master. Anti master: y (coeff +4).
143+
// Probe(x=1,y=1): probed_min = 0-0-0+(3+4) = 7 > 5 => proven.
144+
// Favorable(y=0 for anti-upward): A_max - max(0,4) + 0 = 7-4 = 3 <= 5 => safe.
145+
TEST(SingleLockDualAggregation, AntiSubstitution)
146+
{
147+
auto problem = build_problem(2,
148+
2,
149+
{{0, 0, 3.0}, {0, 1, 4.0}, {1, 0, 1.0}, {1, 1, 1.0}},
150+
{-1.0, 0.0},
151+
{0.0, 0.0},
152+
{1.0, 1.0},
153+
{true, true},
154+
{0.0, 0.0},
155+
{5.0, 0.0},
156+
{true, false},
157+
{false, true});
158+
159+
SingleLockDualAggregation<double> presolver;
160+
papilo_harness_t h;
161+
auto status = h.run(problem, presolver);
162+
163+
EXPECT_EQ(status, papilo::PresolveStatus::kReduced);
164+
EXPECT_TRUE(h.has_replace(0, 1, -1.0, 1.0)); // x = 1 - y
165+
}
166+
167+
// A free row (both sides infinite) produces no locks and no candidates.
168+
TEST(SingleLockDualAggregation, FreeRowNonCase)
169+
{
170+
auto problem = build_problem(1,
171+
2,
172+
{{0, 0, 2.0}, {0, 1, 3.0}},
173+
{-1.0, 0.0},
174+
{0.0, 0.0},
175+
{1.0, 1.0},
176+
{true, true},
177+
{0.0},
178+
{0.0},
179+
{true},
180+
{true});
181+
182+
SingleLockDualAggregation<double> presolver;
183+
papilo_harness_t h;
184+
auto status = h.run(problem, presolver);
185+
186+
EXPECT_EQ(status, papilo::PresolveStatus::kUnchanged);
187+
EXPECT_EQ(h.reductions.size(), 0u);
188+
}
189+
190+
// Probe proves the implication but the favorable-state check fails,
191+
// so the substitution is correctly rejected.
192+
//
193+
// min -x
194+
// s.t. 3x - 2y <= 0
195+
// x, y in {0,1}
196+
//
197+
// A_min=-2, A_max=3. Probe(x=1,y=0): probed_min = -2-0-(-2)+3 = 3 > 0 => proven.
198+
// Favorable(y=1): A_max - max(0,-2) + (-2) = 3-0-2 = 1 > 0 => FAILS.
199+
TEST(SingleLockDualAggregation, FavorableStateRejects)
200+
{
201+
auto problem = build_problem(1,
202+
2,
203+
{{0, 0, 3.0}, {0, 1, -2.0}},
204+
{-1.0, 0.0},
205+
{0.0, 0.0},
206+
{1.0, 1.0},
207+
{true, true},
208+
{0.0},
209+
{0.0},
210+
{true},
211+
{false});
212+
213+
SingleLockDualAggregation<double> presolver;
214+
papilo_harness_t h;
215+
auto status = h.run(problem, presolver);
216+
217+
EXPECT_EQ(status, papilo::PresolveStatus::kUnchanged);
218+
EXPECT_EQ(h.reductions.size(), 0u);
219+
}
220+
221+
} // namespace cuopt::linear_programming::detail::test

cpp/tests/mip/presolve_test.cu

Lines changed: 0 additions & 65 deletions
This file was deleted.

cpp/tests/mip/problem_test.cu

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "../linear_programming/utilities/pdlp_test_utilities.cuh"
99

1010
#include <cuopt/linear_programming/solve.hpp>
11+
#include <mip_heuristics/presolve/third_party_presolve.hpp>
1112
#include <mip_heuristics/presolve/trivial_presolve.cuh>
1213
#include <mip_heuristics/problem/problem.cuh>
1314
#include <mps_parser/mps_data_model.hpp>
@@ -484,4 +485,37 @@ TEST(optimization_problem_t_DeathTest, test_check_problem_validity)
484485
}
485486
#endif
486487

488+
TEST(problem, find_implied_integers)
489+
{
490+
const raft::handle_t handle_{};
491+
492+
auto path = make_path_absolute("mip/fiball.mps");
493+
auto mps_data_model = cuopt::mps_parser::parse_mps<int, double>(path, false);
494+
auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_data_model);
495+
auto presolver = std::make_unique<detail::third_party_presolve_t<int, double>>();
496+
auto result = presolver->apply(op_problem,
497+
cuopt::linear_programming::problem_category_t::MIP,
498+
cuopt::linear_programming::presolver_t::Papilo,
499+
false,
500+
1e-6,
501+
1e-12,
502+
20,
503+
1);
504+
ASSERT_TRUE(result.has_value());
505+
506+
auto problem = detail::problem_t<int, double>(result->reduced_problem);
507+
problem.set_implied_integers(result->implied_integer_indices);
508+
ASSERT_TRUE(result->implied_integer_indices.size() > 0);
509+
auto var_types = host_copy(problem.variable_types, handle_.get_stream());
510+
// Find the index of the one continuous variable
511+
auto it = std::find_if(var_types.begin(), var_types.end(), [](var_t var_type) {
512+
return var_type == var_t::CONTINUOUS;
513+
});
514+
ASSERT_NE(it, var_types.end());
515+
ASSERT_EQ(problem.presolve_data.var_flags.size(), var_types.size());
516+
// Ensure it is an implied integer
517+
EXPECT_EQ(problem.presolve_data.var_flags.element(it - var_types.begin(), handle_.get_stream()),
518+
((int)detail::problem_t<int, double>::var_flags_t::VAR_IMPLIED_INTEGER));
519+
}
520+
487521
} // namespace cuopt::linear_programming::test

0 commit comments

Comments
 (0)