mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
Merge branch 'feature/new-py-api' into feature/docs
This commit is contained in:
2
Makefile
2
Makefile
@@ -27,7 +27,7 @@ docs:
|
|||||||
|
|
||||||
install-deps:
|
install-deps:
|
||||||
$(PIP) install --upgrade pip
|
$(PIP) install --upgrade pip
|
||||||
$(PIP) install --upgrade -i https://pypi.gurobi.com gurobipy
|
$(PIP) install --upgrade -i https://pypi.gurobi.com 'gurobipy>=9.5,<9.6'
|
||||||
$(PIP) install --upgrade xpress
|
$(PIP) install --upgrade xpress
|
||||||
$(PIP) install --upgrade -r requirements.txt
|
$(PIP) install --upgrade -r requirements.txt
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ html_theme_options = {
|
|||||||
"extra_navbar": "",
|
"extra_navbar": "",
|
||||||
}
|
}
|
||||||
html_title = f"MIPLearn {release}"
|
html_title = f"MIPLearn {release}"
|
||||||
nbsphinx_execute = 'never'
|
nbsphinx_execute = "never"
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from .instance.picklegz import (
|
|||||||
write_pickle_gz,
|
write_pickle_gz,
|
||||||
read_pickle_gz,
|
read_pickle_gz,
|
||||||
write_pickle_gz_multiple,
|
write_pickle_gz_multiple,
|
||||||
|
save,
|
||||||
|
load,
|
||||||
)
|
)
|
||||||
from .log import setup_logger
|
from .log import setup_logger
|
||||||
from .solvers.gurobi import GurobiSolver
|
from .solvers.gurobi import GurobiSolver
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ def run_benchmarks(
|
|||||||
n_jobs: int = 4,
|
n_jobs: int = 4,
|
||||||
n_trials: int = 1,
|
n_trials: int = 1,
|
||||||
progress: bool = False,
|
progress: bool = False,
|
||||||
solver=None,
|
solver: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if solver is None:
|
if solver is None:
|
||||||
solver = GurobiPyomoSolver()
|
solver = GurobiPyomoSolver()
|
||||||
@@ -175,7 +175,7 @@ def run_benchmarks(
|
|||||||
|
|
||||||
def plot(
|
def plot(
|
||||||
results: pd.DataFrame,
|
results: pd.DataFrame,
|
||||||
output: str = None,
|
output: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -280,4 +280,4 @@ def plot(
|
|||||||
|
|
||||||
fig.tight_layout()
|
fig.tight_layout()
|
||||||
if output is not None:
|
if output is not None:
|
||||||
plt.savefig(output)
|
plt.savefig(output)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple, Optional, Any, Set
|
from typing import Dict, List, Tuple, Optional, Any, Set
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class DynamicConstraintsComponent(Component):
|
|||||||
self.classifier_prototype: Classifier = classifier
|
self.classifier_prototype: Classifier = classifier
|
||||||
self.classifiers: Dict[ConstraintCategory, Classifier] = {}
|
self.classifiers: Dict[ConstraintCategory, Classifier] = {}
|
||||||
self.thresholds: Dict[ConstraintCategory, Threshold] = {}
|
self.thresholds: Dict[ConstraintCategory, Threshold] = {}
|
||||||
self.known_cids: List[ConstraintName] = []
|
self.known_violations: Dict[ConstraintName, Any] = {}
|
||||||
self.attr = attr
|
self.attr = attr
|
||||||
|
|
||||||
def sample_xy_with_cids(
|
def sample_xy_with_cids(
|
||||||
@@ -48,18 +48,19 @@ class DynamicConstraintsComponent(Component):
|
|||||||
Dict[ConstraintCategory, List[List[bool]]],
|
Dict[ConstraintCategory, List[List[bool]]],
|
||||||
Dict[ConstraintCategory, List[ConstraintName]],
|
Dict[ConstraintCategory, List[ConstraintName]],
|
||||||
]:
|
]:
|
||||||
if len(self.known_cids) == 0:
|
if len(self.known_violations) == 0:
|
||||||
return {}, {}, {}
|
return {}, {}, {}
|
||||||
assert instance is not None
|
assert instance is not None
|
||||||
x: Dict[ConstraintCategory, List[List[float]]] = {}
|
x: Dict[ConstraintCategory, List[List[float]]] = {}
|
||||||
y: Dict[ConstraintCategory, List[List[bool]]] = {}
|
y: Dict[ConstraintCategory, List[List[bool]]] = {}
|
||||||
cids: Dict[ConstraintCategory, List[ConstraintName]] = {}
|
cids: Dict[ConstraintCategory, List[ConstraintName]] = {}
|
||||||
known_cids = np.array(self.known_cids, dtype="S")
|
known_cids = np.array(sorted(list(self.known_violations.keys())), dtype="S")
|
||||||
|
|
||||||
enforced_cids = None
|
enforced_cids = None
|
||||||
enforced_cids_np = sample.get_array(self.attr)
|
enforced_encoded = sample.get_scalar(self.attr)
|
||||||
if enforced_cids_np is not None:
|
if enforced_encoded is not None:
|
||||||
enforced_cids = list(enforced_cids_np)
|
enforced = self.decode(enforced_encoded)
|
||||||
|
enforced_cids = list(enforced.keys())
|
||||||
|
|
||||||
# Get user-provided constraint features
|
# Get user-provided constraint features
|
||||||
(
|
(
|
||||||
@@ -100,13 +101,10 @@ class DynamicConstraintsComponent(Component):
|
|||||||
@overrides
|
@overrides
|
||||||
def pre_fit(self, pre: List[Any]) -> None:
|
def pre_fit(self, pre: List[Any]) -> None:
|
||||||
assert pre is not None
|
assert pre is not None
|
||||||
known_cids: Set = set()
|
self.known_violations.clear()
|
||||||
for cids in pre:
|
for violations in pre:
|
||||||
if cids is None:
|
for (vname, vdata) in violations.items():
|
||||||
continue
|
self.known_violations[vname] = vdata
|
||||||
known_cids |= set(list(cids))
|
|
||||||
self.known_cids.clear()
|
|
||||||
self.known_cids.extend(sorted(known_cids))
|
|
||||||
|
|
||||||
def sample_predict(
|
def sample_predict(
|
||||||
self,
|
self,
|
||||||
@@ -114,7 +112,7 @@ class DynamicConstraintsComponent(Component):
|
|||||||
sample: Sample,
|
sample: Sample,
|
||||||
) -> List[ConstraintName]:
|
) -> List[ConstraintName]:
|
||||||
pred: List[ConstraintName] = []
|
pred: List[ConstraintName] = []
|
||||||
if len(self.known_cids) == 0:
|
if len(self.known_violations) == 0:
|
||||||
logger.info("Classifiers not fitted. Skipping.")
|
logger.info("Classifiers not fitted. Skipping.")
|
||||||
return pred
|
return pred
|
||||||
x, _, cids = self.sample_xy_with_cids(instance, sample)
|
x, _, cids = self.sample_xy_with_cids(instance, sample)
|
||||||
@@ -133,7 +131,9 @@ class DynamicConstraintsComponent(Component):
|
|||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def pre_sample_xy(self, instance: Instance, sample: Sample) -> Any:
|
def pre_sample_xy(self, instance: Instance, sample: Sample) -> Any:
|
||||||
return sample.get_array(self.attr)
|
attr_encoded = sample.get_scalar(self.attr)
|
||||||
|
assert attr_encoded is not None
|
||||||
|
return self.decode(attr_encoded)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def fit_xy(
|
def fit_xy(
|
||||||
@@ -155,11 +155,13 @@ class DynamicConstraintsComponent(Component):
|
|||||||
instance: Instance,
|
instance: Instance,
|
||||||
sample: Sample,
|
sample: Sample,
|
||||||
) -> Dict[str, float]:
|
) -> Dict[str, float]:
|
||||||
actual = sample.get_array(self.attr)
|
attr_encoded = sample.get_scalar(self.attr)
|
||||||
assert actual is not None
|
assert attr_encoded is not None
|
||||||
|
actual_violations = DynamicConstraintsComponent.decode(attr_encoded)
|
||||||
|
actual = set(actual_violations.keys())
|
||||||
pred = set(self.sample_predict(instance, sample))
|
pred = set(self.sample_predict(instance, sample))
|
||||||
tp, tn, fp, fn = 0, 0, 0, 0
|
tp, tn, fp, fn = 0, 0, 0, 0
|
||||||
for cid in self.known_cids:
|
for cid in self.known_violations.keys():
|
||||||
if cid in pred:
|
if cid in pred:
|
||||||
if cid in actual:
|
if cid in actual:
|
||||||
tp += 1
|
tp += 1
|
||||||
@@ -171,3 +173,12 @@ class DynamicConstraintsComponent(Component):
|
|||||||
else:
|
else:
|
||||||
tn += 1
|
tn += 1
|
||||||
return classifier_evaluation_dict(tp=tp, tn=tn, fp=fp, fn=fn)
|
return classifier_evaluation_dict(tp=tp, tn=tn, fp=fp, fn=fn)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode(violations: Dict[ConstraintName, Any]) -> str:
|
||||||
|
return json.dumps({k.decode(): v for (k, v) in violations.items()})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(violations_encoded: str) -> Dict[ConstraintName, Any]:
|
||||||
|
violations = json.loads(violations_encoded)
|
||||||
|
return {k.encode(): v for (k, v) in violations.items()}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import pdb
|
from typing import Dict, List, TYPE_CHECKING, Tuple, Any, Optional
|
||||||
from typing import Dict, List, TYPE_CHECKING, Tuple, Any, Optional, Set
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from overrides import overrides
|
from overrides import overrides
|
||||||
@@ -37,23 +35,23 @@ class DynamicLazyConstraintsComponent(Component):
|
|||||||
self.dynamic: DynamicConstraintsComponent = DynamicConstraintsComponent(
|
self.dynamic: DynamicConstraintsComponent = DynamicConstraintsComponent(
|
||||||
classifier=classifier,
|
classifier=classifier,
|
||||||
threshold=threshold,
|
threshold=threshold,
|
||||||
attr="mip_constr_lazy_enforced",
|
attr="mip_constr_lazy",
|
||||||
)
|
)
|
||||||
self.classifiers = self.dynamic.classifiers
|
self.classifiers = self.dynamic.classifiers
|
||||||
self.thresholds = self.dynamic.thresholds
|
self.thresholds = self.dynamic.thresholds
|
||||||
self.known_cids = self.dynamic.known_cids
|
self.known_violations = self.dynamic.known_violations
|
||||||
self.lazy_enforced: Set[ConstraintName] = set()
|
self.lazy_enforced: Dict[ConstraintName, Any] = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enforce(
|
def enforce(
|
||||||
cids: List[ConstraintName],
|
violations: Dict[ConstraintName, Any],
|
||||||
instance: Instance,
|
instance: Instance,
|
||||||
model: Any,
|
model: Any,
|
||||||
solver: "LearningSolver",
|
solver: "LearningSolver",
|
||||||
) -> None:
|
) -> None:
|
||||||
assert solver.internal_solver is not None
|
assert solver.internal_solver is not None
|
||||||
for cid in cids:
|
for (vname, vdata) in violations.items():
|
||||||
instance.enforce_lazy_constraint(solver.internal_solver, model, cid)
|
instance.enforce_lazy_constraint(solver.internal_solver, model, vdata)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def before_solve_mip(
|
def before_solve_mip(
|
||||||
@@ -66,9 +64,10 @@ class DynamicLazyConstraintsComponent(Component):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.lazy_enforced.clear()
|
self.lazy_enforced.clear()
|
||||||
logger.info("Predicting violated (dynamic) lazy constraints...")
|
logger.info("Predicting violated (dynamic) lazy constraints...")
|
||||||
cids = self.dynamic.sample_predict(instance, sample)
|
vnames = self.dynamic.sample_predict(instance, sample)
|
||||||
logger.info("Enforcing %d lazy constraints..." % len(cids))
|
violations = {c: self.dynamic.known_violations[c] for c in vnames}
|
||||||
self.enforce(cids, instance, model, solver)
|
logger.info("Enforcing %d lazy constraints..." % len(vnames))
|
||||||
|
self.enforce(violations, instance, model, solver)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def after_solve_mip(
|
def after_solve_mip(
|
||||||
@@ -79,10 +78,7 @@ class DynamicLazyConstraintsComponent(Component):
|
|||||||
stats: LearningSolveStats,
|
stats: LearningSolveStats,
|
||||||
sample: Sample,
|
sample: Sample,
|
||||||
) -> None:
|
) -> None:
|
||||||
sample.put_array(
|
sample.put_scalar("mip_constr_lazy", self.dynamic.encode(self.lazy_enforced))
|
||||||
"mip_constr_lazy_enforced",
|
|
||||||
np.array(list(self.lazy_enforced), dtype="S"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def iteration_cb(
|
def iteration_cb(
|
||||||
@@ -93,14 +89,17 @@ class DynamicLazyConstraintsComponent(Component):
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
assert solver.internal_solver is not None
|
assert solver.internal_solver is not None
|
||||||
logger.debug("Finding violated lazy constraints...")
|
logger.debug("Finding violated lazy constraints...")
|
||||||
cids = instance.find_violated_lazy_constraints(solver.internal_solver, model)
|
violations = instance.find_violated_lazy_constraints(
|
||||||
if len(cids) == 0:
|
solver.internal_solver, model
|
||||||
|
)
|
||||||
|
if len(violations) == 0:
|
||||||
logger.debug("No violations found")
|
logger.debug("No violations found")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
self.lazy_enforced |= set(cids)
|
for v in violations:
|
||||||
logger.debug(" %d violations found" % len(cids))
|
self.lazy_enforced[v] = violations[v]
|
||||||
self.enforce(cids, instance, model, solver)
|
logger.debug(" %d violations found" % len(violations))
|
||||||
|
self.enforce(violations, instance, model, solver)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Delegate ML methods to self.dynamic
|
# Delegate ML methods to self.dynamic
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, TYPE_CHECKING, Set, Tuple, Dict, List, Optional
|
from typing import Any, TYPE_CHECKING, Tuple, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from overrides import overrides
|
from overrides import overrides
|
||||||
@@ -32,9 +31,9 @@ class UserCutsComponent(Component):
|
|||||||
self.dynamic = DynamicConstraintsComponent(
|
self.dynamic = DynamicConstraintsComponent(
|
||||||
classifier=classifier,
|
classifier=classifier,
|
||||||
threshold=threshold,
|
threshold=threshold,
|
||||||
attr="mip_user_cuts_enforced",
|
attr="mip_user_cuts",
|
||||||
)
|
)
|
||||||
self.enforced: Set[ConstraintName] = set()
|
self.enforced: Dict[ConstraintName, Any] = {}
|
||||||
self.n_added_in_callback = 0
|
self.n_added_in_callback = 0
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
@@ -50,11 +49,12 @@ class UserCutsComponent(Component):
|
|||||||
self.enforced.clear()
|
self.enforced.clear()
|
||||||
self.n_added_in_callback = 0
|
self.n_added_in_callback = 0
|
||||||
logger.info("Predicting violated user cuts...")
|
logger.info("Predicting violated user cuts...")
|
||||||
cids = self.dynamic.sample_predict(instance, sample)
|
vnames = self.dynamic.sample_predict(instance, sample)
|
||||||
logger.info("Enforcing %d user cuts ahead-of-time..." % len(cids))
|
logger.info("Enforcing %d user cuts ahead-of-time..." % len(vnames))
|
||||||
for cid in cids:
|
for vname in vnames:
|
||||||
instance.enforce_user_cut(solver.internal_solver, model, cid)
|
vdata = self.dynamic.known_violations[vname]
|
||||||
stats["UserCuts: Added ahead-of-time"] = len(cids)
|
instance.enforce_user_cut(solver.internal_solver, model, vdata)
|
||||||
|
stats["UserCuts: Added ahead-of-time"] = len(vnames)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def user_cut_cb(
|
def user_cut_cb(
|
||||||
@@ -65,18 +65,17 @@ class UserCutsComponent(Component):
|
|||||||
) -> None:
|
) -> None:
|
||||||
assert solver.internal_solver is not None
|
assert solver.internal_solver is not None
|
||||||
logger.debug("Finding violated user cuts...")
|
logger.debug("Finding violated user cuts...")
|
||||||
cids = instance.find_violated_user_cuts(model)
|
violations = instance.find_violated_user_cuts(model)
|
||||||
logger.debug(f"Found {len(cids)} violated user cuts")
|
logger.debug(f"Found {len(violations)} violated user cuts")
|
||||||
logger.debug("Building violated user cuts...")
|
logger.debug("Building violated user cuts...")
|
||||||
for cid in cids:
|
for (vname, vdata) in violations.items():
|
||||||
if cid in self.enforced:
|
if vname in self.enforced:
|
||||||
continue
|
continue
|
||||||
assert isinstance(cid, ConstraintName)
|
instance.enforce_user_cut(solver.internal_solver, model, vdata)
|
||||||
instance.enforce_user_cut(solver.internal_solver, model, cid)
|
self.enforced[vname] = vdata
|
||||||
self.enforced.add(cid)
|
|
||||||
self.n_added_in_callback += 1
|
self.n_added_in_callback += 1
|
||||||
if len(cids) > 0:
|
if len(violations) > 0:
|
||||||
logger.debug(f"Added {len(cids)} violated user cuts")
|
logger.debug(f"Added {len(violations)} violated user cuts")
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def after_solve_mip(
|
def after_solve_mip(
|
||||||
@@ -87,10 +86,7 @@ class UserCutsComponent(Component):
|
|||||||
stats: LearningSolveStats,
|
stats: LearningSolveStats,
|
||||||
sample: Sample,
|
sample: Sample,
|
||||||
) -> None:
|
) -> None:
|
||||||
sample.put_array(
|
sample.put_scalar("mip_user_cuts", self.dynamic.encode(self.enforced))
|
||||||
"mip_user_cuts_enforced",
|
|
||||||
np.array(list(self.enforced), dtype="S"),
|
|
||||||
)
|
|
||||||
stats["UserCuts: Added in callback"] = self.n_added_in_callback
|
stats["UserCuts: Added in callback"] = self.n_added_in_callback
|
||||||
if self.n_added_in_callback > 0:
|
if self.n_added_in_callback > 0:
|
||||||
logger.info(f"{self.n_added_in_callback} user cuts added in callback")
|
logger.info(f"{self.n_added_in_callback} user cuts added in callback")
|
||||||
@@ -133,5 +129,5 @@ class UserCutsComponent(Component):
|
|||||||
self,
|
self,
|
||||||
instance: "Instance",
|
instance: "Instance",
|
||||||
sample: Sample,
|
sample: Sample,
|
||||||
) -> Dict[ConstraintCategory, Dict[str, float]]:
|
) -> Dict[ConstraintCategory, Dict[ConstraintName, float]]:
|
||||||
return self.dynamic.sample_evaluate(instance, sample)
|
return self.dynamic.sample_evaluate(instance, sample)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Any, List, TYPE_CHECKING, Dict
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from miplearn.features.sample import Sample, MemorySample
|
from miplearn.features.sample import Sample, MemorySample
|
||||||
from miplearn.types import ConstraintName, ConstraintCategory
|
from miplearn.types import ConstraintName
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ class Instance(ABC):
|
|||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
) -> List[ConstraintName]:
|
) -> Dict[ConstraintName, Any]:
|
||||||
"""
|
"""
|
||||||
Returns lazy constraint violations found for the current solution.
|
Returns lazy constraint violations found for the current solution.
|
||||||
|
|
||||||
@@ -124,40 +124,46 @@ class Instance(ABC):
|
|||||||
resolve the problem. The process repeats until no further lazy constraint
|
resolve the problem. The process repeats until no further lazy constraint
|
||||||
violations are found.
|
violations are found.
|
||||||
|
|
||||||
Each "violation" is simply a string which allows the instance to identify
|
Violations should be returned in a dictionary mapping the name of the violation
|
||||||
unambiguously which lazy constraint should be generated. In the Traveling
|
to some user-specified data that allows the instance to unambiguously generate
|
||||||
Salesman Problem, for example, a subtour violation could be a string
|
the lazy constraints at a later time. In the Traveling Salesman Problem, for
|
||||||
containing the cities in the subtour.
|
example, this function could return a dictionary identifying violated subtour
|
||||||
|
inequalities. More concretely, it could return:
|
||||||
|
{
|
||||||
|
"s1": [1, 2, 3],
|
||||||
|
"s2": [4, 5, 6, 7],
|
||||||
|
}
|
||||||
|
where "s1" and "s2" are the names of the subtours, and [1,2,3] and [4,5,6,7]
|
||||||
|
are the cities in each subtour. The names of the violations should be kept
|
||||||
|
stable across instances. In our example, "s1" should always correspond to
|
||||||
|
[1,2,3] across all instances. The user-provided data should be picklable.
|
||||||
|
|
||||||
The current solution can be queried with `solver.get_solution()`. If the solver
|
The current solution can be queried with `solver.get_solution()`. If the solver
|
||||||
is configured to use lazy callbacks, this solution may be non-integer.
|
is configured to use lazy callbacks, this solution may be non-integer.
|
||||||
|
|
||||||
For a concrete example, see TravelingSalesmanInstance.
|
For a concrete example, see TravelingSalesmanInstance.
|
||||||
"""
|
"""
|
||||||
return []
|
return {}
|
||||||
|
|
||||||
def enforce_lazy_constraint(
|
def enforce_lazy_constraint(
|
||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: ConstraintName,
|
violation_data: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Adds constraints to the model to ensure that the given violation is fixed.
|
Adds constraints to the model to ensure that the given violation is fixed.
|
||||||
|
|
||||||
This method is typically called immediately after
|
This method is typically called immediately after
|
||||||
find_violated_lazy_constraints. The violation object provided to this method
|
`find_violated_lazy_constraints`. The argument `violation_data` is the
|
||||||
is exactly the same object returned earlier by
|
user-provided data, previously returned by `find_violated_lazy_constraints`.
|
||||||
find_violated_lazy_constraints. After some training, LearningSolver may
|
In the Traveling Salesman Problem, for example, it could be a list of cities
|
||||||
decide to proactively build some lazy constraints at the beginning of the
|
in the subtour.
|
||||||
optimization process, before a solution is even available. In this case,
|
|
||||||
enforce_lazy_constraints will be called without a corresponding call to
|
|
||||||
find_violated_lazy_constraints.
|
|
||||||
|
|
||||||
Note that this method can be called either before the optimization starts or
|
After some training, LearningSolver may decide to proactively build some lazy
|
||||||
from within a callback. To ensure that constraints are added correctly in
|
constraints at the beginning of the optimization process, before a solution
|
||||||
either case, it is recommended to use `solver.add_constraint`, instead of
|
is even available. In this case, `enforce_lazy_constraints` will be called
|
||||||
modifying the `model` object directly.
|
without a corresponding call to `find_violated_lazy_constraints`.
|
||||||
|
|
||||||
For a concrete example, see TravelingSalesmanInstance.
|
For a concrete example, see TravelingSalesmanInstance.
|
||||||
"""
|
"""
|
||||||
@@ -166,14 +172,14 @@ class Instance(ABC):
|
|||||||
def has_user_cuts(self) -> bool:
|
def has_user_cuts(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]:
|
def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
|
||||||
return []
|
return {}
|
||||||
|
|
||||||
def enforce_user_cut(
|
def enforce_user_cut(
|
||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: ConstraintName,
|
violation_data: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
import gc
|
import gc
|
||||||
import os
|
import os
|
||||||
from typing import Any, Optional, List, Dict, TYPE_CHECKING
|
|
||||||
import pickle
|
import pickle
|
||||||
|
from typing import Any, Optional, List, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from overrides import overrides
|
from overrides import overrides
|
||||||
|
|
||||||
from miplearn.features.sample import Hdf5Sample, Sample
|
from miplearn.features.sample import Hdf5Sample, Sample
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
from miplearn.types import ConstraintName, ConstraintCategory
|
from miplearn.types import ConstraintName
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from miplearn.solvers.learning import InternalSolver
|
from miplearn.solvers.learning import InternalSolver
|
||||||
@@ -71,7 +71,7 @@ class FileInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
) -> List[ConstraintName]:
|
) -> Dict[ConstraintName, Any]:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
return self.instance.find_violated_lazy_constraints(solver, model)
|
return self.instance.find_violated_lazy_constraints(solver, model)
|
||||||
|
|
||||||
@@ -80,13 +80,13 @@ class FileInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: ConstraintName,
|
violation_data: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
self.instance.enforce_lazy_constraint(solver, model, violation)
|
self.instance.enforce_lazy_constraint(solver, model, violation_data)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]:
|
def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
return self.instance.find_violated_user_cuts(model)
|
return self.instance.find_violated_user_cuts(model)
|
||||||
|
|
||||||
@@ -95,10 +95,10 @@ class FileInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: ConstraintName,
|
violation_data: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
self.instance.enforce_user_cut(solver, model, violation)
|
self.instance.enforce_user_cut(solver, model, violation_data)
|
||||||
|
|
||||||
# Input & Output
|
# Input & Output
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import gc
|
|||||||
import gzip
|
import gzip
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
from typing import Optional, Any, List, cast, IO, TYPE_CHECKING, Dict
|
from typing import Optional, Any, List, cast, IO, TYPE_CHECKING, Dict, Callable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from overrides import overrides
|
from overrides import overrides
|
||||||
|
|
||||||
from miplearn.features.sample import Sample
|
from miplearn.features.sample import Sample
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
from miplearn.types import ConstraintName, ConstraintCategory
|
from miplearn.types import ConstraintName
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from miplearn.solvers.learning import InternalSolver
|
from miplearn.solvers.learning import InternalSolver
|
||||||
@@ -83,7 +83,7 @@ class PickleGzInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
) -> List[ConstraintName]:
|
) -> Dict[ConstraintName, Any]:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
return self.instance.find_violated_lazy_constraints(solver, model)
|
return self.instance.find_violated_lazy_constraints(solver, model)
|
||||||
|
|
||||||
@@ -92,13 +92,13 @@ class PickleGzInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: ConstraintName,
|
violation_data: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
self.instance.enforce_lazy_constraint(solver, model, violation)
|
self.instance.enforce_lazy_constraint(solver, model, violation_data)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]:
|
def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
return self.instance.find_violated_user_cuts(model)
|
return self.instance.find_violated_user_cuts(model)
|
||||||
|
|
||||||
@@ -107,10 +107,10 @@ class PickleGzInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: "InternalSolver",
|
solver: "InternalSolver",
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: ConstraintName,
|
violation_name: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert self.instance is not None
|
assert self.instance is not None
|
||||||
self.instance.enforce_user_cut(solver, model, violation)
|
self.instance.enforce_user_cut(solver, model, violation_name)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
@@ -153,3 +153,33 @@ def read_pickle_gz(filename: str) -> Any:
|
|||||||
def write_pickle_gz_multiple(objs: List[Any], dirname: str) -> None:
|
def write_pickle_gz_multiple(objs: List[Any], dirname: str) -> None:
|
||||||
for (i, obj) in enumerate(objs):
|
for (i, obj) in enumerate(objs):
|
||||||
write_pickle_gz(obj, f"{dirname}/{i:05d}.pkl.gz")
|
write_pickle_gz(obj, f"{dirname}/{i:05d}.pkl.gz")
|
||||||
|
|
||||||
|
|
||||||
|
def save(objs: List[Any], dirname: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Saves the provided objects to gzipped pickled files. Files are named sequentially
|
||||||
|
as `dirname/00000.pkl.gz`, `dirname/00001.pkl.gz`, etc.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
objs: List[any]
|
||||||
|
List of files to save
|
||||||
|
dirname: str
|
||||||
|
Output directory
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List containing the relative paths of the saved files.
|
||||||
|
"""
|
||||||
|
filenames = []
|
||||||
|
for (i, obj) in enumerate(objs):
|
||||||
|
filename = f"{dirname}/{i:05d}.pkl.gz"
|
||||||
|
filenames.append(filename)
|
||||||
|
write_pickle_gz(obj, filename)
|
||||||
|
return filenames
|
||||||
|
|
||||||
|
|
||||||
|
def load(filename: str, build_model: Callable) -> Any:
|
||||||
|
with gzip.GzipFile(filename, "rb") as file:
|
||||||
|
data = pickle.load(cast(IO[bytes], file))
|
||||||
|
return build_model(data)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -13,6 +14,13 @@ from scipy.stats.distributions import rv_frozen
|
|||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MultiKnapsackData:
|
||||||
|
prices: np.ndarray
|
||||||
|
capacities: np.ndarray
|
||||||
|
weights: np.ndarray
|
||||||
|
|
||||||
|
|
||||||
class MultiKnapsackInstance(Instance):
|
class MultiKnapsackInstance(Instance):
|
||||||
"""Representation of the Multidimensional 0-1 Knapsack Problem.
|
"""Representation of the Multidimensional 0-1 Knapsack Problem.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
from typing import List, Dict
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -15,6 +16,12 @@ from scipy.stats.distributions import rv_frozen
|
|||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MaxWeightStableSetData:
|
||||||
|
graph: Graph
|
||||||
|
weights: np.ndarray
|
||||||
|
|
||||||
|
|
||||||
class MaxWeightStableSetInstance(Instance):
|
class MaxWeightStableSetInstance(Instance):
|
||||||
"""An instance of the Maximum-Weight Stable Set Problem.
|
"""An instance of the Maximum-Weight Stable Set Problem.
|
||||||
|
|
||||||
@@ -87,16 +94,30 @@ class MaxWeightStableSetGenerator:
|
|||||||
if fix_graph:
|
if fix_graph:
|
||||||
self.graph = self._generate_graph()
|
self.graph = self._generate_graph()
|
||||||
|
|
||||||
def generate(self, n_samples: int) -> List[MaxWeightStableSetInstance]:
|
def generate(self, n_samples: int) -> List[MaxWeightStableSetData]:
|
||||||
def _sample() -> MaxWeightStableSetInstance:
|
def _sample() -> MaxWeightStableSetData:
|
||||||
if self.graph is not None:
|
if self.graph is not None:
|
||||||
graph = self.graph
|
graph = self.graph
|
||||||
else:
|
else:
|
||||||
graph = self._generate_graph()
|
graph = self._generate_graph()
|
||||||
weights = self.w.rvs(graph.number_of_nodes())
|
weights = self.w.rvs(graph.number_of_nodes())
|
||||||
return MaxWeightStableSetInstance(graph, weights)
|
return MaxWeightStableSetData(graph, weights)
|
||||||
|
|
||||||
return [_sample() for _ in range(n_samples)]
|
return [_sample() for _ in range(n_samples)]
|
||||||
|
|
||||||
def _generate_graph(self) -> Graph:
|
def _generate_graph(self) -> Graph:
|
||||||
return nx.generators.random_graphs.binomial_graph(self.n.rvs(), self.p.rvs())
|
return nx.generators.random_graphs.binomial_graph(self.n.rvs(), self.p.rvs())
|
||||||
|
|
||||||
|
|
||||||
|
def build_stab_model(data: MaxWeightStableSetData) -> pe.ConcreteModel:
|
||||||
|
model = pe.ConcreteModel()
|
||||||
|
nodes = list(data.graph.nodes)
|
||||||
|
model.x = pe.Var(nodes, domain=pe.Binary)
|
||||||
|
model.OBJ = pe.Objective(
|
||||||
|
expr=sum(model.x[v] * data.weights[v] for v in nodes),
|
||||||
|
sense=pe.maximize,
|
||||||
|
)
|
||||||
|
model.clique_eqs = pe.ConstraintList()
|
||||||
|
for clique in nx.find_cliques(data.graph):
|
||||||
|
model.clique_eqs.add(sum(model.x[v] for v in clique) <= 1)
|
||||||
|
return model
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
from typing import List, Tuple, FrozenSet, Any, Optional, Dict
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Tuple, Any, Optional, Dict
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -17,6 +19,12 @@ from miplearn.solvers.pyomo.base import BasePyomoSolver
|
|||||||
from miplearn.types import ConstraintName
|
from miplearn.types import ConstraintName
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TravelingSalesmanData:
|
||||||
|
n_cities: int
|
||||||
|
distances: np.ndarray
|
||||||
|
|
||||||
|
|
||||||
class TravelingSalesmanInstance(Instance):
|
class TravelingSalesmanInstance(Instance):
|
||||||
"""An instance ot the Traveling Salesman Problem.
|
"""An instance ot the Traveling Salesman Problem.
|
||||||
|
|
||||||
@@ -62,14 +70,15 @@ class TravelingSalesmanInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: InternalSolver,
|
solver: InternalSolver,
|
||||||
model: Any,
|
model: Any,
|
||||||
) -> List[ConstraintName]:
|
) -> Dict[ConstraintName, List]:
|
||||||
selected_edges = [e for e in self.edges if model.x[e].value > 0.5]
|
selected_edges = [e for e in self.edges if model.x[e].value > 0.5]
|
||||||
graph = nx.Graph()
|
graph = nx.Graph()
|
||||||
graph.add_edges_from(selected_edges)
|
graph.add_edges_from(selected_edges)
|
||||||
violations = []
|
violations = {}
|
||||||
for c in list(nx.connected_components(graph)):
|
for c in list(nx.connected_components(graph)):
|
||||||
if len(c) < self.n_cities:
|
if len(c) < self.n_cities:
|
||||||
violations.append(",".join(map(str, c)).encode())
|
cname = ("st[" + ",".join(map(str, c)) + "]").encode()
|
||||||
|
violations[cname] = list(c)
|
||||||
return violations
|
return violations
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
@@ -77,10 +86,9 @@ class TravelingSalesmanInstance(Instance):
|
|||||||
self,
|
self,
|
||||||
solver: InternalSolver,
|
solver: InternalSolver,
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: ConstraintName,
|
component: List,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert isinstance(solver, BasePyomoSolver)
|
assert isinstance(solver, BasePyomoSolver)
|
||||||
component = [int(v) for v in violation.decode().split(",")]
|
|
||||||
cut_edges = [
|
cut_edges = [
|
||||||
e
|
e
|
||||||
for e in self.edges
|
for e in self.edges
|
||||||
@@ -156,8 +164,8 @@ class TravelingSalesmanGenerator:
|
|||||||
self.fixed_n = None
|
self.fixed_n = None
|
||||||
self.fixed_cities = None
|
self.fixed_cities = None
|
||||||
|
|
||||||
def generate(self, n_samples: int) -> List[TravelingSalesmanInstance]:
|
def generate(self, n_samples: int) -> List[TravelingSalesmanData]:
|
||||||
def _sample() -> TravelingSalesmanInstance:
|
def _sample() -> TravelingSalesmanData:
|
||||||
if self.fixed_cities is not None:
|
if self.fixed_cities is not None:
|
||||||
assert self.fixed_n is not None
|
assert self.fixed_n is not None
|
||||||
n, cities = self.fixed_n, self.fixed_cities
|
n, cities = self.fixed_n, self.fixed_cities
|
||||||
@@ -167,7 +175,7 @@ class TravelingSalesmanGenerator:
|
|||||||
distances = np.tril(distances) + np.triu(distances.T, 1)
|
distances = np.tril(distances) + np.triu(distances.T, 1)
|
||||||
if self.round:
|
if self.round:
|
||||||
distances = distances.round()
|
distances = distances.round()
|
||||||
return TravelingSalesmanInstance(n, distances)
|
return TravelingSalesmanData(n, distances)
|
||||||
|
|
||||||
return [_sample() for _ in range(n_samples)]
|
return [_sample() for _ in range(n_samples)]
|
||||||
|
|
||||||
|
|||||||
@@ -710,7 +710,7 @@ class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack):
|
|||||||
self,
|
self,
|
||||||
solver: InternalSolver,
|
solver: InternalSolver,
|
||||||
model: Any,
|
model: Any,
|
||||||
violation: str,
|
violation_data: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
x0 = model.getVarByName("x[0]")
|
x0 = model.getVarByName("x[0]")
|
||||||
model.cbLazy(x0 <= 0)
|
model.cbLazy(x0 <= 0)
|
||||||
|
|||||||
@@ -5,11 +5,13 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Optional, List, Any, cast, Dict, Tuple
|
from typing import Optional, List, Any, cast, Dict, Tuple, Callable, IO, Union
|
||||||
|
|
||||||
|
from overrides import overrides
|
||||||
from p_tqdm import p_map, p_umap
|
from p_tqdm import p_map, p_umap
|
||||||
from tqdm.auto import tqdm
|
from tqdm.auto import tqdm
|
||||||
|
|
||||||
|
from miplearn.features.sample import Hdf5Sample, Sample
|
||||||
from miplearn.components.component import Component
|
from miplearn.components.component import Component
|
||||||
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
||||||
from miplearn.components.dynamic_user_cuts import UserCutsComponent
|
from miplearn.components.dynamic_user_cuts import UserCutsComponent
|
||||||
@@ -17,15 +19,58 @@ from miplearn.components.objective import ObjectiveValueComponent
|
|||||||
from miplearn.components.primal import PrimalSolutionComponent
|
from miplearn.components.primal import PrimalSolutionComponent
|
||||||
from miplearn.features.extractor import FeaturesExtractor
|
from miplearn.features.extractor import FeaturesExtractor
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
from miplearn.instance.picklegz import PickleGzInstance
|
|
||||||
from miplearn.solvers import _RedirectOutput
|
from miplearn.solvers import _RedirectOutput
|
||||||
from miplearn.solvers.internal import InternalSolver
|
from miplearn.solvers.internal import InternalSolver
|
||||||
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
|
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
|
||||||
from miplearn.types import LearningSolveStats
|
from miplearn.types import LearningSolveStats
|
||||||
|
import gzip
|
||||||
|
import pickle
|
||||||
|
import miplearn
|
||||||
|
from os.path import exists
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FileInstanceWrapper(Instance):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data_filename: Any,
|
||||||
|
build_model: Callable,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
assert data_filename.endswith(".pkl.gz")
|
||||||
|
self.filename = data_filename
|
||||||
|
self.sample_filename = data_filename.replace(".pkl.gz", ".h5")
|
||||||
|
self.sample = Hdf5Sample(
|
||||||
|
self.sample_filename,
|
||||||
|
mode="r+" if exists(self.sample_filename) else "w",
|
||||||
|
)
|
||||||
|
self.build_model = build_model
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def to_model(self) -> Any:
|
||||||
|
return miplearn.load(self.filename, self.build_model)
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def create_sample(self) -> Sample:
|
||||||
|
return self.sample
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def get_samples(self) -> List[Sample]:
|
||||||
|
return [self.sample]
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryInstanceWrapper(Instance):
|
||||||
|
def __init__(self, model: Any) -> None:
|
||||||
|
super().__init__()
|
||||||
|
assert model is not None
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def to_model(self) -> Any:
|
||||||
|
return self.model
|
||||||
|
|
||||||
|
|
||||||
class _GlobalVariables:
|
class _GlobalVariables:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.solver: Optional[LearningSolver] = None
|
self.solver: Optional[LearningSolver] = None
|
||||||
@@ -48,7 +93,7 @@ def _parallel_solve(
|
|||||||
assert solver is not None
|
assert solver is not None
|
||||||
assert instances is not None
|
assert instances is not None
|
||||||
try:
|
try:
|
||||||
stats = solver.solve(
|
stats = solver._solve(
|
||||||
instances[idx],
|
instances[idx],
|
||||||
discard_output=discard_outputs,
|
discard_output=discard_outputs,
|
||||||
)
|
)
|
||||||
@@ -87,11 +132,6 @@ class LearningSolver:
|
|||||||
option should be activated if the LP relaxation is not very
|
option should be activated if the LP relaxation is not very
|
||||||
expensive to solve and if it provides good hints for the integer
|
expensive to solve and if it provides good hints for the integer
|
||||||
solution.
|
solution.
|
||||||
simulate_perfect: bool
|
|
||||||
If true, each call to solve actually performs three actions: solve
|
|
||||||
the original problem, train the ML models on the data that was just
|
|
||||||
collected, and solve the problem again. This is useful for evaluating
|
|
||||||
the theoretical performance of perfect ML models.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -101,7 +141,6 @@ class LearningSolver:
|
|||||||
solver: Optional[InternalSolver] = None,
|
solver: Optional[InternalSolver] = None,
|
||||||
use_lazy_cb: bool = False,
|
use_lazy_cb: bool = False,
|
||||||
solve_lp: bool = True,
|
solve_lp: bool = True,
|
||||||
simulate_perfect: bool = False,
|
|
||||||
extractor: Optional[FeaturesExtractor] = None,
|
extractor: Optional[FeaturesExtractor] = None,
|
||||||
extract_lhs: bool = True,
|
extract_lhs: bool = True,
|
||||||
extract_sa: bool = True,
|
extract_sa: bool = True,
|
||||||
@@ -118,7 +157,6 @@ class LearningSolver:
|
|||||||
self.internal_solver: Optional[InternalSolver] = None
|
self.internal_solver: Optional[InternalSolver] = None
|
||||||
self.internal_solver_prototype: InternalSolver = solver
|
self.internal_solver_prototype: InternalSolver = solver
|
||||||
self.mode: str = mode
|
self.mode: str = mode
|
||||||
self.simulate_perfect: bool = simulate_perfect
|
|
||||||
self.solve_lp: bool = solve_lp
|
self.solve_lp: bool = solve_lp
|
||||||
self.tee = False
|
self.tee = False
|
||||||
self.use_lazy_cb: bool = use_lazy_cb
|
self.use_lazy_cb: bool = use_lazy_cb
|
||||||
@@ -140,6 +178,44 @@ class LearningSolver:
|
|||||||
discard_output: bool = False,
|
discard_output: bool = False,
|
||||||
tee: bool = False,
|
tee: bool = False,
|
||||||
) -> LearningSolveStats:
|
) -> LearningSolveStats:
|
||||||
|
"""
|
||||||
|
Solves the given instance. If trained machine-learning models are
|
||||||
|
available, they will be used to accelerate the solution process.
|
||||||
|
|
||||||
|
The argument `instance` may be either an Instance object or a
|
||||||
|
filename pointing to a pickled Instance object.
|
||||||
|
|
||||||
|
This method adds a new training sample to `instance.training_sample`.
|
||||||
|
If a filename is provided, then the file is modified in-place. That is,
|
||||||
|
the original file is overwritten.
|
||||||
|
|
||||||
|
If `solver.solve_lp_first` is False, the properties lp_solution and
|
||||||
|
lp_value will be set to dummy values.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
instance: Instance
|
||||||
|
The instance to be solved.
|
||||||
|
model: Any
|
||||||
|
The corresponding Pyomo model. If not provided, it will be created.
|
||||||
|
discard_output: bool
|
||||||
|
If True, do not write the modified instances anywhere; simply discard
|
||||||
|
them. Useful during benchmarking.
|
||||||
|
tee: bool
|
||||||
|
If true, prints solver log to screen.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
LearningSolveStats
|
||||||
|
A dictionary of solver statistics containing at least the following
|
||||||
|
keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes",
|
||||||
|
"Sense", "Log", "Warm start value" and "LP value".
|
||||||
|
|
||||||
|
Additional components may generate additional keys. For example,
|
||||||
|
ObjectiveValueComponent adds the keys "Predicted LB" and
|
||||||
|
"Predicted UB". See the documentation of each component for more
|
||||||
|
details.
|
||||||
|
"""
|
||||||
|
|
||||||
# Generate model
|
# Generate model
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
@@ -300,65 +376,25 @@ class LearningSolver:
|
|||||||
|
|
||||||
def solve(
|
def solve(
|
||||||
self,
|
self,
|
||||||
instance: Instance,
|
arg: Union[Any, List[str]],
|
||||||
model: Any = None,
|
build_model: Optional[Callable] = None,
|
||||||
discard_output: bool = False,
|
|
||||||
tee: bool = False,
|
tee: bool = False,
|
||||||
) -> LearningSolveStats:
|
) -> Union[LearningSolveStats, List[LearningSolveStats]]:
|
||||||
"""
|
if isinstance(arg, list):
|
||||||
Solves the given instance. If trained machine-learning models are
|
assert build_model is not None
|
||||||
available, they will be used to accelerate the solution process.
|
stats = []
|
||||||
|
for i in arg:
|
||||||
|
s = self._solve(FileInstanceWrapper(i, build_model), tee=tee)
|
||||||
|
stats.append(s)
|
||||||
|
return stats
|
||||||
|
else:
|
||||||
|
return self._solve(MemoryInstanceWrapper(arg), tee=tee)
|
||||||
|
|
||||||
The argument `instance` may be either an Instance object or a
|
def fit(self, filenames: List[str], build_model: Callable) -> None:
|
||||||
filename pointing to a pickled Instance object.
|
instances: List[Instance] = [
|
||||||
|
FileInstanceWrapper(f, build_model) for f in filenames
|
||||||
This method adds a new training sample to `instance.training_sample`.
|
]
|
||||||
If a filename is provided, then the file is modified in-place. That is,
|
self._fit(instances)
|
||||||
the original file is overwritten.
|
|
||||||
|
|
||||||
If `solver.solve_lp_first` is False, the properties lp_solution and
|
|
||||||
lp_value will be set to dummy values.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
instance: Instance
|
|
||||||
The instance to be solved.
|
|
||||||
model: Any
|
|
||||||
The corresponding Pyomo model. If not provided, it will be created.
|
|
||||||
discard_output: bool
|
|
||||||
If True, do not write the modified instances anywhere; simply discard
|
|
||||||
them. Useful during benchmarking.
|
|
||||||
tee: bool
|
|
||||||
If true, prints solver log to screen.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
LearningSolveStats
|
|
||||||
A dictionary of solver statistics containing at least the following
|
|
||||||
keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes",
|
|
||||||
"Sense", "Log", "Warm start value" and "LP value".
|
|
||||||
|
|
||||||
Additional components may generate additional keys. For example,
|
|
||||||
ObjectiveValueComponent adds the keys "Predicted LB" and
|
|
||||||
"Predicted UB". See the documentation of each component for more
|
|
||||||
details.
|
|
||||||
"""
|
|
||||||
if self.simulate_perfect:
|
|
||||||
if not isinstance(instance, PickleGzInstance):
|
|
||||||
raise Exception("Not implemented")
|
|
||||||
self._solve(
|
|
||||||
instance=instance,
|
|
||||||
model=model,
|
|
||||||
tee=tee,
|
|
||||||
)
|
|
||||||
self.fit([instance])
|
|
||||||
instance.instance = None
|
|
||||||
return self._solve(
|
|
||||||
instance=instance,
|
|
||||||
model=model,
|
|
||||||
discard_output=discard_output,
|
|
||||||
tee=tee,
|
|
||||||
)
|
|
||||||
|
|
||||||
def parallel_solve(
|
def parallel_solve(
|
||||||
self,
|
self,
|
||||||
@@ -397,7 +433,7 @@ class LearningSolver:
|
|||||||
"""
|
"""
|
||||||
if n_jobs == 1:
|
if n_jobs == 1:
|
||||||
return [
|
return [
|
||||||
self.solve(p)
|
self._solve(p)
|
||||||
for p in tqdm(
|
for p in tqdm(
|
||||||
instances,
|
instances,
|
||||||
disable=not progress,
|
disable=not progress,
|
||||||
@@ -425,7 +461,7 @@ class LearningSolver:
|
|||||||
self._restore_miplearn_logger()
|
self._restore_miplearn_logger()
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def fit(
|
def _fit(
|
||||||
self,
|
self,
|
||||||
training_instances: List[Instance],
|
training_instances: List[Instance],
|
||||||
n_jobs: int = 1,
|
n_jobs: int = 1,
|
||||||
|
|||||||
@@ -322,8 +322,14 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
|
|
||||||
# Bounds
|
# Bounds
|
||||||
lb, ub = v.bounds
|
lb, ub = v.bounds
|
||||||
upper_bounds.append(float(ub))
|
if ub is not None:
|
||||||
lower_bounds.append(float(lb))
|
upper_bounds.append(float(ub))
|
||||||
|
else:
|
||||||
|
upper_bounds.append(float("inf"))
|
||||||
|
if lb is not None:
|
||||||
|
lower_bounds.append(float(lb))
|
||||||
|
else:
|
||||||
|
lower_bounds.append(-float("inf"))
|
||||||
|
|
||||||
# Objective coefficient
|
# Objective coefficient
|
||||||
if v.name in self._obj:
|
if v.name in self._obj:
|
||||||
@@ -391,7 +397,9 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
) -> None:
|
) -> None:
|
||||||
if model is None:
|
if model is None:
|
||||||
model = instance.to_model()
|
model = instance.to_model()
|
||||||
assert isinstance(model, pe.ConcreteModel)
|
assert isinstance(
|
||||||
|
model, pe.ConcreteModel
|
||||||
|
), f"expected pe.ConcreteModel; found {model.__class__} instead"
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.model = model
|
self.model = model
|
||||||
self.model.extra_constraints = ConstraintList()
|
self.model.extra_constraints = ConstraintList()
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ def run_lazy_cb_tests(solver: InternalSolver) -> None:
|
|||||||
assert relsol is not None
|
assert relsol is not None
|
||||||
assert relsol[b"x[0]"] is not None
|
assert relsol[b"x[0]"] is not None
|
||||||
if relsol[b"x[0]"] > 0:
|
if relsol[b"x[0]"] > 0:
|
||||||
instance.enforce_lazy_constraint(cb_solver, cb_model, b"cut")
|
instance.enforce_lazy_constraint(cb_solver, cb_model, None)
|
||||||
|
|
||||||
solver.set_instance(instance, model)
|
solver.set_instance(instance, model)
|
||||||
solver.solve(lazy_cb=lazy_cb)
|
solver.solve(lazy_cb=lazy_cb)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import pytest
|
|||||||
from miplearn.classifiers import Classifier
|
from miplearn.classifiers import Classifier
|
||||||
from miplearn.classifiers.threshold import MinProbabilityThreshold
|
from miplearn.classifiers.threshold import MinProbabilityThreshold
|
||||||
from miplearn.components import classifier_evaluation_dict
|
from miplearn.components import classifier_evaluation_dict
|
||||||
|
from miplearn.components.dynamic_common import DynamicConstraintsComponent
|
||||||
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
||||||
from miplearn.features.sample import MemorySample
|
from miplearn.features.sample import MemorySample
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
@@ -24,13 +25,23 @@ def training_instances() -> List[Instance]:
|
|||||||
samples_0 = [
|
samples_0 = [
|
||||||
MemorySample(
|
MemorySample(
|
||||||
{
|
{
|
||||||
"mip_constr_lazy_enforced": np.array(["c1", "c2"], dtype="S"),
|
"mip_constr_lazy": DynamicConstraintsComponent.encode(
|
||||||
|
{
|
||||||
|
b"c1": 0,
|
||||||
|
b"c2": 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
"static_instance_features": np.array([5.0]),
|
"static_instance_features": np.array([5.0]),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MemorySample(
|
MemorySample(
|
||||||
{
|
{
|
||||||
"mip_constr_lazy_enforced": np.array(["c2", "c3"], dtype="S"),
|
"mip_constr_lazy": DynamicConstraintsComponent.encode(
|
||||||
|
{
|
||||||
|
b"c2": 0,
|
||||||
|
b"c3": 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
"static_instance_features": np.array([5.0]),
|
"static_instance_features": np.array([5.0]),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -55,7 +66,12 @@ def training_instances() -> List[Instance]:
|
|||||||
samples_1 = [
|
samples_1 = [
|
||||||
MemorySample(
|
MemorySample(
|
||||||
{
|
{
|
||||||
"mip_constr_lazy_enforced": np.array(["c3", "c4"], dtype="S"),
|
"mip_constr_lazy": DynamicConstraintsComponent.encode(
|
||||||
|
{
|
||||||
|
b"c3": 0,
|
||||||
|
b"c4": 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
"static_instance_features": np.array([8.0]),
|
"static_instance_features": np.array([8.0]),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -83,8 +99,8 @@ def test_sample_xy(training_instances: List[Instance]) -> None:
|
|||||||
comp = DynamicLazyConstraintsComponent()
|
comp = DynamicLazyConstraintsComponent()
|
||||||
comp.pre_fit(
|
comp.pre_fit(
|
||||||
[
|
[
|
||||||
np.array(["c1", "c3", "c4"], dtype="S"),
|
{b"c1": 0, b"c3": 0, b"c4": 0},
|
||||||
np.array(["c1", "c2", "c4"], dtype="S"),
|
{b"c1": 0, b"c2": 0, b"c4": 0},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
x_expected = {
|
x_expected = {
|
||||||
@@ -105,7 +121,10 @@ def test_sample_xy(training_instances: List[Instance]) -> None:
|
|||||||
|
|
||||||
def test_sample_predict_evaluate(training_instances: List[Instance]) -> None:
|
def test_sample_predict_evaluate(training_instances: List[Instance]) -> None:
|
||||||
comp = DynamicLazyConstraintsComponent()
|
comp = DynamicLazyConstraintsComponent()
|
||||||
comp.known_cids.extend([b"c1", b"c2", b"c3", b"c4"])
|
comp.known_violations[b"c1"] = 0
|
||||||
|
comp.known_violations[b"c2"] = 0
|
||||||
|
comp.known_violations[b"c3"] = 0
|
||||||
|
comp.known_violations[b"c4"] = 0
|
||||||
comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
||||||
comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5])
|
comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5])
|
||||||
comp.classifiers[b"type-a"] = Mock(spec=Classifier)
|
comp.classifiers[b"type-a"] = Mock(spec=Classifier)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, FrozenSet, List
|
from typing import Any, List, Dict
|
||||||
|
|
||||||
|
import gurobipy
|
||||||
import gurobipy as gp
|
import gurobipy as gp
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import pytest
|
import pytest
|
||||||
@@ -12,12 +13,11 @@ from gurobipy import GRB
|
|||||||
from networkx import Graph
|
from networkx import Graph
|
||||||
from overrides import overrides
|
from overrides import overrides
|
||||||
|
|
||||||
from miplearn.solvers.learning import InternalSolver
|
|
||||||
from miplearn.components.dynamic_user_cuts import UserCutsComponent
|
from miplearn.components.dynamic_user_cuts import UserCutsComponent
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
from miplearn.solvers.gurobi import GurobiSolver
|
from miplearn.solvers.gurobi import GurobiSolver
|
||||||
from miplearn.solvers.learning import LearningSolver
|
from miplearn.solvers.learning import LearningSolver
|
||||||
from miplearn.types import ConstraintName, ConstraintCategory
|
from miplearn.types import ConstraintName
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -41,25 +41,32 @@ class GurobiStableSetProblem(Instance):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]:
|
def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
|
||||||
assert isinstance(model, gp.Model)
|
assert isinstance(model, gp.Model)
|
||||||
vals = model.cbGetNodeRel(model.getVars())
|
try:
|
||||||
violations = []
|
vals = model.cbGetNodeRel(model.getVars())
|
||||||
|
except gurobipy.GurobiError:
|
||||||
|
return {}
|
||||||
|
violations = {}
|
||||||
for clique in nx.find_cliques(self.graph):
|
for clique in nx.find_cliques(self.graph):
|
||||||
if sum(vals[i] for i in clique) > 1:
|
if sum(vals[i] for i in clique) > 1:
|
||||||
violations.append(",".join([str(i) for i in clique]).encode())
|
vname = (",".join([str(i) for i in clique])).encode()
|
||||||
|
violations[vname] = list(clique)
|
||||||
return violations
|
return violations
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def enforce_user_cut(
|
def enforce_user_cut(
|
||||||
self,
|
self,
|
||||||
solver: InternalSolver,
|
solver: GurobiSolver,
|
||||||
model: Any,
|
model: Any,
|
||||||
cid: ConstraintName,
|
clique: List[int],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
clique = [int(i) for i in cid.decode().split(",")]
|
|
||||||
x = model.getVars()
|
x = model.getVars()
|
||||||
model.addConstr(gp.quicksum([x[i] for i in clique]) <= 1)
|
constr = gp.quicksum([x[i] for i in clique]) <= 1
|
||||||
|
if solver.cb_where:
|
||||||
|
model.cbCut(constr)
|
||||||
|
else:
|
||||||
|
model.addConstr(constr)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -71,7 +78,7 @@ def stab_instance() -> Instance:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def solver() -> LearningSolver:
|
def solver() -> LearningSolver:
|
||||||
return LearningSolver(
|
return LearningSolver(
|
||||||
solver=GurobiSolver(),
|
solver=GurobiSolver(params={"Threads": 1}),
|
||||||
components=[UserCutsComponent()],
|
components=[UserCutsComponent()],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,16 +87,18 @@ def test_usage(
|
|||||||
stab_instance: Instance,
|
stab_instance: Instance,
|
||||||
solver: LearningSolver,
|
solver: LearningSolver,
|
||||||
) -> None:
|
) -> None:
|
||||||
stats_before = solver.solve(stab_instance)
|
stats_before = solver._solve(stab_instance)
|
||||||
sample = stab_instance.get_samples()[0]
|
sample = stab_instance.get_samples()[0]
|
||||||
user_cuts_enforced = sample.get_array("mip_user_cuts_enforced")
|
user_cuts_encoded = sample.get_scalar("mip_user_cuts")
|
||||||
assert user_cuts_enforced is not None
|
assert user_cuts_encoded is not None
|
||||||
assert len(user_cuts_enforced) > 0
|
user_cuts = json.loads(user_cuts_encoded)
|
||||||
|
assert user_cuts is not None
|
||||||
|
assert len(user_cuts) > 0
|
||||||
assert stats_before["UserCuts: Added ahead-of-time"] == 0
|
assert stats_before["UserCuts: Added ahead-of-time"] == 0
|
||||||
assert stats_before["UserCuts: Added in callback"] > 0
|
assert stats_before["UserCuts: Added in callback"] > 0
|
||||||
|
|
||||||
solver.fit([stab_instance])
|
solver._fit([stab_instance])
|
||||||
stats_after = solver.solve(stab_instance)
|
stats_after = solver._solve(stab_instance)
|
||||||
assert (
|
assert (
|
||||||
stats_after["UserCuts: Added ahead-of-time"]
|
stats_after["UserCuts: Added ahead-of-time"]
|
||||||
== stats_before["UserCuts: Added in callback"]
|
== stats_before["UserCuts: Added in callback"]
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ def test_sample_evaluate(sample: Sample) -> None:
|
|||||||
def test_usage() -> None:
|
def test_usage() -> None:
|
||||||
solver = LearningSolver(components=[ObjectiveValueComponent()])
|
solver = LearningSolver(components=[ObjectiveValueComponent()])
|
||||||
instance = GurobiPyomoSolver().build_test_instance_knapsack()
|
instance = GurobiPyomoSolver().build_test_instance_knapsack()
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
solver.fit([instance])
|
solver._fit([instance])
|
||||||
stats = solver.solve(instance)
|
stats = solver._solve(instance)
|
||||||
assert stats["mip_lower_bound"] == stats["Objective: Predicted lower bound"]
|
assert stats["mip_lower_bound"] == stats["Objective: Predicted lower bound"]
|
||||||
assert stats["mip_upper_bound"] == stats["Objective: Predicted upper bound"]
|
assert stats["mip_upper_bound"] == stats["Objective: Predicted upper bound"]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from miplearn.classifiers.threshold import Threshold
|
|||||||
from miplearn.components import classifier_evaluation_dict
|
from miplearn.components import classifier_evaluation_dict
|
||||||
from miplearn.components.primal import PrimalSolutionComponent
|
from miplearn.components.primal import PrimalSolutionComponent
|
||||||
from miplearn.features.sample import Sample, MemorySample
|
from miplearn.features.sample import Sample, MemorySample
|
||||||
from miplearn.problems.tsp import TravelingSalesmanGenerator
|
from miplearn.problems.tsp import TravelingSalesmanGenerator, TravelingSalesmanInstance
|
||||||
from miplearn.solvers.learning import LearningSolver
|
from miplearn.solvers.learning import LearningSolver
|
||||||
from miplearn.solvers.tests import assert_equals
|
from miplearn.solvers.tests import assert_equals
|
||||||
|
|
||||||
@@ -108,10 +108,11 @@ def test_usage() -> None:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
gen = TravelingSalesmanGenerator(n=randint(low=5, high=6))
|
gen = TravelingSalesmanGenerator(n=randint(low=5, high=6))
|
||||||
instance = gen.generate(1)[0]
|
data = gen.generate(1)
|
||||||
solver.solve(instance)
|
instance = TravelingSalesmanInstance(data[0].n_cities, data[0].distances)
|
||||||
solver.fit([instance])
|
solver._solve(instance)
|
||||||
stats = solver.solve(instance)
|
solver._fit([instance])
|
||||||
|
stats = solver._solve(instance)
|
||||||
assert stats["Primal: Free"] == 0
|
assert stats["Primal: Free"] == 0
|
||||||
assert stats["Primal: One"] + stats["Primal: Zero"] == 10
|
assert stats["Primal: One"] + stats["Primal: Zero"] == 10
|
||||||
assert stats["mip_lower_bound"] == stats["mip_warm_start_value"]
|
assert stats["mip_lower_bound"] == stats["mip_warm_start_value"]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def test_usage() -> None:
|
|||||||
|
|
||||||
# Solve instance from disk
|
# Solve instance from disk
|
||||||
solver = LearningSolver(solver=GurobiSolver())
|
solver = LearningSolver(solver=GurobiSolver())
|
||||||
solver.solve(FileInstance(filename))
|
solver._solve(FileInstance(filename))
|
||||||
|
|
||||||
# Assert HDF5 contains training data
|
# Assert HDF5 contains training data
|
||||||
sample = FileInstance(filename).get_samples()[0]
|
sample = FileInstance(filename).get_samples()[0]
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import cast, IO
|
||||||
|
|
||||||
from miplearn.instance.picklegz import write_pickle_gz, PickleGzInstance
|
from miplearn.instance.picklegz import write_pickle_gz, PickleGzInstance
|
||||||
from miplearn.solvers.gurobi import GurobiSolver
|
from miplearn.solvers.gurobi import GurobiSolver
|
||||||
|
from miplearn import save
|
||||||
|
from os.path import exists
|
||||||
|
import gzip
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
|
||||||
def test_usage() -> None:
|
def test_usage() -> None:
|
||||||
@@ -14,3 +20,14 @@ def test_usage() -> None:
|
|||||||
pickled = PickleGzInstance(file.name)
|
pickled = PickleGzInstance(file.name)
|
||||||
pickled.load()
|
pickled.load()
|
||||||
assert pickled.to_model() is not None
|
assert pickled.to_model() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_save() -> None:
|
||||||
|
objs = [1, "ABC", True]
|
||||||
|
with tempfile.TemporaryDirectory() as dirname:
|
||||||
|
filenames = save(objs, dirname)
|
||||||
|
assert len(filenames) == 3
|
||||||
|
for (idx, f) in enumerate(filenames):
|
||||||
|
assert exists(f)
|
||||||
|
with gzip.GzipFile(f, "rb") as file:
|
||||||
|
assert pickle.load(cast(IO[bytes], file)) == objs[idx]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import numpy as np
|
|||||||
from scipy.stats import uniform, randint
|
from scipy.stats import uniform, randint
|
||||||
|
|
||||||
from miplearn import LearningSolver
|
from miplearn import LearningSolver
|
||||||
from miplearn.problems.knapsack import MultiKnapsackGenerator
|
from miplearn.problems.knapsack import MultiKnapsackGenerator, MultiKnapsackInstance
|
||||||
|
|
||||||
|
|
||||||
def test_knapsack_generator() -> None:
|
def test_knapsack_generator() -> None:
|
||||||
@@ -18,17 +18,22 @@ def test_knapsack_generator() -> None:
|
|||||||
u=uniform(loc=1.0, scale=1.0),
|
u=uniform(loc=1.0, scale=1.0),
|
||||||
alpha=uniform(loc=0.50, scale=0.0),
|
alpha=uniform(loc=0.50, scale=0.0),
|
||||||
)
|
)
|
||||||
instances = gen.generate(100)
|
data = gen.generate(100)
|
||||||
w_sum = sum(instance.weights for instance in instances) / len(instances)
|
w_sum = sum(d.weights for d in data) / len(data)
|
||||||
b_sum = sum(instance.capacities for instance in instances) / len(instances)
|
b_sum = sum(d.capacities for d in data) / len(data)
|
||||||
assert round(float(np.mean(w_sum)), -1) == 500.0
|
assert round(float(np.mean(w_sum)), -1) == 500.0
|
||||||
assert round(float(np.mean(b_sum)), -3) == 25000.0
|
assert round(float(np.mean(b_sum)), -3) == 25000.0
|
||||||
|
|
||||||
|
|
||||||
def test_knapsack() -> None:
|
def test_knapsack() -> None:
|
||||||
instance = MultiKnapsackGenerator(
|
data = MultiKnapsackGenerator(
|
||||||
n=randint(low=5, high=6),
|
n=randint(low=5, high=6),
|
||||||
m=randint(low=5, high=6),
|
m=randint(low=5, high=6),
|
||||||
).generate(1)[0]
|
).generate(1)
|
||||||
|
instance = MultiKnapsackInstance(
|
||||||
|
prices=data[0].prices,
|
||||||
|
capacities=data[0].capacities,
|
||||||
|
weights=data[0].weights,
|
||||||
|
)
|
||||||
solver = LearningSolver()
|
solver = LearningSolver()
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def test_stab() -> None:
|
|||||||
weights = np.array([1.0, 1.0, 1.0, 1.0, 1.0])
|
weights = np.array([1.0, 1.0, 1.0, 1.0, 1.0])
|
||||||
instance = MaxWeightStableSetInstance(graph, weights)
|
instance = MaxWeightStableSetInstance(graph, weights)
|
||||||
solver = LearningSolver()
|
solver = LearningSolver()
|
||||||
stats = solver.solve(instance)
|
stats = solver._solve(instance)
|
||||||
assert stats["mip_lower_bound"] == 2.0
|
assert stats["mip_lower_bound"] == 2.0
|
||||||
|
|
||||||
|
|
||||||
@@ -29,8 +29,8 @@ def test_stab_generator_fixed_graph() -> None:
|
|||||||
p=uniform(loc=0.05, scale=0.0),
|
p=uniform(loc=0.05, scale=0.0),
|
||||||
fix_graph=True,
|
fix_graph=True,
|
||||||
)
|
)
|
||||||
instances = gen.generate(1_000)
|
data = gen.generate(1_000)
|
||||||
weights = np.array([instance.weights for instance in instances])
|
weights = np.array([d.weights for d in data])
|
||||||
weights_avg_actual = np.round(np.average(weights, axis=0))
|
weights_avg_actual = np.round(np.average(weights, axis=0))
|
||||||
weights_avg_expected = [55.0] * 10
|
weights_avg_expected = [55.0] * 10
|
||||||
assert list(weights_avg_actual) == weights_avg_expected
|
assert list(weights_avg_actual) == weights_avg_expected
|
||||||
@@ -46,8 +46,8 @@ def test_stab_generator_random_graph() -> None:
|
|||||||
p=uniform(loc=0.5, scale=0.0),
|
p=uniform(loc=0.5, scale=0.0),
|
||||||
fix_graph=False,
|
fix_graph=False,
|
||||||
)
|
)
|
||||||
instances = gen.generate(1_000)
|
data = gen.generate(1_000)
|
||||||
n_nodes = [instance.graph.number_of_nodes() for instance in instances]
|
n_nodes = [d.graph.number_of_nodes() for d in data]
|
||||||
n_edges = [instance.graph.number_of_edges() for instance in instances]
|
n_edges = [d.graph.number_of_edges() for d in data]
|
||||||
assert np.round(np.mean(n_nodes)) == 35.0
|
assert np.round(np.mean(n_nodes)) == 35.0
|
||||||
assert np.round(np.mean(n_edges), -1) == 300.0
|
assert np.round(np.mean(n_edges), -1) == 300.0
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
import json
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
@@ -13,17 +14,17 @@ from miplearn.solvers.tests import assert_equals
|
|||||||
|
|
||||||
|
|
||||||
def test_generator() -> None:
|
def test_generator() -> None:
|
||||||
instances = TravelingSalesmanGenerator(
|
data = TravelingSalesmanGenerator(
|
||||||
x=uniform(loc=0.0, scale=1000.0),
|
x=uniform(loc=0.0, scale=1000.0),
|
||||||
y=uniform(loc=0.0, scale=1000.0),
|
y=uniform(loc=0.0, scale=1000.0),
|
||||||
n=randint(low=100, high=101),
|
n=randint(low=100, high=101),
|
||||||
gamma=uniform(loc=0.95, scale=0.1),
|
gamma=uniform(loc=0.95, scale=0.1),
|
||||||
fix_cities=True,
|
fix_cities=True,
|
||||||
).generate(100)
|
).generate(100)
|
||||||
assert len(instances) == 100
|
assert len(data) == 100
|
||||||
assert instances[0].n_cities == 100
|
assert data[0].n_cities == 100
|
||||||
assert norm(instances[0].distances - instances[0].distances.T) < 1e-6
|
assert norm(data[0].distances - data[0].distances.T) < 1e-6
|
||||||
d = [instance.distances[0, 1] for instance in instances]
|
d = [d.distances[0, 1] for d in data]
|
||||||
assert np.std(d) > 0
|
assert np.std(d) > 0
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ def test_instance() -> None:
|
|||||||
)
|
)
|
||||||
instance = TravelingSalesmanInstance(n_cities, distances)
|
instance = TravelingSalesmanInstance(n_cities, distances)
|
||||||
solver = LearningSolver()
|
solver = LearningSolver()
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
assert len(instance.get_samples()) == 1
|
assert len(instance.get_samples()) == 1
|
||||||
sample = instance.get_samples()[0]
|
sample = instance.get_samples()[0]
|
||||||
assert_equals(sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 0.0, 1.0])
|
assert_equals(sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 0.0, 1.0])
|
||||||
@@ -62,13 +63,19 @@ def test_subtour() -> None:
|
|||||||
distances = squareform(pdist(cities))
|
distances = squareform(pdist(cities))
|
||||||
instance = TravelingSalesmanInstance(n_cities, distances)
|
instance = TravelingSalesmanInstance(n_cities, distances)
|
||||||
solver = LearningSolver()
|
solver = LearningSolver()
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
samples = instance.get_samples()
|
samples = instance.get_samples()
|
||||||
assert len(samples) == 1
|
assert len(samples) == 1
|
||||||
sample = samples[0]
|
sample = samples[0]
|
||||||
lazy_enforced = sample.get_array("mip_constr_lazy_enforced")
|
|
||||||
assert lazy_enforced is not None
|
lazy_encoded = sample.get_scalar("mip_constr_lazy")
|
||||||
assert len(lazy_enforced) > 0
|
assert lazy_encoded is not None
|
||||||
|
lazy = json.loads(lazy_encoded)
|
||||||
|
assert lazy == {
|
||||||
|
"st[0,1,4]": [0, 1, 4],
|
||||||
|
"st[2,3,5]": [2, 3, 5],
|
||||||
|
}
|
||||||
|
|
||||||
assert_equals(
|
assert_equals(
|
||||||
sample.get_array("mip_var_values"),
|
sample.get_array("mip_var_values"),
|
||||||
[
|
[
|
||||||
@@ -89,5 +96,5 @@ def test_subtour() -> None:
|
|||||||
1.0,
|
1.0,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
solver.fit([instance])
|
solver._fit([instance])
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
|
|||||||
@@ -5,19 +5,27 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from os.path import exists
|
||||||
from typing import List, cast
|
from typing import List, cast
|
||||||
|
|
||||||
import dill
|
import dill
|
||||||
|
from scipy.stats import randint
|
||||||
|
|
||||||
|
from miplearn.features.sample import Hdf5Sample
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
from miplearn.instance.picklegz import PickleGzInstance, write_pickle_gz, read_pickle_gz
|
from miplearn.instance.picklegz import (
|
||||||
from miplearn.solvers.gurobi import GurobiSolver
|
PickleGzInstance,
|
||||||
|
write_pickle_gz,
|
||||||
|
read_pickle_gz,
|
||||||
|
save,
|
||||||
|
)
|
||||||
|
from miplearn.problems.stab import MaxWeightStableSetGenerator, build_stab_model
|
||||||
from miplearn.solvers.internal import InternalSolver
|
from miplearn.solvers.internal import InternalSolver
|
||||||
from miplearn.solvers.learning import LearningSolver
|
from miplearn.solvers.learning import LearningSolver
|
||||||
|
from miplearn.solvers.tests import assert_equals
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from tests.solvers.test_internal_solver import internal_solvers
|
from tests.solvers.test_internal_solver import internal_solvers
|
||||||
from miplearn.solvers.tests import assert_equals
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -34,7 +42,7 @@ def test_learning_solver(
|
|||||||
mode=mode,
|
mode=mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
assert len(instance.get_samples()) > 0
|
assert len(instance.get_samples()) > 0
|
||||||
sample = instance.get_samples()[0]
|
sample = instance.get_samples()[0]
|
||||||
|
|
||||||
@@ -55,8 +63,8 @@ def test_learning_solver(
|
|||||||
assert lp_log is not None
|
assert lp_log is not None
|
||||||
assert len(lp_log) > 100
|
assert len(lp_log) > 100
|
||||||
|
|
||||||
solver.fit([instance], n_jobs=4)
|
solver._fit([instance], n_jobs=4)
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
|
|
||||||
# Assert solver is picklable
|
# Assert solver is picklable
|
||||||
with tempfile.TemporaryFile() as file:
|
with tempfile.TemporaryFile() as file:
|
||||||
@@ -73,9 +81,9 @@ def test_solve_without_lp(
|
|||||||
solver=internal_solver,
|
solver=internal_solver,
|
||||||
solve_lp=False,
|
solve_lp=False,
|
||||||
)
|
)
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
solver.fit([instance])
|
solver._fit([instance])
|
||||||
solver.solve(instance)
|
solver._solve(instance)
|
||||||
|
|
||||||
|
|
||||||
def test_parallel_solve(
|
def test_parallel_solve(
|
||||||
@@ -104,7 +112,7 @@ def test_solve_fit_from_disk(
|
|||||||
|
|
||||||
# Test: solve
|
# Test: solve
|
||||||
solver = LearningSolver(solver=internal_solver)
|
solver = LearningSolver(solver=internal_solver)
|
||||||
solver.solve(instances[0])
|
solver._solve(instances[0])
|
||||||
instance_loaded = read_pickle_gz(cast(PickleGzInstance, instances[0]).filename)
|
instance_loaded = read_pickle_gz(cast(PickleGzInstance, instances[0]).filename)
|
||||||
assert len(instance_loaded.get_samples()) > 0
|
assert len(instance_loaded.get_samples()) > 0
|
||||||
|
|
||||||
@@ -119,17 +127,30 @@ def test_solve_fit_from_disk(
|
|||||||
os.remove(cast(PickleGzInstance, instance).filename)
|
os.remove(cast(PickleGzInstance, instance).filename)
|
||||||
|
|
||||||
|
|
||||||
def test_simulate_perfect() -> None:
|
def test_basic_usage() -> None:
|
||||||
internal_solver = GurobiSolver()
|
with tempfile.TemporaryDirectory() as dirname:
|
||||||
instance = internal_solver.build_test_instance_knapsack()
|
# Generate instances
|
||||||
with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as tmp:
|
data = MaxWeightStableSetGenerator(n=randint(low=20, high=21)).generate(4)
|
||||||
write_pickle_gz(instance, tmp.name)
|
train_files = save(data[0:3], f"{dirname}/train")
|
||||||
solver = LearningSolver(
|
test_files = save(data[3:4], f"{dirname}/test")
|
||||||
solver=internal_solver,
|
|
||||||
simulate_perfect=True,
|
# Solve training instances
|
||||||
)
|
solver = LearningSolver()
|
||||||
stats = solver.solve(PickleGzInstance(tmp.name))
|
stats = solver.solve(train_files, build_stab_model)
|
||||||
assert stats["mip_lower_bound"] == stats["Objective: Predicted lower bound"]
|
assert len(stats) == 3
|
||||||
|
for f in train_files:
|
||||||
|
sample_filename = f.replace(".pkl.gz", ".h5")
|
||||||
|
assert exists(sample_filename)
|
||||||
|
sample = Hdf5Sample(sample_filename)
|
||||||
|
assert sample.get_scalar("mip_lower_bound") > 0
|
||||||
|
|
||||||
|
# Fit
|
||||||
|
solver.fit(train_files, build_stab_model)
|
||||||
|
|
||||||
|
# Solve test instances
|
||||||
|
stats = solver.solve(test_files, build_stab_model)
|
||||||
|
assert isinstance(stats, list)
|
||||||
|
assert "Objective: Predicted lower bound" in stats[0].keys()
|
||||||
|
|
||||||
|
|
||||||
def test_gap() -> None:
|
def test_gap() -> None:
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import os.path
|
|||||||
from scipy.stats import randint
|
from scipy.stats import randint
|
||||||
|
|
||||||
from miplearn.benchmark import BenchmarkRunner
|
from miplearn.benchmark import BenchmarkRunner
|
||||||
from miplearn.problems.stab import MaxWeightStableSetGenerator
|
from miplearn.problems.stab import (
|
||||||
|
MaxWeightStableSetInstance,
|
||||||
|
MaxWeightStableSetGenerator,
|
||||||
|
)
|
||||||
from miplearn.solvers.learning import LearningSolver
|
from miplearn.solvers.learning import LearningSolver
|
||||||
|
|
||||||
|
|
||||||
@@ -15,8 +18,14 @@ def test_benchmark() -> None:
|
|||||||
for n_jobs in [1, 4]:
|
for n_jobs in [1, 4]:
|
||||||
# Generate training and test instances
|
# Generate training and test instances
|
||||||
generator = MaxWeightStableSetGenerator(n=randint(low=25, high=26))
|
generator = MaxWeightStableSetGenerator(n=randint(low=25, high=26))
|
||||||
train_instances = generator.generate(5)
|
train_instances = [
|
||||||
test_instances = generator.generate(3)
|
MaxWeightStableSetInstance(data.graph, data.weights)
|
||||||
|
for data in generator.generate(5)
|
||||||
|
]
|
||||||
|
test_instances = [
|
||||||
|
MaxWeightStableSetInstance(data.graph, data.weights)
|
||||||
|
for data in generator.generate(3)
|
||||||
|
]
|
||||||
|
|
||||||
# Solve training instances
|
# Solve training instances
|
||||||
training_solver = LearningSolver()
|
training_solver = LearningSolver()
|
||||||
|
|||||||
Reference in New Issue
Block a user