export math_opt/ from google3

This commit is contained in:
Corentin Le Molgat
2024-07-29 15:15:15 +02:00
committed by Mizux Seiha
parent 286089e617
commit 6dc31775a1
95 changed files with 3404 additions and 611 deletions

View File

@@ -25,6 +25,7 @@ py_library(
deps = [
":callback",
":compute_infeasible_subsystem_result",
":errors",
":expressions",
":hash_model_storage",
":message_callback",
@@ -158,12 +159,14 @@ py_library(
deps = [
":callback",
":compute_infeasible_subsystem_result",
":errors",
":message_callback",
":model",
":model_parameters",
":parameters",
":result",
"//ortools/math_opt:parameters_py_pb2",
"//ortools/math_opt:rpc_py_pb2",
"//ortools/math_opt/core/python:solver",
],
)
@@ -201,3 +204,9 @@ py_library(
srcs = ["solver_resources.py"],
deps = ["//ortools/math_opt:rpc_py_pb2"],
)
py_library(
name = "errors",
srcs = ["errors.py"],
deps = ["//ortools/math_opt:rpc_py_pb2"],
)

View File

@@ -0,0 +1,104 @@
# Copyright 2010-2024 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.
"""Translate C++'s absl::Status errors to Python standard errors.
Here we try to use the standard Python errors we would use if the C++ code was
instead implemented in Python. This will give Python users a more familiar API.
"""
import enum
from typing import Optional, Type
from ortools.math_opt import rpc_pb2
class _StatusCode(enum.Enum):
"""The C++ absl::Status::code() values."""
OK = 0
CANCELLED = 1
UNKNOWN = 2
INVALID_ARGUMENT = 3
DEADLINE_EXCEEDED = 4
NOT_FOUND = 5
ALREADY_EXISTS = 6
PERMISSION_DENIED = 7
UNAUTHENTICATED = 16
RESOURCE_EXHAUSTED = 8
FAILED_PRECONDITION = 9
ABORTED = 10
OUT_OF_RANGE = 11
UNIMPLEMENTED = 12
INTERNAL = 13
UNAVAILABLE = 14
DATA_LOSS = 15
class InternalMathOptError(RuntimeError):
"""Some MathOpt internal error.
This error is usually raised because of a bug in MathOpt or one of the solver
library it wraps.
"""
def status_proto_to_exception(
status_proto: rpc_pb2.StatusProto,
) -> Optional[Exception]:
"""Returns the Python exception that best match the input absl::Status.
There are some Status that we expect the MathOpt code to return, for those the
matching exceptions are:
- InvalidArgument: ValueError
- FailedPrecondition: AssertionError
- Unimplemented: NotImplementedError
- Internal: InternalMathOptError
Other Status's are not used by MathOpt, if they are seen a
InternalMathOptError is raised (as if the Status was Internal) and the error
message contains the unexpected code.
Args:
status_proto: The input proto to convert to an exception.
Returns:
The corresponding exception. None if the input status is OK.
"""
try:
code = _StatusCode(status_proto.code)
except ValueError:
return InternalMathOptError(
f"unknown C++ error (code = {status_proto.code}):"
f" {status_proto.message}"
)
if code == _StatusCode.OK:
return None
# For expected errors we compute the corresponding class.
error_type: Optional[Type[Exception]] = None
if code == _StatusCode.INVALID_ARGUMENT:
error_type = ValueError
if code == _StatusCode.FAILED_PRECONDITION:
error_type = AssertionError
if code == _StatusCode.UNIMPLEMENTED:
error_type = NotImplementedError
if code == _StatusCode.INTERNAL:
error_type = InternalMathOptError
if error_type is not None:
return error_type(f"{status_proto.message} (was C++ {code.name})")
return InternalMathOptError(
f"unexpected C++ error {code.name}: {status_proto.message}"
)

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# Copyright 2010-2024 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.
"""Tests of the `errors` package."""
from absl.testing import absltest
from ortools.math_opt import rpc_pb2
from ortools.math_opt.python import errors
class StatusProtoToExceptionTest(absltest.TestCase):
def test_ok(self) -> None:
self.assertIsNone(
errors.status_proto_to_exception(
rpc_pb2.StatusProto(code=errors._StatusCode.OK.value)
)
)
def test_invalid_argument(self) -> None:
error = errors.status_proto_to_exception(
rpc_pb2.StatusProto(
code=errors._StatusCode.INVALID_ARGUMENT.value, message="something"
)
)
self.assertIsInstance(error, ValueError)
self.assertEqual(str(error), "something (was C++ INVALID_ARGUMENT)")
def test_failed_precondition(self) -> None:
error = errors.status_proto_to_exception(
rpc_pb2.StatusProto(
code=errors._StatusCode.FAILED_PRECONDITION.value,
message="something",
)
)
self.assertIsInstance(error, AssertionError)
self.assertEqual(str(error), "something (was C++ FAILED_PRECONDITION)")
def test_unimplemented(self) -> None:
error = errors.status_proto_to_exception(
rpc_pb2.StatusProto(
code=errors._StatusCode.UNIMPLEMENTED.value, message="something"
)
)
self.assertIsInstance(error, NotImplementedError)
self.assertEqual(str(error), "something (was C++ UNIMPLEMENTED)")
def test_internal(self) -> None:
error = errors.status_proto_to_exception(
rpc_pb2.StatusProto(
code=errors._StatusCode.INTERNAL.value, message="something"
)
)
self.assertIsInstance(error, errors.InternalMathOptError)
self.assertEqual(str(error), "something (was C++ INTERNAL)")
def test_unexpected_code(self) -> None:
error = errors.status_proto_to_exception(
rpc_pb2.StatusProto(
code=errors._StatusCode.DEADLINE_EXCEEDED.value, message="something"
)
)
self.assertIsInstance(error, errors.InternalMathOptError)
self.assertEqual(
str(error), "unexpected C++ error DEADLINE_EXCEEDED: something"
)
def test_unknown_code(self) -> None:
error = errors.status_proto_to_exception(
rpc_pb2.StatusProto(code=-5, message="something")
)
self.assertIsInstance(error, errors.InternalMathOptError)
self.assertEqual(str(error), "unknown C++ error (code = -5): something")
if __name__ == "__main__":
absltest.main()

View File

@@ -96,7 +96,7 @@ def create_optimization_service_session(
Returns:
requests.Session a session with the necessary headers to call the
optimization serive.
optimization service.
"""
session = requests.Session()
server_timeout = deadline_sec * (1 - _RELATIVE_TIME_BUFFER)

View File

@@ -60,6 +60,8 @@ from ortools.math_opt.python.compute_infeasible_subsystem_result import (
from ortools.math_opt.python.compute_infeasible_subsystem_result import (
parse_model_subset_bounds,
)
from ortools.math_opt.python.errors import InternalMathOptError
from ortools.math_opt.python.errors import status_proto_to_exception
from ortools.math_opt.python.expressions import evaluate_expression
from ortools.math_opt.python.expressions import fast_sum
from ortools.math_opt.python.hash_model_storage import HashModelStorage

View File

@@ -16,9 +16,11 @@ import types
from typing import Callable, Optional
from ortools.math_opt import parameters_pb2
from ortools.math_opt import rpc_pb2
from ortools.math_opt.core.python import solver
from ortools.math_opt.python import callback
from ortools.math_opt.python import compute_infeasible_subsystem_result
from ortools.math_opt.python import errors
from ortools.math_opt.python import message_callback
from ortools.math_opt.python import model
from ortools.math_opt.python import model_parameters
@@ -86,7 +88,7 @@ def solve(
None,
)
except StatusNotOk as e:
raise RuntimeError(str(e)) from None
raise _status_not_ok_to_exception(e) from None
return result.parse_solve_result(proto_result, opt_model)
@@ -126,7 +128,7 @@ def compute_infeasible_subsystem(
None,
)
except StatusNotOk as e:
raise RuntimeError(str(e)) from None
raise _status_not_ok_to_exception(e) from None
return (
compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result(
proto_result, opt_model
@@ -172,7 +174,7 @@ class IncrementalSolver:
parameters_pb2.SolverInitializerProto(),
)
except StatusNotOk as e:
raise RuntimeError(str(e)) from None
raise _status_not_ok_to_exception(e) from None
self._closed = False
def solve(
@@ -212,7 +214,7 @@ class IncrementalSolver:
parameters_pb2.SolverInitializerProto(),
)
except StatusNotOk as e:
raise RuntimeError(str(e)) from None
raise _status_not_ok_to_exception(e) from None
self._update_tracker.advance_checkpoint()
params = params or parameters.SolveParameters()
model_params = model_params or model_parameters.ModelSolveParameters()
@@ -232,7 +234,7 @@ class IncrementalSolver:
None,
)
except StatusNotOk as e:
raise RuntimeError(str(e)) from None
raise _status_not_ok_to_exception(e) from None
return result.parse_solve_result(result_proto, self._model)
def close(self) -> None:
@@ -268,3 +270,20 @@ class IncrementalSolver:
) -> None:
"""Closes the solver."""
self.close()
def _status_not_ok_to_exception(err: StatusNotOk) -> Exception:
"""Converts a StatusNotOk to the best matching Python exception.
Args:
err: The input errors.
Returns:
The corresponding exception.
"""
ret = errors.status_proto_to_exception(
rpc_pb2.StatusProto(code=err.canonical_code, message=err.message)
)
# We never expect StatusNotOk to be OK.
assert ret is not None, err
return ret

View File

@@ -68,9 +68,7 @@ class SolveTest(absltest.TestCase):
def test_solve_error(self) -> None:
mod = model.Model(name="test_model")
mod.add_variable(lb=1.0, ub=-1.0, name="x1")
with self.assertRaisesRegex(
RuntimeError, "variables.*lower_bound > upper_bound"
):
with self.assertRaisesRegex(ValueError, "variables.*lower_bound > upper_bound"):
solve.solve(mod, parameters.SolverType.GLOP)
def test_lp_solve(self) -> None:
@@ -213,16 +211,14 @@ class SolveTest(absltest.TestCase):
mod = model.Model(name="test_model")
mod.add_variable(lb=1.0, ub=1.0, name="x1")
mod.add_variable(lb=1.0, ub=1.0, name="x1")
with self.assertRaisesRegex(RuntimeError, "duplicate name*"):
with self.assertRaisesRegex(ValueError, "duplicate name*"):
solve.IncrementalSolver(mod, parameters.SolverType.GLOP)
def test_incremental_solve_error(self) -> None:
mod = model.Model(name="test_model")
mod.add_variable(lb=1.0, ub=-1.0, name="x1")
solver = solve.IncrementalSolver(mod, parameters.SolverType.GLOP)
with self.assertRaisesRegex(
RuntimeError, "variables.*lower_bound > upper_bound"
):
with self.assertRaisesRegex(ValueError, "variables.*lower_bound > upper_bound"):
solver.solve()
def test_incremental_solve_error_on_reject(self) -> None:
@@ -250,7 +246,7 @@ class SolveTest(absltest.TestCase):
)
opt_model.add_binary_variable(name="x")
with self.assertRaisesRegex(RuntimeError, "duplicate name*"):
with self.assertRaisesRegex(ValueError, "duplicate name*"):
solver.solve(
msg_cb=message_callback.printer_message_callback(prefix="[solve 2] ")
)