254 lines
10 KiB
Python
254 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# 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.
|
|
|
|
import datetime
|
|
import math
|
|
|
|
import unittest
|
|
from ortools.math_opt import callback_pb2
|
|
from ortools.math_opt import sparse_containers_pb2
|
|
from ortools.math_opt.python import callback
|
|
from ortools.math_opt.python import model
|
|
from ortools.math_opt.python import sparse_containers
|
|
from ortools.math_opt.python.testing import compare_proto
|
|
|
|
|
|
class CallbackDataTest(compare_proto.MathOptProtoAssertions, unittest.TestCase):
|
|
def test_parse_callback_data_no_solution(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
cb_data_proto = callback_pb2.CallbackDataProto(
|
|
event=callback_pb2.CALLBACK_EVENT_PRESOLVE
|
|
)
|
|
cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=16.0))
|
|
cb_data_proto.presolve_stats.removed_variables = 10
|
|
cb_data_proto.simplex_stats.iteration_count = 3
|
|
cb_data_proto.barrier_stats.primal_objective = 2.0
|
|
cb_data_proto.mip_stats.open_nodes = 5
|
|
cb_data = callback.parse_callback_data(cb_data_proto, mod)
|
|
self.assertEqual(cb_data.event, callback.Event.PRESOLVE)
|
|
self.assertIsNone(cb_data.solution)
|
|
self.assertEqual(16.0, cb_data.runtime.seconds)
|
|
self.assert_protos_equiv(
|
|
cb_data.presolve_stats,
|
|
callback_pb2.CallbackDataProto.PresolveStats(removed_variables=10),
|
|
)
|
|
self.assert_protos_equiv(
|
|
cb_data.simplex_stats,
|
|
callback_pb2.CallbackDataProto.SimplexStats(iteration_count=3),
|
|
)
|
|
self.assert_protos_equiv(
|
|
cb_data.barrier_stats,
|
|
callback_pb2.CallbackDataProto.BarrierStats(primal_objective=2.0),
|
|
)
|
|
self.assert_protos_equiv(
|
|
cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats(open_nodes=5)
|
|
)
|
|
|
|
def test_parse_callback_data_with_solution(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
cb_data_proto = callback_pb2.CallbackDataProto(
|
|
event=callback_pb2.CALLBACK_EVENT_MIP_SOLUTION
|
|
)
|
|
solution = cb_data_proto.primal_solution_vector
|
|
solution.ids[:] = [0, 1]
|
|
solution.values[:] = [0.0, 1.0]
|
|
cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=12.0))
|
|
cb_data = callback.parse_callback_data(cb_data_proto, mod)
|
|
self.assertEqual(cb_data.event, callback.Event.MIP_SOLUTION)
|
|
self.assertDictEqual(cb_data.solution, {x: 0.0, y: 1.0})
|
|
self.assertListEqual(cb_data.messages, [])
|
|
self.assertEqual(12.0, cb_data.runtime.seconds)
|
|
self.assert_protos_equiv(
|
|
cb_data.presolve_stats, callback_pb2.CallbackDataProto.PresolveStats()
|
|
)
|
|
self.assert_protos_equiv(
|
|
cb_data.simplex_stats, callback_pb2.CallbackDataProto.SimplexStats()
|
|
)
|
|
self.assert_protos_equiv(
|
|
cb_data.barrier_stats, callback_pb2.CallbackDataProto.BarrierStats()
|
|
)
|
|
self.assert_protos_equiv(
|
|
cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats()
|
|
)
|
|
|
|
|
|
class CallbackRegistrationTest(compare_proto.MathOptProtoAssertions, unittest.TestCase):
|
|
def testToProto(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
mod.add_binary_variable(name="y")
|
|
z = mod.add_binary_variable(name="z")
|
|
|
|
reg = callback.CallbackRegistration()
|
|
reg.events = {callback.Event.MIP_SOLUTION, callback.Event.MIP_NODE}
|
|
reg.mip_node_filter = sparse_containers.VariableFilter(filtered_items=(z, x))
|
|
reg.mip_solution_filter = sparse_containers.VariableFilter(
|
|
skip_zero_values=True
|
|
)
|
|
reg.add_lazy_constraints = True
|
|
reg.add_cuts = False
|
|
|
|
self.assert_protos_equiv(
|
|
reg.to_proto(),
|
|
callback_pb2.CallbackRegistrationProto(
|
|
request_registration=[
|
|
callback_pb2.CALLBACK_EVENT_MIP_SOLUTION,
|
|
callback_pb2.CALLBACK_EVENT_MIP_NODE,
|
|
],
|
|
mip_node_filter=sparse_containers_pb2.SparseVectorFilterProto(
|
|
filter_by_ids=True, filtered_ids=[0, 2]
|
|
),
|
|
mip_solution_filter=sparse_containers_pb2.SparseVectorFilterProto(
|
|
skip_zero_values=True
|
|
),
|
|
add_lazy_constraints=True,
|
|
add_cuts=False,
|
|
),
|
|
)
|
|
|
|
|
|
class GeneratedLinearConstraintTest(
|
|
compare_proto.MathOptProtoAssertions, unittest.TestCase
|
|
):
|
|
def testToProto(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
mod.add_binary_variable(name="y")
|
|
z = mod.add_binary_variable(name="z")
|
|
|
|
gen_con = callback.GeneratedConstraint()
|
|
gen_con.terms = {x: 2.0, z: 4.0}
|
|
gen_con.upper_bound = 5.0
|
|
gen_con.is_lazy = True
|
|
|
|
self.assert_protos_equiv(
|
|
gen_con.to_proto(),
|
|
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
|
|
lower_bound=-math.inf,
|
|
upper_bound=5.0,
|
|
is_lazy=True,
|
|
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 2], values=[2.0, 4.0]
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
class CallbackResultTest(compare_proto.MathOptProtoAssertions, unittest.TestCase):
|
|
def testToProto(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
z = mod.add_binary_variable(name="z")
|
|
|
|
result = callback.CallbackResult()
|
|
result.terminate = True
|
|
# Test le/ge combinations to avoid mutants.
|
|
result.add_lazy_constraint(2 * x <= 0)
|
|
result.add_lazy_constraint(2 * x >= 0)
|
|
result.add_user_cut(2 * z >= 2)
|
|
result.add_user_cut(2 * z <= 2)
|
|
result.add_generated_constraint(expr=2 * z, lb=2, is_lazy=False)
|
|
result.add_generated_constraint(expr=2 * z, ub=2, is_lazy=False)
|
|
result.suggested_solutions.append({x: 1.0, y: 0.0, z: 1.0})
|
|
result.suggested_solutions.append({x: 0.0, y: 0.0, z: 0.0})
|
|
|
|
expected = callback_pb2.CallbackResultProto(
|
|
terminate=True,
|
|
cuts=[
|
|
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
|
|
lower_bound=-math.inf,
|
|
upper_bound=0.0,
|
|
is_lazy=True,
|
|
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[2.0]
|
|
),
|
|
),
|
|
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
|
|
lower_bound=0.0,
|
|
upper_bound=math.inf,
|
|
is_lazy=True,
|
|
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[2.0]
|
|
),
|
|
),
|
|
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
|
|
lower_bound=2.0,
|
|
upper_bound=math.inf,
|
|
is_lazy=False,
|
|
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[2], values=[2.0]
|
|
),
|
|
),
|
|
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
|
|
lower_bound=-math.inf,
|
|
upper_bound=2.0,
|
|
is_lazy=False,
|
|
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[2], values=[2.0]
|
|
),
|
|
),
|
|
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
|
|
lower_bound=2.0,
|
|
upper_bound=math.inf,
|
|
is_lazy=False,
|
|
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[2], values=[2.0]
|
|
),
|
|
),
|
|
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
|
|
lower_bound=-math.inf,
|
|
upper_bound=2.0,
|
|
is_lazy=False,
|
|
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[2], values=[2.0]
|
|
),
|
|
),
|
|
],
|
|
suggested_solutions=[
|
|
sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1, 2], values=[1.0, 0.0, 1.0]
|
|
),
|
|
sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1, 2], values=[0.0, 0.0, 0.0]
|
|
),
|
|
],
|
|
)
|
|
self.assert_protos_equiv(result.to_proto(), expected)
|
|
|
|
def testConstraintErrors(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
z = mod.add_binary_variable(name="z")
|
|
|
|
result = callback.CallbackResult()
|
|
with self.assertRaisesRegex(
|
|
TypeError,
|
|
"unsupported operand.*\n.*two or more non-constant linear expressions",
|
|
):
|
|
result.add_lazy_constraint(x <= (y <= z))
|
|
with self.assertRaisesRegex(ValueError, "lb cannot be specified.*"):
|
|
result.add_user_cut(x + y == 1, lb=1)
|
|
|
|
def testToProtoEmpty(self) -> None:
|
|
result = callback.CallbackResult()
|
|
self.assert_protos_equiv(result.to_proto(), callback_pb2.CallbackResultProto())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|