+
+Expand source code
+
+# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
+# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
+# Released under the modified BSD license. See COPYING.md for more details.
+
+from unittest.mock import Mock, call
+
+import numpy as np
+
+from miplearn.classifiers import Classifier
+from miplearn.components.relaxation import DropRedundantInequalitiesStep
+from miplearn.instance import Instance
+from miplearn.solvers.internal import InternalSolver
+from miplearn.solvers.learning import LearningSolver
+
+
+def _setup():
+ solver = Mock(spec=LearningSolver)
+
+ internal = solver.internal_solver = Mock(spec=InternalSolver)
+ internal.get_constraint_ids = Mock(return_value=["c1", "c2", "c3", "c4"])
+ internal.get_inequality_slacks = Mock(
+ side_effect=lambda: {
+ "c1": 0.5,
+ "c2": 0.0,
+ "c3": 0.0,
+ "c4": 1.4,
+ }
+ )
+ internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
+ internal.is_constraint_satisfied = Mock(return_value=False)
+
+ instance = Mock(spec=Instance)
+ instance.get_constraint_features = Mock(
+ side_effect=lambda cid: {
+ "c2": np.array([1.0, 0.0]),
+ "c3": np.array([0.5, 0.5]),
+ "c4": np.array([1.0]),
+ }[cid]
+ )
+ instance.get_constraint_category = Mock(
+ side_effect=lambda cid: {
+ "c1": None,
+ "c2": "type-a",
+ "c3": "type-a",
+ "c4": "type-b",
+ }[cid]
+ )
+
+ classifiers = {
+ "type-a": Mock(spec=Classifier),
+ "type-b": Mock(spec=Classifier),
+ }
+ classifiers["type-a"].predict_proba = Mock(
+ return_value=np.array(
+ [
+ [0.20, 0.80],
+ [0.05, 0.95],
+ ]
+ )
+ )
+ classifiers["type-b"].predict_proba = Mock(
+ return_value=np.array(
+ [
+ [0.02, 0.98],
+ ]
+ )
+ )
+
+ return solver, internal, instance, classifiers
+
+
+def test_drop_redundant():
+ solver, internal, instance, classifiers = _setup()
+
+ component = DropRedundantInequalitiesStep()
+ component.classifiers = classifiers
+
+ # LearningSolver calls before_solve
+ component.before_solve(solver, instance, None)
+
+ # Should query list of constraints
+ internal.get_constraint_ids.assert_called_once()
+
+ # Should query category and features for each constraint in the model
+ assert instance.get_constraint_category.call_count == 4
+ instance.get_constraint_category.assert_has_calls(
+ [
+ call("c1"),
+ call("c2"),
+ call("c3"),
+ call("c4"),
+ ]
+ )
+
+ # For constraint with non-null categories, should ask for features
+ assert instance.get_constraint_features.call_count == 3
+ instance.get_constraint_features.assert_has_calls(
+ [
+ call("c2"),
+ call("c3"),
+ call("c4"),
+ ]
+ )
+
+ # Should ask ML to predict whether constraint should be removed
+ type_a_actual = component.classifiers["type-a"].predict_proba.call_args[0][0]
+ type_b_actual = component.classifiers["type-b"].predict_proba.call_args[0][0]
+ np.testing.assert_array_equal(type_a_actual, np.array([[1.0, 0.0], [0.5, 0.5]]))
+ np.testing.assert_array_equal(type_b_actual, np.array([[1.0]]))
+
+ # Should ask internal solver to remove constraints predicted as redundant
+ assert internal.extract_constraint.call_count == 2
+ internal.extract_constraint.assert_has_calls(
+ [
+ call("c3"),
+ call("c4"),
+ ]
+ )
+
+ # LearningSolver calls after_solve
+ training_data = {}
+ component.after_solve(solver, instance, None, {}, training_data)
+
+ # Should query slack for all inequalities
+ internal.get_inequality_slacks.assert_called_once()
+
+ # Should store constraint slacks in instance object
+ assert training_data["slacks"] == {
+ "c1": 0.5,
+ "c2": 0.0,
+ "c3": 0.0,
+ "c4": 1.4,
+ }
+
+
+def test_drop_redundant_with_check_feasibility():
+ solver, internal, instance, classifiers = _setup()
+
+ component = DropRedundantInequalitiesStep(
+ check_feasibility=True,
+ violation_tolerance=1e-3,
+ )
+ component.classifiers = classifiers
+
+ # LearningSolver call before_solve
+ component.before_solve(solver, instance, None)
+
+ # Assert constraints are extracted
+ assert internal.extract_constraint.call_count == 2
+ internal.extract_constraint.assert_has_calls(
+ [
+ call("c3"),
+ call("c4"),
+ ]
+ )
+
+ # LearningSolver calls iteration_cb (first time)
+ should_repeat = component.iteration_cb(solver, instance, None)
+
+ # Should ask LearningSolver to repeat
+ assert should_repeat
+
+ # Should ask solver if removed constraints are satisfied (mock always returns false)
+ internal.is_constraint_satisfied.assert_has_calls(
+ [
+ call("<c3>", 1e-3),
+ call("<c4>", 1e-3),
+ ]
+ )
+
+ # Should add constraints back to LP relaxation
+ internal.add_constraint.assert_has_calls([call("<c3>"), call("<c4>")])
+
+ # LearningSolver calls iteration_cb (second time)
+ should_repeat = component.iteration_cb(solver, instance, None)
+ assert not should_repeat
+
+
+def test_x_y_fit_predict_evaluate():
+ instances = [Mock(spec=Instance), Mock(spec=Instance)]
+ component = DropRedundantInequalitiesStep(slack_tolerance=0.05, threshold=0.80)
+ component.classifiers = {
+ "type-a": Mock(spec=Classifier),
+ "type-b": Mock(spec=Classifier),
+ }
+ component.classifiers["type-a"].predict_proba = Mock(
+ return_value=[
+ np.array([0.20, 0.80]),
+ ]
+ )
+ component.classifiers["type-b"].predict_proba = Mock(
+ return_value=np.array(
+ [
+ [0.50, 0.50],
+ [0.05, 0.95],
+ ]
+ )
+ )
+
+ # First mock instance
+ instances[0].training_data = [
+ {
+ "slacks": {
+ "c1": 0.00,
+ "c2": 0.05,
+ "c3": 0.00,
+ "c4": 30.0,
+ }
+ }
+ ]
+ instances[0].get_constraint_category = Mock(
+ side_effect=lambda cid: {
+ "c1": None,
+ "c2": "type-a",
+ "c3": "type-a",
+ "c4": "type-b",
+ }[cid]
+ )
+ instances[0].get_constraint_features = Mock(
+ side_effect=lambda cid: {
+ "c2": np.array([1.0, 0.0]),
+ "c3": np.array([0.5, 0.5]),
+ "c4": np.array([1.0]),
+ }[cid]
+ )
+
+ # Second mock instance
+ instances[1].training_data = [
+ {
+ "slacks": {
+ "c1": 0.00,
+ "c3": 0.30,
+ "c4": 0.00,
+ "c5": 0.00,
+ }
+ }
+ ]
+ instances[1].get_constraint_category = Mock(
+ side_effect=lambda cid: {
+ "c1": None,
+ "c3": "type-a",
+ "c4": "type-b",
+ "c5": "type-b",
+ }[cid]
+ )
+ instances[1].get_constraint_features = Mock(
+ side_effect=lambda cid: {
+ "c3": np.array([0.3, 0.4]),
+ "c4": np.array([0.7]),
+ "c5": np.array([0.8]),
+ }[cid]
+ )
+
+ expected_x = {
+ "type-a": np.array(
+ [
+ [1.0, 0.0],
+ [0.5, 0.5],
+ [0.3, 0.4],
+ ]
+ ),
+ "type-b": np.array(
+ [
+ [1.0],
+ [0.7],
+ [0.8],
+ ]
+ ),
+ }
+ expected_y = {
+ "type-a": np.array([[0], [0], [1]]),
+ "type-b": np.array([[1], [0], [0]]),
+ }
+
+ # Should build X and Y matrices correctly
+ actual_x = component.x(instances)
+ actual_y = component.y(instances)
+ for category in ["type-a", "type-b"]:
+ np.testing.assert_array_equal(actual_x[category], expected_x[category])
+ np.testing.assert_array_equal(actual_y[category], expected_y[category])
+
+ # Should pass along X and Y matrices to classifiers
+ component.fit(instances)
+ for category in ["type-a", "type-b"]:
+ actual_x = component.classifiers[category].fit.call_args[0][0]
+ actual_y = component.classifiers[category].fit.call_args[0][1]
+ np.testing.assert_array_equal(actual_x, expected_x[category])
+ np.testing.assert_array_equal(actual_y, expected_y[category])
+
+ assert component.predict(expected_x) == {"type-a": [[1]], "type-b": [[0], [1]]}
+
+ ev = component.evaluate(instances[1])
+ assert ev["True positive"] == 1
+ assert ev["True negative"] == 1
+ assert ev["False positive"] == 1
+ assert ev["False negative"] == 0
+
+
+def test_x_multiple_solves():
+ instance = Mock(spec=Instance)
+ instance.training_data = [
+ {
+ "slacks": {
+ "c1": 0.00,
+ "c2": 0.05,
+ "c3": 0.00,
+ "c4": 30.0,
+ }
+ },
+ {
+ "slacks": {
+ "c1": 0.00,
+ "c2": 0.00,
+ "c3": 1.00,
+ "c4": 0.0,
+ }
+ },
+ ]
+ instance.get_constraint_category = Mock(
+ side_effect=lambda cid: {
+ "c1": None,
+ "c2": "type-a",
+ "c3": "type-a",
+ "c4": "type-b",
+ }[cid]
+ )
+ instance.get_constraint_features = Mock(
+ side_effect=lambda cid: {
+ "c2": np.array([1.0, 0.0]),
+ "c3": np.array([0.5, 0.5]),
+ "c4": np.array([1.0]),
+ }[cid]
+ )
+
+ expected_x = {
+ "type-a": np.array(
+ [
+ [1.0, 0.0],
+ [0.5, 0.5],
+ [1.0, 0.0],
+ [0.5, 0.5],
+ ]
+ ),
+ "type-b": np.array(
+ [
+ [1.0],
+ [1.0],
+ ]
+ ),
+ }
+
+ expected_y = {
+ "type-a": np.array([[1], [0], [0], [1]]),
+ "type-b": np.array([[1], [0]]),
+ }
+
+ # Should build X and Y matrices correctly
+ component = DropRedundantInequalitiesStep()
+ actual_x = component.x([instance])
+ actual_y = component.y([instance])
+ print(actual_x)
+ for category in ["type-a", "type-b"]:
+ np.testing.assert_array_equal(actual_x[category], expected_x[category])
+ np.testing.assert_array_equal(actual_y[category], expected_y[category])
+
+