diff --git a/miplearn/components/steps/drop_redundant.py b/miplearn/components/steps/drop_redundant.py index 4bd3c5f..1acd9c0 100644 --- a/miplearn/components/steps/drop_redundant.py +++ b/miplearn/components/steps/drop_redundant.py @@ -50,11 +50,9 @@ class DropRedundantInequalitiesStep(Component): self.current_iteration = 0 logger.info("Predicting redundant LP constraints...") - cids = solver.internal_solver.get_constraint_ids() - x, constraints = self.x( - [instance], - constraint_ids=cids, - return_constraints=True, + x, constraints = self._x_test( + instance, + constraint_ids=solver.internal_solver.get_constraint_ids(), ) y = self.predict(x) @@ -84,11 +82,16 @@ class DropRedundantInequalitiesStep(Component): stats, training_data, ): - instance.slacks = solver.internal_solver.get_inequality_slacks() - stats["DropRedundant: Kept"] = self.total_kept - stats["DropRedundant: Dropped"] = self.total_dropped - stats["DropRedundant: Restored"] = self.total_restored - stats["DropRedundant: Iterations"] = self.total_iterations + if "slacks" not in training_data.keys(): + training_data["slacks"] = solver.internal_solver.get_inequality_slacks() + stats.update( + { + "DropRedundant: Kept": self.total_kept, + "DropRedundant: Dropped": self.total_dropped, + "DropRedundant: Restored": self.total_restored, + "DropRedundant: Iterations": self.total_iterations, + } + ) def fit(self, training_instances): logger.debug("Extracting x and y...") @@ -100,33 +103,45 @@ class DropRedundantInequalitiesStep(Component): self.classifiers[category] = deepcopy(self.classifier_prototype) self.classifiers[category].fit(x[category], y[category]) - def x(self, instances, constraint_ids=None, return_constraints=False): + def _x_test(self, instance, constraint_ids): x = {} constraints = {} + cids = constraint_ids + for cid in cids: + category = instance.get_constraint_category(cid) + if category is None: + continue + if category not in x: + x[category] = [] + constraints[category] = [] + x[category] += [instance.get_constraint_features(cid)] + constraints[category] += [cid] + for category in x.keys(): + x[category] = np.array(x[category]) + return x, constraints + + def _x_train(self, instances): + x = {} for instance in tqdm( InstanceIterator(instances), desc="Extract (rlx:drop_ineq:x)", disable=len(instances) < 5, ): - if constraint_ids is not None: - cids = constraint_ids - else: - cids = instance.slacks.keys() - for cid in cids: - category = instance.get_constraint_category(cid) - if category is None: - continue - if category not in x: - x[category] = [] - constraints[category] = [] - x[category] += [instance.get_constraint_features(cid)] - constraints[category] += [cid] + for training_data in instance.training_data: + cids = training_data["slacks"].keys() + for cid in cids: + category = instance.get_constraint_category(cid) + if category is None: + continue + if category not in x: + x[category] = [] + x[category] += [instance.get_constraint_features(cid)] for category in x.keys(): x[category] = np.array(x[category]) - if return_constraints: - return x, constraints - else: - return x + return x + + def x(self, instances): + return self._x_train(instances) def y(self, instances): y = {} @@ -135,16 +150,17 @@ class DropRedundantInequalitiesStep(Component): desc="Extract (rlx:drop_ineq:y)", disable=len(instances) < 5, ): - for (cid, slack) in instance.slacks.items(): - category = instance.get_constraint_category(cid) - if category is None: - continue - if category not in y: - y[category] = [] - if slack > self.slack_tolerance: - y[category] += [[1]] - else: - y[category] += [[0]] + for training_data in instance.training_data: + for (cid, slack) in training_data["slacks"].items(): + category = instance.get_constraint_category(cid) + if category is None: + continue + if category not in y: + y[category] = [] + if slack > self.slack_tolerance: + y[category] += [[1]] + else: + y[category] += [[0]] return y def predict(self, x): diff --git a/miplearn/components/steps/tests/convert_tight_test.py b/miplearn/components/steps/tests/test_convert_tight.py similarity index 100% rename from miplearn/components/steps/tests/convert_tight_test.py rename to miplearn/components/steps/tests/test_convert_tight.py diff --git a/miplearn/components/tests/test_relaxation.py b/miplearn/components/steps/tests/test_drop_redundant.py similarity index 75% rename from miplearn/components/tests/test_relaxation.py rename to miplearn/components/steps/tests/test_drop_redundant.py index 14f3923..bebafdf 100644 --- a/miplearn/components/tests/test_relaxation.py +++ b/miplearn/components/steps/tests/test_drop_redundant.py @@ -5,9 +5,18 @@ import numpy as np from unittest.mock import Mock, call -from miplearn import LearningSolver, Instance, InternalSolver +from miplearn import ( + LearningSolver, + Instance, + InternalSolver, + GurobiSolver, +) from miplearn.classifiers import Classifier -from miplearn.components.relaxation import DropRedundantInequalitiesStep +from miplearn.components.relaxation import ( + DropRedundantInequalitiesStep, + RelaxIntegralityStep, +) +from miplearn.problems.knapsack import GurobiKnapsackInstance def _setup(): @@ -115,14 +124,14 @@ def test_drop_redundant(): ) # LearningSolver calls after_solve - component.after_solve(solver, instance, None, {}, {}) + 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 hasattr(instance, "slacks") - assert instance.slacks == { + assert training_data["slacks"] == { "c1": 0.5, "c2": 0.0, "c3": 0.0, @@ -130,7 +139,7 @@ def test_drop_redundant(): } -def test_drop_redundant_with_check_dropped(): +def test_drop_redundant_with_check_feasibility(): solver, internal, instance, classifiers = _setup() component = DropRedundantInequalitiesStep( @@ -195,12 +204,16 @@ def test_x_y_fit_predict_evaluate(): ) # First mock instance - instances[0].slacks = { - "c1": 0.00, - "c2": 0.05, - "c3": 0.00, - "c4": 30.0, - } + 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, @@ -218,12 +231,16 @@ def test_x_y_fit_predict_evaluate(): ) # Second mock instance - instances[1].slacks = { - "c1": 0.00, - "c3": 0.30, - "c4": 0.00, - "c5": 0.00, - } + 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, @@ -283,3 +300,71 @@ def test_x_y_fit_predict_evaluate(): 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]) \ No newline at end of file