-
Notifications
You must be signed in to change notification settings - Fork 222
Improve sorting for the diving queue via pseudocost estimate and randomness #564
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
Closed
Closed
Changes from all commits
Commits
Show all changes
101 commits
Select commit
Hold shift + click to select a range
181aef4
added a dual simplex method for calculating the phase 2 from an exist…
nguidotti 09a5e7e
child nodes now reuse the bounds of the parent. fixed incorrect bound…
nguidotti 0a7a7eb
refactor based on the reuse-parent-branch
nguidotti 7997b12
fixed value of the bounds_change for the child
nguidotti 2688a87
Merge remote-tracking branch 'cuopt/branch-25.10' into dual-simplex-u…
nguidotti 0cb0dcf
changed dual simplex to keep the `basis_update_mpf_t` for future use
nguidotti 21d4bcf
Merge remote-tracking branch 'cuopt/branch-25.10' into bounds-propaga…
nguidotti 316d14f
Merge remote-tracking branch 'cuopt/branch-25.10' into dual-simplex-u…
nguidotti 9368feb
Merge branch 'fix-diving-bounds' into bounds-propagation
nguidotti 5a5090c
Merge remote-tracking branch 'cuopt/branch-25.10' into bounds-propaga…
nguidotti 7cadb8a
Merge branch 'branch-25.10' into dual-simplex-update-basis
nguidotti 80aa125
Merge branch 'branch-25.10' into dual-simplex-update-basis
nguidotti 6ec4806
child nodes can reuse the basis factorization from the parent
nguidotti 861a12e
Merge branch 'branch-25.10' into bounds-propagation
nguidotti 95de283
revert refactor
nguidotti f44b134
fix incorrect bounds when two nodes shares the same branch variable
nguidotti 1cedfd6
small refactor
nguidotti 1ca6055
fix incorrect thread type in diving
nguidotti 320f162
Merge branch 'branch-25.10' into bounds-propagation
nguidotti 517b54a
Merge branch 'dual-simplex-update-basis' into reuse-basis-factorization
nguidotti 42daa5b
Merge branch 'branch-25.10' into reuse-basis-factorization
nguidotti bfeb660
constraints_changed is only set for branched variables. refactor boun…
nguidotti 42dee91
Merge branch 'branch-25.10' into bounds-propagation
nguidotti 73b7ad1
Merge branch 'main' into dual-simplex-update-basis
nguidotti ccb41eb
Merge branch 'main' into reuse-basis-factorization
nguidotti f954dcf
Merge branch 'branch-25.10' into bounds-propagation
nguidotti 222f95b
Merge branch 'main' into bounds-propagation
nguidotti b5ed956
small refactor
nguidotti abeb2cf
fix log spacing
nguidotti 3237913
fix log spacing for repaired solutions
nguidotti 5768f50
fix incorrect matrix dimension in LU after LP presolve
nguidotti 8155322
Merge branch 'dual-simplex-update-basis' into reuse-basis-factorization
nguidotti f9853b2
initialize the presolver only once per thread
nguidotti 0ed38a9
Refactor, simplify interface, and reduce number of code changes
chris-maes 166c446
Further removal; missing when cherry-picking previous commit
chris-maes 27884e4
Rename functions. Address code-rabbit issues
chris-maes deffe68
Style fixes
chris-maes f779cb7
Remove confusing col_permutation_ from middle-product update
chris-maes ad756d1
Merge branch 'main' into dual-simplex-update-basis
nguidotti f669f64
removed unused variable
nguidotti 8a06d3e
added missing function declaration
nguidotti 6ccb6d9
fixed race condition
nguidotti 8f55932
small refactor
nguidotti 7f337d4
fixed missing template parameters
nguidotti 06ca86d
fixed crash
nguidotti b5fc52c
added asserts
nguidotti d0b5501
Merge branch 'main' into bounds-propagation
nguidotti f7a38bc
fix compilation error
nguidotti 649c8ca
Update cpp/src/dual_simplex/mip_node.hpp
nguidotti 4afb71d
addresses reviewer's comments
nguidotti 1c92230
fix style
nguidotti 7d3f392
Merge branch 'main' into bounds-propagation
nguidotti b285251
Merge branch 'dual-simplex-update-basis' into reuse-basis-factorization
nguidotti a98c792
Merge branch 'bounds-propagation' into reuse-basis-factorization
nguidotti bbd410b
small refactor. added missing recompute flag to diving.
nguidotti 8c420c4
small fixes
nguidotti 5bd70e1
small fix
nguidotti 56747e5
fix logs
nguidotti c7facc6
fixed log
nguidotti e26c8d7
Merge branch 'bounds-propagation' into reuse-basis-factorization
nguidotti 28f55f0
fixed bugs after merge
nguidotti eae9ca5
fixed crash due to incorrect nonbasic list
nguidotti 382cbe8
fixed diving queue. added asserts.
nguidotti 8c2e06e
fixed typo
nguidotti 12eb12d
minimize code changes
nguidotti a1caad8
reserve memory space for nonbasic list
nguidotti 8b294be
fixed starting bounds for diving nodes. replaced rounding direction w…
nguidotti 27f9264
fix missing enum
nguidotti bf142d5
fixed typo
nguidotti 65981b4
Merge branch 'main' into bounds-propagation
nguidotti 6f77e1d
Merge branch 'main' into bounds-propagation
nguidotti 64d2744
Merge branch 'main' into reuse-basis-factorization
nguidotti b8e295d
adjusting refactor frequency
nguidotti 8a00e93
removed debug leftover
nguidotti 5b23554
fixed incorrect reporting of an infeasible solution due to timeout
nguidotti 42e583c
Merge branch 'main' into reuse-basis-factorization
nguidotti 971380c
Merge branch 'main' into reuse-basis-factorization
nguidotti 40b2bc9
fix compilation issue for solve_MIP. removed debug leftover
nguidotti d891e59
Merge branch 'fix-compilation-issue' into reuse-basis-factorization
nguidotti 28ad838
fixed incorrect infeasibility report
nguidotti 6830648
Merge branch 'main' into reuse-basis-factorization
nguidotti de4e292
Merge branch 'main' into bounds-propagation
nguidotti cc89861
renamed files and classes
nguidotti 9951513
Merge branch 'main' into reuse-basis-factorization
nguidotti f152b25
Merge branch 'bounds-propagation' into reuse-basis-factorization
nguidotti 874d45f
fix incorrect report of infeasible solution
nguidotti 5bfda56
fixed typo
nguidotti d71f910
Merge branch 'bounds-propagation' into reuse-basis-factorization
nguidotti 04e3063
fixed typo
nguidotti 2d79969
Merge branch 'bounds-propagation' into reuse-basis-factorization
nguidotti 093c7a7
Merge branch 'main' into reuse-basis-factorization
nguidotti d94220b
rename variable
nguidotti 9e6ae78
improved selection of the starting node for diving using the pseudoco…
nguidotti ba94749
adressing reviewer's comments
nguidotti 899d2d3
small refactor
nguidotti 2306ab8
revert refactor frequency change
nguidotti 1b05911
Merge branch 'main' into reuse-basis-factorization
nguidotti be79001
Merge branch 'reuse-basis-factorization' into pseudocost-estimate
nguidotti f2dcedf
simplifying code
nguidotti f074902
added iteration limit for diving. limited backtracking to 1.
nguidotti 466b7bd
revert log changes
nguidotti 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
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,297 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| #include <dual_simplex/bounds_strengthening.hpp> | ||
|
|
||
| #include <algorithm> | ||
| #include <cmath> | ||
|
|
||
| namespace cuopt::linear_programming::dual_simplex { | ||
|
|
||
| template <typename f_t> | ||
| static inline f_t update_lb(f_t curr_lb, f_t coeff, f_t delta_min_act, f_t delta_max_act) | ||
| { | ||
| auto comp_bnd = (coeff < 0.) ? delta_min_act / coeff : delta_max_act / coeff; | ||
| return std::max(curr_lb, comp_bnd); | ||
| } | ||
|
|
||
| template <typename f_t> | ||
| static inline f_t update_ub(f_t curr_ub, f_t coeff, f_t delta_min_act, f_t delta_max_act) | ||
| { | ||
| auto comp_bnd = (coeff < 0.) ? delta_max_act / coeff : delta_min_act / coeff; | ||
| return std::min(curr_ub, comp_bnd); | ||
| } | ||
|
|
||
| template <typename i_t, typename f_t> | ||
| static inline bool check_infeasibility(f_t min_a, f_t max_a, f_t cnst_lb, f_t cnst_ub, f_t eps) | ||
| { | ||
| return (min_a > cnst_ub + eps) || (max_a < cnst_lb - eps); | ||
| } | ||
|
|
||
| #define DEBUG_BOUND_STRENGTHENING 0 | ||
|
|
||
| template <typename i_t, typename f_t> | ||
| void print_bounds_stats(const std::vector<f_t>& lower, | ||
| const std::vector<f_t>& upper, | ||
| const simplex_solver_settings_t<i_t, f_t>& settings, | ||
| const std::string msg) | ||
| { | ||
| #if DEBUG_BOUND_STRENGTHENING | ||
| f_t lb_norm = 0.0; | ||
| f_t ub_norm = 0.0; | ||
|
|
||
| i_t sz = lower.size(); | ||
| for (i_t i = 0; i < sz; ++i) { | ||
| if (std::isfinite(lower[i])) { lb_norm += abs(lower[i]); } | ||
| if (std::isfinite(upper[i])) { ub_norm += abs(upper[i]); } | ||
| } | ||
| settings.log.printf("%s :: lb norm %e, ub norm %e\n", msg.c_str(), lb_norm, ub_norm); | ||
| #endif | ||
| } | ||
|
|
||
| template <typename i_t, typename f_t> | ||
| bounds_strengthening_t<i_t, f_t>::bounds_strengthening_t( | ||
| const lp_problem_t<i_t, f_t>& problem, | ||
| const csr_matrix_t<i_t, f_t>& Arow, | ||
| const std::vector<char>& row_sense, | ||
| const std::vector<variable_type_t>& var_types) | ||
| : bounds_changed(problem.num_cols, false), | ||
| A(problem.A), | ||
| Arow(Arow), | ||
| var_types(var_types), | ||
| delta_min_activity(problem.num_rows), | ||
| delta_max_activity(problem.num_rows), | ||
| constraint_lb(problem.num_rows), | ||
| constraint_ub(problem.num_rows) | ||
| { | ||
| const bool is_row_sense_empty = row_sense.empty(); | ||
| if (is_row_sense_empty) { | ||
| std::copy(problem.rhs.begin(), problem.rhs.end(), constraint_lb.begin()); | ||
| std::copy(problem.rhs.begin(), problem.rhs.end(), constraint_ub.begin()); | ||
| } else { | ||
| // Set the constraint bounds | ||
| for (i_t i = 0; i < problem.num_rows; ++i) { | ||
| if (row_sense[i] == 'E') { | ||
| constraint_lb[i] = problem.rhs[i]; | ||
| constraint_ub[i] = problem.rhs[i]; | ||
| } else if (row_sense[i] == 'L') { | ||
| constraint_ub[i] = problem.rhs[i]; | ||
| constraint_lb[i] = -inf; | ||
| } else { | ||
| constraint_lb[i] = problem.rhs[i]; | ||
| constraint_ub[i] = inf; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| template <typename i_t, typename f_t> | ||
| bool bounds_strengthening_t<i_t, f_t>::bounds_strengthening( | ||
| std::vector<f_t>& lower_bounds, | ||
| std::vector<f_t>& upper_bounds, | ||
| const simplex_solver_settings_t<i_t, f_t>& settings) | ||
| { | ||
| const i_t m = A.m; | ||
| const i_t n = A.n; | ||
|
|
||
| constraint_changed.assign(m, false); | ||
| variable_changed.assign(n, false); | ||
| constraint_changed_next.assign(m, false); | ||
|
|
||
| for (i_t i = 0; i < bounds_changed.size(); ++i) { | ||
| if (bounds_changed[i]) { | ||
| const i_t row_start = A.col_start[i]; | ||
| const i_t row_end = A.col_start[i + 1]; | ||
| for (i_t p = row_start; p < row_end; ++p) { | ||
| const i_t j = A.i[p]; | ||
| constraint_changed[j] = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| lower = lower_bounds; | ||
| upper = upper_bounds; | ||
| print_bounds_stats(lower, upper, settings, "Initial bounds"); | ||
|
|
||
| i_t iter = 0; | ||
| const i_t iter_limit = 10; | ||
| while (iter < iter_limit) { | ||
| for (i_t i = 0; i < m; ++i) { | ||
| if (!constraint_changed[i]) { continue; } | ||
| const i_t row_start = Arow.row_start[i]; | ||
| const i_t row_end = Arow.row_start[i + 1]; | ||
|
|
||
| f_t min_a = 0.0; | ||
| f_t max_a = 0.0; | ||
| for (i_t p = row_start; p < row_end; ++p) { | ||
| const i_t j = Arow.j[p]; | ||
| const f_t a_ij = Arow.x[p]; | ||
|
|
||
| variable_changed[j] = true; | ||
| if (a_ij > 0) { | ||
| min_a += a_ij * lower[j]; | ||
| max_a += a_ij * upper[j]; | ||
| } else if (a_ij < 0) { | ||
| min_a += a_ij * upper[j]; | ||
| max_a += a_ij * lower[j]; | ||
| } | ||
| if (upper[j] == inf && a_ij > 0) { max_a = inf; } | ||
| if (lower[j] == -inf && a_ij < 0) { max_a = inf; } | ||
|
|
||
| if (lower[j] == -inf && a_ij > 0) { min_a = -inf; } | ||
| if (upper[j] == inf && a_ij < 0) { min_a = -inf; } | ||
| } | ||
|
|
||
| f_t cnst_lb = constraint_lb[i]; | ||
| f_t cnst_ub = constraint_ub[i]; | ||
| bool is_infeasible = | ||
| check_infeasibility<i_t, f_t>(min_a, max_a, cnst_lb, cnst_ub, settings.primal_tol); | ||
| if (is_infeasible) { | ||
| settings.log.printf( | ||
| "Iter:: %d, Infeasible constraint %d, cnst_lb %e, cnst_ub %e, min_a %e, max_a %e\n", | ||
| iter, | ||
| i, | ||
| cnst_lb, | ||
| cnst_ub, | ||
| min_a, | ||
| max_a); | ||
| return false; | ||
| } | ||
|
|
||
| delta_min_activity[i] = cnst_ub - min_a; | ||
| delta_max_activity[i] = cnst_lb - max_a; | ||
| } | ||
|
|
||
| i_t num_bounds_changed = 0; | ||
|
|
||
| for (i_t k = 0; k < n; ++k) { | ||
| if (!variable_changed[k]) { continue; } | ||
| f_t old_lb = lower[k]; | ||
| f_t old_ub = upper[k]; | ||
|
|
||
| f_t new_lb = old_lb; | ||
| f_t new_ub = old_ub; | ||
|
|
||
| const i_t row_start = A.col_start[k]; | ||
| const i_t row_end = A.col_start[k + 1]; | ||
| for (i_t p = row_start; p < row_end; ++p) { | ||
| const i_t i = A.i[p]; | ||
|
|
||
| if (!constraint_changed[i]) { continue; } | ||
| const f_t a_ik = A.x[p]; | ||
|
|
||
| f_t delta_min_act = delta_min_activity[i]; | ||
| f_t delta_max_act = delta_max_activity[i]; | ||
|
|
||
| delta_min_act += (a_ik < 0) ? a_ik * old_ub : a_ik * old_lb; | ||
| delta_max_act += (a_ik > 0) ? a_ik * old_ub : a_ik * old_lb; | ||
|
|
||
| new_lb = std::max(new_lb, update_lb(old_lb, a_ik, delta_min_act, delta_max_act)); | ||
| new_ub = std::min(new_ub, update_ub(old_ub, a_ik, delta_min_act, delta_max_act)); | ||
| } | ||
|
|
||
| // Integer rounding | ||
| if (!var_types.empty() && | ||
| (var_types[k] == variable_type_t::INTEGER || var_types[k] == variable_type_t::BINARY)) { | ||
| new_lb = std::ceil(new_lb - settings.integer_tol); | ||
| new_ub = std::floor(new_ub + settings.integer_tol); | ||
| } | ||
|
|
||
| bool lb_updated = std::abs(new_lb - old_lb) > 1e3 * settings.primal_tol; | ||
| bool ub_updated = std::abs(new_ub - old_ub) > 1e3 * settings.primal_tol; | ||
|
|
||
| new_lb = std::max(new_lb, lower_bounds[k]); | ||
| new_ub = std::min(new_ub, upper_bounds[k]); | ||
|
|
||
| if (new_lb > new_ub + 1e-6) { | ||
| settings.log.printf( | ||
| "Iter:: %d, Infeasible variable after update %d, %e > %e\n", iter, k, new_lb, new_ub); | ||
| return false; | ||
| } | ||
| if (new_lb != old_lb || new_ub != old_ub) { | ||
| for (i_t p = row_start; p < row_end; ++p) { | ||
| const i_t i = A.i[p]; | ||
| constraint_changed_next[i] = true; | ||
| } | ||
| } | ||
|
|
||
| lower[k] = std::min(new_lb, new_ub); | ||
| upper[k] = std::max(new_lb, new_ub); | ||
|
|
||
| bool bounds_changed = lb_updated || ub_updated; | ||
| if (bounds_changed) { num_bounds_changed++; } | ||
| } | ||
|
|
||
| if (num_bounds_changed == 0) { break; } | ||
|
|
||
| std::swap(constraint_changed, constraint_changed_next); | ||
| std::fill(constraint_changed_next.begin(), constraint_changed_next.end(), false); | ||
| std::fill(variable_changed.begin(), variable_changed.end(), false); | ||
|
|
||
| iter++; | ||
| } | ||
|
|
||
| // settings.log.printf("Total strengthened variables %d\n", total_strengthened_variables); | ||
|
|
||
| #if DEBUG_BOUND_STRENGTHENING | ||
| f_t lb_change = 0.0; | ||
| f_t ub_change = 0.0; | ||
| int num_lb_changed = 0; | ||
| int num_ub_changed = 0; | ||
|
|
||
| for (i_t i = 0; i < n; ++i) { | ||
| if (lower[i] > problem.lower[i] + settings.primal_tol || | ||
| (!std::isfinite(problem.lower[i]) && std::isfinite(lower[i]))) { | ||
| num_lb_changed++; | ||
| lb_change += | ||
| std::isfinite(problem.lower[i]) | ||
| ? (lower[i] - problem.lower[i]) / (1e-6 + std::max(abs(lower[i]), abs(problem.lower[i]))) | ||
| : 1.0; | ||
| } | ||
| if (upper[i] < problem.upper[i] - settings.primal_tol || | ||
| (!std::isfinite(problem.upper[i]) && std::isfinite(upper[i]))) { | ||
| num_ub_changed++; | ||
| ub_change += | ||
| std::isfinite(problem.upper[i]) | ||
| ? (problem.upper[i] - upper[i]) / (1e-6 + std::max(abs(problem.upper[i]), abs(upper[i]))) | ||
| : 1.0; | ||
| } | ||
| } | ||
|
|
||
| if (num_lb_changed > 0 || num_ub_changed > 0) { | ||
| settings.log.printf( | ||
| "lb change %e, ub change %e, num lb changed %d, num ub changed %d, iter %d\n", | ||
| 100 * lb_change / std::max(1, num_lb_changed), | ||
| 100 * ub_change / std::max(1, num_ub_changed), | ||
| num_lb_changed, | ||
| num_ub_changed, | ||
| iter); | ||
| } | ||
| print_bounds_stats(lower, upper, settings, "Final bounds"); | ||
| #endif | ||
|
|
||
| lower_bounds = lower; | ||
| upper_bounds = upper; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE | ||
| template class bounds_strengthening_t<int, double>; | ||
| #endif | ||
|
|
||
| } // namespace cuopt::linear_programming::dual_simplex | ||
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,56 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <dual_simplex/presolve.hpp> | ||
|
|
||
| namespace cuopt::linear_programming::dual_simplex { | ||
|
|
||
| template <typename i_t, typename f_t> | ||
| class bounds_strengthening_t { | ||
| public: | ||
| // For pure LP bounds strengthening, var_types should be defaulted (i.e. left empty) | ||
| bounds_strengthening_t(const lp_problem_t<i_t, f_t>& problem, | ||
| const csr_matrix_t<i_t, f_t>& Arow, | ||
| const std::vector<char>& row_sense, | ||
| const std::vector<variable_type_t>& var_types); | ||
|
|
||
| bool bounds_strengthening(std::vector<f_t>& lower_bounds, | ||
| std::vector<f_t>& upper_bounds, | ||
| const simplex_solver_settings_t<i_t, f_t>& settings); | ||
|
|
||
| std::vector<bool> bounds_changed; | ||
|
|
||
| private: | ||
| const csc_matrix_t<i_t, f_t>& A; | ||
| const csr_matrix_t<i_t, f_t>& Arow; | ||
| const std::vector<variable_type_t>& var_types; | ||
|
|
||
| std::vector<bool> constraint_changed; | ||
| std::vector<bool> variable_changed; | ||
| std::vector<bool> constraint_changed_next; | ||
|
|
||
| std::vector<f_t> lower; | ||
| std::vector<f_t> upper; | ||
|
|
||
| std::vector<f_t> delta_min_activity; | ||
| std::vector<f_t> delta_max_activity; | ||
| std::vector<f_t> constraint_lb; | ||
| std::vector<f_t> constraint_ub; | ||
| }; | ||
| } // namespace cuopt::linear_programming::dual_simplex |
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.