math_opt: export from google3

This commit is contained in:
Corentin Le Molgat
2024-04-19 13:40:42 +02:00
parent faced59676
commit e97e98d83d
32 changed files with 139 additions and 95 deletions

View File

@@ -144,9 +144,11 @@ from ortools.math_opt.python.solution import Basis
from ortools.math_opt.python.solution import BasisStatus
from ortools.math_opt.python.solution import DualRay
from ortools.math_opt.python.solution import DualSolution
from ortools.math_opt.python.solution import optional_solution_status_to_proto
from ortools.math_opt.python.solution import parse_basis
from ortools.math_opt.python.solution import parse_dual_ray
from ortools.math_opt.python.solution import parse_dual_solution
from ortools.math_opt.python.solution import parse_optional_solution_status
from ortools.math_opt.python.solution import parse_primal_ray
from ortools.math_opt.python.solution import parse_primal_solution
from ortools.math_opt.python.solution import parse_solution

View File

@@ -100,9 +100,6 @@ class ModelParametersTest(compare_proto.MathOptProtoAssertions, absltest.TestCas
expected.initial_basis.variable_status.values.append(
solution_pb2.BASIS_STATUS_AT_UPPER_BOUND
)
expected.initial_basis.basic_dual_feasibility = (
solution_pb2.SOLUTION_STATUS_UNDETERMINED
)
self.assert_protos_equiv(expected, actual)

View File

@@ -58,6 +58,24 @@ class SolutionStatus(enum.Enum):
INFEASIBLE = solution_pb2.SOLUTION_STATUS_INFEASIBLE
def parse_optional_solution_status(
proto: solution_pb2.SolutionStatusProto,
) -> Optional[SolutionStatus]:
"""Converts a proto SolutionStatus to an optional Python SolutionStatus."""
return (
None
if proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED
else SolutionStatus(proto)
)
def optional_solution_status_to_proto(
status: Optional[SolutionStatus],
) -> solution_pb2.SolutionStatusProto:
"""Converts an optional Python SolutionStatus to a proto SolutionStatus."""
return solution_pb2.SOLUTION_STATUS_UNSPECIFIED if status is None else status.value
@dataclasses.dataclass
class PrimalSolution:
"""A solution to the optimization problem in a Model.
@@ -312,12 +330,13 @@ class Basis:
For two-sided LPs it may be different in some edge cases (e.g. incomplete
solves with primal simplex). For more details see
go/mathopt-basis-advanced#dualfeasibility. If you are providing a starting
basis via ModelSolveParameters.initial_basis, this value is ignored. It is
only relevant for the basis returned by Solution.basis. This is an
advanced status. 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). For more details see go/mathopt-basis-advanced#dualfeasibility.
basis via ModelSolveParameters.initial_basis, this value is ignored and
can be None. It is only relevant for the basis returned by Solution.basis,
and it is never None when returned from solve(). This is an advanced
status. 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). For more
details see go/mathopt-basis-advanced#dualfeasibility.
"""
variable_status: Dict[model.Variable, BasisStatus] = dataclasses.field(
@@ -326,7 +345,7 @@ class Basis:
constraint_status: Dict[model.LinearConstraint, BasisStatus] = dataclasses.field(
default_factory=dict
)
basic_dual_feasibility: SolutionStatus = SolutionStatus.UNDETERMINED
basic_dual_feasibility: Optional[SolutionStatus] = None
def to_proto(self) -> solution_pb2.BasisProto:
"""Returns an equivalent proto for the basis."""
@@ -335,7 +354,9 @@ class Basis:
constraint_status=_to_sparse_basis_status_vector_proto(
self.constraint_status
),
basic_dual_feasibility=self.basic_dual_feasibility.value,
basic_dual_feasibility=optional_solution_status_to_proto(
self.basic_dual_feasibility
),
)
@@ -354,10 +375,9 @@ def parse_basis(proto: solution_pb2.BasisProto, mod: model.Model) -> Basis:
result.constraint_status[mod.get_linear_constraint(cid)] = BasisStatus(
status_proto
)
status_proto = proto.basic_dual_feasibility
if status_proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED:
raise ValueError("Basic dual feasibility status should not be UNSPECIFIED")
result.basic_dual_feasibility = SolutionStatus(status_proto)
result.basic_dual_feasibility = parse_optional_solution_status(
proto.basic_dual_feasibility
)
return result

View File

@@ -19,6 +19,18 @@ from ortools.math_opt.python import solution
from ortools.math_opt.python.testing import compare_proto
class SolutionStatusTest(absltest.TestCase):
def test_optional_status_round_trip(self):
for status in solution_pb2.SolutionStatusProto.values():
self.assertEqual(
status,
solution.optional_solution_status_to_proto(
solution.parse_optional_solution_status(status)
),
)
class ParsePrimalSolutionTest(compare_proto.MathOptProtoAssertions, absltest.TestCase):
def test_empty_primal_solution_proto_round_trip(self) -> None:
@@ -164,13 +176,11 @@ class BasisTest(compare_proto.MathOptProtoAssertions, absltest.TestCase):
empty_basis = solution.Basis()
empty_proto = empty_basis.to_proto()
expected_proto = solution_pb2.BasisProto()
expected_proto.basic_dual_feasibility = (
solution_pb2.SOLUTION_STATUS_UNDETERMINED
)
self.assert_protos_equiv(expected_proto, empty_proto)
round_trip_basis = solution.parse_basis(empty_proto, mod)
self.assertEmpty(round_trip_basis.constraint_status)
self.assertEmpty(round_trip_basis.variable_status)
self.assertIsNone(round_trip_basis.basic_dual_feasibility)
def test_basis_proto_round_trip(self) -> None:
mod = model.Model(name="test_model")
@@ -252,24 +262,9 @@ class BasisTest(compare_proto.MathOptProtoAssertions, absltest.TestCase):
def test_basic_dual_feasibility_unspecified(self) -> None:
mod = model.Model(name="test_model")
mod.add_binary_variable(name="x")
mod.add_binary_variable(name="y")
mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
mod.add_linear_constraint(lb=0.0, ub=1.0, name="d")
basis_proto = solution_pb2.BasisProto()
basis_proto.constraint_status.ids[:] = [0, 1]
basis_proto.constraint_status.values[:] = [
solution_pb2.BASIS_STATUS_BASIC,
solution_pb2.BASIS_STATUS_AT_UPPER_BOUND,
]
basis_proto.variable_status.ids[:] = [0, 1]
basis_proto.variable_status.values[:] = [
solution_pb2.BASIS_STATUS_AT_UPPER_BOUND,
solution_pb2.BASIS_STATUS_BASIC,
]
basis_proto.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_UNSPECIFIED
with self.assertRaisesRegex(ValueError, "Basic dual feasibility.*UNSPECIFIED"):
solution.parse_basis(basis_proto, mod)
basis = solution.parse_basis(basis_proto, mod)
self.assertIsNone(basis.basic_dual_feasibility)
class ParseSolutionTest(compare_proto.MathOptProtoAssertions, absltest.TestCase):

View File

@@ -37,8 +37,9 @@ class SolverResources:
MOE:begin_intracomment_strip
The go/uoss server will use these parameters to do a bin-packing of all
requests. They are generally used as soft-limits though instead of
hard-limits and a solve may be able to consume more resources than requested.
requests. Parameter cpu is a soft-limit, the solve may still be able to use
more CPUs. The ram parameter is an hard-limit, an out-of-memory error will
occur if the solve attempts to use more memory.
MOE:end_intracomment_strip
@@ -58,9 +59,12 @@ class SolverResources:
better to consult each solver documentation to set this parameter. Note
that if the SolveParameters.threads is not set then this parameter should
also be left unset.
ram: The limit of RAM for the solve in bytes. Must be finite and >=1.0 (even
though it should in practice be much larger).
"""
cpu: Optional[float] = None
ram: Optional[float] = None
def to_proto(self) -> rpc_pb2.SolverResourcesProto:
return rpc_pb2.SolverResourcesProto(cpu=self.cpu)
return rpc_pb2.SolverResourcesProto(cpu=self.cpu, ram=self.ram)

View File

@@ -34,6 +34,12 @@ class SolverResourcesTest(compare_proto.MathOptProtoAssertions, absltest.TestCas
rpc_pb2.SolverResourcesProto(cpu=3.5),
)
def test_to_proto_with_ram(self):
self.assert_protos_equiv(
solver_resources.SolverResources(ram=50 * 1024 * 1024).to_proto(),
rpc_pb2.SolverResourcesProto(ram=50 * 1024 * 1024),
)
if __name__ == "__main__":
absltest.main()