@@ -1599,7 +1599,7 @@ bool HighsPrimalHeuristics::localMip() {
15991599 HighsSparseMatrix a_matrix_row;
16001600 a_matrix_row.createRowwise (a_matrix);
16011601
1602- // Create global objects used in lambda expressions
1602+ // Initialise values and reserve vectors
16031603 HighsInt num_violated_rows_sample = 250 ;
16041604 HighsInt num_satisfied_rows_sample = 20 ;
16051605 HighsInt iters = 0 ;
@@ -1662,20 +1662,22 @@ bool HighsPrimalHeuristics::localMip() {
16621662 return false ;
16631663 };
16641664
1665- struct Violated {
1665+ struct RowStatus {
16661666 std::vector<HighsInt> viol;
16671667 std::vector<HighsInt> viol_index;
16681668
1669- explicit Violated (const HighsInt n) : viol(n, -1 ) { viol_index.reserve (n); }
1669+ explicit RowStatus (const HighsInt n) : viol(n, -1 ) {
1670+ viol_index.reserve (n);
1671+ }
16701672 };
16711673
1672- auto add_entry = [&](Violated & v, HighsInt r) {
1674+ auto add_entry = [&](RowStatus & v, HighsInt r) {
16731675 assert (v.viol [r] == -1 );
16741676 v.viol [r] = static_cast <HighsInt>(v.viol_index .size ());
16751677 v.viol_index .push_back (r);
16761678 };
16771679
1678- auto remove_entry = [&](Violated & v, HighsInt r) {
1680+ auto remove_entry = [&](RowStatus & v, HighsInt r) {
16791681 assert (v.viol [r] != -1 );
16801682 v.viol_index [v.viol [r]] = v.viol_index .back ();
16811683 v.viol [v.viol_index .back ()] = v.viol [r];
@@ -1688,11 +1690,11 @@ bool HighsPrimalHeuristics::localMip() {
16881690 HighsInt momentum = 1 ;
16891691 std::vector<HighsInt> allow_neg_delta (mipsolver.numCol ());
16901692 std::vector<HighsInt> allow_pos_delta (mipsolver.numCol ());
1691- Violated viol (mipsolver.numRow ());
1692- Violated satisfied (mipsolver.numRow ());
1693+ RowStatus viol (mipsolver.numRow ());
1694+ RowStatus satisfied (mipsolver.numRow ());
16931695 std::vector<HighsCDouble> activities (mipsolver.numRow ());
16941696
1695- auto calc_activites = [&](bool reset_violated) {
1697+ auto calc_activities = [&](bool reset_violated) {
16961698 std::fill (activities.begin (), activities.end (), 0.0 );
16971699 if (reset_violated) {
16981700 viol.viol_index .clear ();
@@ -1751,8 +1753,10 @@ bool HighsPrimalHeuristics::localMip() {
17511753 // printf("Applying move col %d with delta %g\n", c, delta);
17521754 };
17531755
1754- auto one_opt_calc_delta = [&](HighsInt c) -> double {
1755- // Only check lower bound delta if positive column cost, likewise for neg.
1756+ // Calculate how much a column can be shifted while maintaining feasibility
1757+ // of all rows containing given column.
1758+ auto one_opt_calc_col_delta = [&](HighsInt c) -> double {
1759+ // Only check lower bound delta if positive column cost and vice versa.
17561760 const HighsInt dir = mipsolver.colCost (c) > 0 ? -1 : 1 ;
17571761 bool integral = mipsolver.isColIntegral (c);
17581762 double delta = dir == -1 ? sol[c] - globaldom.col_lower_ [c]
@@ -1785,11 +1789,14 @@ bool HighsPrimalHeuristics::localMip() {
17851789 };
17861790
17871791 std::vector<double > one_opt_deltas (mipsolver.numCol ());
1792+
1793+ // Find single column shift with the best objective change that maintains
1794+ // feasibility of all rows containing the column
17881795 auto one_opt = [&](bool recalc) -> bool {
17891796 std::vector<double > one_opt_scores (mipsolver.numCol ());
17901797 if (recalc) {
17911798 for (const HighsInt c : cols_with_obj) {
1792- one_opt_deltas[c] = one_opt_calc_delta (c);
1799+ one_opt_deltas[c] = one_opt_calc_col_delta (c);
17931800 }
17941801 }
17951802 HighsInt best_col = -1 ;
@@ -1803,7 +1810,7 @@ bool HighsPrimalHeuristics::localMip() {
18031810 }
18041811 if (best_col == -1 ) return false ;
18051812 apply_move (best_col, one_opt_deltas[best_col]);
1806- one_opt_deltas[best_col] = one_opt_calc_delta (best_col);
1813+ one_opt_deltas[best_col] = one_opt_calc_col_delta (best_col);
18071814 HighsInt start = a_matrix.start_ [best_col];
18081815 HighsInt end = a_matrix.start_ [best_col + 1 ];
18091816 for (HighsInt i = start; i < end; ++i) {
@@ -1813,50 +1820,56 @@ bool HighsPrimalHeuristics::localMip() {
18131820 for (HighsInt j = rstart; j < rend; ++j) {
18141821 HighsInt c = a_matrix_row.index_ [j];
18151822 if (mipsolver.colCost (c) != 0 ) {
1816- one_opt_deltas[c] = one_opt_calc_delta (c);
1823+ one_opt_deltas[c] = one_opt_calc_col_delta (c);
18171824 }
18181825 }
18191826 }
18201827 return true ;
18211828 };
18221829
1823- auto feas_one_opt = [&](HighsInt c, HighsInt r, double coef,
1824- std::vector<std::pair<HighsInt, double >>& moves) {
1825- // Get direction column can be moved to maximise constraint feasibility
1826- double row_lower = mipsolver.model_ ->row_lower_ [r];
1827- double row_upper = mipsolver.model_ ->row_upper_ [r];
1828- bool lower_inf = row_lower == -kHighsInf ;
1829- bool upper_inf = row_upper == kHighsInf ;
1830- double slack_lower = static_cast <double >(activities[r]) - row_lower;
1831- double slack_upper = row_upper - static_cast <double >(activities[r]);
1832- HighsInt increase_activity;
1833- if (!upper_inf && activities[r] > row_upper + feastol) {
1834- increase_activity = -1 ;
1835- } else if (!lower_inf && activities[r] < row_lower - feastol) {
1836- increase_activity = 1 ;
1837- } else {
1838- return ;
1839- }
1840- double delta = increase_activity == 1 ? std::abs (slack_lower / coef)
1841- : std::abs (slack_upper / coef);
1842- HighsInt dir = coef < 0 ? -increase_activity : increase_activity;
1843- if (mipsolver.isColIntegral (c)) {
1844- delta = std::ceil (delta - feastol);
1845- }
1846- delta = dir == -1
1830+ // Find shift of given column that respects column bounds and maximises
1831+ // feasibility of given row.
1832+ auto calc_col_max_row_feas_delta =
1833+ [&](HighsInt c, HighsInt r, double coef,
1834+ std::vector<std::pair<HighsInt, double >>& moves) {
1835+ // Get direction column can be moved to maximise row feasibility
1836+ double row_lower = mipsolver.model_ ->row_lower_ [r];
1837+ double row_upper = mipsolver.model_ ->row_upper_ [r];
1838+ bool lower_inf = row_lower == -kHighsInf ;
1839+ bool upper_inf = row_upper == kHighsInf ;
1840+ double slack_lower = static_cast <double >(activities[r]) - row_lower;
1841+ double slack_upper = row_upper - static_cast <double >(activities[r]);
1842+ HighsInt increase_activity;
1843+ if (!upper_inf && activities[r] > row_upper + feastol) {
1844+ increase_activity = -1 ;
1845+ } else if (!lower_inf && activities[r] < row_lower - feastol) {
1846+ increase_activity = 1 ;
1847+ } else {
1848+ return ;
1849+ }
1850+ double delta = increase_activity == 1 ? std::abs (slack_lower / coef)
1851+ : std::abs (slack_upper / coef);
1852+ HighsInt dir = coef < 0 ? -increase_activity : increase_activity;
1853+ if (mipsolver.isColIntegral (c)) {
1854+ delta = std::ceil (delta - feastol);
1855+ }
1856+ delta =
1857+ dir == -1
18471858 ? std::min (std::abs (sol[c] - globaldom.col_lower_ [c]), delta)
18481859 : std::min (std::abs (globaldom.col_upper_ [c] - sol[c]), delta);
1849- if (mipsolver.isColIntegral (c)) {
1850- delta = std::floor (delta + feastol);
1851- }
1852- if (delta < feastol) return ;
1853- if (!tabu (c, dir * delta)) return ;
1854- // TODO: Need to add some safety net for unbounded variables
1855- assert (sol[c] + delta * dir > globaldom.col_lower_ [c] - feastol);
1856- assert (sol[c] + delta * dir < globaldom.col_upper_ [c] + feastol);
1857- moves.emplace_back (c, dir * delta);
1858- };
1860+ // Don't try and shift unbounded columns
1861+ if (delta == kHighsInf ) return ;
1862+ if (mipsolver.isColIntegral (c)) {
1863+ delta = std::floor (delta + feastol);
1864+ }
1865+ if (delta < feastol) return ;
1866+ if (!tabu (c, dir * delta)) return ;
1867+ assert (sol[c] + delta * dir > globaldom.col_lower_ [c] - feastol);
1868+ assert (sol[c] + delta * dir < globaldom.col_upper_ [c] + feastol);
1869+ moves.emplace_back (c, dir * delta);
1870+ };
18591871
1872+ // Sample row indices
18601873 auto sample_indices = [&](std::vector<HighsInt>& indices,
18611874 HighsInt max_samples,
18621875 HighsInt& n) -> std::vector<HighsInt>& {
@@ -1889,6 +1902,7 @@ bool HighsPrimalHeuristics::localMip() {
18891902 return sampled_indices;
18901903 };
18911904
1905+ // Sample operations (how much to move a column)
18921906 auto sample_operations = [&](std::vector<std::pair<HighsInt, double >>& moves,
18931907 HighsInt max_samples, HighsInt& n) {
18941908 HighsInt available = static_cast <HighsInt>(moves.size ());
@@ -1903,6 +1917,7 @@ bool HighsPrimalHeuristics::localMip() {
19031917 }
19041918 };
19051919
1920+ // Propose column shift that improves the objective. Ignore feasibility.
19061921 auto explore_breakthrough =
19071922 [&](std::vector<std::pair<HighsInt, double >>& moves) {
19081923 for (HighsInt c : cols_with_obj) {
@@ -1912,15 +1927,15 @@ bool HighsPrimalHeuristics::localMip() {
19121927 std::max (static_cast <double >(obj - bestobj), feastol);
19131928 HighsInt dir = obj_coef < 0 ? 1 : -1 ;
19141929 double delta = obj_change / std::abs (obj_coef);
1915- // TODO: THis feastol should be a negative.... Why does it work better?
1930+ // TODO: THis feastol should be a negative.... Why does it work
1931+ // better?
19161932 if (mipsolver.isColIntegral (c)) {
19171933 delta = std::ceil (delta - kHighsTiny );
19181934 }
19191935 delta =
19201936 dir == -1
19211937 ? std::min (delta, std::abs (sol[c] - globaldom.col_lower_ [c]))
19221938 : std::min (delta, std::abs (globaldom.col_upper_ [c] - sol[c]));
1923- // TODO: Need to add this in case integral variables have continuous bounds?
19241939 if (mipsolver.isColIntegral (c)) {
19251940 delta = std::floor (delta + feastol);
19261941 }
@@ -1931,6 +1946,8 @@ bool HighsPrimalHeuristics::localMip() {
19311946 }
19321947 };
19331948
1949+ // Iterate over sample violated rows and compute candidate moves
1950+ // Also compute breakthrough moves
19341951 auto explore_violated = [&](std::vector<std::pair<HighsInt, double >>& moves,
19351952 HighsInt& n) {
19361953 if (!viol.viol_index .empty ()) {
@@ -1943,7 +1960,7 @@ bool HighsPrimalHeuristics::localMip() {
19431960 for (HighsInt i = start; i < end; ++i) {
19441961 HighsInt c = a_matrix_row.index_ [i];
19451962 double val = a_matrix_row.value_ [i];
1946- feas_one_opt (c, r, val, moves);
1963+ calc_col_max_row_feas_delta (c, r, val, moves);
19471964 }
19481965 }
19491966 }
@@ -1953,6 +1970,7 @@ bool HighsPrimalHeuristics::localMip() {
19531970 sample_operations (moves, num_violated_rows_sample, n);
19541971 };
19551972
1973+ // Iterate over sample satisfied rows and compute candidate moves
19561974 auto explore_satisfied = [&](std::vector<std::pair<HighsInt, double >>& moves,
19571975 HighsInt& n) {
19581976 if (!satisfied.viol_index .empty ()) {
@@ -1965,13 +1983,24 @@ bool HighsPrimalHeuristics::localMip() {
19651983 for (HighsInt i = start; i < end; ++i) {
19661984 HighsInt c = a_matrix_row.index_ [i];
19671985 double val = a_matrix_row.value_ [i];
1968- feas_one_opt (c, r, val, moves);
1986+ calc_col_max_row_feas_delta (c, r, val, moves);
19691987 }
19701988 }
19711989 }
19721990 sample_operations (moves, num_satisfied_rows_sample, n);
19731991 };
19741992
1993+ // Score candidate moves. Given a move (c, delta), the score components are:
1994+ // + obj_weight if objective improves, - obj_weight otherwise
1995+ // For each row:
1996+ // + momentum * weights[row] if row feasibility improves
1997+ // - weights[row] if row feasibility worsens
1998+ // an additional multiplier of two is used if the row becomes feasible or
1999+ // becomes infeasible
2000+ // The candidate selected in order of priority:
2001+ // - Largest positive score
2002+ // - Last candidate with 0 score that improved feasibility TODO: Work on this
2003+ // - Either no candidate or with some small chance a random "bad" candidate
19752004 auto score_moves = [&](std::vector<std::pair<HighsInt, double >>& moves,
19762005 HighsInt n) {
19772006 HighsInt best_i = -1 ;
@@ -2015,10 +2044,10 @@ bool HighsPrimalHeuristics::localMip() {
20152044 } else if (was_satisfied && !now_satisfied) {
20162045 score -= 2 * weights[r];
20172046 } else if (!now_satisfied && !was_satisfied) {
2018- if (old_violation > new_violation) {
2047+ if (old_violation > new_violation - kHighsTiny ) {
20192048 score += momentum * weights[r];
20202049 feas_improvements++;
2021- } else {
2050+ } else if (new_violation > old_violation - kHighsTiny ) {
20222051 score -= weights[r];
20232052 }
20242053 }
@@ -2064,10 +2093,12 @@ bool HighsPrimalHeuristics::localMip() {
20642093
20652094 auto terminate = [&]() { return iters > 5000 ; };
20662095
2067- calc_activites (false );
2096+ calc_activities (false );
20682097 bool last_iter_feas = false ;
20692098 std::vector<std::pair<HighsInt, double >> candidate_moves;
2070- candidate_moves.reserve (5000 );
2099+ candidate_moves.reserve (std::max (
2100+ static_cast <HighsInt>(cols_with_obj.size ()) + num_violated_rows_sample,
2101+ num_satisfied_rows_sample));
20712102 // Main solving loop
20722103 while (!terminate ()) {
20732104 // No rows are violated, so check the solution
@@ -2079,7 +2110,7 @@ bool HighsPrimalHeuristics::localMip() {
20792110 momentum = 1 ;
20802111 recalc_objective ();
20812112 // If solution is improving then store it
2082- // TODO: Should we undo the transformation here??
2113+ // TODO: Undo the transformation here??
20832114 if (obj + feastol < bestobj) {
20842115 found_feas_before = true ;
20852116 save_sol ();
@@ -2103,6 +2134,7 @@ bool HighsPrimalHeuristics::localMip() {
21032134 for (HighsInt r : viol.viol_index ) {
21042135 weights[r]++;
21052136 }
2137+ // Increase or reset momentum depending if candidate is found
21062138 if (candidate == -1 ) {
21072139 momentum++;
21082140 } else {
@@ -2113,6 +2145,7 @@ bool HighsPrimalHeuristics::localMip() {
21132145 // viol.viol_index.size(), found_feas_before,
21142146 // static_cast<double>(bestobj));
21152147 }
2148+ // Try and add incumbent to storage
21162149 if (found_feas_before) {
21172150 recalc_objective ();
21182151 mipsolver.mipdata_ ->addIncumbent (intsol, static_cast <double >(obj),
0 commit comments