From 804888fc5a30ba99bd6a48db6bfcbdc37cff1e1b Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 5 Dec 2023 23:50:35 +0100 Subject: [PATCH] update math_opt, add service; python code --- ortools/math_opt/core/python/BUILD.bazel | 14 +- ortools/math_opt/cpp/solve_result.cc | 10 +- ortools/math_opt/cpp/solve_result.h | 11 +- ortools/math_opt/python/ipc/BUILD.bazel | 40 + .../math_opt/python/ipc/proto_converter.py | 85 ++ .../math_opt/python/ipc/remote_http_solve.py | 147 ++++ .../cpp/advanced_linear_programming.cc | 2 +- ortools/math_opt/samples/cpp/area_socp.cc | 2 +- ortools/math_opt/samples/cpp/basic_example.cc | 2 +- .../math_opt/samples/cpp/branch_and_bound.cc | 796 ------------------ .../math_opt/samples/cpp/branch_and_bound.h | 180 ---- .../samples/cpp/branch_and_bound_main.cc | 77 -- ortools/math_opt/samples/cpp/cocktail_hour.cc | 2 +- ortools/math_opt/samples/cpp/cutting_stock.cc | 6 +- .../samples/cpp/facility_lp_benders.cc | 4 +- .../math_opt/samples/cpp/graph_coloring.cc | 2 +- .../samples/cpp/integer_programming.cc | 2 +- .../samples/cpp/lagrangian_relaxation.cc | 6 +- .../samples/cpp/linear_programming.cc | 2 +- .../math_opt/samples/cpp/linear_regression.cc | 2 +- ortools/math_opt/samples/cpp/tsp.cc | 2 +- ortools/service/v1/BUILD.bazel | 41 + ortools/service/v1/mathopt/BUILD.bazel | 151 ++++ ortools/service/v1/mathopt/model.proto | 309 +++++++ .../service/v1/mathopt/model_parameters.proto | 108 +++ ortools/service/v1/mathopt/parameters.proto | 323 +++++++ ortools/service/v1/mathopt/result.proto | 337 ++++++++ ortools/service/v1/mathopt/solution.proto | 248 ++++++ .../v1/mathopt/sparse_containers.proto | 88 ++ ortools/service/v1/optimization.proto | 68 ++ 30 files changed, 1985 insertions(+), 1082 deletions(-) create mode 100644 ortools/math_opt/python/ipc/BUILD.bazel create mode 100644 ortools/math_opt/python/ipc/proto_converter.py create mode 100644 ortools/math_opt/python/ipc/remote_http_solve.py delete mode 100644 ortools/math_opt/samples/cpp/branch_and_bound.cc delete mode 100644 ortools/math_opt/samples/cpp/branch_and_bound.h delete mode 100644 ortools/math_opt/samples/cpp/branch_and_bound_main.cc create mode 100644 ortools/service/v1/BUILD.bazel create mode 100644 ortools/service/v1/mathopt/BUILD.bazel create mode 100644 ortools/service/v1/mathopt/model.proto create mode 100644 ortools/service/v1/mathopt/model_parameters.proto create mode 100644 ortools/service/v1/mathopt/parameters.proto create mode 100644 ortools/service/v1/mathopt/result.proto create mode 100644 ortools/service/v1/mathopt/solution.proto create mode 100644 ortools/service/v1/mathopt/sparse_containers.proto create mode 100644 ortools/service/v1/optimization.proto diff --git a/ortools/math_opt/core/python/BUILD.bazel b/ortools/math_opt/core/python/BUILD.bazel index 8450038f35..a74312aaef 100644 --- a/ortools/math_opt/core/python/BUILD.bazel +++ b/ortools/math_opt/core/python/BUILD.bazel @@ -35,11 +35,11 @@ pybind_extension( "//ortools/linear_solver:use_scip": ["//ortools/math_opt/solvers:gscip_solver"], "//conditions:default": [], }) + [ - "//ortools/math_opt:result_cc_proto", - "//ortools/math_opt/core:solve_interrupter", - "//ortools/math_opt/core:solver", - "//ortools/math_opt/core:solver_debug", - "@pybind11_abseil//pybind11_abseil:status_casters", - "@pybind11_protobuf//pybind11_protobuf:native_proto_caster", - ], + "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt/core:solve_interrupter", + "//ortools/math_opt/core:solver", + "//ortools/math_opt/core:solver_debug", + "@pybind11_abseil//pybind11_abseil:status_casters", + "@pybind11_protobuf//pybind11_protobuf:native_proto_caster", + ], ) diff --git a/ortools/math_opt/cpp/solve_result.cc b/ortools/math_opt/cpp/solve_result.cc index 0552b2ae3e..61149e49ca 100644 --- a/ortools/math_opt/cpp/solve_result.cc +++ b/ortools/math_opt/cpp/solve_result.cc @@ -347,15 +347,21 @@ absl::Status Termination::EnsureReasonIsAnyOf( << "} but got " << *this; } -absl::Status Termination::IsOptimal() const { +absl::Status Termination::EnsureIsOptimal() const { return EnsureReasonIs(TerminationReason::kOptimal); } -absl::Status Termination::IsOptimalOrFeasible() const { +absl::Status Termination::IsOptimal() const { return EnsureIsOptimal(); } + +absl::Status Termination::EnsureIsOptimalOrFeasible() const { return EnsureReasonIsAnyOf( {TerminationReason::kOptimal, TerminationReason::kFeasible}); } +absl::Status Termination::IsOptimalOrFeasible() const { + return EnsureIsOptimalOrFeasible(); +} + absl::StatusOr Termination::FromProto( const TerminationProto& termination_proto) { const std::optional reason = diff --git a/ortools/math_opt/cpp/solve_result.h b/ortools/math_opt/cpp/solve_result.h index db0be39b5d..5b602cb84b 100644 --- a/ortools/math_opt/cpp/solve_result.h +++ b/ortools/math_opt/cpp/solve_result.h @@ -323,14 +323,19 @@ struct Termination { // Returns an OkStatus if the reason of this `Termination` is // `TerminationReason::kOptimal` or `TerminationReason::kFeasible`, or an // `InternalError` otherwise. + absl::Status EnsureIsOptimalOrFeasible() const; + // TODO(b/314776390): Delete this function. + ABSL_DEPRECATED("Prefer EnsureIsOptimalOrFeasible()") absl::Status IsOptimalOrFeasible() const; // Returns an OkStatus if the reason of this `Termination` is // `TerminationReason::kOptimal`, or an `InternalError` otherwise. // - // In most use cases, at least for MIPs, `IsOptimalOrFeasible` should be used - // instead. - absl::Status IsOptimal() const; + // In most use cases, at least for MIPs, `EnsureIsOptimalOrFeasible` should be + // used instead. + absl::Status EnsureIsOptimal() const; + // TODO(b/314776390): Delete this function. + ABSL_DEPRECATED("Prefer EnsureIsOptimal()") absl::Status IsOptimal() const; // Returns an OkStatus if the reason of this `Termination` is `reason`, or an // `InternalError` otherwise. diff --git a/ortools/math_opt/python/ipc/BUILD.bazel b/ortools/math_opt/python/ipc/BUILD.bazel new file mode 100644 index 0000000000..2b1664fbe4 --- /dev/null +++ b/ortools/math_opt/python/ipc/BUILD.bazel @@ -0,0 +1,40 @@ +# Copyright 2010-2022 Google LLC +# 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. + +load("@pip_deps//:requirements.bzl", "requirement") +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "remote_http_solve", + srcs = ["remote_http_solve.py"], + visibility = ["//visibility:public"], + deps = [ + ":proto_converter", + "//ortools/service/v1:optimization_py_pb2", + requirement("requests"), + "//ortools/math_opt:rpc_py_pb2", + "//ortools/math_opt/python:mathopt", + "@com_google_protobuf//:protobuf_python", + ], +) + +py_library( + name = "proto_converter", + srcs = ["proto_converter.py"], + deps = [ + "//ortools/math_opt:rpc_py_pb2", + "//ortools/math_opt/python:normalize", + "//ortools/service/v1:optimization_py_pb2", + "@com_google_protobuf//:protobuf_python", + ], +) diff --git a/ortools/math_opt/python/ipc/proto_converter.py b/ortools/math_opt/python/ipc/proto_converter.py new file mode 100644 index 0000000000..d03c2b5e24 --- /dev/null +++ b/ortools/math_opt/python/ipc/proto_converter.py @@ -0,0 +1,85 @@ +# Copyright 2010-2022 Google LLC +# 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. + +"""Conversion functions for MathOpt protos. + +Provides several conversion functions to transform from/to protos exposed in the +Operations Research API to the internal protos in +/ortools/math_opt/.*.proto. +""" + +from google.protobuf import message +from ortools.service.v1 import optimization_pb2 +from ortools.math_opt import rpc_pb2 +from ortools.math_opt.python import normalize + +_UNSUPPORTED_SOLVER_SPECIFIC_PARAMETERS = ( + "gscip", + "gurobi", + "glop", + "cp_sat", + "osqp", + "glpk", + "highs", +) + + +def convert_request( + request: rpc_pb2.SolveRequest, +) -> optimization_pb2.SolveMathOptModelRequest: + """Converts a `SolveRequest` to a `SolveMathOptModelRequest`. + + Args: + request: A `SolveRequest` request built from a MathOpt model. + + Returns: + A `SolveMathOptModelRequest` for the Operations Research API. + + Raises: + ValueError: If a field that is not supported in the expernal proto is + present in the request or if the request can't be parsed to a + `SolveMathOptModelRequest`. + """ + normalize.math_opt_normalize_proto(request) + if request.HasField("initializer"): + raise ValueError(str("initializer is not supported")) + for param in _UNSUPPORTED_SOLVER_SPECIFIC_PARAMETERS: + if request.parameters.HasField(param): + raise ValueError(f"SolveParameters.{param} not supported") + + try: + external_request = optimization_pb2.SolveMathOptModelRequest.FromString( + request.SerializeToString() + ) + return external_request + except (message.DecodeError, message.EncodeError): + raise ValueError("request can not be parsed") from None + + +def convert_response( + api_response: optimization_pb2.SolveMathOptModelResponse, +) -> rpc_pb2.SolveResponse: + """Converts a `SolveMathOptModelResponse` to a `SolveResponse`. + + Args: + api_response: A `SolveMathOptModelResponse` response built from a MathOpt + model. + + Returns: + A `SolveResponse` response built from a MathOpt model. + """ + api_response.DiscardUnknownFields() + normalize.math_opt_normalize_proto(api_response) + response = rpc_pb2.SolveResponse.FromString(api_response.SerializeToString()) + response.DiscardUnknownFields() + return response diff --git a/ortools/math_opt/python/ipc/remote_http_solve.py b/ortools/math_opt/python/ipc/remote_http_solve.py new file mode 100644 index 0000000000..da312cdd4a --- /dev/null +++ b/ortools/math_opt/python/ipc/remote_http_solve.py @@ -0,0 +1,147 @@ +# Copyright 2010-2022 Google LLC +# 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. + +"""Solve MathOpt models via HTTP request to the OR API.""" + +import json +from typing import Optional, Tuple, List +from google.protobuf import json_format +import requests +from ortools.service.v1 import optimization_pb2 +from ortools.math_opt import rpc_pb2 +from ortools.math_opt.python import mathopt +from ortools.math_opt.python.ipc import proto_converter + +_DEFAULT_DEADLINE_SEC = 10 +_DEFAULT_ENDPOINT = "https://optimization.googleapis.com/v1/mathopt:solveMathOptModel" + + +class OptimizationServiceError(Exception): + """Error produced when solving a MathOpt model via HTTP request.""" + + +def remote_http_solve( + model: mathopt.Model, + solver_type: mathopt.SolverType, + params: Optional[mathopt.SolveParameters] = None, + model_params: Optional[mathopt.ModelSolveParameters] = None, + endpoint: Optional[str] = _DEFAULT_ENDPOINT, + api_key: Optional[str] = None, + deadline_sec: Optional[float] = _DEFAULT_DEADLINE_SEC, +) -> Tuple[mathopt.SolveResult, List[str]]: + """Solves a MathOpt model via HTTP request to the OR API. + + Args: + model: The optimization model. + solver_type: The underlying solver to use. + params: Optional configuration of the underlying solver. + model_params: Optional configuration of the solver that is model specific. + endpoint: An URI identifying the service for remote solves. + api_key: Tey to the OR API. + deadline_sec: The number of seconds before the request times out. + + Returns: + A SolveResult containing the termination reason, solution(s) and stats. + A list of messages with the logs (if specified in the `params`). + + Raises: + OptimizationServiceError: if an HTTP error is returned while solving a + model. + """ + if api_key is None: + # TODO(b/306709279): Relax this when unauthenticated solves are allowed. + raise ValueError("api_key can't be None when solving remotely") + + payload = _build_json_payload(model, solver_type, params, model_params) + + response = requests.post( + url=f"{endpoint}?key={api_key}", + json=payload, + timeout=deadline_sec, + ) + + if not response.ok: + http_error = json.loads(response.content)["error"] + raise OptimizationServiceError( + f'status code {http_error["code"]}: {http_error["message"]}' + ) from None + + return _build_solve_result(response.content, model) + + +def _build_json_payload( + model: mathopt.Model, + solver_type: mathopt.SolverType, + params: Optional[mathopt.SolveParameters], + model_params: Optional[mathopt.ModelSolveParameters], +): + """Builds a JSON payload. + + Args: + model: The optimization model. + solver_type: The underlying solver to use. + params: Optional configuration of the underlying solver. + model_params: Optional configuration of the solver that is model specific. + + Returns: + A JSON object with a MathOpt model and corresponding parameters. + + Raises: + SerializationError: If building the OR API proto is not successful or + deserializing to JSON fails. + """ + params = params or mathopt.SolveParameters() + model_params = model_params or mathopt.ModelSolveParameters() + try: + request = rpc_pb2.SolveRequest( + model=model.export_model(), + solver_type=solver_type.value, + parameters=params.to_proto(), + model_parameters=model_params.to_proto(), + ) + api_request = proto_converter.convert_request(request) + except ValueError as err: + raise ValueError from err + + return json.loads(json_format.MessageToJson(api_request)) + + +def _build_solve_result( + json_response: bytes, model: mathopt.Model +) -> Tuple[mathopt.SolveResult, List[str]]: + """Parses a JSON representation of a response to a SolveResult object. + + Args: + json_response: bytes representing the `SolveMathOptModelResponse` in JSON + format + model: The optimization model that was solved + + Returns: + A SolveResult of the model. + A list of messages with the logs. + + Raises: + SerializationError: If parsing the json response fails or if converting the + OR API response to the internal MathOpt response fails. + """ + try: + api_response = json_format.Parse( + json_response, optimization_pb2.SolveMathOptModelResponse() + ) + except json_format.ParseError as json_err: + raise ValueError( + "api response is not a valid SolveMathOptModelResponse JSON" + ) from json_err + + response = proto_converter.convert_response(api_response) + return mathopt.parse_solve_result(response.result, model), list(response.messages) diff --git a/ortools/math_opt/samples/cpp/advanced_linear_programming.cc b/ortools/math_opt/samples/cpp/advanced_linear_programming.cc index 475acfff1f..78bb13614b 100644 --- a/ortools/math_opt/samples/cpp/advanced_linear_programming.cc +++ b/ortools/math_opt/samples/cpp/advanced_linear_programming.cc @@ -67,7 +67,7 @@ absl::Status Main() { ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kGlop)); - RETURN_IF_ERROR(result.termination.IsOptimal()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); std::cout << "Problem solved in " << result.solve_time() << std::endl; std::cout << "Objective value: " << result.objective_value() << std::endl; diff --git a/ortools/math_opt/samples/cpp/area_socp.cc b/ortools/math_opt/samples/cpp/area_socp.cc index 4c127ea047..27beab4683 100644 --- a/ortools/math_opt/samples/cpp/area_socp.cc +++ b/ortools/math_opt/samples/cpp/area_socp.cc @@ -65,7 +65,7 @@ absl::Status Main(const double target_area) { model.Minimize(width + height); ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kEcos)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); std::cout << "Target area: " << target_area << std::endl; std::cout << "Area: " << result.variable_values().at(width) * diff --git a/ortools/math_opt/samples/cpp/basic_example.cc b/ortools/math_opt/samples/cpp/basic_example.cc index e4ed504748..a99a8b07a4 100644 --- a/ortools/math_opt/samples/cpp/basic_example.cc +++ b/ortools/math_opt/samples/cpp/basic_example.cc @@ -48,7 +48,7 @@ absl::Status Main() { model.Maximize(objective_expression); ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kGscip)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); std::cout << "Objective value: " << result.objective_value() << std::endl << "Value for variable x: " << result.variable_values().at(x) << std::endl; diff --git a/ortools/math_opt/samples/cpp/branch_and_bound.cc b/ortools/math_opt/samples/cpp/branch_and_bound.cc deleted file mode 100644 index ba755b5b87..0000000000 --- a/ortools/math_opt/samples/cpp/branch_and_bound.cc +++ /dev/null @@ -1,796 +0,0 @@ -// Copyright 2010-2022 Google LLC -// 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 "ortools/math_opt/examples/cpp/branch_and_bound.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/container/flat_hash_map.h" -#include "absl/log/check.h" -#include "absl/memory/memory.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_format.h" -#include "absl/time/clock.h" -#include "absl/time/time.h" -#include "ortools/base/status_macros.h" -#include "ortools/base/strong_int.h" -#include "ortools/base/strong_vector.h" -#include "ortools/math_opt/cpp/math_opt.h" - -namespace operations_research_example { -namespace { - -namespace math_opt = operations_research::math_opt; -constexpr double kInf = std::numeric_limits::infinity(); - -DEFINE_STRONG_INT_TYPE(VarIndex, int32_t); -DEFINE_STRONG_INT_TYPE(NodeId, int64_t); - -//////////////////////////////////////////////////////////////////////////////// -// Search Tree -//////////////////////////////////////////////////////////////////////////////// - -// A node in the search tree. Each node stores the branching decision leading to -// this node, the parent node, and the best known LP bound for this node. To -// recover all variable bounds, you must traverse back to the root node. -struct Node { - std::optional parent; - // In the unexplored state, this is the parent bound. In the explored state, - // it is the lp bound. - double bound = 0.0; - VarIndex branch_variable = VarIndex{-1}; - int64_t variable_bound = 0; - bool is_upper_bound = false; - int num_children_live = 0; - bool explored = false; -}; - -// Variable bounds to further restrict the LP relaxation at a node in the search -// tree. -struct Bounds { - absl::flat_hash_map lower_bounds; - absl::flat_hash_map upper_bounds; -}; - -// Stores the nodes of the search tree that are either not yet explored, or -// have a child that is not yet explored. -// -// The unexplored nodes are stored in "frontier_", which, for minimization, -// stores them in the order of lowest LP bound first. You can access this node -// and its id by `top()` and `top_id()`. -// -// To process a node, you first must get all the variable bounds for this node -// (with `RecoverBounds()`) and then solve the LP relaxation with these bounds. -// -// After processing the top node, you can: -// * Close it (by `CloseTop()`), which deletes it and potentially its parents. -// Take this action if the solution was integer, or if the LP bound was -// larger (for minimization) than the best integer solution found. -// * Branch into two new nodes. Take this action for a fractional solution -// with LP bound less than (for minimization) the best integer solution. -// -// A global bound on your problem is (for minimization) the minimum of the -// objective value found for any integer solution, and the lowest bound on any -// open node from the frontier. This is a valid bound because we only close a -// node when either it is integer or when the bound is larger than the best -// integer solution we have found. If we have not found an integer solution and -// there are no nodes remaining, then we have proven the problem infeasible (the -// bound is +inf). Note that that open nodes typically have bound lower than the -// best integer solution found (as otherwise we immediately close them). We can -// efficiently the compute the bound over all open nodes by looking at `top()` -// because we store the nodes in a priority queue with the order of lowest bound -// first. -class SearchTree { - public: - explicit SearchTree(bool is_maximize); - - // Indicates there are no nodes left to process. - bool frontier_empty() const { return frontier_.empty(); } - - // The id of the next node to process. - // - // Behavior is undefined when `frontier_empty()` is true. - NodeId top_id() const { return frontier_.top().second; } - - // A mutable reference to the next node to process. - // - // Behavior is undefined when `frontier_empty()` is true. - Node& top() { return nodes_.at(top_id()); } - - // A const reference to the next node to process. - // - // Behavior is undefined when `frontier_empty()` is true. - const Node& top() const { return nodes_.at(top_id()); } - - // Marks the top node as explored and adds two child nodes to the frontier. - // - // Behavior is undefined when `frontier_empty()` is true. - void BranchOnTop(VarIndex branching_var, int64_t branch_down_value); - - // Deletes the top node and then recursively deletes ancestors that have no - // open children. - // - // Behavior is undefined when `frontier_empty()` is true. - void CloseTop(); - - // Traverses the tree back to the root node to get variable bounds for node. - Bounds RecoverBounds(NodeId node) const; - - // Returns the global bound on the objective value of this problem. Is nullopt - // when `frontier_empty()`, see class level comment. - std::optional bound() const; - - private: - void frontier_push(double bound, NodeId node); - NodeId add_node(const Node& node); - - // If the optimization problem is has a maximization objective. - bool is_maximize_; - - // The nodes that are unexplored or that have unexplored children. - absl::flat_hash_map nodes_; - - // The nodes that are unexplored, ordered by: - // * For minimization, lowest LP relaxation first, - // * For maximization, highest LP relaxation first. - std::priority_queue> frontier_; - - // The id to use for the next node created (ids are not reused). - NodeId next_id_ = NodeId{0}; -}; - -void SearchTree::frontier_push(double bound, const NodeId node) { - // frontier returns the largest elements first, which is correct for - // maximization problems, but the opposite of what we want for minimization. - if (!is_maximize_) { - bound = -bound; - } - frontier_.push({bound, node}); -} - -NodeId SearchTree::add_node(const Node& node) { - const NodeId id = next_id_; - nodes_[id] = node; - frontier_push(node.bound, id); - ++next_id_; - return id; -} - -SearchTree::SearchTree(const bool is_maximize) : is_maximize_(is_maximize) { - double bound = is_maximize ? kInf : -kInf; - add_node({.bound = bound}); -} - -Bounds SearchTree::RecoverBounds(const NodeId node_id) const { - Bounds result; - std::optional next = node_id; - while (next.has_value()) { - const Node& node = nodes_.at(*next); - if (node.branch_variable >= VarIndex{0}) { - if (node.is_upper_bound) { - // If the key is already present, the key from lower in the tree is - // tighter, so discard this value. - result.upper_bounds.insert({node.branch_variable, node.variable_bound}); - } else { - result.lower_bounds.insert({node.branch_variable, node.variable_bound}); - } - } - next = node.parent; - } - return result; -} - -void SearchTree::BranchOnTop(const VarIndex branching_var, - const int64_t branch_down_value) { - const auto [unused, top] = frontier_.top(); - frontier_.pop(); - Node& top_node = nodes_.at(top); - const double new_bound = top_node.bound; - top_node.num_children_live = 2; - top_node.explored = true; - const Node down = {.parent = top, - .bound = new_bound, - .branch_variable = branching_var, - .variable_bound = branch_down_value, - .is_upper_bound = true}; - const Node up = {.parent = top, - .bound = new_bound, - .branch_variable = branching_var, - .variable_bound = branch_down_value + 1, - .is_upper_bound = false}; - add_node(down); - add_node(up); -} - -void SearchTree::CloseTop() { - std::optional to_delete = frontier_.top().second; - frontier_.pop(); - while (to_delete.has_value()) { - const auto node_it = nodes_.find(*to_delete); - CHECK(node_it != nodes_.end()); - Node& node = node_it->second; - if (node.explored) { - --node.num_children_live; - if (node.num_children_live > 0) { - break; - } - } - to_delete = node.parent; - nodes_.erase(node_it); - } -} - -std::optional SearchTree::bound() const { - if (frontier_.empty()) { - return std::nullopt; - } - return top().bound; -} - -//////////////////////////////////////////////////////////////////////////////// -// LP Relaxation -//////////////////////////////////////////////////////////////////////////////// - -// All fields other than termination are filled only when termination reason is -// optimal. -struct LpSolution { - math_opt::Termination termination; - // TODO(b/290091715): delete these once they are part of termination - double objective_value = 0.0; - double dual_bound = 0.0; - absl::StrongVector variable_values; - std::vector integer_vars_with_fractional_values; - int64_t pivots = 0; - - bool is_integer() const { - return integer_vars_with_fractional_values.empty(); - } -}; - -// Solves the linear programming (LP) relaxation of an input optimization model. -// -// Copies the input model and to build a modified model, and builds a solver on -// the relaxed model. -class LpRelaxation { - public: - // Holds a reference to `model`. The caller MUST ensure that `model` outlives - // this. - static absl::StatusOr> New( - const math_opt::Model& model, math_opt::SolverType solver, - double integrality_abs_tolerance); - - // Modifies the variable bounds of the LP relaxation to `bounds`. Typically - // call `RestoreBounds()` first. - void SetBounds(const Bounds& bounds); - - // Sets the variable bounds of the LP relaxation back to their bounds in the - // input model. - void RestoreBounds(); - - // Solves the LP relaxation and returns the result. - absl::StatusOr Solve( - const math_opt::SolveParameters& solve_params); - - // Given a solution to the LP relaxation, rewrite it on the Variable objects - // of the input model. - math_opt::VariableMap RestoreMipSolution( - const absl::StrongVector& lp_solution) const; - - private: - // Maintains a 1:1 mapping between variables of the LP relaxation and the - // input model. - struct VarData { - // From the LP relaxation. - math_opt::Variable variable; - // From the input model. - math_opt::Variable orig_variable; - - bool was_integer() const { return orig_variable.is_integer(); } - - double init_lb() const { return orig_variable.lower_bound(); } - double init_ub() const { return orig_variable.upper_bound(); } - }; - - LpRelaxation() = default; - - std::unique_ptr relaxed_model_; - std::unique_ptr solver_; - double integrality_abs_tolerance_ = 0.0; - absl::StrongVector var_data_; -}; - -absl::StatusOr> LpRelaxation::New( - const math_opt::Model& model, const math_opt::SolverType solver, - const double integrality_abs_tolerance) { - const std::vector orig_variables = - model.SortedVariables(); - auto result = absl::WrapUnique(new LpRelaxation()); - result->relaxed_model_ = model.Clone(); - const std::vector new_variables = - result->relaxed_model_->SortedVariables(); - - for (int i = 0; i < orig_variables.size(); ++i) { - result->relaxed_model_->set_continuous(new_variables[i]); - result->var_data_.push_back( - {.variable = new_variables[i], .orig_variable = orig_variables[i]}); - } - result->integrality_abs_tolerance_ = integrality_abs_tolerance; - ASSIGN_OR_RETURN(result->solver_, math_opt::IncrementalSolver::New( - result->relaxed_model_.get(), solver)); - return result; -} - -void LpRelaxation::SetBounds(const Bounds& bounds) { - for (const auto [var_index, value] : bounds.lower_bounds) { - relaxed_model_->set_lower_bound(var_data_[var_index].variable, value); - } - for (const auto [var_index, value] : bounds.upper_bounds) { - relaxed_model_->set_upper_bound(var_data_[var_index].variable, value); - } -} - -void LpRelaxation::RestoreBounds() { - for (const VarData& var_data : var_data_) { - relaxed_model_->set_lower_bound(var_data.variable, var_data.init_lb()); - relaxed_model_->set_upper_bound(var_data.variable, var_data.init_ub()); - } -} - -absl::StatusOr LpRelaxation::Solve( - const math_opt::SolveParameters& params) { - ASSIGN_OR_RETURN(const math_opt::SolveResult lp_result, - solver_->Solve({.parameters = params})); - LpSolution solution = {.termination = lp_result.termination, - .pivots = lp_result.solve_stats.simplex_iterations}; - if (lp_result.termination.reason == math_opt::TerminationReason::kOptimal) { - solution.objective_value = lp_result.objective_value(); - solution.dual_bound = lp_result.best_objective_bound(); - for (const VarIndex v : var_data_.index_range()) { - const VarData& var_data = var_data_[v]; - const double var_value = - lp_result.variable_values().at(var_data.variable); - solution.variable_values.push_back(var_value); - if (var_data.was_integer()) { - double err = std::abs(std::round(var_value) - var_value); - if (err > integrality_abs_tolerance_) { - solution.integer_vars_with_fractional_values.push_back(v); - } - } - } - } - return solution; -} - -math_opt::VariableMap LpRelaxation::RestoreMipSolution( - const absl::StrongVector& lp_solution) const { - math_opt::VariableMap result; - for (const VarIndex v : var_data_.index_range()) { - result[var_data_[v].orig_variable] = lp_solution[v]; - } - return result; -} - -//////////////////////////////////////////////////////////////////////////////// -// Solve State and Stats (measure progress) -//////////////////////////////////////////////////////////////////////////////// - -// Tracks the progress of the solver and if we have reached a termination -// criteria. -class SolveState { - public: - SolveState(BranchAndBoundParameters parameters, bool is_maximize); - - bool should_terminate() const { - return absl::Now() >= deadline_ || is_within_gap(); - } - - bool is_within_gap() const { - if (!std::isfinite(best_primal_bound_)) { - return false; - } - double absolute_gap = best_primal_bound_ - best_dual_bound_; - if (is_maximize_) { - absolute_gap = -absolute_gap; - } - return absolute_gap <= - parameters_.abs_gap_tolerance + - parameters_.rel_gap_tolerance * std::abs(best_primal_bound_); - } - - // Providing a value of std::nullopt indicates that the search tree is empty. - // In this case, the problem either optimal if we have found an integer, - // or infeasible if we have not. In both cases, the dual bound is now equal - // to the primal bound. - void update_dual_bound(std::optional bound); - - void update_primal_bound(absl::StrongVector solution, - double objective_value); - - absl::Duration time_remaining() const { return deadline_ - absl::Now(); } - absl::Duration elapsed_time() const { return absl::Now() - start_; } - - SimpleSolveResult Result(const LpRelaxation& relaxation, - bool search_tree_empty, - const SimpleSolveResult::Stats& stats) const; - - bool is_better_than_best_solution(double new_obj) { - if (!best_integer_solution_.has_value()) { - return true; - } - if (is_maximize_) { - return new_obj > best_primal_bound_; - } else { - return new_obj < best_primal_bound_; - } - } - - double best_primal_bound() const { return best_primal_bound_; } - double best_dual_bound() const { return best_dual_bound_; } - absl::Time start_time() const { return start_; } - - private: - BranchAndBoundParameters parameters_; - bool is_maximize_; - absl::Time start_; - absl::Time deadline_; - std::optional> best_integer_solution_; - double best_primal_bound_; - double best_dual_bound_; -}; - -SimpleSolveResult SolveState::Result( - const LpRelaxation& relaxation, const bool search_tree_empty, - const SimpleSolveResult::Stats& stats) const { - SimpleSolveResult solve_result{.primal_bound = best_primal_bound_, - .dual_bound = best_dual_bound_, - .stats = stats}; - if (!best_integer_solution_.has_value()) { - if (search_tree_empty) { - return InfeasibleSolveResult(is_maximize_, stats); - } - solve_result.termination_reason = SimpleSolveResult::Reason::kNoSolution; - return solve_result; - } - solve_result.variable_values = - relaxation.RestoreMipSolution(*best_integer_solution_); - if (is_within_gap()) { - solve_result.termination_reason = SimpleSolveResult::Reason::kOptimal; - } else { - solve_result.termination_reason = SimpleSolveResult::Reason::kFeasible; - } - return solve_result; -} - -SolveState::SolveState(const BranchAndBoundParameters parameters, - const bool is_maximize) - : parameters_(std::move(parameters)), is_maximize_(is_maximize) { - start_ = absl::Now(); - deadline_ = start_ + parameters_.time_limit; - if (is_maximize_) { - best_primal_bound_ = -kInf; - best_dual_bound_ = kInf; - } else { - best_primal_bound_ = kInf; - best_dual_bound_ = -kInf; - } -} - -void SolveState::update_dual_bound(std::optional bound) { - if (bound.has_value()) { - best_dual_bound_ = *bound; - } else { - // This is subtle, see documentation on update_dual_bound declaration. - best_dual_bound_ = best_primal_bound_; - } -} - -void SolveState::update_primal_bound( - absl::StrongVector solution, double objective_value) { - if (is_better_than_best_solution(objective_value)) { - best_primal_bound_ = objective_value; - best_integer_solution_ = std::move(solution); - } -} - -void PrintSearchHeader(const BranchAndBoundParameters& params) { - if (params.enable_output) { - std::cout << absl::StrFormat("%13s | %8s | %8s | %13s | %13s | %10s", - "time", "nodes", "closed", "objective", - "bound", "pivot/node") - << std::endl; - } -} - -void PrintSearchRow(const BranchAndBoundParameters& params, - const SimpleSolveResult::Stats& stats, - const SolveState& solve_state) { - const int64_t n = stats.nodes_closed; - // Print a log line for the first 10 nodes solved, and then only when the - // number of nodes solved is a power of two. - if (params.enable_output && (n <= 10 || (n & (n - 1)) == 0)) { - const double pivots_per_closed_node = - stats.nodes_closed == 0 - ? 0.0 - : static_cast(stats.tree_pivots) / stats.nodes_closed; - std::cout << absl::StrFormat( - "%13s | %8d | %8d | %13.4f | %13.4f | %10.2f", - absl::FormatDuration(solve_state.elapsed_time()), - stats.nodes_created, stats.nodes_closed, - solve_state.best_primal_bound(), - solve_state.best_dual_bound(), pivots_per_closed_node) - << std::endl; - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Branch and Bound Algorithm -//////////////////////////////////////////////////////////////////////////////// - -// NOTE: this is a very simple but not very good branching rule, typically -// prefer strong branching or pseudo-costs. Better branching rules are stateful -// and/or need access to the LP relaxation to do extra solves. -absl::StatusOr MostFractionalBranchingRule( - const LpSolution& lp_solution) { - std::optional most_fractional; - double most_fractional_size = 0.0; - for (const VarIndex v : lp_solution.integer_vars_with_fractional_values) { - const double v_val = lp_solution.variable_values[v]; - const double fractional_value = std::abs(std::round(v_val) - v_val); - if (fractional_value > most_fractional_size) { - most_fractional_size = fractional_value; - most_fractional = v; - } - } - if (!most_fractional.has_value()) { - return absl::InternalError( - "failed to find a fractional variable for branching, should be " - "impossible"); - } - return *most_fractional; -} - -absl::StatusOr SolveWithBranchAndBoundImpl( - const math_opt::Model& model, const BranchAndBoundParameters& params) { - const bool is_maximize = model.is_maximize(); - SimpleSolveResult::Stats stats; - - SolveState solve_state(params, is_maximize); - if (params.enable_output) { - std::cout << "Solving LP Relaxation: " << std::endl; - } - ASSIGN_OR_RETURN(auto lp_solver, - LpRelaxation::New(model, params.lp_solver, - params.integrality_absolute_tolerance)); - - // Solve the root separately, a few extra special cases to take care of: - // * The problem can actually be unbounded (infeasible or unbounded does not - // necessarily imply infeasible). - // * We need to ensure that we use dual simplex in the tree, but solver can - // decide the method used in the root. - // * Future versions may want to save the basis or add cuts at the root. - math_opt::SolveParameters root_params = { - .enable_output = params.enable_output, - .time_limit = - std::max(absl::ZeroDuration(), solve_state.time_remaining())}; - // We do not get effective incremental solves with GLOP when presolve is on. - if (params.lp_solver == math_opt::SolverType::kGlop) { - root_params.presolve = math_opt::Emphasis::kOff; - } - ASSIGN_OR_RETURN(LpSolution root_solution, lp_solver->Solve(root_params)); - stats.root_pivots = root_solution.pivots; - if (params.enable_output) { - std::cout << "LP Relaxation termination: " << root_solution.termination - << " pivots: " << root_solution.pivots << std::endl; - } - switch (root_solution.termination.reason) { - case math_opt::TerminationReason::kImprecise: - case math_opt::TerminationReason::kNumericalError: - case math_opt::TerminationReason::kOtherError: - return ErrorSolveResult(is_maximize, stats); - case math_opt::TerminationReason::kInfeasible: - return InfeasibleSolveResult(is_maximize, stats); - case math_opt::TerminationReason::kInfeasibleOrUnbounded: - case math_opt::TerminationReason::kUnbounded: - // When the LP is unbounded, we do not yet have an integer feasible point, - // so the problem may be infeasible. You need to solve with zero - // objective and find an integer feasible point to conclude unbounded. - return InfeasibleOrUnboundedSolveResult(is_maximize, stats); - case math_opt::TerminationReason::kNoSolutionFound: - case math_opt::TerminationReason::kFeasible: - solve_state.update_dual_bound(root_solution.dual_bound); - return solve_state.Result(*lp_solver, false, stats); - case math_opt::TerminationReason::kOptimal: - solve_state.update_dual_bound(root_solution.dual_bound); - if (root_solution.is_integer()) { - solve_state.update_primal_bound(root_solution.variable_values, - root_solution.objective_value); - } - if (solve_state.is_within_gap()) { - return solve_state.Result(*lp_solver, false, stats); - } - break; - } - // Invariant: we have solved the LP relaxation to optimality (and thus the - // problem is bounded, although could still be infeasible). - SearchTree tree(is_maximize); - ++stats.nodes_created; - // NOTE: we solve the root LP twice, but because the solve is incremental, - // the second solve is essentially free. - PrintSearchHeader(params); - while (!tree.frontier_empty() && !solve_state.should_terminate()) { - PrintSearchRow(params, stats, solve_state); - NodeId top_id = tree.top_id(); - Node& top = tree.top(); - lp_solver->RestoreBounds(); - const Bounds bounds = tree.RecoverBounds(top_id); - lp_solver->SetBounds(bounds); - math_opt::SolveParameters tree_params = { - .time_limit = - std::max(absl::ZeroDuration(), solve_state.time_remaining())}; - // We do not get effective incremental solves with GLOP when presolve is on. - // We want dual simplex, since our old solution is dual feasible, but GLOP - // does not automatically select it with the default settings. - if (params.lp_solver == math_opt::SolverType::kGlop) { - tree_params.presolve = math_opt::Emphasis::kOff; - tree_params.lp_algorithm = math_opt::LPAlgorithm::kDualSimplex; - } - ASSIGN_OR_RETURN(LpSolution lp_solution, lp_solver->Solve(tree_params)); - stats.tree_pivots += lp_solution.pivots; - ++stats.nodes_closed; - switch (lp_solution.termination.reason) { - case math_opt::TerminationReason::kImprecise: - case math_opt::TerminationReason::kNumericalError: - case math_opt::TerminationReason::kOtherError: - case math_opt::TerminationReason::kUnbounded: - // Unbounded is now an error, this should have been caught at the root. - return ErrorSolveResult(is_maximize, stats); - case math_opt::TerminationReason::kNoSolutionFound: - case math_opt::TerminationReason::kFeasible: - // We are out of time, terminate. - // Warning: if more termination criteria are added (e.g. the use of a - // cutoff when solving the LP relaxation, as is typical in branch and - // bound), then you need to check `lp_solution.termination.limit` to - // decide what to do here. - return solve_state.Result(*lp_solver, false, stats); - case math_opt::TerminationReason::kInfeasible: - case math_opt::TerminationReason::kInfeasibleOrUnbounded: - // Infeasible or unbounded must be infeasible, as we have already - // ruled out unbounded. - tree.CloseTop(); - break; - case math_opt::TerminationReason::kOptimal: { - top.bound = lp_solution.objective_value; - if (lp_solution.is_integer()) { - solve_state.update_primal_bound(lp_solution.variable_values, - lp_solution.objective_value); - tree.CloseTop(); - } else if (solve_state.is_better_than_best_solution(top.bound)) { - ASSIGN_OR_RETURN(const VarIndex branch_var, - MostFractionalBranchingRule(lp_solution)); - const int64_t branch_down_val = static_cast( - std::floor(lp_solution.variable_values[branch_var])); - tree.BranchOnTop(branch_var, branch_down_val); - stats.nodes_created += 2; - } else { - tree.CloseTop(); - } - break; - } - } - solve_state.update_dual_bound(tree.bound()); - } - PrintSearchRow(params, stats, solve_state); - return solve_state.Result(*lp_solver, tree.frontier_empty(), stats); -} - -} // namespace - -SimpleSolveResult TrivialSolveResult(bool is_maximize, - const SimpleSolveResult::Stats& stats) { - SimpleSolveResult result{ - .termination_reason = SimpleSolveResult::Reason::kNoSolution, - .stats = stats}; - if (is_maximize) { - result.primal_bound = -kInf; - result.dual_bound = kInf; - } else { - result.primal_bound = kInf; - result.dual_bound = -kInf; - } - return result; -} - -SimpleSolveResult ErrorSolveResult(bool is_maximize, - const SimpleSolveResult::Stats& stats) { - SimpleSolveResult result = TrivialSolveResult(is_maximize, stats); - result.termination_reason = SimpleSolveResult::Reason::kError; - return result; -} - -SimpleSolveResult InfeasibleSolveResult(bool is_maximize, - const SimpleSolveResult::Stats& stats) { - SimpleSolveResult result = TrivialSolveResult(is_maximize, stats); - result.termination_reason = SimpleSolveResult::Reason::kInfeasible; - return result; -} -SimpleSolveResult InfeasibleOrUnboundedSolveResult( - bool is_maximize, const SimpleSolveResult::Stats& stats) { - SimpleSolveResult result = TrivialSolveResult(is_maximize, stats); - result.termination_reason = SimpleSolveResult::Reason::kInfeasibleOrUnbounded; - return result; -} - -std::ostream& operator<<(std::ostream& ostr, - const SimpleSolveResult::Reason reason) { - switch (reason) { - case SimpleSolveResult::Reason::kOptimal: - ostr << "optimal"; - return ostr; - case SimpleSolveResult::Reason::kFeasible: - ostr << "feasible"; - return ostr; - case SimpleSolveResult::Reason::kNoSolution: - ostr << "no_solution"; - return ostr; - case SimpleSolveResult::Reason::kInfeasible: - ostr << "infeasible"; - return ostr; - case SimpleSolveResult::Reason::kInfeasibleOrUnbounded: - ostr << "infeasible_or_unbounded"; - return ostr; - case SimpleSolveResult::Reason::kError: - ostr << "error"; - return ostr; - } -} - -std::ostream& operator<<(std::ostream& ostr, - const SimpleSolveResult::Stats& stats) { - ostr << "solve_time: " << stats.solve_time - << " root_pivots: " << stats.root_pivots - << " tree_pivots: " << stats.tree_pivots - << " nodes created: " << stats.nodes_created - << " nodes closed: " << stats.nodes_closed; - return ostr; -} - -absl::StatusOr SolveWithBranchAndBound( - const math_opt::Model& model, const BranchAndBoundParameters& params) { - const absl::Time start = absl::Now(); - ASSIGN_OR_RETURN(SimpleSolveResult result, - SolveWithBranchAndBoundImpl(model, params)); - result.stats.solve_time = absl::Now() - start; - if (params.enable_output) { - std::cout << "Branch and bound terminated." << std::endl; - std::cout << "termination reason: " << result.termination_reason - << std::endl; - std::cout << "primal bound: " << result.primal_bound << std::endl; - std::cout << "dual bound: " << result.dual_bound << std::endl; - std::cout << "final stats:\n" << result.stats << std::endl; - } - return result; -} - -} // namespace operations_research_example diff --git a/ortools/math_opt/samples/cpp/branch_and_bound.h b/ortools/math_opt/samples/cpp/branch_and_bound.h deleted file mode 100644 index 9abe9364fa..0000000000 --- a/ortools/math_opt/samples/cpp/branch_and_bound.h +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2010-2022 Google LLC -// 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. - -// A very simple branch-and-bound solver for MIPs, using MathOpt to solve the LP -// relaxation at every node. -// -// This example: -// * Demonstrates incremental solving with MathOpt. -// * Shows how to process various termination reasons for an LP solver. -// * Can be used a skeleton for a custom branch and bound. -// -// This implementation of branch and bound does not do cut generation, does not -// have any primal heuristics, and uses a very naive branching rule (most -// fractional variable). It cannot solve large problems, it just demonstrates a -// few techniques. -#ifndef OR_TOOLS_MATH_OPT_EXAMPLES_CPP_BRANCH_AND_BOUND_H_ -#define OR_TOOLS_MATH_OPT_EXAMPLES_CPP_BRANCH_AND_BOUND_H_ - -#include - -#include "absl/container/flat_hash_map.h" -#include "absl/status/statusor.h" -#include "absl/time/time.h" -#include "ortools/math_opt/cpp/math_opt.h" - -namespace operations_research_example { - -// Configuration for SolveWithBranchAndBound(). -struct BranchAndBoundParameters { - // Used to solve the underlying LP relaxation of the model (when all integer - // variables are made continuous). The solver selected must be able to solve - // the LP relaxation for a given input model (e.g. use OSQP if the problem is - // quadratic and convex). - operations_research::math_opt::SolverType lp_solver = - operations_research::math_opt::SolverType::kGlop; - - // If progress should be printed to standard output. - bool enable_output = false; - - // The criteria on solution quality for termination. - // - // Let obj* be the objective value of the best solution found, and bound* be - // the dual bound found from search. For minimization (bound* <= obj*), we - // stop when: - // obj* - bound* <= abs_gap_tolerance + rel_gap_tolerance * obj*. - double abs_gap_tolerance = 1.0e-4; - - // Se abs_gap_tolerance for details. - double rel_gap_tolerance = 1.0e-4; - - // A solution found the LP solver is feasible for the integrality constraints - // if every integer variable takes a value within - // `integrality_absolute_tolerance` of some integer. - double integrality_absolute_tolerance = 1.0e-5; - - // A limit on how long to run the solver for. - absl::Duration time_limit = absl::InfiniteDuration(); -}; - -// The result of SolveWithBranchAndBound(). -struct SimpleSolveResult { - // The reason a BranchAndBoundSolve terminated. - enum class Reason { - // Solved the problem to optimality - kOptimal, - // Found a feasible solution, but hit a limit (e.g., time limit). - kFeasible, - // Hit a limit (e.g., time limit) without finding any solution. - kNoSolution, - // The problem was provably primal infeasible. - kInfeasible, - // A primal ray was found, or the LP solver returned infeasible or - // unbounded. - kInfeasibleOrUnbounded, - // Something went wrong, including an imprecise LP solve. - kError - }; - - // A summary of the resources used during SolveWithBranchAndBound(). - struct Stats { - // How long the function took. - absl::Duration solve_time; - - // The number of simplex pivots to solve the root LP relaxation. - int64_t root_pivots = 0; - - // The number of simplex pivots for all nodes in the search tree (excluding - // the root LP relaxation). - int64_t tree_pivots = 0; - - // The number of nodes in the search tree created. - int64_t nodes_created = 0; - - // The number of nodes in the search tree processed. - int64_t nodes_closed = 0; - }; - - // The reason SolveWithBranchAndBound() stopped for this solve. - Reason termination_reason = Reason::kNoSolution; - - // The best solution found in the search, or empty if no solution was found. - // - // For problems with at least one variable, will be non-empty iff - // `termination_reason` is kFeasible or kOptimal. - absl::flat_hash_map - variable_values; - - // The objective value of the best solution found. - // - // Is +inf for minimization and -inf for maximization if no solution is found. - double primal_bound = 0.0; - - // A bound on objective value of any solution for this problem. - // - // For minimization, the bound is less than `primal_bound` (up to tolerances), - // and -inf if no bound is found. - // - // For maximization, the bound is greater than `primal_bound` (up to - // tolerances) and is +inf if no bound is found. - double dual_bound = 0.0; - - // A summary of the resources used during this SolveWithBranchAndBound(). - Stats stats; -}; - -// Solves the optimization problem `model` with the branch and bound algorithm. -// -// The LP relaxation of `model` is taken by simply converting all integer -// variables to continuous variables. If the underlying solver (from -// `params.lp_solver`) supports this model, the function will succeed. Note that -// no special action is taken to relax SOS constraints, so if Gurobi is your -// underlying solver, you will solve a MIP at each node. -// -// Callers must ensure that the underlying solver for the LP relaxation is -// linked in their binary. -absl::StatusOr SolveWithBranchAndBound( - const operations_research::math_opt::Model& model, - const BranchAndBoundParameters& params = {}); - -//////////////////////////////////////////////////////////////////////////////// -// Implementation details/visible for testing -//////////////////////////////////////////////////////////////////////////////// - -std::ostream& operator<<(std::ostream& ostr, SimpleSolveResult::Reason reason); -std::ostream& operator<<(std::ostream& ostr, - const SimpleSolveResult::Stats& stats); - -// Has termination reason kNoSolutionFound, trivial primal and dual bounds, -// and empty variable_values. -SimpleSolveResult TrivialSolveResult(bool is_maximize, - const SimpleSolveResult::Stats& stats); - -// Has termination reason kError, trivial primal and dual bounds, and empty -// variable_values. -SimpleSolveResult ErrorSolveResult(bool is_maximize, - const SimpleSolveResult::Stats& stats); - -// Has termination reason kInfeasible, trivial primal and dual bounds, and empty -// variable_values. -SimpleSolveResult InfeasibleSolveResult(bool is_maximize, - const SimpleSolveResult::Stats& stats); - -// Has termination reason kInfeasibleOrUnbounded, trivial primal and dual -// bounds, and empty variable_values. -SimpleSolveResult InfeasibleOrUnboundedSolveResult( - bool is_maximize, const SimpleSolveResult::Stats& stats); - -} // namespace operations_research_example - -#endif // OR_TOOLS_MATH_OPT_EXAMPLES_CPP_BRANCH_AND_BOUND_H_ diff --git a/ortools/math_opt/samples/cpp/branch_and_bound_main.cc b/ortools/math_opt/samples/cpp/branch_and_bound_main.cc deleted file mode 100644 index 207c44d3fe..0000000000 --- a/ortools/math_opt/samples/cpp/branch_and_bound_main.cc +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2010-2022 Google LLC -// 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 -#include - -#include "absl/flags/flag.h" -#include "absl/status/status.h" -#include "absl/strings/str_cat.h" -#include "absl/time/time.h" -#include "devtools/build/runtime/get_runfiles_dir.h" -#include "ortools/base/init_google.h" -#include "ortools/base/logging.h" -#include "ortools/base/status_macros.h" -#include "ortools/math_opt/cpp/math_opt.h" -#include "ortools/math_opt/examples/cpp/branch_and_bound.h" -#include "ortools/math_opt/io/mps_converter.h" - -// NOLINTBEGIN(whitespace/line_length) -// clang-format off -// See -// cs/operations_research_data/MIP_MIPLIB/BUILD symbol:miplib2017_tiny_filegroup -// for values. -// clang-format on -// NOLINTEND(whitespace/line_length) -ABSL_FLAG(std::string, instance, "flugpl", "A miplib problem to solve"); -ABSL_FLAG(operations_research::math_opt::SolverType, lp_solver, - operations_research::math_opt::SolverType::kGlop, - "The underlying LP solver to use."); -ABSL_FLAG(absl::Duration, time_limit, absl::Minutes(1), - "A limit on how long to run the solver."); - -namespace operations_research_example { -namespace { - -namespace math_opt = ::operations_research::math_opt; - -absl::Status Main() { - // TODO(b/303820831): figure out how to make this work in open source. - ASSIGN_OR_RETURN( - const math_opt::ModelProto model_proto, - math_opt::ReadMpsFile(devtools_build::GetDataDependencyFilepath( - absl::StrCat("operations_research_data/" - "MIP_MIPLIB/miplib2017/", - absl::GetFlag(FLAGS_instance), ".mps.gz")))); - ASSIGN_OR_RETURN(const std::unique_ptr model, - math_opt::Model::FromModelProto(model_proto)); - const BranchAndBoundParameters params = { - .lp_solver = absl::GetFlag(FLAGS_lp_solver), - .enable_output = true, - .time_limit = absl::GetFlag(FLAGS_time_limit)}; - ASSIGN_OR_RETURN(SimpleSolveResult result, - SolveWithBranchAndBound(*model, params)); - return absl::OkStatus(); -} - -} // namespace -} // namespace operations_research_example - -int main(int argc, char** argv) { - InitGoogle(argv[0], &argc, &argv, true); - const absl::Status status = operations_research_example::Main(); - if (!status.ok()) { - LOG(QFATAL) << status; - } - return 0; -} diff --git a/ortools/math_opt/samples/cpp/cocktail_hour.cc b/ortools/math_opt/samples/cpp/cocktail_hour.cc index 3017f4ccf1..27485e1af5 100644 --- a/ortools/math_opt/samples/cpp/cocktail_hour.cc +++ b/ortools/math_opt/samples/cpp/cocktail_hour.cc @@ -255,7 +255,7 @@ absl::StatusOr SolveForMenu( ASSIGN_OR_RETURN(const math_opt::SolveResult result, math_opt::Solve(model, math_opt::SolverType::kGscip, args)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); Menu menu; for (const absl::string_view ingredient : kIngredients) { if (result.variable_values().at(ingredient_vars.at(ingredient)) > 0.5) { diff --git a/ortools/math_opt/samples/cpp/cutting_stock.cc b/ortools/math_opt/samples/cpp/cutting_stock.cc index 12e8370763..1ebf5c4c43 100644 --- a/ortools/math_opt/samples/cpp/cutting_stock.cc +++ b/ortools/math_opt/samples/cpp/cutting_stock.cc @@ -143,7 +143,7 @@ absl::StatusOr> BestConfiguration( board_length); ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result, math_opt::Solve(model, math_opt::SolverType::kCpSat)); - RETURN_IF_ERROR(solve_result.termination.IsOptimal()); + RETURN_IF_ERROR(solve_result.termination.EnsureIsOptimal()); Configuration config; for (int i = 0; i < num_pieces; ++i) { const int use = static_cast( @@ -192,7 +192,7 @@ absl::StatusOr SolveCuttingStock( int pricing_round = 0; while (true) { ASSIGN_OR_RETURN(math_opt::SolveResult solve_result, solver->Solve()); - RETURN_IF_ERROR(solve_result.termination.IsOptimal()) + RETURN_IF_ERROR(solve_result.termination.EnsureIsOptimal()) << " at iteration " << pricing_round; if (!solve_result.has_dual_feasible_solution()) { // MathOpt does not require solvers to return a dual solution on optimal, @@ -224,7 +224,7 @@ absl::StatusOr SolveCuttingStock( } ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result, math_opt::Solve(model, math_opt::SolverType::kCpSat)); - RETURN_IF_ERROR(solve_result.termination.IsOptimalOrFeasible()) + RETURN_IF_ERROR(solve_result.termination.EnsureIsOptimalOrFeasible()) << " in final cutting stock MIP"; CuttingStockSolution solution; for (const auto& [config, var] : configs) { diff --git a/ortools/math_opt/samples/cpp/facility_lp_benders.cc b/ortools/math_opt/samples/cpp/facility_lp_benders.cc index 65002b6a6f..c2daeebfa2 100644 --- a/ortools/math_opt/samples/cpp/facility_lp_benders.cc +++ b/ortools/math_opt/samples/cpp/facility_lp_benders.cc @@ -248,7 +248,7 @@ absl::Status FullProblem(const FacilityLocationInstance& instance, } ASSIGN_OR_RETURN(const math_opt::SolveResult result, math_opt::Solve(model, solver_type)); - RETURN_IF_ERROR(result.termination.IsOptimal()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); std::cout << "Full problem optimal objective: " << absl::StrFormat("%.9f", result.objective_value()) << std::endl; @@ -606,7 +606,7 @@ absl::Status Benders(const FacilityLocationInstance& instance, // Solve and process first stage. ASSIGN_OR_RETURN(const math_opt::SolveResult first_stage_result, first_stage_solver->Solve()); - RETURN_IF_ERROR(first_stage_result.termination.IsOptimal()) + RETURN_IF_ERROR(first_stage_result.termination.EnsureIsOptimal()) << " in first stage problem"; for (int j = 0; j < num_facilities; j++) { z_values[j] = first_stage_result.variable_values().at(first_stage.z[j]); diff --git a/ortools/math_opt/samples/cpp/graph_coloring.cc b/ortools/math_opt/samples/cpp/graph_coloring.cc index fb5926ebab..a0763b19a1 100644 --- a/ortools/math_opt/samples/cpp/graph_coloring.cc +++ b/ortools/math_opt/samples/cpp/graph_coloring.cc @@ -110,7 +110,7 @@ absl::StatusOr SolveGraphColoring( // solve the model and check the result ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kCpSat)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); // build solution from solver output GraphColoringSolution solution; diff --git a/ortools/math_opt/samples/cpp/integer_programming.cc b/ortools/math_opt/samples/cpp/integer_programming.cc index b64ad20199..eaa9b985c0 100644 --- a/ortools/math_opt/samples/cpp/integer_programming.cc +++ b/ortools/math_opt/samples/cpp/integer_programming.cc @@ -55,7 +55,7 @@ absl::Status Main() { ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kGscip)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); // A feasible solution is always available on termination reason kOptimal, and // kFeasible, but in the later case the solution may be sub-optimal. std::cout << "Problem solved in " << result.solve_time() << std::endl; diff --git a/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc b/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc index f4c43bd1c7..0e4fa7a293 100644 --- a/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc +++ b/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc @@ -240,7 +240,7 @@ absl::StatusOr SolveMip(const Graph graph, model.Minimize(flow_model.cost); ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kGscip)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); std::cout << "MIP Solution with 2 side constraints" << std::endl; std::cout << absl::StrFormat("MIP objective value: %6.3f", result.objective_value()) @@ -263,7 +263,7 @@ absl::Status SolveLinearRelaxation(FlowModel& flow_model, const Graph& graph, math_opt::Model& model = *flow_model.model; ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kGscip)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); std::cout << "LP relaxation with 2 side constraints" << std::endl; std::cout << absl::StrFormat("LP objective value: %6.3f", result.objective_value()) @@ -354,7 +354,7 @@ absl::Status SolveLagrangianRelaxation(const Graph graph, model.Minimize(lagrangian_function); ASSIGN_OR_RETURN(math_opt::SolveResult result, Solve(model, math_opt::SolverType::kGscip)); - RETURN_IF_ERROR(result.termination.IsOptimalOrFeasible()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); const math_opt::VariableMap& vars_val = result.variable_values(); bool feasible = true; diff --git a/ortools/math_opt/samples/cpp/linear_programming.cc b/ortools/math_opt/samples/cpp/linear_programming.cc index 31cbe79274..883a8fdc18 100644 --- a/ortools/math_opt/samples/cpp/linear_programming.cc +++ b/ortools/math_opt/samples/cpp/linear_programming.cc @@ -67,7 +67,7 @@ absl::Status Main() { ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, math_opt::SolverType::kGlop)); - RETURN_IF_ERROR(result.termination.IsOptimal()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); std::cout << "Problem solved in " << result.solve_time() << std::endl; std::cout << "Objective value: " << result.objective_value() << std::endl; diff --git a/ortools/math_opt/samples/cpp/linear_regression.cc b/ortools/math_opt/samples/cpp/linear_regression.cc index e376e3a3bd..5dcdd82e87 100644 --- a/ortools/math_opt/samples/cpp/linear_regression.cc +++ b/ortools/math_opt/samples/cpp/linear_regression.cc @@ -163,7 +163,7 @@ absl::StatusOr Train( args.parameters.enable_output = true; ASSIGN_OR_RETURN(const math_opt::SolveResult result, Solve(model, absl::GetFlag(FLAGS_solver_type), args)); - RETURN_IF_ERROR(result.termination.IsOptimal()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); std::cout << "Training time: " << result.solve_time() << std::endl; return LinearModel{.betas = Values(result.variable_values(), betas)}; } diff --git a/ortools/math_opt/samples/cpp/tsp.cc b/ortools/math_opt/samples/cpp/tsp.cc index d2ca1d9b32..177d9e49cc 100644 --- a/ortools/math_opt/samples/cpp/tsp.cc +++ b/ortools/math_opt/samples/cpp/tsp.cc @@ -291,7 +291,7 @@ absl::StatusOr SolveTsp( }; ASSIGN_OR_RETURN(const math_opt::SolveResult result, math_opt::Solve(model, math_opt::SolverType::kGurobi, args)); - RETURN_IF_ERROR(result.termination.IsOptimal()); + RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); std::cout << "Route length: " << result.objective_value() << std::endl; const std::vector cycles = FindCycles(EdgeValues(edge_vars, result.variable_values())); diff --git a/ortools/service/v1/BUILD.bazel b/ortools/service/v1/BUILD.bazel new file mode 100644 index 0000000000..bdb39a3d11 --- /dev/null +++ b/ortools/service/v1/BUILD.bazel @@ -0,0 +1,41 @@ +# Copyright 2010-2022 Google LLC +# 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. + +load("@rules_python//python:proto.bzl", "py_proto_library") +load("@rules_cc//cc:defs.bzl", "cc_proto_library") + +package(default_visibility = [ + "//ortools/math_opt:__subpackages__", + "//ortools/service:__subpackages__", +]) + +proto_library( + name = "optimization_proto", + srcs = ["optimization.proto"], + deps = [ + "//ortools/service/v1/mathopt:model_parameters_proto", + "//ortools/service/v1/mathopt:model_proto", + "//ortools/service/v1/mathopt:parameters_proto", + "//ortools/service/v1/mathopt:result_proto", + ], +) + +py_proto_library( + name = "optimization_py_pb2", + deps = [":optimization_proto"], +) + +cc_proto_library( + name = "optimization_cc_proto", + deps = [":optimization_proto"], +) diff --git a/ortools/service/v1/mathopt/BUILD.bazel b/ortools/service/v1/mathopt/BUILD.bazel new file mode 100644 index 0000000000..ee2b248ccc --- /dev/null +++ b/ortools/service/v1/mathopt/BUILD.bazel @@ -0,0 +1,151 @@ +# Copyright 2010-2022 Google LLC +# 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. + +load("@rules_python//python:proto.bzl", "py_proto_library") +load("@rules_cc//cc:defs.bzl", "cc_proto_library") + +package(default_visibility = [ + "//ortools/math_opt:__subpackages__", + "//ortools/service:__subpackages__", +]) + +proto_library( + name = "model_proto", + srcs = ["model.proto"], + deps = [":sparse_containers_proto"], +) + +cc_proto_library( + name = "model_cc_proto", + deps = [":model_proto"], +) + +java_proto_library( + name = "model_java_proto", + deps = [":model_proto"], +) + +py_proto_library( + name = "model_py_pb2", + deps = [":model_proto"], +) + +proto_library( + name = "solution_proto", + srcs = ["solution.proto"], + deps = [":sparse_containers_proto"], +) + +py_proto_library( + name = "solution_py_pb2", + deps = [":solution_proto"], +) + +cc_proto_library( + name = "solution_cc_proto", + deps = [":solution_proto"], +) + +java_proto_library( + name = "solution_java_proto", + deps = [":solution_proto"], +) + +proto_library( + name = "result_proto", + srcs = ["result.proto"], + deps = [ + ":solution_proto", + "@com_google_protobuf//:duration_proto", + ], +) + +py_proto_library( + name = "result_py_pb2", + deps = [":result_proto"], +) + +cc_proto_library( + name = "result_cc_proto", + deps = [":result_proto"], +) + +java_proto_library( + name = "result_java_proto", + deps = [":result_proto"], +) + +proto_library( + name = "model_parameters_proto", + srcs = ["model_parameters.proto"], + deps = [ + ":solution_proto", + ":sparse_containers_proto", + ], +) + +cc_proto_library( + name = "model_parameters_cc_proto", + deps = [":model_parameters_proto"], +) + +java_proto_library( + name = "model_parameters_java_proto", + deps = [":model_parameters_proto"], +) + +py_proto_library( + name = "model_parameters_py_pb2", + deps = [":model_parameters_proto"], +) + +proto_library( + name = "sparse_containers_proto", + srcs = ["sparse_containers.proto"], +) + +cc_proto_library( + name = "sparse_containers_cc_proto", + deps = [":sparse_containers_proto"], +) + +java_proto_library( + name = "sparse_containers_java_proto", + deps = [":sparse_containers_proto"], +) + +py_proto_library( + name = "sparse_containers_py_pb2", + deps = [":sparse_containers_proto"], +) + +proto_library( + name = "parameters_proto", + srcs = ["parameters.proto"], + deps = ["@com_google_protobuf//:duration_proto"], +) + +py_proto_library( + name = "parameters_py_pb2", + deps = [":parameters_proto"], +) + +cc_proto_library( + name = "parameters_cc_proto", + deps = [":parameters_proto"], +) + +java_proto_library( + name = "parameters_java_proto", + deps = [":parameters_proto"], +) diff --git a/ortools/service/v1/mathopt/model.proto b/ortools/service/v1/mathopt/model.proto new file mode 100644 index 0000000000..87c2eb5f50 --- /dev/null +++ b/ortools/service/v1/mathopt/model.proto @@ -0,0 +1,309 @@ +// Copyright 2010-2022 Google LLC +// 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. + +// An encoding format for mathematical optimization problems. + +syntax = "proto3"; + +package operations_research.service.v1.mathopt; + +import "ortools/service/v1/mathopt/sparse_containers.proto"; + +option java_multiple_files = true; +option java_package = "com.google.ortools.service.v1.mathopt"; + +option csharp_namespace = "Google.OrTools.Service"; + +// As used below, we define "#variables" = size(VariablesProto.ids). +message VariablesProto { + // Must be nonnegative and strictly increasing. The max(int64) value can't be + // used. + repeated int64 ids = 1; + // Should have length equal to #variables, values in [-inf, inf). + repeated double lower_bounds = 2; + // Should have length equal to #variables, values in (-inf, inf]. + repeated double upper_bounds = 3; + // Should have length equal to #variables. Value is false for continuous + // variables and true for integer variables. + repeated bool integers = 4; + // If not set, assumed to be all empty strings. Otherwise, should have length + // equal to #variables. + // + // All nonempty names must be distinct. TODO(b/169575522): we may relax this. + repeated string names = 5; +} + +message ObjectiveProto { + // false is minimize, true is maximize + bool maximize = 1; + + // Must be finite and not NaN. + double offset = 2; + + // ObjectiveProto terms that are linear in the decision variables. + // + // Requirements: + // * linear_coefficients.ids are elements of VariablesProto.ids. + // * VariablesProto not specified correspond to zero. + // * linear_coefficients.values must all be finite. + // * linear_coefficients.values can be zero, but this just wastes space. + SparseDoubleVectorProto linear_coefficients = 3; + + // Objective terms that are quadratic in the decision variables. + // + // Requirements in addition to those on SparseDoubleMatrixProto messages: + // * Each element of quadratic_coefficients.row_ids and each element of + // quadratic_coefficients.column_ids must be an element of + // VariablesProto.ids. + // * The matrix must be upper triangular: for each i, + // quadratic_coefficients.row_ids[i] <= + // quadratic_coefficients.column_ids[i]. + // + // Notes: + // * Terms not explicitly stored have zero coefficient. + // * Elements of quadratic_coefficients.coefficients can be zero, but this + // just wastes space. + SparseDoubleMatrixProto quadratic_coefficients = 4; + + // Parent messages may have uniqueness requirements on this field; e.g., see + // ModelProto.objectives and AuxiliaryObjectivesUpdatesProto.new_objectives. + string name = 5; + + // For multi-objective problems, the priority of this objective relative to + // the others (lower is more important). This value must be nonnegative. + // Furthermore, each objective priority in the model must be distinct at solve + // time. This condition is not validated at the proto level, so models may + // temporarily have objectives with the same priority. + int64 priority = 6; +} + +// As used below, we define "#linear constraints" = +// size(LinearConstraintsProto.ids). +message LinearConstraintsProto { + // Must be nonnegative and strictly increasing. The max(int64) value can't be + // used. + repeated int64 ids = 1; + // Should have length equal to #linear constraints, values in [-inf, inf). + repeated double lower_bounds = 2; + // Should have length equal to #linear constraints, values in (-inf, inf]. + repeated double upper_bounds = 3; + // If not set, assumed to be all empty strings. Otherwise, should have length + // equal to #linear constraints. + // + // All nonempty names must be distinct. TODO(b/169575522): we may relax this. + repeated string names = 4; +} + +// A single quadratic constraint of the form: +// lb <= sum{linear_terms} + sum{quadratic_terms} <= ub. +// +// If a variable involved in this constraint is deleted, it is treated as if it +// were set to zero. +message QuadraticConstraintProto { + // Terms that are linear in the decision variables. + // + // In addition to requirements on SparseDoubleVectorProto messages we require + // that: + // * linear_terms.ids are elements of VariablesProto.ids. + // * linear_terms.values must all be finite and not-NaN. + // + // Notes: + // * Variable ids omitted have a corresponding coefficient of zero. + // * linear_terms.values can be zero, but this just wastes space. + SparseDoubleVectorProto linear_terms = 1; + + // Terms that are quadratic in the decision variables. + // + // In addition to requirements on SparseDoubleMatrixProto messages we require + // that: + // * Each element of quadratic_terms.row_ids and each element of + // quadratic_terms.column_ids must be an element of VariablesProto.ids. + // * The matrix must be upper triangular: for each i, + // quadratic_terms.row_ids[i] <= quadratic_terms.column_ids[i]. + // + // Notes: + // * Terms not explicitly stored have zero coefficient. + // * Elements of quadratic_terms.coefficients can be zero, but this just + // wastes space. + SparseDoubleMatrixProto quadratic_terms = 2; + + // Must have value in [-inf, inf), and be less than or equal to `upper_bound`. + double lower_bound = 3; + + // Must have value in (-inf, inf], and be greater than or equal to + // `lower_bound`. + double upper_bound = 4; + + // Parent messages may have uniqueness requirements on this field; e.g., see + // ModelProto.quadratic_constraints and + // QuadraticConstraintUpdatesProto.new_constraints. + string name = 5; +} + +// A single second-order cone constraint of the form: +// +// ||`arguments_to_norm`||_2 <= `upper_bound`, +// +// where `upper_bound` and each element of `arguments_to_norm` are linear +// expressions. +// +// If a variable involved in this constraint is deleted, it is treated as if it +// were set to zero. +message SecondOrderConeConstraintProto { + LinearExpressionProto upper_bound = 1; + repeated LinearExpressionProto arguments_to_norm = 2; + + // Parent messages may have uniqueness requirements on this field; e.g., see + // `ModelProto.second_order_cone_constraints` and + // `SecondOrderConeConstraintUpdatesProto.new_constraints`. + string name = 3; +} + +// Data for representing a single SOS1 or SOS2 constraint. +// +// If a variable involved in this constraint is deleted, it is treated as if it +// were set to zero. +message SosConstraintProto { + // The expressions over which to apply the SOS constraint: + // * SOS1: At most one element takes a nonzero value. + // * SOS2: At most two elements take nonzero values, and they must be + // adjacent in the repeated ordering. + repeated LinearExpressionProto expressions = 1; + + // Either empty or of equal length to expressions. If empty, default weights + // are 1, 2, ... + // If present, the entries must be unique. + repeated double weights = 2; + + // Parent messages may have uniqueness requirements on this field; e.g., see + // ModelProto.sos1_constraints and SosConstraintUpdatesProto.new_constraints. + string name = 3; +} + +// Data for representing a single indicator constraint of the form: +// Variable(indicator_id) = (activate_on_zero ? 0 : 1) ⇒ +// lower_bound <= expression <= upper_bound. +// +// If a variable involved in this constraint (either the indicator, or appearing +// in `expression`) is deleted, it is treated as if it were set to zero. In +// particular, deleting the indicator variable means that the indicator +// constraint is vacuous if `activate_on_zero` is false, and that it is +// equivalent to a linear constraint if `activate_on_zero` is true. +message IndicatorConstraintProto { + // An ID corresponding to a binary variable, or unset. If unset, the indicator + // constraint is ignored. If set, we require that: + // * VariablesProto.integers[indicator_id] = true, + // * VariablesProto.lower_bounds[indicator_id] >= 0, + // * VariablesProto.upper_bounds[indicator_id] <= 1. + // These conditions are not validated by MathOpt, but if not satisfied will + // lead to the solver returning an error upon solving. + optional int64 indicator_id = 1; + + // If true, then if the indicator variable takes value 0, the implied + // constraint must hold. Otherwise, if the indicator variable takes value 1, + // then the implied constraint must hold. + bool activate_on_zero = 6; + + // Must be a valid linear expression with respect to the containing model: + // * All stated conditions on `SparseDoubleVectorProto`, + // * All elements of `expression.values` must be finite, + // * `expression.ids` are a subset of `VariablesProto.ids`. + SparseDoubleVectorProto expression = 2; + + // Must have value in [-inf, inf); cannot be NaN. + double lower_bound = 3; + + // Must have value in (-inf, inf]; cannot be NaN. + double upper_bound = 4; + + // Parent messages may have uniqueness requirements on this field; e.g., see + // `ModelProto.indicator_constraints` and + // `IndicatorConstraintUpdatesProto.new_constraints`. + string name = 5; +} + +// An optimization problem. +// MathOpt supports: +// - Continuous and integer decision variables with optional finite bounds. +// - Linear and quadratic objectives (single or multiple objectives), either +// minimized or maximized. +// - A number of constraints types, including: +// * Linear constraints +// * Quadratic constraints +// * Second-order cone constraints +// * Logical constraints +// > SOS1 and SOS2 constraints +// > Indicator constraints +// +// By default, constraints are represented in "id-to-data" maps. However, we +// represent linear constraints in a more efficient "struct-of-arrays" format. +message ModelProto { + string name = 1; + VariablesProto variables = 2; + + // The primary objective in the model. + ObjectiveProto objective = 3; + + // Auxiliary objectives for use in multi-objective models. + // + // Map key IDs must be in [0, max(int64)). Each priority, and each nonempty + // name, must be unique and also distinct from the primary `objective`. + map auxiliary_objectives = 10; + + LinearConstraintsProto linear_constraints = 4; + + // The variable coefficients for the linear constraints. + // + // If a variable involved in this constraint is deleted, it is treated as if + // it were set to zero. + // + // Requirements: + // * linear_constraint_matrix.row_ids are elements of linear_constraints.ids. + // * linear_constraint_matrix.column_ids are elements of variables.ids. + // * Matrix entries not specified are zero. + // * linear_constraint_matrix.values must all be finite. + SparseDoubleMatrixProto linear_constraint_matrix = 5; + + // Mapped constraints (i.e., stored in "constraint ID"-to-"constraint data" + // map). For each subsequent submessage, we require that: + // * Each key is in [0, max(int64)). + // * Each key is unique in its respective map (but not necessarily across + // constraint types) + // * Each value contains a name field (called `name`), and each nonempty + // name must be distinct across all map entries (but not necessarily + // across constraint types). + + // Quadratic constraints in the model. + map quadratic_constraints = 6; + + // Second-order cone constraints in the model. + map second_order_cone_constraints = 11; + + // SOS1 constraints in the model, which constrain that at most one + // `expression` can be nonzero. The optional `weights` entries are an + // implementation detail used by the solver to (hopefully) converge more + // quickly. In more detail, solvers may (or may not) use these weights to + // select branching decisions that produce "balanced" children nodes. + map sos1_constraints = 7; + + // SOS2 constraints in the model, which constrain that at most two entries of + // `expression` can be nonzero, and they must be adjacent in their ordering. + // If no `weights` are provided, this ordering is their linear ordering in the + // `expressions` list; if `weights` are presented, the ordering is taken with + // respect to these values in increasing order. + map sos2_constraints = 8; + + // Indicator constraints in the model, which enforce that, if a binary + // "indicator variable" is set to one, then an "implied constraint" must hold. + map indicator_constraints = 9; +} diff --git a/ortools/service/v1/mathopt/model_parameters.proto b/ortools/service/v1/mathopt/model_parameters.proto new file mode 100644 index 0000000000..0cd7660a57 --- /dev/null +++ b/ortools/service/v1/mathopt/model_parameters.proto @@ -0,0 +1,108 @@ +// Copyright 2010-2022 Google LLC +// 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. + +// Solve parameters that are specific to the model. + +syntax = "proto3"; + +package operations_research.service.v1.mathopt; + +import "ortools/service/v1/mathopt/solution.proto"; +import "ortools/service/v1/mathopt/sparse_containers.proto"; + +option java_multiple_files = true; +option java_package = "com.google.ortools.service.v1.mathopt"; + +option csharp_namespace = "Google.OrTools.Service"; + +// A suggested starting solution for the solver. +// +// MIP solvers generally only want primal information (`variable_values`), while +// LP solvers want both primal and dual information (`dual_values`). +// +// Many MIP solvers can work with: (1) partial solutions that do not specify all +// variables or (2) infeasible solutions. In these cases, solvers typically +// solve a sub-MIP to complete/correct the hint. +// +// How the hint is used by the solver, if at all, is highly dependent on the +// solver, the problem type, and the algorithm used. The most reliable way to +// ensure your hint has an effect is to read the underlying solvers logs with +// and without the hint. +// +// Simplex-based LP solvers typically prefer an initial basis to a solution hint +// (they need to crossover to convert the hint to a basic feasible solution +// otherwise). +// +// TODO(b/183616124): Add hint-priorities to variable_values. +message SolutionHintProto { + // A possibly partial assignment of values to the primal variables of the + // problem. The solver-independent requirements for this sub-message are: + // * variable_values.ids are elements of VariablesProto.ids. + // * variable_values.values must all be finite. + SparseDoubleVectorProto variable_values = 1; + + // A (potentially partial) assignment of values to the linear constraints of + // the problem. + // + // Requirements: + // * dual_values.ids are elements of LinearConstraintsProto.ids. + // * dual_values.values must all be finite. + SparseDoubleVectorProto dual_values = 2; +} + +// TODO(b/183628247): follow naming convention in fields below. +// Parameters to control a single solve that that are specific to the input +// model (see SolveParametersProto for model independent parameters). +message ModelSolveParametersProto { + // Filter that is applied to all returned sparse containers keyed by variables + // in PrimalSolutionProto and PrimalRayProto + // (PrimalSolutionProto.variable_values, PrimalRayProto.variable_values). + // + // Requirements: + // * filtered_ids are elements of VariablesProto.ids. + SparseVectorFilterProto variable_values_filter = 1; + + // Filter that is applied to all returned sparse containers keyed by linear + // constraints in DualSolutionProto and DualRay + // (DualSolutionProto.dual_values, DualRay.dual_values). + // + // Requirements: + // * filtered_ids are elements of LinearConstraints.ids. + SparseVectorFilterProto dual_values_filter = 2; + + // Filter that is applied to all returned sparse containers keyed by variables + // in DualSolutionProto and DualRay (DualSolutionProto.reduced_costs, + // DualRay.reduced_costs). + // + // Requirements: + // * filtered_ids are elements of VariablesProto.ids. + SparseVectorFilterProto reduced_costs_filter = 3; + + // Optional initial basis for warm starting simplex LP solvers. If set, it is + // expected to be valid according to `ValidateBasis` in + // `validators/solution_validator.h` for the current `ModelSummary`. + BasisProto initial_basis = 4; + + // Optional solution hints. If the underlying solver only accepts a single + // hint, the first hint is used. + repeated SolutionHintProto solution_hints = 5; + + // Optional branching priorities. Variables with higher values will be + // branched on first. Variables for which priorities are not set get the + // solver's default priority (usually zero). + // + // Requirements: + // * branching_priorities.values must be finite. + // * branching_priorities.ids must be elements of VariablesProto.ids. + SparseInt32VectorProto branching_priorities = 6; +} diff --git a/ortools/service/v1/mathopt/parameters.proto b/ortools/service/v1/mathopt/parameters.proto new file mode 100644 index 0000000000..f26b4ca095 --- /dev/null +++ b/ortools/service/v1/mathopt/parameters.proto @@ -0,0 +1,323 @@ +// Copyright 2010-2022 Google LLC +// 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. + +// Configures the behavior of a MathOpt solver. + +syntax = "proto3"; + +package operations_research.service.v1.mathopt; + +import "google/protobuf/duration.proto"; + +option java_multiple_files = true; +option java_package = "com.google.ortools.service.v1.mathopt"; + +option csharp_namespace = "Google.OrTools.Service"; + +// The solvers supported by MathOpt. +enum SolverTypeProto { + SOLVER_TYPE_UNSPECIFIED = 0; + + // Solving Constraint Integer Programs (SCIP) solver (third party). + // + // Supports LP, MIP, and nonconvex integer quadratic problems. No dual data + // for LPs is returned though. Prefer GLOP for LPs. + SOLVER_TYPE_GSCIP = 1; + + // Gurobi solver (third party). + // + // Supports LP, MIP, and nonconvex integer quadratic problems. Generally the + // fastest option, but has special licensing. + SOLVER_TYPE_GUROBI = 2; + + // Google's Glop solver. + // + // Supports LP with primal and dual simplex methods. + SOLVER_TYPE_GLOP = 3; + + // Google's CP-SAT solver. + // + // Supports problems where all variables are integer and bounded (or implied + // to be after presolve). Experimental support to rescale and discretize + // problems with continuous variables. + SOLVER_TYPE_CP_SAT = 4; + + // Google's PDLP solver. + // + // Supports LP and convex diagonal quadratic objectives. Uses first order + // methods rather than simplex. Can solve very large problems. + SOLVER_TYPE_PDLP = 5; + + // GNU Linear Programming Kit (GLPK) (third party). + // + // Supports MIP and LP. + // + // Thread-safety: GLPK use thread-local storage for memory allocations. As a + // consequence Solver instances must be destroyed on the same thread as they + // are created or GLPK will crash. It seems OK to call Solver::Solve() from + // another thread than the one used to create the Solver but it is not + // documented by GLPK and should be avoided. + // + // When solving a LP with the presolver, a solution (and the unbound rays) are + // only returned if an optimal solution has been found. Else nothing is + // returned. See glpk-5.0/doc/glpk.pdf page #40 available from glpk-5.0.tar.gz + // for details. + SOLVER_TYPE_GLPK = 6; + + // The Operator Splitting Quadratic Program (OSQP) solver (third party). + // + // Supports continuous problems with linear constraints and linear or convex + // quadratic objectives. Uses a first-order method. + SOLVER_TYPE_OSQP = 7; + + // The Embedded Conic Solver (ECOS) (third party). + // + // Supports LP and SOCP problems. Uses interior point methods (barrier). + SOLVER_TYPE_ECOS = 8; + + // The Splitting Conic Solver (SCS) (third party). + // + // Supports LP and SOCP problems. Uses a first-order method. + SOLVER_TYPE_SCS = 9; + + // The HiGHS Solver (third party). + // + // Supports LP and MIP problems (convex QPs are unimplemented). + SOLVER_TYPE_HIGHS = 10; + + // MathOpt's reference implementation of a MIP solver. + // + // Slow/not recommended for production. Not an LP solver (no dual information + // returned). + SOLVER_TYPE_SANTORINI = 11; +} + +// Selects an algorithm for solving linear programs. +enum LPAlgorithmProto { + LP_ALGORITHM_UNSPECIFIED = 0; + + // The (primal) simplex method. Typically can provide primal and dual + // solutions, primal/dual rays on primal/dual unbounded problems, and a basis. + LP_ALGORITHM_PRIMAL_SIMPLEX = 1; + + // The dual simplex method. Typically can provide primal and dual + // solutions, primal/dual rays on primal/dual unbounded problems, and a basis. + LP_ALGORITHM_DUAL_SIMPLEX = 2; + + // The barrier method, also commonly called an interior point method (IPM). + // Can typically give both primal and dual solutions. Some implementations can + // also produce rays on unbounded/infeasible problems. A basis is not given + // unless the underlying solver does "crossover" and finishes with simplex. + LP_ALGORITHM_BARRIER = 3; + + // An algorithm based around a first-order method. These will typically + // produce both primal and dual solutions, and potentially also certificates + // of primal and/or dual infeasibility. First-order methods typically will + // provide solutions with lower accuracy, so users should take care to set + // solution quality parameters (e.g., tolerances) and to validate solutions. + LP_ALGORITHM_FIRST_ORDER = 4; +} + +// Effort level applied to an optional task while solving (see +// SolveParametersProto for use). +// +// Emphasis is used to configure a solver feature as follows: +// * If a solver doesn't support the feature, only UNSPECIFIED will always be +// valid, any other setting will typically an invalid argument error (some +// solvers may also accept OFF). +// * If the solver supports the feature: +// - When set to UNSPECIFIED, the underlying default is used. +// - When the feature cannot be turned off, OFF will return an error. +// - If the feature is enabled by default, the solver default is typically +// mapped to MEDIUM. +// - If the feature is supported, LOW, MEDIUM, HIGH, and VERY HIGH will never +// give an error, and will map onto their best match. +enum EmphasisProto { + EMPHASIS_UNSPECIFIED = 0; + EMPHASIS_OFF = 1; + EMPHASIS_LOW = 2; + EMPHASIS_MEDIUM = 3; + EMPHASIS_HIGH = 4; + EMPHASIS_VERY_HIGH = 5; +} + +// Parameters to control a single solve. +// +// Contains both parameters common to all solvers e.g. time_limit, and +// parameters for a specific solver, e.g. gscip. If a value is set in both +// common and solver specific field, the solver specific setting is used. +// +// The common parameters that are optional and unset or an enum with value +// unspecified indicate that the solver default is used. +// +// Solver specific parameters for solvers other than the one in use are ignored. +// +// Parameters that depends on the model (e.g. branching priority is set for +// each variable) are passed in ModelSolveParametersProto. +message SolveParametersProto { + // Maximum time a solver should spend on the problem (or infinite if not set). + // + // This value is not a hard limit, solve time may slightly exceed this value. + // This parameter is always passed to the underlying solver, the solver + // default is not used. + google.protobuf.Duration time_limit = 1; + + // Limit on the iterations of the underlying algorithm (e.g. simplex pivots). + // The specific behavior is dependent on the solver and algorithm used, but + // often can give a deterministic solve limit (further configuration may be + // needed, e.g. one thread). + // + // Typically supported by LP, QP, and MIP solvers, but for MIP solvers see + // also node_limit. + optional int64 iteration_limit = 2; + + // Limit on the number of subproblems solved in enumerative search (e.g. + // branch and bound). For many solvers this can be used to deterministically + // limit computation (further configuration may be needed, e.g. one thread). + // + // Typically for MIP solvers, see also iteration_limit. + optional int64 node_limit = 24; + + // The solver stops early if it can prove there are no primal solutions at + // least as good as cutoff. + // + // On an early stop, the solver returns termination reason NO_SOLUTION_FOUND + // and with limit CUTOFF and is not required to give any extra solution + // information. Has no effect on the return value if there is no early stop. + // + // It is recommended that you use a tolerance if you want solutions with + // objective exactly equal to cutoff to be returned. + // + // See the user guide for more details and a comparison with best_bound_limit. + optional double cutoff_limit = 20; + + // The solver stops early as soon as it finds a solution at least this good, + // with termination reason FEASIBLE and limit OBJECTIVE. + optional double objective_limit = 21; + + // The solver stops early as soon as it proves the best bound is at least this + // good, with termination reason FEASIBLE or NO_SOLUTION_FOUND and limit + // OBJECTIVE. + // + // See the user guide for more details and a comparison with cutoff_limit. + optional double best_bound_limit = 22; + + // The solver stops early after finding this many feasible solutions, with + // termination reason FEASIBLE and limit SOLUTION. Must be greater than zero + // if set. It is often used get the solver to stop on the first feasible + // solution found. Note that there is no guarantee on the objective value for + // any of the returned solutions. + // + // Solvers will typically not return more solutions than the solution limit, + // but this is not enforced by MathOpt, see also b/214041169. + // + // Currently supported for Gurobi and SCIP, and for CP-SAT only with value 1. + optional int32 solution_limit = 23; + + // Enables printing the solver implementation traces. The location of those + // traces depend on the solver. For SCIP and Gurobi this will be the standard + // output streams. For Glop and CP-SAT this will LOG(INFO). + // + // Note that if the solver supports message callback and the user registers a + // callback for it, then this parameter value is ignored and no traces are + // printed. + bool enable_output = 3; + + // If set, it must be >= 1. + optional int32 threads = 4; + + // Seed for the pseudo-random number generator in the underlying + // solver. Note that all solvers use pseudo-random numbers to select things + // such as perturbation in the LP algorithm, for tie-break-up rules, and for + // heuristic fixings. Varying this can have a noticeable impact on solver + // behavior. + // + // Although all solvers have a concept of seeds, note that valid values + // depend on the actual solver. + // - Gurobi: [0:GRB_MAXINT] (which as of Gurobi 9.0 is 2x10^9). + // - GSCIP: [0:2147483647] (which is MAX_INT or kint32max or 2^31-1). + // - GLOP: [0:2147483647] (same as above) + // In all cases, the solver will receive a value equal to: + // MAX(0, MIN(MAX_VALID_VALUE_FOR_SOLVER, random_seed)). + optional int32 random_seed = 5; + + // An absolute optimality tolerance (primarily) for MIP solvers. + // + // The absolute GAP is the absolute value of the difference between: + // * the objective value of the best feasible solution found, + // * the dual bound produced by the search. + // The solver can stop once the absolute GAP is at most absolute_gap_tolerance + // (when set), and return TERMINATION_REASON_OPTIMAL. + // + // Must be >= 0 if set. + // + // See also relative_gap_tolerance. + optional double absolute_gap_tolerance = 18; + + // A relative optimality tolerance (primarily) for MIP solvers. + // + // The relative GAP is a normalized version of the absolute GAP (defined on + // absolute_gap_tolerance), where the normalization is solver-dependent, e.g. + // the absolute GAP divided by the objective value of the best feasible + // solution found. + // + // The solver can stop once the relative GAP is at most relative_gap_tolerance + // (when set), and return TERMINATION_REASON_OPTIMAL. + // + // Must be >= 0 if set. + // + // See also absolute_gap_tolerance. + optional double relative_gap_tolerance = 17; + + // Maintain up to `solution_pool_size` solutions while searching. The solution + // pool generally has two functions: + // (1) For solvers that can return more than one solution, this limits how + // many solutions will be returned. + // (2) Some solvers may run heuristics using solutions from the solution + // pool, so changing this value may affect the algorithm's path. + // To force the solver to fill the solution pool, e.g. with the n best + // solutions, requires further, solver specific configuration. + optional int32 solution_pool_size = 25; + + // The algorithm for solving a linear program. If LP_ALGORITHM_UNSPECIFIED, + // use the solver default algorithm. + // + // For problems that are not linear programs but where linear programming is + // a subroutine, solvers may use this value. E.g. MIP solvers will typically + // use this for the root LP solve only (and use dual simplex otherwise). + LPAlgorithmProto lp_algorithm = 6; + + // Effort on simplifying the problem before starting the main algorithm, or + // the solver default effort level if EMPHASIS_UNSPECIFIED. + EmphasisProto presolve = 7; + + // Effort on getting a stronger LP relaxation (MIP only), or the solver + // default effort level if EMPHASIS_UNSPECIFIED. + // + // NOTE: disabling cuts may prevent callbacks from having a chance to add cuts + // at MIP_NODE, this behavior is solver specific. + EmphasisProto cuts = 8; + + // Effort in finding feasible solutions beyond those encountered in the + // complete search procedure (MIP only), or the solver default effort level if + // EMPHASIS_UNSPECIFIED. + EmphasisProto heuristics = 9; + + // Effort in rescaling the problem to improve numerical stability, or the + // solver default effort level if EMPHASIS_UNSPECIFIED. + EmphasisProto scaling = 10; + + reserved 12, 13, 14, 15, 16, 19, 26, 27; + + reserved 11; // Deleted +} diff --git a/ortools/service/v1/mathopt/result.proto b/ortools/service/v1/mathopt/result.proto new file mode 100644 index 0000000000..35e372a0c1 --- /dev/null +++ b/ortools/service/v1/mathopt/result.proto @@ -0,0 +1,337 @@ +// Copyright 2010-2022 Google LLC +// 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. + +// The result of solving a MathOpt model, both the Solution and metadata. + +syntax = "proto3"; + +package operations_research.service.v1.mathopt; + +import "google/protobuf/duration.proto"; +import "ortools/service/v1/mathopt/solution.proto"; + +option java_multiple_files = true; +option java_package = "com.google.ortools.service.v1.mathopt"; + +option csharp_namespace = "Google.OrTools.Service"; + +// Problem feasibility status as claimed by the solver (solver is not required +// to return a certificate for the claim). +enum FeasibilityStatusProto { + // Guard value representing no status. + FEASIBILITY_STATUS_UNSPECIFIED = 0; + + // Solver does not claim a status. + FEASIBILITY_STATUS_UNDETERMINED = 1; + + // Solver claims the problem is feasible. + FEASIBILITY_STATUS_FEASIBLE = 2; + + // Solver claims the problem is infeasible. + FEASIBILITY_STATUS_INFEASIBLE = 3; +} + +// Feasibility status of the primal problem and its dual (or the dual of a +// continuous relaxation) as claimed by the solver. The solver is not required +// to return a certificate for the claim (e.g. the solver may claim primal +// feasibility without returning a primal feasible solutuion). This combined +// status gives a comprehensive description of a solver's claims about +// feasibility and unboundedness of the solved problem. For instance, +// +// * a feasible status for primal and dual problems indicates the primal is +// feasible and bounded and likely has an optimal solution (guaranteed for +// problems without non-linear constraints). +// * a primal feasible and a dual infeasible status indicates the primal +// problem is unbounded (i.e. has arbitrarily good solutions). +// +// Note that a dual infeasible status by itself (i.e. accompanied by an +// undetermined primal status) does not imply the primal problem is unbounded as +// we could have both problems be infeasible. Also, while a primal and dual +// feasible status may imply the existence of an optimal solution, it does not +// guarantee the solver has actually found such optimal solution. +message ProblemStatusProto { + // Status for the primal problem. + FeasibilityStatusProto primal_status = 1; + + // Status for the dual problem (or for the dual of a continuous relaxation). + FeasibilityStatusProto dual_status = 2; + + // If true, the solver claims the primal or dual problem is infeasible, but + // it does not know which (or if both are infeasible). Can be true only when + // primal_problem_status = dual_problem_status = kUndetermined. This extra + // information is often needed when preprocessing determines there is no + // optimal solution to the problem (but can't determine if it is due to + // infeasibility, unboundedness, or both). + bool primal_or_dual_infeasible = 3; +} + +message SolveStatsProto { + // Elapsed wall clock time as measured by math_opt, roughly the time inside + // Solver::Solve(). Note: this does not include work done building the model. + google.protobuf.Duration solve_time = 1; + + // Fields previously used for `best_primal_bound` and `best_dual_bound`. + reserved 2, 3; + + // Feasibility statuses for primal and dual problems. + ProblemStatusProto problem_status = 4; + + int64 simplex_iterations = 5; + + int64 barrier_iterations = 6; + + int64 first_order_iterations = 8; + + int64 node_count = 7; + + // Next id: 9 +} + +// The reason a call to Solve() terminates. +enum TerminationReasonProto { + TERMINATION_REASON_UNSPECIFIED = 0; + + // A provably optimal solution (up to numerical tolerances) has been found. + TERMINATION_REASON_OPTIMAL = 1; + + // The primal problem has no feasible solutions. + TERMINATION_REASON_INFEASIBLE = 2; + + // The primal problem is feasible and arbitrarily good solutions can be + // found along a primal ray. + TERMINATION_REASON_UNBOUNDED = 3; + + // The primal problem is either infeasible or unbounded. More details on the + // problem status may be available in solve_stats.problem_status. Note that + // Gurobi's unbounded status may be mapped here. + TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED = 4; + + // The problem was solved to one of the criteria above (Optimal, Infeasible, + // Unbounded, or InfeasibleOrUnbounded), but one or more tolerances was not + // met. Some primal/dual solutions/rays be present, but either they will be + // slightly infeasible, or (if the problem was nearly optimal) their may be + // a gap between the best solution objective and best objective bound. + // + // Users can still query primal/dual solutions/rays and solution stats, but + // they are responsible for dealing with the numerical imprecision. + TERMINATION_REASON_IMPRECISE = 5; + + // The optimizer reached some kind of limit and a primal feasible solution + // is returned. See SolveResultProto.limit_detail for detailed description of + // the kind of limit that was reached. + TERMINATION_REASON_FEASIBLE = 9; + + // The optimizer reached some kind of limit and it did not find a primal + // feasible solution. See SolveResultProto.limit_detail for detailed + // description of the kind of limit that was reached. + TERMINATION_REASON_NO_SOLUTION_FOUND = 6; + + // The algorithm stopped because it encountered unrecoverable numerical + // error. No solution information is available. + TERMINATION_REASON_NUMERICAL_ERROR = 7; + + // The algorithm stopped because of an error not covered by one of the + // statuses defined above. No solution information is available. + TERMINATION_REASON_OTHER_ERROR = 8; +} + +// When a Solve() stops early with TerminationReasonProto FEASIBLE or +// NO_SOLUTION_FOUND, the specific limit that was hit. +enum LimitProto { + // Used as a null value when we terminated not from a limit (e.g. + // TERMINATION_REASON_OPTIMAL). + LIMIT_UNSPECIFIED = 0; + + // The underlying solver does not expose which limit was reached. + LIMIT_UNDETERMINED = 1; + + // An iterative algorithm stopped after conducting the maximum number of + // iterations (e.g. simplex or barrier iterations). + LIMIT_ITERATION = 2; + + // The algorithm stopped after a user-specified computation time. + LIMIT_TIME = 3; + + // A branch-and-bound algorithm stopped because it explored a maximum number + // of nodes in the branch-and-bound tree. + LIMIT_NODE = 4; + + // The algorithm stopped because it found the required number of solutions. + // This is often used in MIPs to get the solver to return the first feasible + // solution it encounters. + LIMIT_SOLUTION = 5; + + // The algorithm stopped because it ran out of memory. + LIMIT_MEMORY = 6; + + // The solver was run with a cutoff (e.g. SolveParameters.cutoff_limit was + // set) on the objective, indicating that the user did not want any solution + // worse than the cutoff, and the solver concluded there were no solutions at + // least as good as the cutoff. Typically no further solution information is + // provided. + LIMIT_CUTOFF = 12; + + // The algorithm stopped because it either found a solution or a bound better + // than a limit set by the user (see SolveParameters.objective_limit and + // SolveParameters.best_bound_limit). + LIMIT_OBJECTIVE = 7; + + // The algorithm stopped because the norm of an iterate became too large. + LIMIT_NORM = 8; + + // The algorithm stopped because of an interrupt signal or a user interrupt + // request. + LIMIT_INTERRUPTED = 9; + + // The algorithm stopped because it was unable to continue making progress + // towards the solution. + LIMIT_SLOW_PROGRESS = 10; + + // The algorithm stopped due to a limit not covered by one of the above. Note + // that LIMIT_UNDETERMINED is used when the reason cannot be determined, and + // LIMIT_OTHER is used when the reason is known but does not fit into any of + // the above alternatives. + // + // TerminationProto.detail may contain additional information about the limit. + LIMIT_OTHER = 11; +} + +// Bounds on the optimal objective value. +message ObjectiveBoundsProto { + // Solver claims the optimal value is equal or better (smaller for + // minimization and larger for maximization) than primal_bound up to the + // solvers primal feasibility tolerance (see warning below): + // * primal_bound is trivial (+inf for minimization and -inf + // maximization) when the solver does not claim to have such bound. + // * primal_bound can be closer to the optimal value than the objective + // of the best primal feasible solution. In particular, primal_bound + // may be non-trivial even when no primal feasible solutions are returned. + // Warning: The precise claim is that there exists a primal solution that: + // * is numerically feasible (i.e. feasible up to the solvers tolerance), and + // * has an objective value primal_bound. + // This numerically feasible solution could be slightly infeasible, in which + // case primal_bound could be strictly better than the optimal value. + // Translating a primal feasibility tolerance to a tolerance on + // primal_bound is non-trivial, specially when the feasibility tolerance + // is relatively large (e.g. when solving with PDLP). + double primal_bound = 2; + + // Solver claims the optimal value is equal or worse (larger for + // minimization and smaller for maximization) than dual_bound up to the + // solvers dual feasibility tolerance (see warning below): + // * dual_bound is trivial (-inf for minimization and +inf + // maximization) when the solver does not claim to have such bound. + // Similarly to primal_bound, this may happen for some solvers even + // when returning optimal. MIP solvers will typically report a bound even + // if it is imprecise. + // * for continuous problems dual_bound can be closer to the optimal + // value than the objective of the best dual feasible solution. For MIP + // one of the first non-trivial values for dual_bound is often the + // optimal value of the LP relaxation of the MIP. + // * dual_bound should be better (smaller for minimization and larger + // for maximization) than primal_bound up to the solvers tolerances + // (see warning below). + // Warning: + // * For continuous problems, the precise claim is that there exists a + // dual solution that: + // * is numerically feasible (i.e. feasible up to the solvers tolerance), + // and + // * has an objective value dual_bound. + // This numerically feasible solution could be slightly infeasible, in + // which case dual_bound could be strictly worse than the optimal + // value and primal_bound. Similar to the primal case, translating a + // dual feasibility tolerance to a tolerance on dual_bound is + // non-trivial, specially when the feasibility tolerance is relatively + // large. However, some solvers provide a corrected version of + // dual_bound that can be numerically safer. This corrected version + // can be accessed through the solver's specific output (e.g. for PDLP, + // pdlp_output.convergence_information.corrected_dual_objective). + // * For MIP solvers, dual_bound may be associated to a dual solution + // for some continuous relaxation (e.g. LP relaxation), but it is often a + // complex consequence of the solvers execution and is typically more + // imprecise than the bounds reported by LP solvers. + double dual_bound = 3; +} + +// All information regarding why a call to Solve() terminated. +message TerminationProto { + // Additional information in `limit` when value is TERMINATION_REASON_FEASIBLE + // or TERMINATION_REASON_NO_SOLUTION_FOUND, see `limit` for details. + TerminationReasonProto reason = 1; + + // Is LIMIT_UNSPECIFIED unless reason is TERMINATION_REASON_FEASIBLE or + // TERMINATION_REASON_NO_SOLUTION_FOUND. Not all solvers can always determine + // the limit which caused termination, LIMIT_UNDETERMINED is used when the + // cause cannot be determined. + LimitProto limit = 2; + + // Additional typically solver specific information about termination. + string detail = 3; + + // Feasibility statuses for primal and dual problems. + // As of July 18, 2023 this message may be missing. If missing, problem_status + // can be found in SolveResultProto.solve_stats. + ProblemStatusProto problem_status = 4; + + // Bounds on the optimal objective value. + // As of July 18, 2023 this message may be missing. If missing, + // objective_bounds.primal_bound can be found in + // SolveResultProto.solve.stats.best_primal_bound and + // objective_bounds.dual_bound can be found in + // SolveResultProto.solve.stats.best_dual_bound + ObjectiveBoundsProto objective_bounds = 5; +} + +// The contract of when primal/dual solutions/rays is complex, see +// termination_reasons.md for a complete description. +// +// Until an exact contract is finalized, it is safest to simply check if a +// solution/ray is present rather than relying on the termination reason. +message SolveResultProto { + // The reason the solver stopped. + TerminationProto termination = 2; + + // Basic solutions use, as of Nov 2021: + // * All convex optimization solvers (LP, convex QP) return only one + // solution as a primal dual pair. + // * Only MI(Q)P solvers return more than one solution. MIP solvers do not + // return any dual information, or primal infeasible solutions. Solutions + // are returned in order of best primal objective first. Gurobi solves + // nonconvex QP (integer or continuous) as MIQP. + + // The general contract for the order of solutions that future solvers should + // implement is to order by: + // 1. The solutions with a primal feasible solution, ordered by best primal + // objective first. + // 2. The solutions with a dual feasible solution, ordered by best dual + // objective (unknown dual objective is worst) + // 3. All remaining solutions can be returned in any order. + repeated SolutionProto solutions = 3; + + // Directions of unbounded primal improvement, or equivalently, dual + // infeasibility certificates. Typically provided for TerminationReasonProtos + // UNBOUNDED and DUAL_INFEASIBLE + repeated PrimalRayProto primal_rays = 4; + + // Directions of unbounded dual improvement, or equivalently, primal + // infeasibility certificates. Typically provided for TerminationReasonProto + // INFEASIBLE. + repeated DualRayProto dual_rays = 5; + + // Statistics on the solve process, e.g. running time, iterations. + SolveStatsProto solve_stats = 6; + + reserved 7, 8, 9; + + reserved 1; // Deleted fields. +} diff --git a/ortools/service/v1/mathopt/solution.proto b/ortools/service/v1/mathopt/solution.proto new file mode 100644 index 0000000000..c0c5b2be02 --- /dev/null +++ b/ortools/service/v1/mathopt/solution.proto @@ -0,0 +1,248 @@ +// Copyright 2010-2022 Google LLC +// 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. + +// The solution to an optimization model. + +syntax = "proto3"; + +package operations_research.service.v1.mathopt; + +import "ortools/service/v1/mathopt/sparse_containers.proto"; + +option java_multiple_files = true; +option java_package = "com.google.ortools.service.v1.mathopt"; + +option csharp_namespace = "Google.OrTools.Service"; + +// Feasibility of a primal or dual solution as claimed by the solver. +enum SolutionStatusProto { + // Guard value representing no status. + SOLUTION_STATUS_UNSPECIFIED = 0; + // Solver does not claim a feasibility status. + SOLUTION_STATUS_UNDETERMINED = 1; + // Solver claims the solution is feasible. + SOLUTION_STATUS_FEASIBLE = 2; + // Solver claims the solution is infeasible. + SOLUTION_STATUS_INFEASIBLE = 3; +} + +// A solution to an optimization problem. +// +// E.g. consider a simple linear program: +// min c * x +// s.t. A * x >= b +// x >= 0. +// A primal solution is assignment values to x. It is feasible if it satisfies +// A * x >= b and x >= 0 from above. In the message PrimalSolutionProto below, +// variable_values is x and objective_value is c * x. +message PrimalSolutionProto { + // Requirements: + // * variable_values.ids are elements of VariablesProto.ids. + // * variable_values.values must all be finite. + SparseDoubleVectorProto variable_values = 1; + + // Objective value as computed by the underlying solver. Cannot be infinite or + // NaN. + double objective_value = 2; + + // Auxiliary objective values as computed by the underlying solver. Keys must + // be valid auxiliary objective IDs. Values cannot be infinite or NaN. + map auxiliary_objective_values = 4; + + // Feasibility status of the solution according to the underlying solver. + SolutionStatusProto feasibility_status = 3; +} + +// A direction of unbounded improvement to an optimization problem; +// equivalently, a certificate of infeasibility for the dual of the +// optimization problem. +// +// E.g. consider a simple linear program: +// min c * x +// s.t. A * x >= b +// x >= 0 +// A primal ray is an x that satisfies: +// c * x < 0 +// A * x >= 0 +// x >= 0 +// Observe that given a feasible solution, any positive multiple of the primal +// ray plus that solution is still feasible, and gives a better objective +// value. A primal ray also proves the dual optimization problem infeasible. +// +// In the message PrimalRay below, variable_values is x. +message PrimalRayProto { + // Requirements: + // * variable_values.ids are elements of VariablesProto.ids. + // * variable_values.values must all be finite. + SparseDoubleVectorProto variable_values = 1; + + // TODO(b/185365397): indicate if the ray is feasible. +} + +// A solution to the dual of an optimization problem. +// +// E.g. consider the primal dual pair linear program pair: +// (Primal) (Dual) +// min c * x max b * y +// s.t. A * x >= b s.t. y * A + r = c +// x >= 0 y, r >= 0. +// The dual solution is the pair (y, r). It is feasible if it satisfies the +// constraints from (Dual) above. +// +// In the message below, y is dual_values, r is reduced_costs, and +// b * y is objective value. +message DualSolutionProto { + // Requirements: + // * dual_values.ids are elements of LinearConstraints.ids. + // * dual_values.values must all be finite. + SparseDoubleVectorProto dual_values = 1; + + // Requirements: + // * reduced_costs.ids are elements of VariablesProto.ids. + // * reduced_costs.values must all be finite. + SparseDoubleVectorProto reduced_costs = 2; + + // TODO(b/195295177): consider making this non-optional + // Objective value as computed by the underlying solver. + optional double objective_value = 3; + + // Feasibility status of the solution according to the underlying solver. + SolutionStatusProto feasibility_status = 4; +} + +// A direction of unbounded improvement to the dual of an optimization, +// problem; equivalently, a certificate of primal infeasibility. +// +// E.g. consider the primal dual pair linear program pair: +// (Primal) (Dual) +// min c * x max b * y +// s.t. A * x >= b s.t. y * A + r = c +// x >= 0 y, r >= 0. +// The dual ray is the pair (y, r) satisfying: +// b * y > 0 +// y * A + r = 0 +// y, r >= 0 +// Observe that adding a positive multiple of (y, r) to dual feasible solution +// maintains dual feasibility and improves the objective (proving the dual is +// unbounded). The dual ray also proves the primal problem is infeasible. +// +// In the message DualRay below, y is dual_values and r is reduced_costs. +message DualRayProto { + // Requirements: + // * dual_values.ids are elements of LinearConstraints.ids. + // * dual_values.values must all be finite. + SparseDoubleVectorProto dual_values = 1; + + // Requirements: + // * reduced_costs.ids are elements of VariablesProto.ids. + // * reduced_costs.values must all be finite. + SparseDoubleVectorProto reduced_costs = 2; + + // TODO(b/185365397): indicate if the ray is feasible. +} + +// Status of a variable/constraint in a LP basis. +enum BasisStatusProto { + // Guard value representing no status. + BASIS_STATUS_UNSPECIFIED = 0; + + // The variable/constraint is free (it has no finite bounds). + BASIS_STATUS_FREE = 1; + + // The variable/constraint is at its lower bound (which must be finite). + BASIS_STATUS_AT_LOWER_BOUND = 2; + + // The variable/constraint is at its upper bound (which must be finite). + BASIS_STATUS_AT_UPPER_BOUND = 3; + + // The variable/constraint has identical finite lower and upper bounds. + BASIS_STATUS_FIXED_VALUE = 4; + + // The variable/constraint is basic. + BASIS_STATUS_BASIC = 5; +} + +// A sparse representation of a vector of basis statuses. +message SparseBasisStatusVector { + // Must be sorted (in increasing ordering) with all elements distinct. + repeated int64 ids = 1; + + // Must have equal length to ids. + repeated BasisStatusProto values = 2; +} + +// A combinatorial characterization for a solution to a linear program. +// +// The simplex method for solving linear programs always returns a "basic +// feasible solution" which can be described combinatorially by a Basis. A basis +// assigns a BasisStatusProto for every variable and linear constraint. +// +// E.g. consider a standard form LP: +// min c * x +// s.t. A * x = b +// x >= 0 +// that has more variables than constraints and with full row rank A. +// +// Let n be the number of variables and m the number of linear constraints. A +// valid basis for this problem can be constructed as follows: +// * All constraints will have basis status FIXED. +// * Pick m variables such that the columns of A are linearly independent and +// assign the status BASIC. +// * Assign the status AT_LOWER for the remaining n - m variables. +// +// The basic solution for this basis is the unique solution of A * x = b that +// has all variables with status AT_LOWER fixed to their lower bounds (all +// zero). The resulting solution is called a basic feasible solution if it also +// satisfies x >= 0. +message BasisProto { + // Constraint basis status. + // + // Requirements: + // * constraint_status.ids is equal to LinearConstraints.ids. + SparseBasisStatusVector constraint_status = 1; + + // Variable basis status. + // + // Requirements: + // * constraint_status.ids is equal to VariablesProto.ids. + SparseBasisStatusVector variable_status = 2; + + // This is an advanced feature used by MathOpt to characterize feasibility of + // suboptimal LP solutions (optimal solutions will always have status + // SOLUTION_STATUS_FEASIBLE). + // + // For single-sided LPs it should be equal to the feasibility status of the + // associated dual solution. For two-sided LPs it may be different in some + // edge cases (e.g. incomplete solves with primal simplex). + // + // If you are providing a starting basis via + // ModelSolveParametersProto.initial_basis, this value is ignored. It is only + // relevant for the basis returned by SolutionProto.basis. + SolutionStatusProto basic_dual_feasibility = 3; +} + +// What is included in a solution depends on the kind of problem and solver. +// The current common patterns are +// 1. MIP solvers return only a primal solution. +// 2. Simplex LP solvers often return a basis and the primal and dual +// solutions associated to this basis. +// 3. Other continuous solvers often return a primal and dual solution +// solution that are connected in a solver-dependent form. +// +// Requirements: +// * at least one field must be set; a solution can't be empty. +message SolutionProto { + optional PrimalSolutionProto primal_solution = 1; + optional DualSolutionProto dual_solution = 2; + optional BasisProto basis = 3; +} diff --git a/ortools/service/v1/mathopt/sparse_containers.proto b/ortools/service/v1/mathopt/sparse_containers.proto new file mode 100644 index 0000000000..fbfb960633 --- /dev/null +++ b/ortools/service/v1/mathopt/sparse_containers.proto @@ -0,0 +1,88 @@ +// Copyright 2010-2022 Google LLC +// 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. + +// Data structures used throughout MathOpt to model sparse vectors and matrices. + +syntax = "proto3"; + +package operations_research.service.v1.mathopt; + +option java_multiple_files = true; +option java_package = "com.google.ortools.service.v1.mathopt"; + +option csharp_namespace = "Google.OrTools.Service"; + +// A sparse representation of a vector of doubles. +message SparseDoubleVectorProto { + // Must be sorted (in increasing ordering) with all elements distinct. + repeated int64 ids = 1; + // Must have equal length to ids. May not contain NaN. + repeated double values = 2; +} + +// A sparse representation of a vector of bools. +message SparseBoolVectorProto { + // Should be sorted (in increasing ordering) with all elements distinct. + repeated int64 ids = 1; + // Must have equal length to ids. + repeated bool values = 2; +} + +// A sparse representation of a vector of ints. +message SparseInt32VectorProto { + // Should be sorted (in increasing ordering) with all elements distinct. + repeated int64 ids = 1; + // Must have equal length to ids. + repeated int32 values = 2; +} + +// This message allows to query/set specific parts of a SparseXxxxVector. +// The default behavior is not to filter out anything. +// A common usage is to query only parts of solutions (only non-zero values, +// and/or just a hand-picked set of variable values). +message SparseVectorFilterProto { + // For SparseBoolVectorProto "zero" is `false`. + bool skip_zero_values = 1; + // When true, return only the values corresponding to the IDs listed in + // filtered_ids. + bool filter_by_ids = 2; + // The list of IDs to use when filter_by_ids is true. Must be empty when + // filter_by_ids is false. + // NOTE: if this is empty, and filter_by_ids is true, you are saying that + // you do not want any information in the result. + repeated int64 filtered_ids = 3; +} + +// A sparse representation of a matrix of doubles. +// +// The matrix is stored as triples of row id, column id, and coefficient. These +// three vectors must be of equal length. For all i, the tuple (row_ids[i], +// column_ids[i]) should be distinct. Entries must be in row major order. +message SparseDoubleMatrixProto { + repeated int64 row_ids = 1; + repeated int64 column_ids = 2; + // May not contain NaN. + repeated double coefficients = 3; +} + +// A sparse representation of a linear expression (a weighted sum of variables, +// plus a constant offset). +message LinearExpressionProto { + // Ids of variables. Must be sorted (in increasing ordering) with all elements + // distinct. + repeated int64 ids = 1; + // Must have equal length to ids. Values must be finite may not be NaN. + repeated double coefficients = 2; + // Must be finite and may not be NaN. + double offset = 3; +} diff --git a/ortools/service/v1/optimization.proto b/ortools/service/v1/optimization.proto new file mode 100644 index 0000000000..8973a1ac3e --- /dev/null +++ b/ortools/service/v1/optimization.proto @@ -0,0 +1,68 @@ +// Copyright 2010-2022 Google LLC +// 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. + +syntax = "proto3"; + +package operations_research.service.v1; + +import "ortools/service/v1/mathopt/model.proto"; +import "ortools/service/v1/mathopt/model_parameters.proto"; +import "ortools/service/v1/mathopt/parameters.proto"; +import "ortools/service/v1/mathopt/result.proto"; + +option java_multiple_files = true; +option java_package = "com.google.ortools.service.v1"; + +option csharp_namespace = "Google.OrTools.Service"; + +// A One Platform API exposing a set of optimization solvers for high-level +// operations research problems. +service Optimization { + // Solves the input model and returns the result at once. Use this when you + // don't need callbacks, incrementality and don't need to track the progress + // of a solve. + rpc SolveMathOptModel(SolveMathOptModelRequest) + returns (SolveMathOptModelResponse) {} +} + +// Request for a unary remote solve in MathOpt. +message SolveMathOptModelRequest { + // Solver type to numerically solve the problem. Note that if a solver does + // not support a specific feature in the model, the optimization procedure + // won't be successful. + mathopt.SolverTypeProto solver_type = 1; + + // A mathematical representation of the optimization problem to solve. + mathopt.ModelProto model = 2; + + // Parameters to control a single solve. The enable_output parameter is + // handled specifically. For solvers that support messages callbacks, setting + // it to true will have the server register a message callback. The resulting + // messages will be returned in SolveMathOptModelResponse.messages. For other + // solvers, setting enable_output to true will result in an error. + mathopt.SolveParametersProto parameters = 4; + + // Parameters to control a single solve that are specific to the input model + // (see SolveParametersProto for model independent parameters). + mathopt.ModelSolveParametersProto model_parameters = 5; +} + +// Response for a unary remote solve in MathOpt. +message SolveMathOptModelResponse { + // Description of the output of solving the model in the request. + mathopt.SolveResultProto result = 1; + + // If SolveParametersProto.enable_output has been used, this will contain log + // messages for solvers that support message callbacks. + repeated string messages = 2; +}