Module miplearn.components.tests.test_lazy_static

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

from miplearn.classifiers import Classifier
from miplearn.components.lazy_static import StaticLazyConstraintsComponent
from miplearn.instance import Instance
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver


def test_usage_with_solver():
    solver = Mock(spec=LearningSolver)
    solver.use_lazy_cb = False
    solver.gap_tolerance = 1e-4

    internal = solver.internal_solver = Mock(spec=InternalSolver)
    internal.get_constraint_ids = Mock(return_value=["c1", "c2", "c3", "c4"])
    internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
    internal.is_constraint_satisfied = Mock(return_value=False)

    instance = Mock(spec=Instance)
    instance.has_static_lazy_constraints = Mock(return_value=True)
    instance.is_constraint_lazy = Mock(
        side_effect=lambda cid: {
            "c1": False,
            "c2": True,
            "c3": True,
            "c4": True,
        }[cid]
    )
    instance.get_constraint_features = Mock(
        side_effect=lambda cid: {
            "c2": [1.0, 0.0],
            "c3": [0.5, 0.5],
            "c4": [1.0],
        }[cid]
    )
    instance.get_constraint_category = Mock(
        side_effect=lambda cid: {
            "c2": "type-a",
            "c3": "type-a",
            "c4": "type-b",
        }[cid]
    )

    component = StaticLazyConstraintsComponent(
        threshold=0.90,
        use_two_phase_gap=False,
        violation_tolerance=1.0,
    )
    component.classifiers = {
        "type-a": Mock(spec=Classifier),
        "type-b": Mock(spec=Classifier),
    }
    component.classifiers["type-a"].predict_proba = Mock(
        return_value=[
            [0.20, 0.80],
            [0.05, 0.95],
        ]
    )
    component.classifiers["type-b"].predict_proba = Mock(
        return_value=[
            [0.02, 0.98],
        ]
    )

    # LearningSolver calls before_solve
    component.before_solve(solver, instance, None)

    # Should ask if instance has static lazy constraints
    instance.has_static_lazy_constraints.assert_called_once()

    # Should ask internal solver for a list of constraints in the model
    internal.get_constraint_ids.assert_called_once()

    # Should ask if each constraint in the model is lazy
    instance.is_constraint_lazy.assert_has_calls(
        [
            call("c1"),
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # For the lazy ones, should ask for features
    instance.get_constraint_features.assert_has_calls(
        [
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # Should also ask for categories
    assert instance.get_constraint_category.call_count == 3
    instance.get_constraint_category.assert_has_calls(
        [
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # Should ask internal solver to remove constraints identified as lazy
    assert internal.extract_constraint.call_count == 3
    internal.extract_constraint.assert_has_calls(
        [
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # Should ask ML to predict whether each lazy constraint should be enforced
    component.classifiers["type-a"].predict_proba.assert_called_once_with(
        [[1.0, 0.0], [0.5, 0.5]]
    )
    component.classifiers["type-b"].predict_proba.assert_called_once_with([[1.0]])

    # For the ones that should be enforced, should ask solver to re-add them
    # to the formulation. The remaining ones should remain in the pool.
    assert internal.add_constraint.call_count == 2
    internal.add_constraint.assert_has_calls(
        [
            call("<c3>"),
            call("<c4>"),
        ]
    )
    internal.add_constraint.reset_mock()

    # LearningSolver calls after_iteration (first time)
    should_repeat = component.iteration_cb(solver, instance, None)
    assert should_repeat

    # Should ask internal solver to verify if constraints in the pool are
    # satisfied and add the ones that are not
    internal.is_constraint_satisfied.assert_called_once_with("<c2>", tol=1.0)
    internal.is_constraint_satisfied.reset_mock()
    internal.add_constraint.assert_called_once_with("<c2>")
    internal.add_constraint.reset_mock()

    # LearningSolver calls after_iteration (second time)
    should_repeat = component.iteration_cb(solver, instance, None)
    assert not should_repeat

    # The lazy constraint pool should be empty by now, so no calls should be made
    internal.is_constraint_satisfied.assert_not_called()
    internal.add_constraint.assert_not_called()

    # Should update instance object
    assert instance.found_violated_lazy_constraints == ["c3", "c4", "c2"]


def test_fit():
    instance_1 = Mock(spec=Instance)
    instance_1.found_violated_lazy_constraints = ["c1", "c2", "c4", "c5"]
    instance_1.get_constraint_category = Mock(
        side_effect=lambda cid: {
            "c1": "type-a",
            "c2": "type-a",
            "c3": "type-a",
            "c4": "type-b",
            "c5": "type-b",
        }[cid]
    )
    instance_1.get_constraint_features = Mock(
        side_effect=lambda cid: {
            "c1": [1, 1],
            "c2": [1, 2],
            "c3": [1, 3],
            "c4": [1, 4, 0],
            "c5": [1, 5, 0],
        }[cid]
    )

    instance_2 = Mock(spec=Instance)
    instance_2.found_violated_lazy_constraints = ["c2", "c3", "c4"]
    instance_2.get_constraint_category = Mock(
        side_effect=lambda cid: {
            "c1": "type-a",
            "c2": "type-a",
            "c3": "type-a",
            "c4": "type-b",
            "c5": "type-b",
        }[cid]
    )
    instance_2.get_constraint_features = Mock(
        side_effect=lambda cid: {
            "c1": [2, 1],
            "c2": [2, 2],
            "c3": [2, 3],
            "c4": [2, 4, 0],
            "c5": [2, 5, 0],
        }[cid]
    )

    instances = [instance_1, instance_2]
    component = StaticLazyConstraintsComponent()
    component.classifiers = {
        "type-a": Mock(spec=Classifier),
        "type-b": Mock(spec=Classifier),
    }

    expected_constraints = {
        "type-a": ["c1", "c2", "c3"],
        "type-b": ["c4", "c5"],
    }
    expected_x = {
        "type-a": [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3]],
        "type-b": [[1, 4, 0], [1, 5, 0], [2, 4, 0], [2, 5, 0]],
    }
    expected_y = {
        "type-a": [[0, 1], [0, 1], [1, 0], [1, 0], [0, 1], [0, 1]],
        "type-b": [[0, 1], [0, 1], [0, 1], [1, 0]],
    }
    assert component._collect_constraints(instances) == expected_constraints
    assert component.x(instances) == expected_x
    assert component.y(instances) == expected_y

    component.fit(instances)
    component.classifiers["type-a"].fit.assert_called_once_with(
        expected_x["type-a"],
        expected_y["type-a"],
    )
    component.classifiers["type-b"].fit.assert_called_once_with(
        expected_x["type-b"],
        expected_y["type-b"],
    )

Functions

def test_fit()
Expand source code
def test_fit():
    instance_1 = Mock(spec=Instance)
    instance_1.found_violated_lazy_constraints = ["c1", "c2", "c4", "c5"]
    instance_1.get_constraint_category = Mock(
        side_effect=lambda cid: {
            "c1": "type-a",
            "c2": "type-a",
            "c3": "type-a",
            "c4": "type-b",
            "c5": "type-b",
        }[cid]
    )
    instance_1.get_constraint_features = Mock(
        side_effect=lambda cid: {
            "c1": [1, 1],
            "c2": [1, 2],
            "c3": [1, 3],
            "c4": [1, 4, 0],
            "c5": [1, 5, 0],
        }[cid]
    )

    instance_2 = Mock(spec=Instance)
    instance_2.found_violated_lazy_constraints = ["c2", "c3", "c4"]
    instance_2.get_constraint_category = Mock(
        side_effect=lambda cid: {
            "c1": "type-a",
            "c2": "type-a",
            "c3": "type-a",
            "c4": "type-b",
            "c5": "type-b",
        }[cid]
    )
    instance_2.get_constraint_features = Mock(
        side_effect=lambda cid: {
            "c1": [2, 1],
            "c2": [2, 2],
            "c3": [2, 3],
            "c4": [2, 4, 0],
            "c5": [2, 5, 0],
        }[cid]
    )

    instances = [instance_1, instance_2]
    component = StaticLazyConstraintsComponent()
    component.classifiers = {
        "type-a": Mock(spec=Classifier),
        "type-b": Mock(spec=Classifier),
    }

    expected_constraints = {
        "type-a": ["c1", "c2", "c3"],
        "type-b": ["c4", "c5"],
    }
    expected_x = {
        "type-a": [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3]],
        "type-b": [[1, 4, 0], [1, 5, 0], [2, 4, 0], [2, 5, 0]],
    }
    expected_y = {
        "type-a": [[0, 1], [0, 1], [1, 0], [1, 0], [0, 1], [0, 1]],
        "type-b": [[0, 1], [0, 1], [0, 1], [1, 0]],
    }
    assert component._collect_constraints(instances) == expected_constraints
    assert component.x(instances) == expected_x
    assert component.y(instances) == expected_y

    component.fit(instances)
    component.classifiers["type-a"].fit.assert_called_once_with(
        expected_x["type-a"],
        expected_y["type-a"],
    )
    component.classifiers["type-b"].fit.assert_called_once_with(
        expected_x["type-b"],
        expected_y["type-b"],
    )
def test_usage_with_solver()
Expand source code
def test_usage_with_solver():
    solver = Mock(spec=LearningSolver)
    solver.use_lazy_cb = False
    solver.gap_tolerance = 1e-4

    internal = solver.internal_solver = Mock(spec=InternalSolver)
    internal.get_constraint_ids = Mock(return_value=["c1", "c2", "c3", "c4"])
    internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
    internal.is_constraint_satisfied = Mock(return_value=False)

    instance = Mock(spec=Instance)
    instance.has_static_lazy_constraints = Mock(return_value=True)
    instance.is_constraint_lazy = Mock(
        side_effect=lambda cid: {
            "c1": False,
            "c2": True,
            "c3": True,
            "c4": True,
        }[cid]
    )
    instance.get_constraint_features = Mock(
        side_effect=lambda cid: {
            "c2": [1.0, 0.0],
            "c3": [0.5, 0.5],
            "c4": [1.0],
        }[cid]
    )
    instance.get_constraint_category = Mock(
        side_effect=lambda cid: {
            "c2": "type-a",
            "c3": "type-a",
            "c4": "type-b",
        }[cid]
    )

    component = StaticLazyConstraintsComponent(
        threshold=0.90,
        use_two_phase_gap=False,
        violation_tolerance=1.0,
    )
    component.classifiers = {
        "type-a": Mock(spec=Classifier),
        "type-b": Mock(spec=Classifier),
    }
    component.classifiers["type-a"].predict_proba = Mock(
        return_value=[
            [0.20, 0.80],
            [0.05, 0.95],
        ]
    )
    component.classifiers["type-b"].predict_proba = Mock(
        return_value=[
            [0.02, 0.98],
        ]
    )

    # LearningSolver calls before_solve
    component.before_solve(solver, instance, None)

    # Should ask if instance has static lazy constraints
    instance.has_static_lazy_constraints.assert_called_once()

    # Should ask internal solver for a list of constraints in the model
    internal.get_constraint_ids.assert_called_once()

    # Should ask if each constraint in the model is lazy
    instance.is_constraint_lazy.assert_has_calls(
        [
            call("c1"),
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # For the lazy ones, should ask for features
    instance.get_constraint_features.assert_has_calls(
        [
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # Should also ask for categories
    assert instance.get_constraint_category.call_count == 3
    instance.get_constraint_category.assert_has_calls(
        [
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # Should ask internal solver to remove constraints identified as lazy
    assert internal.extract_constraint.call_count == 3
    internal.extract_constraint.assert_has_calls(
        [
            call("c2"),
            call("c3"),
            call("c4"),
        ]
    )

    # Should ask ML to predict whether each lazy constraint should be enforced
    component.classifiers["type-a"].predict_proba.assert_called_once_with(
        [[1.0, 0.0], [0.5, 0.5]]
    )
    component.classifiers["type-b"].predict_proba.assert_called_once_with([[1.0]])

    # For the ones that should be enforced, should ask solver to re-add them
    # to the formulation. The remaining ones should remain in the pool.
    assert internal.add_constraint.call_count == 2
    internal.add_constraint.assert_has_calls(
        [
            call("<c3>"),
            call("<c4>"),
        ]
    )
    internal.add_constraint.reset_mock()

    # LearningSolver calls after_iteration (first time)
    should_repeat = component.iteration_cb(solver, instance, None)
    assert should_repeat

    # Should ask internal solver to verify if constraints in the pool are
    # satisfied and add the ones that are not
    internal.is_constraint_satisfied.assert_called_once_with("<c2>", tol=1.0)
    internal.is_constraint_satisfied.reset_mock()
    internal.add_constraint.assert_called_once_with("<c2>")
    internal.add_constraint.reset_mock()

    # LearningSolver calls after_iteration (second time)
    should_repeat = component.iteration_cb(solver, instance, None)
    assert not should_repeat

    # The lazy constraint pool should be empty by now, so no calls should be made
    internal.is_constraint_satisfied.assert_not_called()
    internal.add_constraint.assert_not_called()

    # Should update instance object
    assert instance.found_violated_lazy_constraints == ["c3", "c4", "c2"]