From c1adc0b79eb8965a7ec1dcce5426e5ecbc51f2b9 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 26 Oct 2023 15:37:05 -0500 Subject: [PATCH] Implement MemorizingLazyConstrComponent --- miplearn/collectors/basic.py | 4 +- miplearn/collectors/lazy.py | 117 --------------------------- miplearn/components/lazy.py | 43 ---------- miplearn/components/lazy/__init__.py | 0 miplearn/components/lazy/mem.py | 105 ++++++++++++++++++++++++ miplearn/problems/tsp.py | 6 +- tests/components/lazy/__init__.py | 0 tests/components/lazy/test_mem.py | 62 ++++++++++++++ tests/components/primal/test_mem.py | 3 +- tests/conftest.py | 9 ++- tests/fixtures/gen_tsp.py | 22 +++++ tests/fixtures/tsp-n20-00000.h5 | Bin 0 -> 107009 bytes tests/fixtures/tsp-n20-00000.mps.gz | Bin 0 -> 6238 bytes tests/fixtures/tsp-n20-00000.pkl.gz | Bin 0 -> 1145 bytes tests/fixtures/tsp-n20-00001.h5 | Bin 0 -> 29948 bytes tests/fixtures/tsp-n20-00001.mps.gz | Bin 0 -> 5371 bytes tests/fixtures/tsp-n20-00001.pkl.gz | Bin 0 -> 1134 bytes tests/fixtures/tsp-n20-00002.h5 | Bin 0 -> 30868 bytes tests/fixtures/tsp-n20-00002.mps.gz | Bin 0 -> 5653 bytes tests/fixtures/tsp-n20-00002.pkl.gz | Bin 0 -> 1134 bytes 20 files changed, 202 insertions(+), 169 deletions(-) delete mode 100644 miplearn/collectors/lazy.py delete mode 100644 miplearn/components/lazy.py create mode 100644 miplearn/components/lazy/__init__.py create mode 100644 miplearn/components/lazy/mem.py create mode 100644 tests/components/lazy/__init__.py create mode 100644 tests/components/lazy/test_mem.py create mode 100644 tests/fixtures/gen_tsp.py create mode 100644 tests/fixtures/tsp-n20-00000.h5 create mode 100644 tests/fixtures/tsp-n20-00000.mps.gz create mode 100644 tests/fixtures/tsp-n20-00000.pkl.gz create mode 100644 tests/fixtures/tsp-n20-00001.h5 create mode 100644 tests/fixtures/tsp-n20-00001.mps.gz create mode 100644 tests/fixtures/tsp-n20-00001.pkl.gz create mode 100644 tests/fixtures/tsp-n20-00002.h5 create mode 100644 tests/fixtures/tsp-n20-00002.mps.gz create mode 100644 tests/fixtures/tsp-n20-00002.pkl.gz diff --git a/miplearn/collectors/basic.py b/miplearn/collectors/basic.py index f11d72d..d3d7f77 100644 --- a/miplearn/collectors/basic.py +++ b/miplearn/collectors/basic.py @@ -62,9 +62,7 @@ class BasicCollector: and model.fix_violations is not None ): model.fix_violations(model, model.violations_, "aot") - h5.put_scalar( - "mip_constr_violations", json.dumps(model.violations_) - ) + h5.put_scalar("mip_constr_violations", repr(model.violations_)) # Save MPS file model.write(mps_filename) diff --git a/miplearn/collectors/lazy.py b/miplearn/collectors/lazy.py deleted file mode 100644 index 4222f4c..0000000 --- a/miplearn/collectors/lazy.py +++ /dev/null @@ -1,117 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -from io import StringIO -from typing import Callable - -import gurobipy as gp -import numpy as np -from gurobipy import GRB, LinExpr - -from ..h5 import H5File -from ..io import _RedirectOutput - - -class LazyCollector: - def __init__( - self, - min_constrs: int = 100_000, - time_limit: float = 900, - ) -> None: - self.min_constrs = min_constrs - self.time_limit = time_limit - - def collect( - self, data_filename: str, build_model: Callable, tol: float = 1e-6 - ) -> None: - h5_filename = f"{data_filename}.h5" - with H5File(h5_filename, "r+") as h5: - streams = [StringIO()] - lazy = None - with _RedirectOutput(streams): - slacks = h5.get_array("mip_constr_slacks") - assert slacks is not None - - # Check minimum problem size - if len(slacks) < self.min_constrs: - print("Problem is too small. Skipping.") - h5.put_array("mip_constr_lazy", np.zeros(len(slacks))) - return - - # Load model - print("Loading model...") - model = build_model(data_filename) - model.params.LazyConstraints = True - model.params.timeLimit = self.time_limit - gp_constrs = np.array(model.getConstrs()) - gp_vars = np.array(model.getVars()) - - # Load constraints - lhs = h5.get_sparse("static_constr_lhs") - rhs = h5.get_array("static_constr_rhs") - sense = h5.get_array("static_constr_sense") - assert lhs is not None - assert rhs is not None - assert sense is not None - lhs_csr = lhs.tocsr() - lhs_csc = lhs.tocsc() - constr_idx = np.array(range(len(rhs))) - lazy = np.zeros(len(rhs)) - - # Drop loose constraints - selected = (slacks > 0) & ((sense == b"<") | (sense == b">")) - loose_constrs = gp_constrs[selected] - print( - f"Removing {len(loose_constrs):,d} constraints (out of {len(rhs):,d})..." - ) - model.remove(list(loose_constrs)) - - # Filter to constraints that were dropped - lhs_csr = lhs_csr[selected, :] - lhs_csc = lhs_csc[selected, :] - rhs = rhs[selected] - sense = sense[selected] - constr_idx = constr_idx[selected] - lazy[selected] = 1 - - # Load warm start - var_names = h5.get_array("static_var_names") - var_values = h5.get_array("mip_var_values") - assert var_values is not None - assert var_names is not None - for (var_idx, var_name) in enumerate(var_names): - var = model.getVarByName(var_name.decode()) - var.start = var_values[var_idx] - - print("Solving MIP with lazy constraints callback...") - - def callback(model: gp.Model, where: int) -> None: - assert rhs is not None - assert lazy is not None - assert sense is not None - - if where == GRB.Callback.MIPSOL: - x_val = np.array(model.cbGetSolution(model.getVars())) - slack = lhs_csc * x_val - rhs - slack[sense == b">"] *= -1 - is_violated = slack > tol - - for (j, rhs_j) in enumerate(rhs): - if is_violated[j]: - lazy[constr_idx[j]] = 0 - expr = LinExpr( - lhs_csr[j, :].data, gp_vars[lhs_csr[j, :].indices] - ) - if sense[j] == b"<": - model.cbLazy(expr <= rhs_j) - elif sense[j] == b">": - model.cbLazy(expr >= rhs_j) - else: - raise RuntimeError(f"Unknown sense: {sense[j]}") - - model.optimize(callback) - print(f"Marking {lazy.sum():,.0f} constraints as lazy...") - - h5.put_array("mip_constr_lazy", lazy) - h5.put_scalar("mip_constr_lazy_log", streams[0].getvalue()) diff --git a/miplearn/components/lazy.py b/miplearn/components/lazy.py deleted file mode 100644 index 2838117..0000000 --- a/miplearn/components/lazy.py +++ /dev/null @@ -1,43 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -import json -from typing import Any, Dict, List - -import gurobipy as gp - -from ..h5 import H5File - - -class ExpertLazyComponent: - def __init__(self) -> None: - pass - - def fit(self, train_h5: List[str]) -> None: - pass - - def before_mip(self, test_h5: str, model: gp.Model, stats: Dict[str, Any]) -> None: - with H5File(test_h5, "r") as h5: - constr_names = h5.get_array("static_constr_names") - constr_lazy = h5.get_array("mip_constr_lazy") - constr_violations = h5.get_scalar("mip_constr_violations") - - assert constr_names is not None - assert constr_violations is not None - - # Static lazy constraints - n_static_lazy = 0 - if constr_lazy is not None: - for (constr_idx, constr_name) in enumerate(constr_names): - if constr_lazy[constr_idx]: - constr = model.getConstrByName(constr_name.decode()) - constr.lazy = 3 - n_static_lazy += 1 - stats.update({"Static lazy constraints": n_static_lazy}) - - # Dynamic lazy constraints - if hasattr(model, "_fix_violations"): - violations = json.loads(constr_violations) - model._fix_violations(model, violations, "aot") - stats.update({"Dynamic lazy constraints": len(violations)}) diff --git a/miplearn/components/lazy/__init__.py b/miplearn/components/lazy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/miplearn/components/lazy/mem.py b/miplearn/components/lazy/mem.py new file mode 100644 index 0000000..7f25b4d --- /dev/null +++ b/miplearn/components/lazy/mem.py @@ -0,0 +1,105 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. +import logging +from typing import List, Dict, Any, Hashable + +import numpy as np +from sklearn.preprocessing import MultiLabelBinarizer + +from miplearn.extractors.abstract import FeaturesExtractor +from miplearn.h5 import H5File +from miplearn.solvers.gurobi import GurobiModel + +logger = logging.getLogger(__name__) + +# TODO: Replace GurobiModel by AbstractModel +# TODO: fix_violations: remove model.inner +# TODO: fix_violations: remove `where` +# TODO: Write documentation +# TODO: Implement ExpertLazyConstrComponent + + +class MemorizingLazyConstrComponent: + def __init__(self, clf: Any, extractor: FeaturesExtractor) -> None: + self.clf = clf + self.extractor = extractor + self.violations_: List[Hashable] = [] + self.n_features_: int = 0 + self.n_targets_: int = 0 + + def fit(self, train_h5: List[str]) -> None: + logger.info("Reading training data...") + n_samples = len(train_h5) + x, y, violations, n_features = [], [], [], None + violation_to_idx: Dict[Hashable, int] = {} + for h5_filename in train_h5: + with H5File(h5_filename, "r") as h5: + + # Store lazy constraints + sample_violations_str = h5.get_scalar("mip_constr_violations") + assert sample_violations_str is not None + assert isinstance(sample_violations_str, str) + sample_violations = eval(sample_violations_str) + assert isinstance(sample_violations, list) + y_sample = [] + for v in sample_violations: + if v not in violation_to_idx: + violation_to_idx[v] = len(violation_to_idx) + violations.append(v) + y_sample.append(violation_to_idx[v]) + y.append(y_sample) + + # Extract features + x_sample = self.extractor.get_instance_features(h5) + assert len(x_sample.shape) == 1 + if n_features is None: + n_features = len(x_sample) + else: + assert len(x_sample) == n_features + x.append(x_sample) + + logger.info("Constructing matrices...") + assert n_features is not None + self.n_features_ = n_features + self.violations_ = violations + self.n_targets_ = len(violation_to_idx) + x_np = np.vstack(x) + assert x_np.shape == (n_samples, n_features) + y_np = MultiLabelBinarizer().fit_transform(y) + assert y_np.shape == (n_samples, self.n_targets_) + logger.info( + f"Dataset has {n_samples:,d} samples, " + f"{n_features:,d} features and {self.n_targets_:,d} targets" + ) + + logger.info("Training classifier...") + self.clf.fit(x_np, y_np) + + def before_mip( + self, + test_h5: str, + model: GurobiModel, + stats: Dict[str, Any], + ) -> None: + assert self.violations_ is not None + if model.fix_violations is None: + return + + # Read features + with H5File(test_h5, "r") as h5: + x_sample = self.extractor.get_instance_features(h5) + assert x_sample.shape == (self.n_features_,) + x_sample = x_sample.reshape(1, -1) + + # Predict violated constraints + logger.info("Predicting violated lazy constraints...") + y = self.clf.predict(x_sample) + assert y.shape == (1, self.n_targets_) + y = y.reshape(-1) + + # Enforce constraints + violations = [self.violations_[i] for (i, yi) in enumerate(y) if yi > 0.5] + logger.info(f"Enforcing {len(violations)} constraints ahead-of-time...") + model.fix_violations(model, violations, "aot") + stats["Lazy Constraints: AOT"] = len(violations) diff --git a/miplearn/problems/tsp.py b/miplearn/problems/tsp.py index 084f6ba..a23b9bc 100644 --- a/miplearn/problems/tsp.py +++ b/miplearn/problems/tsp.py @@ -150,12 +150,12 @@ def build_tsp_model(data: Union[str, TravelingSalesmanData]) -> GurobiModel: graph.add_edges_from(selected_edges) for component in list(nx.connected_components(graph)): if len(component) < model.inner._n_cities: - cut_edges = [ - e + cut_edges = tuple( + (e[0], e[1]) for e in model.inner._edges if (e[0] in component and e[1] not in component) or (e[0] not in component and e[1] in component) - ] + ) violations.append(cut_edges) return violations diff --git a/tests/components/lazy/__init__.py b/tests/components/lazy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/components/lazy/test_mem.py b/tests/components/lazy/test_mem.py new file mode 100644 index 0000000..9f7618c --- /dev/null +++ b/tests/components/lazy/test_mem.py @@ -0,0 +1,62 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +from typing import List, Dict, Any +from unittest.mock import Mock + +from sklearn.dummy import DummyClassifier +from sklearn.neighbors import KNeighborsClassifier + +from miplearn.components.lazy.mem import MemorizingLazyConstrComponent +from miplearn.extractors.abstract import FeaturesExtractor +from miplearn.problems.tsp import build_tsp_model +from miplearn.solvers.learning import LearningSolver + + +def test_mem_component( + tsp_h5: List[str], + default_extractor: FeaturesExtractor, +) -> None: + clf = Mock(wraps=DummyClassifier()) + comp = MemorizingLazyConstrComponent(clf=clf, extractor=default_extractor) + comp.fit(tsp_h5) + + # Should call fit method with correct arguments + clf.fit.assert_called() + x, y = clf.fit.call_args.args + assert x.shape == (3, 190) + assert y.tolist() == [ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0], + [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1], + ] + + # Should store violations + assert comp.violations_ is not None + assert comp.n_features_ == 190 + assert comp.n_targets_ == 22 + assert len(comp.violations_) == 22 + + # Call before-mip + stats: Dict[str, Any] = {} + model = Mock() + comp.before_mip(tsp_h5[0], model, stats) + + # Should call predict with correct args + clf.predict.assert_called() + (x_test,) = clf.predict.call_args.args + assert x_test.shape == (1, 190) + + +def test_usage_tsp( + tsp_h5: List[str], + default_extractor: FeaturesExtractor, +) -> None: + # Should not crash + data_filenames = [f.replace(".h5", ".pkl.gz") for f in tsp_h5] + clf = KNeighborsClassifier(n_neighbors=1) + comp = MemorizingLazyConstrComponent(clf=clf, extractor=default_extractor) + solver = LearningSolver(components=[comp]) + solver.fit(data_filenames) + solver.optimize(data_filenames[0], build_tsp_model) diff --git a/tests/components/primal/test_mem.py b/tests/components/primal/test_mem.py index ce68aa9..7ab64d1 100644 --- a/tests/components/primal/test_mem.py +++ b/tests/components/primal/test_mem.py @@ -20,7 +20,8 @@ logger = logging.getLogger(__name__) def test_mem_component( - multiknapsack_h5: List[str], default_extractor: FeaturesExtractor + multiknapsack_h5: List[str], + default_extractor: FeaturesExtractor, ) -> None: # Create mock classifier clf = Mock(wraps=DummyClassifier()) diff --git a/tests/conftest.py b/tests/conftest.py index c4c8b61..ac9537e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,13 +8,18 @@ from typing import List import pytest -from miplearn.extractors.fields import H5FieldsExtractor from miplearn.extractors.abstract import FeaturesExtractor +from miplearn.extractors.fields import H5FieldsExtractor @pytest.fixture() def multiknapsack_h5() -> List[str]: - return sorted(glob(f"{dirname(__file__)}/fixtures/multiknapsack*.h5")) + return sorted(glob(f"{dirname(__file__)}/fixtures/multiknapsack-n100*.h5")) + + +@pytest.fixture() +def tsp_h5() -> List[str]: + return sorted(glob(f"{dirname(__file__)}/fixtures/tsp-n20*.h5")) @pytest.fixture() diff --git a/tests/fixtures/gen_tsp.py b/tests/fixtures/gen_tsp.py new file mode 100644 index 0000000..a180caa --- /dev/null +++ b/tests/fixtures/gen_tsp.py @@ -0,0 +1,22 @@ +from os.path import dirname + +import numpy as np +from scipy.stats import uniform, randint + +from miplearn.collectors.basic import BasicCollector +from miplearn.io import write_pkl_gz +from miplearn.problems.tsp import TravelingSalesmanGenerator, build_tsp_model + +np.random.seed(42) +gen = TravelingSalesmanGenerator( + x=uniform(loc=0.0, scale=1000.0), + y=uniform(loc=0.0, scale=1000.0), + n=randint(low=20, high=21), + gamma=uniform(loc=1.0, scale=0.25), + fix_cities=True, + round=True, +) +data = gen.generate(3) +data_filenames = write_pkl_gz(data, dirname(__file__), prefix="tsp-n20-") +collector = BasicCollector() +collector.collect(data_filenames, build_tsp_model) diff --git a/tests/fixtures/tsp-n20-00000.h5 b/tests/fixtures/tsp-n20-00000.h5 new file mode 100644 index 0000000000000000000000000000000000000000..57d49901ae2367668f6e7e08485479d7f8b015cb GIT binary patch literal 107009 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/tsp-n20-00001.h5 b/tests/fixtures/tsp-n20-00001.h5 new file mode 100644 index 0000000000000000000000000000000000000000..469a6ce5bc91fa139f4ea5ad18c270ef85f62bbb GIT binary patch literal 29948 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 QOG`L1ys!;xs_^%J0Hwng8~^|S literal 0 HcmV?d00001 diff --git a/tests/fixtures/tsp-n20-00001.mps.gz b/tests/fixtures/tsp-n20-00001.mps.gz new file mode 100644 index 0000000000000000000000000000000000000000..2845736ac441925b757a458e2d96471e4da60991 GIT binary patch literal 5371 zcma)A2UJt(wnmT%0uod-bQCGl10&Kwq!_ayLuDzHwV{L+ z%X&~*if-fE#D)gTS;kT?(hZqLlz6oQrf+veFgtUR4g&_8E8wlK2`j5H`VA+aIOn-J ze^xPIY)v7zHz%&rCXfb^TYq2-BIsXAz{qV{m-F^=mzzFw`)!Eg-0eE>2xfj*o8(SAu3M@J12O|`k{q?IIdDG~e`W23DQXZLqZSY6(IvkJ** z<8(pPJt?2uGr=B3TEmWmXIEl)rO23U}5# zB8BY?yr?2CgA`W)Ns3I1^EfaRAr3Cs5RXb|p@27#yAl7P zg1pksVKfw#bg)stSMyzb!Cn)Qq!R*d7fmR4c}+m_6&=N-xT9_7SM4vuPM}2DyWLD$ zyjXE*?3o2(COX^!0@Yo+5y%a;3x;{md0gZ_gjM;8)?=h5jI;u|bXor;n>~}AkFV2k zS4r*-)XSX{dn+eTq@CF2iAQ#RRuN=!c43P?V=)y=KY?3tsp8j|hN6tu2FtHNQ~}h{!!Z!u}b> zeLeFx#)u+RfN&N0H!Pb$06_WCuW0u5pHvKOqn1!f(lJY;@VApwp$apvCR~{K;=bXx;ZE0zwqXJZ5Q2P6n z^8wcVi-eq3%l!`W4TJGHU8ok=ykFxnOfmg2RngRRY1!&?hk!1WbILf~Tq@!2!lCZD z)+evtIQ6*(i9gB)$q>=zQt@;Aa>sFVi_1r$!yl!IQTTB8pmx0R^E6S(tby6<4Tle- zK5`PHI>X%q+VMxMjvK_e*L0LJT)D&rqe1c0fQQsMeqikgM%&s0RpRO7F#6oVAMqfu zJ6%*VZb2}b7zs`}-f$$$on_&#maZt30F%~^ovyJ2a`8`k28ll!5%xFufQcI5#Y8B% z)6vocVE1#xtaMEOa1OAuOTt;~zhI9eTE^V>_r=ue9b6W?7ei9k1oN_#7BZ-~IsP(Z z+;oJVn5xxrzJiu%T_{TlWa~LP<%q)^f!&zXm0SE#H0bFx`WWhzbL*~lV5trkPe;jx zxpTMUO|4v9k8rBo1_MQ5AprH;+lZVIsFDkRMu`fx82i%n*$1t=b~cvX^G(dtN4ox( zVK7JKLs_Vjlmmj-si>06k~ZeCPP#CJUP!*&YTJpY$+5Rg)MKm{cwnlNa<6nOX#GlR z-pI^l0spI$-9QaYwyfLVZm<&G{#GG$NT^~@^#!4Mot}u2N6W|Ob=+ius?F=P1!Z;$ zRjf zdHwF4 zcglKtPeF@jk-ea*=GE});sR_#*JRMk-cchnSL;t8Klv7+d8LxN_a_cbM=oBGS>73E zDhrvnO2Z#S8K)~smU)?*ANaLo&lIq?D_fYi%6`MmCF6jzKsBl~hqWD@l+jnyQnhd? z@-iq+d^{YB-vgwuI5BY})=w?%*nU|nFa1(2J+&rQk6Ro*Ac4$&B%^G_m3Lg#I_uFW zVdL?aQ~ZMf|GwHe^IsrkyScdUVpQAjcbq?&$-6M$J2ms$h(Ky8;-Y`oDE6>Xe8 zZC_U+bxSgX&16?6Vk@M?L*};FZur9OPmDCy+Y~+GpgqHBN9Et?kFbnb^)E=z#S2Uf zbyTb7DCC9>%*b#gN~!`&DI~B5x0$$!iPfF_RPK*U`5C*>p zJ6$yD_31&Iq+i(5>Gkol+N&|Rcc~&DQRm^dbSu6aWr*-adb+XD??VZbKd=6x^`YYO zMc{a)Dq?1SY0gKezre(=kDw44fo-QR;q%+My%-fUe9hlSK2d$OH9mLri-N%faLnZp z)jt|)d>^a$c>S?KS?4eKr?SAV{MDmN4F1ZvQ}CyjTW~Fm6;3x5eCzo)#lW4ov--%XrK0L6#FqqpOjiQ5AxSzf2GAQj1Rgv z)56j)Y&GgREhj#LHGlyhji-($E#p2YoU|Dn^XamH1H)+6w=vPr3%^&XXZRV*Caq5e+Tan>O zJa9qx{=&)}WLODU#EyL-La#YJE60)54A6u`)k0(Cm$MhY)G?~wx{!`TwEL2eG0|sF zMo>Rh0!>8}hBUv5dq(^2#Zp>Z|0q)LO%qCmw?JpKaZp_uevij>)*^{I*!Z}X!)R9y zhYN`fqTPX3lHcuAjZB#6^1Z@g+#m&kDwK&CkK(!=wIQrPUywwvR6p_W4;w zj3@cQtAKk0ap90(!6a*MmFJ}K3{Ot69PV=QV)OsDuiV*uxVpl%Xm^shbBKlbHHLKcJB{We?mA-?PZ4x8^pT3T91n3g?4fxa7K-wYGB&JSgKX(CMA` zY7$k-WEno}9SYKNQXY6np+B>>d@6sJs+jqW4`^t0#wy~@1lFQraZdNnZzkmz&VSj~ z(;1CX%r&>=A9Pg4+3$#M42b$>6vhdE0I>`tQ>yx!_! z>9WFt+@u>>EaMh^AR- zsA>dCp<&X8D?bOD0RQD+4nbDzL=Pb zjn0v+xseH0gAvGxfTPpXrX6_8VVC>%xi6k3yn-t*JMTn7^CaMVK&~$hGXn;tvqi}S z*2ZC`DM$v#(_{KS<<)BTAOv`TYy+~HCTTxkb8L+A%-n62(ZXA@U+)~w3*G}VVsxHI z4myg$4}o%e3_SybN|95*4XhtP{_T33Nv_L;qrhD3+L-|XJqC@RBwT3VXPdHBn?|AT z$MT$M;A`}l$IQP+ktN}{c}+rof11owmJzp~Yc0z{>Cc%d#T`8mSGR5+HGM48 zG+)f&ZPuOBQOPQM+JBcsVci*$wn#E@6#Y=xq42P~QUKvx57WjbuhcyWw#a(a7FW-sapkMC}Vvf9~;mj=QCTK@|)S)spVpTFp06BOePa5_`)EKoAT?)ia_1Urem9C zqtvziC2OX;+~LgRPc-|zQT$_gwW(o4 zESEAUx;LtOzT*{N<#me&ajs{eXiBHaq5ZO`-uh`_$-upj>p6UELX&^DcTs@axR-XE z@1(q(RpGY7?!# z{!^WIGa}vmdG&z&KN|*Du5u_aOa$Lrle!B#t82?Z9t z4p+#okX>6!O4Y;NNNw=%2Irr>2N>4b*3^4fR#}$WTGIgCc)5e&L9v?4zP6QB!afxQ z?AQeW=WYgY=Yu{KmpN^J*z`MGrTUK}q4K%47`2rPW~A&m^5YZ!34Ltn1-SH;PpeNF z_vr-E4PeR%znI`on6~5 zB@C#_U}L&d{lMO-kW)@>+3bmDPAIaCZbk%%GCWH*{@yb8%;BmmzY|N z7W^U(GzeNJ-mGM1_&SR^kz}9yd)xh{;kq@oLObLiO06l%mpR8~WER#&bip5vm$b+n zdOsbf9ySvK-)HnZJv%!4{F-95X-KG86;Az9K>uYK#nx11lt7qxBc=Dac$%`nGN?ar zS`>J=y$pnju_n6VM9RK)R)cU58*$T`0jV5B%>W&_MwPaltW7m&i+y!XtgqrF$Ya<= z+nvp&5-jucAH7fXw`n-AKMe~;eg(eBNMLt9_mvf!b8oSL1;N(3c6g#Wfd&dA2YRf04Ed1mU;@W!K6R~f{UGgH^C*2Wp=HMM_E-3ku;2JdLmY}H~?|+4VsoHX;FyA+0>!|+!m$>X0x>%`m^Szi3 zB{e4m*WY}_lTcGj{qQr`ZX<6*&npD%M2Puc#76Qnrx5$9<_bH4Z3VU5Hgu%C#e2lH z?jNu`W~-Z`PiwYpr5^aH_`o%Js&4b=4}-eb))l({E!nl8GVGcz+o|5=nkZWL3Mh-@iZie4oz)FSM|*=s?EpF{P1G z-H0bW+&o_13HQ{*B(sQ8bkf8Zws}i!et$W5M-G?|6RSF)=D_d2u(u3eTjXH6G5Sba`%@=? zDNW;t(pQ)LN*Esy$1x!n`>Dx%{Fq0|faTY%twUelaD(1$;wlsGEfH-Erjhs}Vp)|x z)uhalw_U~K_1iKX4-{=)YPqROz9l8$(20=(rN-0!hcsg$Fl=)eL2qfM#K#za2H=qs z6zym)3+EZa7^ST;HadU`f-Eq(ll;|8fIel(p779eobgRH4L0E=qCZE=M@dtsjPB)U zRwWtNXP!gl6ynShL@uU6X?0BtNaRAAXbwU+#%50Yqs|;esQ6@@q=N@@-F8Ja1M$;G zFZsM%5z|151=pY(NUBpOcM{cY_v;mW_hrSHHr!A&mEgN^RYYxxnF~Szg3GlEvB`=% z?u;kP8uPj1cwE%1HNQ*1L!~N3hbjXy9v|}==dp`5m2^xUQi7S&hYf~aV3C|TYeBRp zB;6jn6@`sltZRE@DwsOFv>mXZcbYst5<^TfU{IMFK@kT5si;d`v6faLHZ7uFTnnx# z!U_Tm7aiuX3ZO^1H?q24*v7A-#e-|!dI2Z&M>Rzb-8)tfG5|Kz*L1}!HdNV z(4lk5x_pW40kBwGe;g6Mymiz7i+Y0Fc;_XzYvW3bTv(qyRYrN=w@D0W6Cn~+z8(r% z(S12uJ0Cyv1FGsgOy1a2)9QjISC^DkCPMmZR9?dh5xPA^1Yh#kqWJ|3)&le~53{>F z5(_)TV%7@7JIOk=t8%XohEZ)P*Jxe0&r8FKYStuDMVI7y4P0)j8&O|LADXF(&ciSL=2{R>QDZ~g!P literal 0 HcmV?d00001 diff --git a/tests/fixtures/tsp-n20-00002.h5 b/tests/fixtures/tsp-n20-00002.h5 new file mode 100644 index 0000000000000000000000000000000000000000..7a2476205e8cc07635810aaaf02394e88e711350 GIT binary patch literal 30868 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/tsp-n20-00002.mps.gz b/tests/fixtures/tsp-n20-00002.mps.gz new file mode 100644 index 0000000000000000000000000000000000000000..de15dd3bd620f1b5f0cd22dc43d333d250bfb2b9 GIT binary patch literal 5653 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/tsp-n20-00002.pkl.gz b/tests/fixtures/tsp-n20-00002.pkl.gz new file mode 100644 index 0000000000000000000000000000000000000000..702ebdcac262bfad05b80c4e7381eab1099e6ce2 GIT binary patch literal 1134 zcmV-!1d;n6iwFn^yE6lURTw-V`uva7!hSLKGdRs3UYfK?+Sibfi^Ra-kQ3APcc-{iTrI9XbB{huZq=imPUHVg?|ifF&QAUJcax=0+~(%yXaCN|&7Wc>a@DiD z@$HNq<0l(8cH@P&7k$llydC}E?O5MmJ=ga=Z||Ra`}U=`gJ<3jzx8&q@9m$LI`7=u zlFq$2(Yl|!&1#Ncj9B+hbpGot?N4;B{285T-GSEW|LLK(!IAveJFkzuUD3HE?JMbB zC9NB4UEHvE;TPkP{Lj6xxWqa?zNh`VXDA#}%|~7Nqx&y*g^T=;V{eyr{^V!fS66(F zZ5+W1zZmOJb$=jS6rZft=YG_8z3=!!ehI&8`CCwY>+)l$IL2k|yR>+1ytMoxj^Kq~ z4B}H3A11=3@K}Bem$CLu4{yR(fw)Y?XTK5mY-SK zp?I15&f8U|`*covWciCukdN>le$C@3yrv8MjK%w5q;;Cd(v?&AJFq&#d+rOLsrZ;G zKc-KtexVcOBfN)S#BrX#y9@jj9;wb9N*`kK`fHmvBl4L1fe(9Df6!I*3!NYz;XVA~ z-Qa~^jA2{vlz(OM@3q#);(ei`cPnmXoxh^E79yJu=qmb!PLPlA9)1zWIbLn+XGysK zuKVATXJ5(ByWajLo-YfhPT%509SRnBNgU7#@)6#{FX9MZ_{9Lf%$DWRisDt)xzohn zA3V18kvfDvp{wW@Izc|dd-#PP;5GM)xGO)dm0#9C}MI6B^x1jTx zs-OR(dnV5<|API6KIg#3k@`p-LhsR4@)(^UAK^XxB97pNUyQ$$hg14G#Ua z{RDNJ`bZr@@6lEC3!NYz;r*On2No~%idL_J#Cc^f~i>VqohdbqKvjSJAKK zg?xnf@QXNt7k)8P{EThCN?%C-MV~`ILEWZ4QisrcbQS)f6XYYj=R0u(uXMrB>5=WD z=~w9s>A&c6=qI?B`bgZ+dvq23LMO;acn`mbBY5H0Y`EDkk$yG*A9EQB_iGIR0DjnT A=>Px# literal 0 HcmV?d00001