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