export math_opt/ from google3
This commit is contained in:
committed by
Mizux Seiha
parent
286089e617
commit
6dc31775a1
@@ -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"],
|
||||
)
|
||||
|
||||
104
ortools/math_opt/python/errors.py
Normal file
104
ortools/math_opt/python/errors.py
Normal 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}"
|
||||
)
|
||||
88
ortools/math_opt/python/errors_test.py
Normal file
88
ortools/math_opt/python/errors_test.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] ")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user