-
-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])
-
-