Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
highs_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14// Unimplemented features:
15// * Quadratic objective
16// * TODO(b/272767311): initial basis, more precise returned basis.
17// * Starting solution
18// * TODO(b/271104776): Returning rays
19
21
22#include <algorithm>
23#include <cmath>
24#include <cstdint>
25#include <cstdlib>
26#include <limits>
27#include <memory>
28#include <optional>
29#include <string>
30#include <utility>
31#include <vector>
32
33#include "Highs.h"
34#include "absl/algorithm/container.h"
35#include "absl/cleanup/cleanup.h"
36#include "absl/container/flat_hash_map.h"
37#include "absl/log/check.h"
38#include "absl/memory/memory.h"
39#include "absl/status/status.h"
40#include "absl/status/statusor.h"
41#include "absl/strings/string_view.h"
42#include "absl/time/clock.h"
43#include "absl/time/time.h"
44#include "io/HighsIO.h"
45#include "lp_data/HConst.h"
46#include "lp_data/HStruct.h"
47#include "lp_data/HighsInfo.h"
48#include "lp_data/HighsLp.h"
49#include "lp_data/HighsModelUtils.h"
50#include "lp_data/HighsOptions.h"
51#include "lp_data/HighsStatus.h"
52#include "model/HighsModel.h"
62#include "ortools/math_opt/parameters.pb.h"
63#include "ortools/math_opt/result.pb.h"
64#include "ortools/math_opt/solution.pb.h"
65#include "ortools/math_opt/solvers/highs.pb.h"
69#include "simplex/SimplexConst.h"
70#include "util/HighsInt.h"
71
73namespace {
74
75constexpr absl::string_view kOutputFlag = "output_flag";
76constexpr absl::string_view kLogToConsole = "log_to_console";
77
78constexpr SupportedProblemStructures kHighsSupportedStructures = {
79 .integer_variables = SupportType::kSupported,
80 .quadratic_objectives = SupportType::kNotImplemented};
81
82absl::Status ToStatus(const HighsStatus status) {
83 switch (status) {
84 case HighsStatus::kOk:
85 return absl::OkStatus();
86 case HighsStatus::kWarning:
87 // There doesn't seem to be much we can do with this beyond ignoring it,
88 // which does not seem best. Highs returns a warning when you solve and
89 // don't get a primal feasible solution, but MathOpt does not consider
90 // this to be warning worthy.
91 return absl::OkStatus();
92 case HighsStatus::kError:
93 return util::InternalErrorBuilder() << "HighsStatus: kError";
94 default:
96 << "unexpected HighsStatus: " << static_cast<int>(status);
97 }
98}
99
100absl::Status ToStatus(const OptionStatus option_status) {
101 switch (option_status) {
102 case OptionStatus::kOk:
103 return absl::OkStatus();
104 case OptionStatus::kUnknownOption:
105 return absl::InvalidArgumentError("option name was unknown");
106 case OptionStatus::kIllegalValue:
107 // NOTE: highs returns this if the option type is wrong or if the value
108 // is out of bounds for the option.
109 return absl::InvalidArgumentError("option value not valid for name");
110 }
112 << "unexpected option_status: " << static_cast<int>(option_status);
113}
114
115absl::StatusOr<int> SafeIntCast(const int64_t i, const absl::string_view name) {
116 if constexpr (sizeof(int) >= sizeof(int64_t)) {
117 return static_cast<int>(i);
118 } else {
119 const int64_t kMin = static_cast<int64_t>(std::numeric_limits<int>::min());
120 const int64_t kMax = static_cast<int64_t>(std::numeric_limits<int>::max());
121 if (i < kMin || i > kMax) {
123 << name << " has value " << i
124 << " not representable as an int (the range [" << kMin << ", "
125 << kMax << "]) and thus is not supported for HiGHS";
126 }
127 return static_cast<int>(i);
128 }
129}
130
131template <typename T>
132int64_t CastInt64StaticAssert(const T value) {
133 static_assert(std::is_integral_v<T>);
134 static_assert(sizeof(T) <= sizeof(int64_t));
135 return static_cast<int64_t>(value);
136}
137
138// Note: the highs solver has very little documentation, but you can find some
139// here https://www.gams.com/latest/docs/S_HIGHS.html.
140absl::StatusOr<std::unique_ptr<HighsOptions>> MakeOptions(
141 const SolveParametersProto& parameters, const bool has_log_callback,
142 const bool is_integer) {
143 // Copy/move seem to be broken for HighsOptions, asan errors.
144 auto result = std::make_unique<HighsOptions>();
145
146 if (parameters.highs().bool_options().contains(kOutputFlag)) {
147 result->output_flag = parameters.highs().bool_options().at(kOutputFlag);
148 } else {
149 result->output_flag = parameters.enable_output() || has_log_callback;
150 }
151 // This feature of highs is pretty confusing/surprising. To use a callback,
152 // you need log_to_console to be true. From this line:
153 // https://github.com/ERGO-Code/HiGHS/blob/master/src/io/HighsIO.cpp#L101
154 // we see that if log_to_console is false and log_file_stream are null, we get
155 // no logging at all.
156 //
157 // Further, when the callback is set, we won't log to console anyway. But from
158 // the names it seems like it should be
159 // result.log_to_console = parameters.enable_output() && !has_log_callback;
160 if (parameters.highs().bool_options().contains(kLogToConsole)) {
161 result->log_to_console =
162 parameters.highs().bool_options().at(kLogToConsole);
163 } else {
164 result->log_to_console = result->output_flag;
165 }
166 if (parameters.has_time_limit()) {
168 const absl::Duration time_limit,
169 util_time::DecodeGoogleApiProto(parameters.time_limit()),
170 _ << "invalid time_limit value for HiGHS.");
171 result->time_limit = absl::ToDoubleSeconds(time_limit);
172 }
173 if (parameters.has_iteration_limit()) {
174 if (is_integer) {
176 << "iteration_limit not supported for HiGHS on problems with "
177 "integer variables";
178 }
180 const int iter_limit,
181 SafeIntCast(parameters.iteration_limit(), "iteration_limit"));
182
183 result->simplex_iteration_limit = iter_limit;
184 result->ipm_iteration_limit = iter_limit;
185 }
186 if (parameters.has_node_limit()) {
187 ASSIGN_OR_RETURN(result->mip_max_nodes,
188 SafeIntCast(parameters.node_limit(), "node_limit"));
189 }
190 if (parameters.has_cutoff_limit()) {
191 // TODO(b/271606858) : It may be possible to get this working for IPs via
192 // objective_bound. For LPs this approach will not work.
193 return absl::InvalidArgumentError("cutoff_limit not supported for HiGHS");
194 }
195 if (parameters.has_objective_limit()) {
196 if (is_integer) {
198 << "objective_limit not supported for HiGHS solver on integer "
199 "problems.";
200 } else {
201 // TODO(b/271616762): it appears that HiGHS intended to support this case
202 // but that it is just broken, we should set result.objective_target.
203 return absl::InvalidArgumentError(
204 "objective_limit for LP appears to have a missing/broken HiGHS "
205 "implementation, see b/271616762");
206 }
207 }
208 if (parameters.has_best_bound_limit()) {
209 if (is_integer) {
211 << "best_bound_limit not supported for HiGHS solver on integer "
212 "problems.";
213 } else {
214 result->objective_bound = parameters.best_bound_limit();
215 }
216 }
217 if (parameters.has_solution_limit()) {
218 result->mip_max_improving_sols = parameters.solution_limit();
219 }
220 if (parameters.has_threads()) {
221 // Do not assign result.threads = parameters.threads() here, this is
222 // requires global synchronization. See
223 // cs/highs/src/lp_data/Highs.cpp:607
225 << "threads not supported for HiGHS solver, this must be set using "
226 "globals, see HiGHS documentation";
227 }
228 if (parameters.has_random_seed()) {
229 result->random_seed = parameters.random_seed();
230 }
231 if (parameters.has_absolute_gap_tolerance()) {
232 result->mip_abs_gap = parameters.absolute_gap_tolerance();
233 }
234 if (parameters.has_relative_gap_tolerance()) {
235 result->mip_rel_gap = parameters.relative_gap_tolerance();
236 }
237 if (parameters.has_solution_pool_size()) {
239 << "solution_pool_size not supported for HiGHS";
240 }
241 if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
242 if (is_integer) {
244 << "lp_algorithm is not supported for HiGHS on problems with "
245 "integer variables";
246 }
247 switch (parameters.lp_algorithm()) {
248 case LP_ALGORITHM_PRIMAL_SIMPLEX:
249 result->solver = ::kSimplexString;
250 result->simplex_strategy = ::kSimplexStrategyPrimal;
251 break;
252 case LP_ALGORITHM_DUAL_SIMPLEX:
253 result->solver = ::kSimplexString;
254 result->simplex_strategy = ::kSimplexStrategyDual;
255 break;
256 case LP_ALGORITHM_BARRIER:
257 result->solver = ::kIpmString;
258 break;
259 default:
261 << "unsupported lp_algorithm: "
262 << LPAlgorithmProto_Name(parameters.lp_algorithm());
263 }
264 }
265 if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
266 if (parameters.presolve() == EMPHASIS_OFF) {
267 result->presolve = ::kHighsOffString;
268 } else {
269 result->presolve = ::kHighsOnString;
270 }
271 }
272 if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
274 << "cuts solve parameter unsupported for HiGHS";
275 }
276 if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
277 switch (parameters.heuristics()) {
278 case EMPHASIS_OFF:
279 result->mip_heuristic_effort = 0.0;
280 break;
281 case EMPHASIS_LOW:
282 result->mip_heuristic_effort = 0.025;
283 break;
284 case EMPHASIS_MEDIUM:
285 result->mip_heuristic_effort = 0.05;
286 break;
287 case EMPHASIS_HIGH:
288 result->mip_heuristic_effort = 0.1;
289 break;
290 case EMPHASIS_VERY_HIGH:
291 result->mip_heuristic_effort = 0.2;
292 break;
293 default:
295 << "unexpected value for solve_parameters.heuristics of: "
296 << parameters.heuristics();
297 }
298 }
299 if (parameters.scaling() != EMPHASIS_UNSPECIFIED) {
300 // Maybe we can do better here? Not clear how highs scaling works
301 if (parameters.scaling() == EMPHASIS_OFF) {
302 result->simplex_scale_strategy = ::kSimplexScaleStrategyOff;
303 }
304 }
305 for (const auto& [name, value] : parameters.highs().string_options()) {
306 if (name == kOutputFlag || name == kLogToConsole) {
307 // This case was handled specially above. We need to do the output
308 // parameters first, as we don't want extra logging while setting options.
309 continue;
310 }
311 RETURN_IF_ERROR(ToStatus(setLocalOptionValue(result->log_options, name,
312 result->log_options,
313 result->records, value)))
314 << "error setting string option name: " << name
315 << " to value:" << value;
316 }
317 for (const auto& [name, value] : parameters.highs().double_options()) {
318 RETURN_IF_ERROR(ToStatus(
319 setLocalOptionValue(result->log_options, name, result->records, value)))
320 << "error setting double option name: " << name
321 << " to value:" << value;
322 }
323 for (const auto& [name, value] : parameters.highs().int_options()) {
324 RETURN_IF_ERROR(ToStatus(
325 setLocalOptionValue(result->log_options, name, result->records, value)))
326 << "error setting int option name: " << name << " to value:" << value;
327 }
328 for (const auto& [name, value] : parameters.highs().bool_options()) {
329 RETURN_IF_ERROR(ToStatus(
330 setLocalOptionValue(result->log_options, name, result->records, value)))
331 << "error setting bool option name: " << name << " to value:" << value;
332 }
333 return result;
334}
335
336double DualObjective(const HighsInfo& highs_info, const bool is_integer) {
337 // TODO(b/290359402): for is_integer = false, consider computing the objective
338 // of a returned dual feasible solution instead.
339 return is_integer ? highs_info.mip_dual_bound
340 : highs_info.objective_function_value;
341}
342// Note that this is the expected/required function signature for highs logging
343// callbacks as set with Highs::setLogCallback().
344void HighsLogCallback(HighsLogType, const char* const message,
345 void* const log_callback_data) {
346 BufferedMessageCallback& buffered_callback =
347 *static_cast<BufferedMessageCallback*>(log_callback_data);
348 buffered_callback.OnMessage(message);
349}
350
351// highs_info must be valid. Does not fill in solve time.
352absl::StatusOr<SolveStatsProto> ToSolveStats(const HighsInfo& highs_info) {
353 SolveStatsProto result;
354 // HiGHS does to not report simplex and barrier count for mip. There is no
355 // way to extract it, as it is held in
356 // HighsMipSolver.mipdata_.total_lp_iterations, but the HighsMipSolver
357 // object is created and destroyed within a single call to Highs.run() here:
358 // https://github.com/ERGO-Code/HiGHS/blob/master/src/lp_data/Highs.cpp#L2976
359 result.set_simplex_iterations(std::max(
360 int64_t{0}, CastInt64StaticAssert(highs_info.simplex_iteration_count)));
361 result.set_barrier_iterations(std::max(
362 int64_t{0}, CastInt64StaticAssert(highs_info.ipm_iteration_count)));
363 result.set_node_count(std::max(int64_t{0}, highs_info.mip_node_count));
364 return result;
365}
366
367// Returns nullopt for nonbasic variables when the upper/lower status is not
368// known.
369absl::StatusOr<std::optional<BasisStatusProto>> ToBasisStatus(
370 const HighsBasisStatus highs_basis, const double lb, const double ub,
371 const std::optional<double> value) {
372 switch (highs_basis) {
373 case HighsBasisStatus::kBasic:
374 return BASIS_STATUS_BASIC;
375 case HighsBasisStatus::kUpper:
376 return BASIS_STATUS_AT_UPPER_BOUND;
377 case HighsBasisStatus::kLower:
378 // Note: highs returns lower for fixed.
379 // https://github.com/ERGO-Code/HiGHS/blob/master/src/lp_data/HConst.h#L192
380 // TODO(b/272767311): investigate returning fixed instead.
381 return BASIS_STATUS_AT_LOWER_BOUND;
382 case HighsBasisStatus::kZero:
383 return BASIS_STATUS_FREE;
384 // TODO(b/272767311): this can potentially be simplified/deleted, we need
385 // to see if HiGHS will ever return kNonbasic/decide if we want to support
386 // kNonbasic as part of the mathopt starting basis API.
387 case HighsBasisStatus::kNonbasic: {
388 const bool lb_finite = std::isfinite(lb);
389 const bool ub_finite = std::isfinite(ub);
390 // TODO(b/272767311): it would be better if this was configurable, use a
391 // small/conservative value for now (if it fails, we fail to return a
392 // basis).
393 constexpr double kAtBoundTolerance = 1.0e-10;
394 if (lb_finite && ub_finite) {
395 if (lb == ub) {
396 return BASIS_STATUS_FIXED_VALUE;
397 } else if (value.has_value() &&
398 std::abs(lb - *value) < kAtBoundTolerance) {
399 return BASIS_STATUS_AT_LOWER_BOUND;
400 } else if (value.has_value() &&
401 std::abs(ub - *value) < kAtBoundTolerance) {
402 return BASIS_STATUS_AT_UPPER_BOUND;
403 }
404 // We cannot infer if we are at upper or at lower. Mathopt does not
405 // an encoding for nonbasic but unknown upper/lower, see b/272767311.
406 return std::nullopt;
407 } else if (lb_finite) {
408 return BASIS_STATUS_AT_LOWER_BOUND;
409 } else if (ub_finite) {
410 return BASIS_STATUS_AT_LOWER_BOUND;
411 } else {
412 return BASIS_STATUS_FREE;
413 }
414 }
415 }
417 << "unexpected highs basis: " << static_cast<int>(highs_basis);
418}
419
420absl::StatusOr<SolutionStatusProto> ToSolutionStatus(
421 const HighsInt highs_solution_status) {
422 switch (highs_solution_status) {
423 case ::kSolutionStatusInfeasible:
424 return SOLUTION_STATUS_INFEASIBLE;
425 case ::kSolutionStatusFeasible:
426 return SOLUTION_STATUS_FEASIBLE;
427 case ::kSolutionStatusNone:
428 return SOLUTION_STATUS_UNDETERMINED;
429 }
431 << "unimplemented highs SolutionStatus: " << highs_solution_status;
432}
433
434} // namespace
435
436absl::StatusOr<FeasibilityStatusProto> HighsSolver::DualFeasibilityStatus(
437 const HighsInfo& highs_info, const bool is_integer,
438 const SolutionClaims solution_claims) {
439 const bool dual_feasible_solution_exists =
440 solution_claims.highs_returned_dual_feasible_solution ||
441 (is_integer && std::isfinite(highs_info.mip_dual_bound));
442 if (dual_feasible_solution_exists &&
443 solution_claims.highs_returned_primal_ray) {
445 << "Found dual feasible solution and primal ray";
446 }
447 if (dual_feasible_solution_exists) {
448 return FEASIBILITY_STATUS_FEASIBLE;
449 }
450 if (solution_claims.highs_returned_primal_ray) {
451 return FEASIBILITY_STATUS_INFEASIBLE;
452 }
453 return FEASIBILITY_STATUS_UNDETERMINED;
454}
455
456absl::StatusOr<FeasibilityStatusProto> HighsSolver::PrimalFeasibilityStatus(
457 const SolutionClaims solution_claims) {
458 if (solution_claims.highs_returned_primal_feasible_solution &&
459 solution_claims.highs_returned_dual_ray) {
461 << "Found primal feasible solution and dual ray";
462 }
463 if (solution_claims.highs_returned_primal_feasible_solution) {
464 return FEASIBILITY_STATUS_FEASIBLE;
465 }
466 if (solution_claims.highs_returned_dual_ray) {
467 return FEASIBILITY_STATUS_INFEASIBLE;
468 }
469 return FEASIBILITY_STATUS_UNDETERMINED;
470}
471
472absl::StatusOr<TerminationProto> HighsSolver::MakeTermination(
473 const HighsModelStatus highs_model_status, const HighsInfo& highs_info,
474 const bool is_integer, const bool had_node_limit,
475 const bool had_solution_limit, const bool is_maximize,
476 const SolutionClaims solution_claims) {
478 const FeasibilityStatusProto dual_feasibility_status,
479 DualFeasibilityStatus(highs_info, is_integer, solution_claims));
480 ASSIGN_OR_RETURN(const FeasibilityStatusProto primal_feasibility_status,
481 PrimalFeasibilityStatus(solution_claims));
482
483 const std::optional<double> optional_finite_primal_objective =
484 (primal_feasibility_status == FEASIBILITY_STATUS_FEASIBLE)
485 ? std::make_optional(highs_info.objective_function_value)
486 : std::nullopt;
487 const std::optional<double> optional_dual_objective =
488 (dual_feasibility_status == FEASIBILITY_STATUS_FEASIBLE)
489 ? std::make_optional(DualObjective(highs_info, is_integer))
490 : std::nullopt;
491 switch (highs_model_status) {
492 case HighsModelStatus::kNotset:
493 case HighsModelStatus::kLoadError:
494 case HighsModelStatus::kModelError:
495 case HighsModelStatus::kPresolveError:
496 case HighsModelStatus::kSolveError:
497 case HighsModelStatus::kPostsolveError:
498 case HighsModelStatus::kUnknown:
499 // Note: we actually deal with kModelEmpty separately in Solve(), this
500 // case should not be hit.
501 case HighsModelStatus::kModelEmpty:
503 << "HighsModelStatus was "
504 << utilModelStatusToString(highs_model_status);
505 case HighsModelStatus::kOptimal: {
506 return OptimalTerminationProto(highs_info.objective_function_value,
507 DualObjective(highs_info, is_integer),
508 "HighsModelStatus is kOptimal");
509 }
510 case HighsModelStatus::kInfeasible:
511 // By convention infeasible MIPs are always dual feasible.
512 return InfeasibleTerminationProto(is_maximize,
513 /*dual_feasibility_status=*/is_integer
514 ? FEASIBILITY_STATUS_FEASIBLE
515 : dual_feasibility_status);
516 case HighsModelStatus::kUnboundedOrInfeasible:
518 is_maximize, dual_feasibility_status,
519 "HighsModelStatus is kUnboundedOrInfeasible");
520 case HighsModelStatus::kUnbounded: {
521 // TODO(b/271104776): we should potentially always return
522 // TERMINATION_REASON_UNBOUNDED instead, we need to determine if
523 // HighsModelStatus::kUnbounded implies the problem is known to be primal
524 // feasible (for LP and MIP).
525 if (highs_info.primal_solution_status == ::kSolutionStatusFeasible) {
526 return UnboundedTerminationProto(is_maximize);
527 } else {
529 is_maximize,
530 /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE,
531 "HighsModelStatus is kUnbounded");
532 }
533 }
534 case HighsModelStatus::kObjectiveBound:
536 is_maximize, LIMIT_OBJECTIVE, optional_finite_primal_objective,
537 optional_dual_objective, "HighsModelStatus is kObjectiveBound");
538 case HighsModelStatus::kObjectiveTarget:
540 is_maximize, LIMIT_OBJECTIVE, optional_finite_primal_objective,
541 optional_dual_objective, "HighsModelStatus is kObjectiveTarget");
542 case HighsModelStatus::kTimeLimit:
543 return LimitTerminationProto(is_maximize, LIMIT_TIME,
544 optional_finite_primal_objective,
545 optional_dual_objective);
546 case HighsModelStatus::kIterationLimit: {
547 if (is_integer) {
548 if (had_node_limit && had_solution_limit) {
550 is_maximize, LIMIT_UNDETERMINED, optional_finite_primal_objective,
551 optional_dual_objective,
552 "Both node limit and solution limit were requested, cannot "
553 "determine reason for termination");
554 } else if (had_node_limit) {
555 return LimitTerminationProto(is_maximize, LIMIT_NODE,
556 optional_finite_primal_objective,
557 optional_dual_objective);
558 } else if (had_solution_limit) {
559 return LimitTerminationProto(is_maximize, LIMIT_SOLUTION,
560 optional_finite_primal_objective,
561 optional_dual_objective);
562 }
563 } else {
564 // For LP, only the MathOpt iteration limit can cause highs to return
565 // HighsModelStatus::kIterationLimit.
566 return LimitTerminationProto(is_maximize, LIMIT_ITERATION,
567 optional_finite_primal_objective,
568 optional_dual_objective);
569 }
570 }
571 }
572 return util::InternalErrorBuilder() << "HighsModelStatus unimplemented: "
573 << static_cast<int>(highs_model_status);
574}
575
576SolveResultProto HighsSolver::ResultForHighsModelStatusModelEmpty(
577 const bool is_maximize, const double objective_offset,
578 const absl::flat_hash_map<int64_t, IndexAndBound>& lin_con_data) {
579 SolveResultProto result;
580 bool feasible = true;
581 for (const auto& [unused, lin_con_bounds] : lin_con_data) {
582 if (lin_con_bounds.lb > 0 || lin_con_bounds.ub < 0) {
583 feasible = false;
584 break;
585 }
586 }
587 result.mutable_termination()->set_reason(
588 feasible ? TERMINATION_REASON_OPTIMAL : TERMINATION_REASON_INFEASIBLE);
589 result.mutable_termination()->set_detail("HighsModelStatus was kEmptyModel");
590 if (feasible) {
591 auto solution = result.add_solutions()->mutable_primal_solution();
592 solution->set_objective_value(objective_offset);
593 solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
594 *result.mutable_termination() =
595 OptimalTerminationProto(objective_offset, objective_offset);
596 } else {
597 // If the primal problem has no variables, the dual problem is unconstrained
598 // and thus always feasible.
599 *result.mutable_termination() =
600 InfeasibleTerminationProto(is_maximize, /*dual_feasibility_status=*/
601 FEASIBILITY_STATUS_FEASIBLE);
602 // It is probably possible to return a ray here as well.
603 }
604 return result;
605}
606
607InvertedBounds HighsSolver::ListInvertedBounds() {
608 const auto find_crossed =
609 [](const absl::flat_hash_map<int64_t, IndexAndBound>& id_to_bound_data) {
610 std::vector<int64_t> result;
611 for (const auto& [id, bound_data] : id_to_bound_data) {
612 if (bound_data.bounds_cross()) {
613 result.push_back(id);
614 }
615 }
616 absl::c_sort(result);
617 return result;
618 };
619 return {.variables = find_crossed(variable_data_),
620 .linear_constraints = find_crossed(lin_con_data_)};
621}
622
623absl::StatusOr<std::optional<BasisProto>> HighsSolver::ExtractBasis() {
624 const HighsInfo& highs_info = highs_->getInfo();
625 const HighsBasis& highs_basis = highs_->getBasis();
626 const HighsSolution& highs_solution = highs_->getSolution();
627 if (highs_info.basis_validity != ::kBasisValidityValid) {
628 return std::nullopt;
629 }
630 // We need the primal/dual solution to try and infer a more precise status
631 // for varaiables and constraints listed as kNonBasic.
632 if (!highs_solution.value_valid || !highs_solution.dual_valid) {
633 return std::nullopt;
634 }
635 // Make sure the solution is the right size
636 RETURN_IF_ERROR(EnsureOneEntryPerVariable(highs_solution.col_value))
637 << "invalid highs_solution.col_value";
638 RETURN_IF_ERROR(EnsureOneEntryPerVariable(highs_solution.col_dual))
639 << "invalid highs_solution.col_dual";
640 // Make sure the basis is the right size
641 RETURN_IF_ERROR(EnsureOneEntryPerVariable(highs_basis.col_status))
642 << "invalid highs_basis.col_status";
643 RETURN_IF_ERROR(EnsureOneEntryPerLinearConstraint(highs_basis.row_status))
644 << "invalid highs_basis.row_status";
645 BasisProto basis;
646
647 if (highs_->getModelStatus() == HighsModelStatus::kOptimal) {
648 basis.set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE);
649 } else if (highs_info.dual_solution_status == kSolutionStatusInfeasible) {
650 basis.set_basic_dual_feasibility(SOLUTION_STATUS_INFEASIBLE);
651 } else {
652 // TODO(b/272767311): we need to do more to fill this in properly.
653 basis.set_basic_dual_feasibility(SOLUTION_STATUS_UNDETERMINED);
654 }
655 for (const int64_t var_id : SortedMapKeys(variable_data_)) {
656 const IndexAndBound& index_and_bounds = variable_data_.at(var_id);
657 const double var_value = highs_solution.col_value[index_and_bounds.index];
659 const std::optional<BasisStatusProto> status,
660 ToBasisStatus(highs_basis.col_status[variable_data_.at(var_id).index],
661 index_and_bounds.lb, index_and_bounds.ub, var_value),
662 _ << "invalid highs_basis.col_status for variable with id: " << var_id);
663 if (!status.has_value()) {
664 return std::nullopt;
665 }
666 basis.mutable_variable_status()->add_ids(var_id);
667 basis.mutable_variable_status()->add_values(*status);
668 }
669 for (const int64_t lin_con_id : SortedMapKeys(lin_con_data_)) {
670 const IndexAndBound& index_and_bounds = lin_con_data_.at(lin_con_id);
671 const double dual_value = highs_solution.row_dual[index_and_bounds.index];
673 const std::optional<BasisStatusProto> status,
674 ToBasisStatus(highs_basis.row_status[index_and_bounds.index],
675 index_and_bounds.lb, index_and_bounds.ub, dual_value),
676 _ << "invalid highs_basis.row_status for linear constraint with id: "
677 << lin_con_id);
678 if (!status.has_value()) {
679 return std::nullopt;
680 }
681 basis.mutable_constraint_status()->add_ids(lin_con_id);
682 basis.mutable_constraint_status()->add_values(*status);
683 }
684 return basis;
685}
686
687absl::StatusOr<bool> HighsSolver::PrimalRayReturned() const {
688 if (!highs_->hasInvert()) {
689 return false;
690 }
691 bool has_primal_ray = false;
692 // Note getPrimalRay may return without modifying has_primal_ray, in which
693 // case it will remain at its default false value.
694 RETURN_IF_ERROR(ToStatus(highs_->getPrimalRay(has_primal_ray,
695 /*primal_ray_value=*/nullptr)));
696 return has_primal_ray;
697}
698
699absl::StatusOr<bool> HighsSolver::DualRayReturned() const {
700 if (!highs_->hasInvert()) {
701 return false;
702 }
703 bool has_dual_ray = false;
704 // Note getPrimalRay may return without modifying has_dual_ray, in which
705 // case it will remain at its default false value.
706 RETURN_IF_ERROR(ToStatus(highs_->getDualRay(has_dual_ray,
707 /*dual_ray_value=*/nullptr)));
708 return has_dual_ray;
709}
710
711absl::StatusOr<HighsSolver::SolutionsAndClaims>
712HighsSolver::ExtractSolutionAndRays(
713 const ModelSolveParametersProto& model_params) {
714 const HighsInfo& highs_info = highs_->getInfo();
715 const HighsSolution& highs_solution = highs_->getSolution();
716 SolutionsAndClaims solution_and_claims;
717 if (highs_info.primal_solution_status == ::kSolutionStatusFeasible &&
718 !highs_solution.value_valid) {
719 return absl::InternalError(
720 "highs_info.primal_solution_status==::kSolutionStatusFeasible, but no "
721 "valid primal solution returned");
722 }
723 if (highs_solution.value_valid || highs_solution.dual_valid) {
724 SolutionProto& solution =
725 solution_and_claims.solutions.emplace_back(SolutionProto());
726 if (highs_solution.value_valid) {
727 RETURN_IF_ERROR(EnsureOneEntryPerVariable(highs_solution.col_value))
728 << "invalid highs_solution.col_value";
729 PrimalSolutionProto& primal_solution =
730 *solution.mutable_primal_solution();
731 primal_solution.set_objective_value(highs_info.objective_function_value);
732 OR_ASSIGN_OR_RETURN3(const SolutionStatusProto primal_solution_status,
733 ToSolutionStatus(highs_info.primal_solution_status),
734 _ << "invalid highs_info.primal_solution_status");
735 primal_solution.set_feasibility_status(primal_solution_status);
736 solution_and_claims.solution_claims
737 .highs_returned_primal_feasible_solution =
738 primal_solution.feasibility_status() == SOLUTION_STATUS_FEASIBLE;
739 for (const int64_t var_id : SortedMapKeys(variable_data_)) {
740 primal_solution.mutable_variable_values()->add_ids(var_id);
741 primal_solution.mutable_variable_values()->add_values(
742 highs_solution.col_value[variable_data_.at(var_id).index]);
743 }
744 }
745 if (highs_solution.dual_valid) {
746 RETURN_IF_ERROR(EnsureOneEntryPerVariable(highs_solution.col_dual))
747 << "invalid highs_solution.col_dual";
749 EnsureOneEntryPerLinearConstraint(highs_solution.row_dual))
750 << "invalid highs_solution.row_dual";
751 DualSolutionProto& dual_solution = *solution.mutable_dual_solution();
752 dual_solution.set_objective_value(highs_info.objective_function_value);
753 OR_ASSIGN_OR_RETURN3(const SolutionStatusProto dual_solution_status,
754 ToSolutionStatus(highs_info.dual_solution_status),
755 _ << "invalid highs_info.dual_solution_status");
756 dual_solution.set_feasibility_status(dual_solution_status);
757 solution_and_claims.solution_claims
758 .highs_returned_dual_feasible_solution =
759 dual_solution.feasibility_status() == SOLUTION_STATUS_FEASIBLE;
760 for (const int64_t var_id : SortedMapKeys(variable_data_)) {
761 dual_solution.mutable_reduced_costs()->add_ids(var_id);
762 dual_solution.mutable_reduced_costs()->add_values(
763 highs_solution.col_dual[variable_data_.at(var_id).index]);
764 }
765 for (const int64_t lin_con_id : SortedMapKeys(lin_con_data_)) {
766 dual_solution.mutable_dual_values()->add_ids(lin_con_id);
767 dual_solution.mutable_dual_values()->add_values(
768 highs_solution.row_dual[lin_con_data_.at(lin_con_id).index]);
769 }
770 }
771 ASSIGN_OR_RETURN(std::optional<BasisProto> basis_proto,
772 HighsSolver::ExtractBasis());
773 if (basis_proto.has_value()) {
774 *solution.mutable_basis() = *std::move(basis_proto);
775 }
776 ApplyAllFilters(model_params, solution);
777 }
778
780 solution_and_claims.solution_claims.highs_returned_primal_ray,
781 PrimalRayReturned());
782 ASSIGN_OR_RETURN(solution_and_claims.solution_claims.highs_returned_dual_ray,
783 DualRayReturned());
784
785 return solution_and_claims;
786}
787
788absl::StatusOr<std::unique_ptr<SolverInterface>> HighsSolver::New(
789 const ModelProto& model, const InitArgs&) {
790 RETURN_IF_ERROR(ModelIsSupported(model, kHighsSupportedStructures, "Highs"));
791 HighsModel highs_model;
792 HighsLp& lp = highs_model.lp_;
793 lp.model_name_ = model.name();
794 lp.objective_name_ = model.objective().name();
795 const int num_vars = model.variables().ids_size();
796 lp.num_col_ = num_vars;
797 // NOTE: HiGHS issues a warning if lp.integrality_ is nonempty but all
798 // variables are continuous. It would be nice to disable this warning, as we
799 // should always just set this, otherwise incrementalism is just more
800 // complicated.
801 //
802 // See
803 // https://github.com/ERGO-Code/HiGHS/blob/master/src/lp_data/HighsLpUtils.cpp#L535
804 bool has_integer_var = false;
805 for (const bool is_integer : model.variables().integers()) {
806 if (is_integer) {
807 has_integer_var = true;
808 break;
809 }
810 }
811
812 absl::flat_hash_map<int64_t, IndexAndBound> variable_data;
813 for (int i = 0; i < num_vars; ++i) {
814 const double raw_lb = model.variables().lower_bounds(i);
815 const double raw_ub = model.variables().upper_bounds(i);
816 const IndexAndBound index_and_bound(/*index=*/i, /*lb=*/raw_lb,
817 /*ub=*/raw_ub,
818 model.variables().integers(i));
819 variable_data.try_emplace(model.variables().ids(i), index_and_bound);
820 lp.col_names_.push_back(
821 model.variables().names_size() > 0 ? model.variables().names(i) : "");
822
823 // If the bounds are crossed, we give an error at solve time (unless they
824 // are uncrossed before the solve begins). Passing crossed bounds to HiGHS
825 // here causes Highs:passModel() below to fail, but we don't want to fail
826 // in New(). So we pass dummy values instead temporarily.
827 // TODO(b/271595607): once HiGHS is updated, check if the unrounded bounds
828 // cross instead.
829 if (index_and_bound.rounded_bounds_cross()) {
830 lp.col_lower_.push_back(0.0);
831 lp.col_upper_.push_back(0.0);
832 } else {
833 // TODO(b/271595607): once HiGHS is updated, pass the original bound, not
834 // the rounded bound.
835 lp.col_lower_.push_back(index_and_bound.rounded_lb());
836 lp.col_upper_.push_back(index_and_bound.rounded_ub());
837 }
838 if (has_integer_var) {
839 lp.integrality_.push_back(model.variables().integers(i)
840 ? HighsVarType::kInteger
841 : HighsVarType::kContinuous);
842 }
843 }
844 lp.offset_ = model.objective().offset();
845 lp.sense_ =
846 model.objective().maximize() ? ObjSense::kMaximize : ObjSense::kMinimize;
847 lp.col_cost_.resize(num_vars);
848 for (const auto [var_id, lin_obj] :
849 MakeView(model.objective().linear_coefficients())) {
850 lp.col_cost_[variable_data.at(var_id).index] = lin_obj;
851 }
852
853 const int num_lin_cons = model.linear_constraints().ids_size();
854 lp.num_row_ = num_lin_cons;
855 absl::flat_hash_map<int64_t, IndexAndBound> lin_con_data;
856 for (int i = 0; i < num_lin_cons; ++i) {
857 const double lb = model.linear_constraints().lower_bounds(i);
858 const double ub = model.linear_constraints().upper_bounds(i);
859 lin_con_data.try_emplace(model.linear_constraints().ids(i),
860 IndexAndBound(/*index=*/i, /*lb=*/lb, /*ub=*/ub,
861 /*is_integer=*/false));
862 lp.row_names_.push_back(model.linear_constraints().names_size() > 0
863 ? model.linear_constraints().names(i)
864 : "");
865 // See comment above for the case when a variable lb > ub, we need to avoid
866 // an immediate error in New().
867 if (lb > ub) {
868 lp.row_lower_.push_back(0.0);
869 lp.row_upper_.push_back(0.0);
870 } else {
871 lp.row_lower_.push_back(lb);
872 lp.row_upper_.push_back(ub);
873 }
874 }
875 lp.a_matrix_.format_ = MatrixFormat::kRowwise;
876 lp.a_matrix_.num_col_ = num_vars;
877 lp.a_matrix_.num_row_ = num_lin_cons;
878 lp.a_matrix_.start_.clear(); // This starts out as {0} by default.
879 const SparseDoubleMatrixProto& lin_con_mat = model.linear_constraint_matrix();
880 int mat_index = 0;
881 for (int highs_con = 0; highs_con < lin_con_data.size(); highs_con++) {
882 lp.a_matrix_.start_.push_back(mat_index);
883 while (mat_index < lin_con_mat.row_ids_size() &&
884 lin_con_data.at(lin_con_mat.row_ids(mat_index)).index <= highs_con) {
885 mat_index++;
886 }
887 }
888 lp.a_matrix_.start_.push_back(lin_con_mat.row_ids_size());
889 for (int i = 0; i < lin_con_mat.row_ids_size(); ++i) {
890 const int var = variable_data.at(lin_con_mat.column_ids(i)).index;
891 const double coef = lin_con_mat.coefficients(i);
892 lp.a_matrix_.index_.push_back(var);
893 lp.a_matrix_.value_.push_back(coef);
894 }
895 auto highs = std::make_unique<Highs>();
896 // Disable output immediately, calling passModel() below will generate output
897 // otherwise.
898 HighsOptions disable_output;
899 disable_output.output_flag = false;
900 disable_output.log_to_console = false;
901 RETURN_IF_ERROR(ToStatus(highs->passOptions(disable_output)));
902 RETURN_IF_ERROR(ToStatus(highs->passModel(std::move(highs_model))));
903 return absl::WrapUnique(new HighsSolver(
904 std::move(highs), std::move(variable_data), std::move(lin_con_data)));
905}
906
907absl::StatusOr<SolveResultProto> HighsSolver::Solve(
908 const SolveParametersProto& parameters,
909 const ModelSolveParametersProto& model_parameters,
910 MessageCallback message_cb, const CallbackRegistrationProto&, Callback,
911 const SolveInterrupter* const) {
913 model_parameters, kHighsSupportedStructures, "Highs"));
914 const absl::Time start = absl::Now();
915 auto set_solve_time = [&start](SolveResultProto& result) -> absl::Status {
916 const absl::Duration solve_time = absl::Now() - start;
917 OR_ASSIGN_OR_RETURN3(*result.mutable_solve_stats()->mutable_solve_time(),
919 _ << "error encoding solve_stats.solve_time");
920 return absl::OkStatus();
921 };
922
923 RETURN_IF_ERROR(ListInvertedBounds().ToStatus());
924 // TODO(b/271595607): delete this code once we upgrade HiGHS, if HiGHS does
925 // return a proper infeasibility status for models with empty integer bounds.
926 const bool is_maximize = highs_->getModel().lp_.sense_ == ObjSense::kMaximize;
927 for (const auto& [var_id, bounds] : variable_data_) {
928 if (bounds.rounded_bounds_cross()) {
929 SolveResultProto result =
930 ResultForIntegerInfeasible(is_maximize, var_id, bounds.lb, bounds.ub);
931 RETURN_IF_ERROR(set_solve_time(result));
932 return result;
933 }
934 }
935
936 BufferedMessageCallback buffered_message_callback(std::move(message_cb));
937 if (buffered_message_callback.has_user_message_callback()) {
938 RETURN_IF_ERROR(ToStatus(
939 highs_->setLogCallback(&HighsLogCallback, &buffered_message_callback)))
940 << "failed to register logging callback";
941 }
942 auto message_cb_cleanup =
943 absl::MakeCleanup([this, &buffered_message_callback]() {
944 if (buffered_message_callback.has_user_message_callback()) {
945 // As of March 6th, 2023, this code never returns an error (see the
946 // HiGHS source). If we really want to be able to recover from errors,
947 // more care is needed, as we need to prevent HiGHS from invoking the
948 // user callback after this function, since it will not be alive (e.g.
949 // wrap the user callback in a new callback that is guarded by an
950 // atomic bool that we disable here). Further, to propagate this
951 // error, we need a class instead of absl::Cleanup.
952 CHECK_OK(ToStatus(highs_->setLogCallback(nullptr, nullptr)));
953 buffered_message_callback.Flush();
954 }
955 });
956
957 bool is_integer = false;
958 // NOTE: lp_.integrality_ may be empty if the problem is an LP.
959 for (const HighsVarType var_type : highs_->getModel().lp_.integrality_) {
960 if (var_type == HighsVarType::kInteger) {
961 is_integer = true;
962 break;
963 }
964 }
965 auto it = parameters.highs().bool_options().find("solve_relaxation");
966 if (it != parameters.highs().bool_options().end() && it->second) {
967 is_integer = false;
968 }
970 const std::unique_ptr<HighsOptions> options,
971 MakeOptions(parameters,
972 buffered_message_callback.has_user_message_callback(),
973 is_integer));
974 RETURN_IF_ERROR(ToStatus(highs_->passOptions(*options)));
975 RETURN_IF_ERROR(ToStatus(highs_->run()));
976 std::move(message_cb_cleanup).Invoke();
977 // When the model is empty, highs_->getInfo() is invalid, so we bail out.
978 if (highs_->getModelStatus() == HighsModelStatus::kModelEmpty) {
979 SolveResultProto result = ResultForHighsModelStatusModelEmpty(
980 is_maximize, highs_->getModel().lp_.offset_, lin_con_data_);
981 RETURN_IF_ERROR(set_solve_time(result));
982 return result;
983 }
984 const HighsInfo& info = highs_->getInfo();
985 if (!info.valid) {
986 return absl::InternalError("HighsInfo not valid");
987 }
988
989 SolveResultProto result;
990 ASSIGN_OR_RETURN(SolutionsAndClaims solutions_and_claims,
991 ExtractSolutionAndRays(model_parameters));
992 for (SolutionProto& solution : solutions_and_claims.solutions) {
993 *result.add_solutions() = std::move(solution);
994 }
995 ASSIGN_OR_RETURN(*result.mutable_termination(),
996 MakeTermination(highs_->getModelStatus(), info, is_integer,
997 parameters.has_node_limit(),
998 parameters.has_solution_limit(), is_maximize,
999 solutions_and_claims.solution_claims));
1000
1001 ASSIGN_OR_RETURN(*result.mutable_solve_stats(), ToSolveStats(info));
1002
1003 RETURN_IF_ERROR(set_solve_time(result));
1004 return result;
1005}
1006
1007absl::StatusOr<bool> HighsSolver::Update(const ModelUpdateProto&) {
1008 return false;
1009}
1010
1011absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
1014 const SolveInterrupter*) {
1015 return absl::UnimplementedError(
1016 "HiGHS does not provide a method to compute an infeasible subsystem");
1017}
1018
1020
1021} // namespace operations_research::math_opt
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
static absl::StatusOr< std::unique_ptr< SolverInterface > > New(const ModelProto &model, const InitArgs &init_args)
absl::StatusOr< SolveResultProto > Solve(const SolveParametersProto &parameters, const ModelSolveParametersProto &model_parameters, MessageCallback message_cb, const CallbackRegistrationProto &callback_registration, Callback cb, const SolveInterrupter *interrupter) override
absl::StatusOr< ComputeInfeasibleSubsystemResultProto > ComputeInfeasibleSubsystem(const SolveParametersProto &parameters, MessageCallback message_cb, const SolveInterrupter *interrupter) override
absl::StatusOr< bool > Update(const ModelUpdateProto &model_update) override
std::function< void(const std::vector< std::string > &)> MessageCallback
std::function< absl::StatusOr< CallbackResultProto >( const CallbackDataProto &)> Callback
time_limit
Definition solve.cc:22
An object oriented wrapper for quadratic constraints in ModelStorage.
Definition gurobi_isv.cc:28
absl::Status ModelIsSupported(const ModelProto &model, const SupportedProblemStructures &support_menu, const absl::string_view solver_name)
absl::Status ModelSolveParametersAreSupported(const ModelSolveParametersProto &model_parameters, const SupportedProblemStructures &support_menu, const absl::string_view solver_name)
TerminationProto OptimalTerminationProto(const double finite_primal_objective, const double dual_objective, const absl::string_view detail)
TerminationProto LimitTerminationProto(const bool is_maximize, const LimitProto limit, const std::optional< double > optional_finite_primal_objective, const std::optional< double > optional_dual_objective, const absl::string_view detail)
TerminationProto InfeasibleOrUnboundedTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
SparseVectorView< T > MakeView(absl::Span< const int64_t > ids, const Collection &values)
TerminationProto InfeasibleTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
TerminationProto UnboundedTerminationProto(const bool is_maximize, const absl::string_view detail)
std::vector< K > SortedMapKeys(const absl::flat_hash_map< K, V > &in_map)
Definition sorted.h:55
void ApplyAllFilters(const ModelSolveParametersProto &model_solve_params, SolutionProto &solution)
SolveResultProto ResultForIntegerInfeasible(const bool is_maximize, const int64_t bad_variable_id, const double lb, const double ub)
dual_gradient T(y - `dual_solution`) class DiagonalTrustRegionProblemFromQp
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
inline ::absl::StatusOr< absl::Duration > DecodeGoogleApiProto(const google::protobuf::Duration &proto)
Definition protoutil.h:42
inline ::absl::StatusOr< google::protobuf::Duration > EncodeGoogleApiProto(absl::Duration d)
Definition protoutil.h:27
StatusBuilder InternalErrorBuilder()
StatusBuilder InvalidArgumentErrorBuilder()
#define MATH_OPT_REGISTER_SOLVER(solver_type, solver_factory)
#define OR_ASSIGN_OR_RETURN3(lhs, rexpr, error_expression)