mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-08 18:38:51 -06:00
Implement TravelingSalesmanPerturber
This commit is contained in:
@@ -1108,15 +1108,18 @@
|
||||
"source": [
|
||||
"### Random instance generator\n",
|
||||
"\n",
|
||||
"The class [TravelingSalesmanGenerator][TravelingSalesmanGenerator] can be used to generate random instances of this problem. Initially, the class samples the user-provided probability distribution `n` to decide how many cities to generate. Then, for each city $i$, the class generates its geographical location $(x_i, y_i)$ by sampling the provided distributions `x` and `y`. The distance $d_{ij}$ between cities $i$ and $j$ is then set to\n",
|
||||
"The class [TravelingSalesmanGenerator][TravelingSalesmanGenerator] can be used to generate random instances of this problem. The class samples the user-provided probability distribution `n` to decide how many cities to generate. Then, for each city $i$, the class generates its geographical location $(x_i, y_i)$ by sampling the provided distributions `x` and `y`. The distance $d_{ij}$ between cities $i$ and $j$ is then set to\n",
|
||||
"$$\n",
|
||||
"\\gamma_{ij} \\sqrt{(x_i - x_j)^2 + (y_i - y_j)^2},\n",
|
||||
"$$\n",
|
||||
"where $\\gamma$ is a random scaling factor sampled from the provided probability distribution `gamma`.\n",
|
||||
"\n",
|
||||
"If `fix_cities=True`, then the list of cities is kept the same for all generated instances. The $\\gamma$ values, however, and therefore also the distances, are still different. By default, all distances $d_{ij}$ are rounded to the nearest integer. If `round=False` is provided, this rounding will be disabled.\n",
|
||||
"By default, all distances $d_{ij}$ are rounded to the nearest integer. If `round=False` is provided, this rounding will be disabled.\n",
|
||||
"\n",
|
||||
"[TravelingSalesmanGenerator]: ../../api/problems/#miplearn.problems.tsp.TravelingSalesmanGenerator"
|
||||
"To create multiple instances with the same cities but slightly different distances, you can use [TravelingSalesmanPerturber][TravelingSalesmanPerturber]. This class takes an existing TravelingSalesmanData instance and generates new instances by applying fresh randomization factors to the distances computed from the original cities while keeping the city locations fixed.\n",
|
||||
"\n",
|
||||
"[TravelingSalesmanGenerator]: ../../api/problems/#miplearn.problems.tsp.TravelingSalesmanGenerator\n",
|
||||
"[TravelingSalesmanPerturber]: ../../api/problems/#miplearn.problems.tsp.TravelingSalesmanPerturber"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1129,7 +1132,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": 11,
|
||||
"id": "9d0c56c6",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
@@ -1146,6 +1149,17 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"cities\n",
|
||||
" [[374.54011885 950.71430641]\n",
|
||||
" [731.99394181 598.6584842 ]\n",
|
||||
" [156.01864044 155.99452034]\n",
|
||||
" [ 58.08361217 866.17614577]\n",
|
||||
" [601.11501174 708.0725778 ]\n",
|
||||
" [ 20.5844943 969.90985216]\n",
|
||||
" [832.4426408 212.33911068]\n",
|
||||
" [181.82496721 183.40450985]\n",
|
||||
" [304.24224296 524.75643163]\n",
|
||||
" [431.94501864 291.2291402 ]]\n",
|
||||
"distances[0]\n",
|
||||
" [[ 0. 513. 762. 358. 325. 374. 932. 731. 391. 634.]\n",
|
||||
" [ 513. 0. 726. 765. 163. 754. 409. 719. 446. 400.]\n",
|
||||
@@ -1221,6 +1235,7 @@
|
||||
"from scipy.stats import uniform, randint\n",
|
||||
"from miplearn.problems.tsp import (\n",
|
||||
" TravelingSalesmanGenerator,\n",
|
||||
" TravelingSalesmanPerturber,\n",
|
||||
" build_tsp_model_gurobipy,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
@@ -1228,18 +1243,25 @@
|
||||
"random.seed(42)\n",
|
||||
"np.random.seed(42)\n",
|
||||
"\n",
|
||||
"# Generate random instances with a fixed ten cities in the 1000x1000 box\n",
|
||||
"# and random distance scaling factors in the [0.90, 1.10] interval.\n",
|
||||
"data = TravelingSalesmanGenerator(\n",
|
||||
"# Generate a reference instance with ten cities in the 1000x1000 box\n",
|
||||
"generator = TravelingSalesmanGenerator(\n",
|
||||
" n=randint(low=10, high=11),\n",
|
||||
" x=uniform(loc=0.0, scale=1000.0),\n",
|
||||
" y=uniform(loc=0.0, scale=1000.0),\n",
|
||||
" gamma=uniform(loc=0.90, scale=0.20),\n",
|
||||
" fix_cities=True,\n",
|
||||
" gamma=uniform(loc=1.0, scale=0.0),\n",
|
||||
" round=True,\n",
|
||||
").generate(10)\n",
|
||||
")\n",
|
||||
"reference_instance = generator.generate(1)[0]\n",
|
||||
"\n",
|
||||
"# Generate perturbed instances with the same cities but different distance scaling factors\n",
|
||||
"perturber = TravelingSalesmanPerturber(\n",
|
||||
" gamma=uniform(loc=0.90, scale=0.20),\n",
|
||||
" round=True,\n",
|
||||
")\n",
|
||||
"data = perturber.perturb(reference_instance, 10)\n",
|
||||
"\n",
|
||||
"# Print distance matrices for the first two instances\n",
|
||||
"print(\"cities\\n\", data[0].cities)\n",
|
||||
"print(\"distances[0]\\n\", data[0].distances)\n",
|
||||
"print(\"distances[1]\\n\", data[1].distances)\n",
|
||||
"print()\n",
|
||||
|
||||
@@ -27,10 +27,21 @@ logger = logging.getLogger(__name__)
|
||||
class TravelingSalesmanData:
|
||||
n_cities: int
|
||||
distances: np.ndarray
|
||||
cities: np.ndarray
|
||||
|
||||
|
||||
class TravelingSalesmanGenerator:
|
||||
"""Random generator for the Traveling Salesman Problem."""
|
||||
"""Random instance generator for the Traveling Salesman Problem.
|
||||
|
||||
Generates instances by creating n cities (x_1,y_1),...,(x_n,y_n) where n,
|
||||
x_i and y_i are sampled independently from the provided probability
|
||||
distributions `n`, `x` and `y`. For each (unordered) pair of cities (i,j),
|
||||
the distance d[i,j] between them is set to:
|
||||
|
||||
d[i,j] = gamma[i,j] \\sqrt{(x_i - x_j)^2 + (y_i - y_j)^2}
|
||||
|
||||
where gamma is sampled from the provided probability distribution `gamma`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -38,27 +49,10 @@ class TravelingSalesmanGenerator:
|
||||
y: rv_frozen = uniform(loc=0.0, scale=1000.0),
|
||||
n: rv_frozen = randint(low=100, high=101),
|
||||
gamma: rv_frozen = uniform(loc=1.0, scale=0.0),
|
||||
fix_cities: bool = True,
|
||||
round: bool = True,
|
||||
) -> None:
|
||||
"""Initializes the problem generator.
|
||||
|
||||
Initially, the generator creates n cities (x_1,y_1),...,(x_n,y_n) where n,
|
||||
x_i and y_i are sampled independently from the provided probability
|
||||
distributions `n`, `x` and `y`. For each (unordered) pair of cities (i,j),
|
||||
the distance d[i,j] between them is set to:
|
||||
|
||||
d[i,j] = gamma[i,j] \\sqrt{(x_i - x_j)^2 + (y_i - y_j)^2}
|
||||
|
||||
where gamma is sampled from the provided probability distribution `gamma`.
|
||||
|
||||
If fix_cities=True, the list of cities is kept the same for all generated
|
||||
instances. The gamma values, and therefore also the distances, are still
|
||||
different.
|
||||
|
||||
By default, all distances d[i,j] are rounded to the nearest integer. If
|
||||
`round=False` is provided, this rounding will be disabled.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
x: rv_continuous
|
||||
@@ -67,9 +61,8 @@ class TravelingSalesmanGenerator:
|
||||
Probability distribution for the y-coordinate of each city.
|
||||
n: rv_discrete
|
||||
Probability distribution for the number of cities.
|
||||
fix_cities: bool
|
||||
If False, cities will be resampled for every generated instance. Otherwise, list
|
||||
of cities will be computed once, during the constructor.
|
||||
gamma: rv_continuous
|
||||
Probability distribution for distance perturbation factors.
|
||||
round: bool
|
||||
If True, distances are rounded to the nearest integer.
|
||||
"""
|
||||
@@ -86,26 +79,11 @@ class TravelingSalesmanGenerator:
|
||||
self.gamma = gamma
|
||||
self.round = round
|
||||
|
||||
if fix_cities:
|
||||
self.fixed_n: Optional[int]
|
||||
self.fixed_cities: Optional[np.ndarray]
|
||||
self.fixed_n, self.fixed_cities = self._generate_cities()
|
||||
else:
|
||||
self.fixed_n = None
|
||||
self.fixed_cities = None
|
||||
|
||||
def generate(self, n_samples: int) -> List[TravelingSalesmanData]:
|
||||
def _sample() -> TravelingSalesmanData:
|
||||
if self.fixed_cities is not None:
|
||||
assert self.fixed_n is not None
|
||||
n, cities = self.fixed_n, self.fixed_cities
|
||||
else:
|
||||
n, cities = self._generate_cities()
|
||||
distances = squareform(pdist(cities)) * self.gamma.rvs(size=(n, n))
|
||||
distances = np.tril(distances) + np.triu(distances.T, 1)
|
||||
if self.round:
|
||||
distances = distances.round()
|
||||
return TravelingSalesmanData(n, distances)
|
||||
n, cities = self._generate_cities()
|
||||
distances = self._compute_distances(cities, self.gamma, self.round)
|
||||
return TravelingSalesmanData(n, distances, cities)
|
||||
|
||||
return [_sample() for _ in range(n_samples)]
|
||||
|
||||
@@ -114,6 +92,62 @@ class TravelingSalesmanGenerator:
|
||||
cities = np.array([(self.x.rvs(), self.y.rvs()) for _ in range(n)])
|
||||
return n, cities
|
||||
|
||||
@staticmethod
|
||||
def _compute_distances(
|
||||
cities: np.ndarray, gamma: rv_frozen, round: bool
|
||||
) -> np.ndarray:
|
||||
n = len(cities)
|
||||
distances = squareform(pdist(cities)) * gamma.rvs(size=(n, n))
|
||||
distances = np.tril(distances) + np.triu(distances.T, 1)
|
||||
if round:
|
||||
distances = distances.round()
|
||||
return distances
|
||||
|
||||
|
||||
class TravelingSalesmanPerturber:
|
||||
"""Perturbation generator for existing Traveling Salesman Problem instances.
|
||||
|
||||
Takes an existing TravelingSalesmanData instance and generates new instances
|
||||
by applying randomization factors to the distances computed from the original cities.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gamma: rv_frozen = uniform(loc=1.0, scale=0.0),
|
||||
round: bool = True,
|
||||
) -> None:
|
||||
"""Initialize the perturbation generator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
gamma: rv_continuous
|
||||
Probability distribution for randomization factors applied to distances.
|
||||
round: bool
|
||||
If True, perturbed distances are rounded to the nearest integer.
|
||||
"""
|
||||
assert isinstance(
|
||||
gamma, rv_frozen
|
||||
), "gamma should be a SciPy probability distribution"
|
||||
self.gamma = gamma
|
||||
self.round = round
|
||||
|
||||
def perturb(
|
||||
self,
|
||||
instance: TravelingSalesmanData,
|
||||
n_samples: int,
|
||||
) -> List[TravelingSalesmanData]:
|
||||
def _sample() -> TravelingSalesmanData:
|
||||
new_distances = TravelingSalesmanGenerator._compute_distances(
|
||||
instance.cities,
|
||||
self.gamma,
|
||||
self.round,
|
||||
)
|
||||
return TravelingSalesmanData(
|
||||
instance.n_cities, new_distances, instance.cities
|
||||
)
|
||||
|
||||
return [_sample() for _ in range(n_samples)]
|
||||
|
||||
|
||||
def build_tsp_model_gurobipy(
|
||||
data: Union[str, TravelingSalesmanData],
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_mem_component(
|
||||
for h5 in [tsp_gp_h5, tsp_pyo_h5]:
|
||||
clf = Mock(wraps=DummyClassifier())
|
||||
comp = MemorizingLazyComponent(clf=clf, extractor=default_extractor)
|
||||
comp.fit(tsp_gp_h5)
|
||||
comp.fit(h5)
|
||||
|
||||
# Should call fit method with correct arguments
|
||||
clf.fit.assert_called()
|
||||
@@ -43,7 +43,7 @@ def test_mem_component(
|
||||
# Call before-mip
|
||||
stats: Dict[str, Any] = {}
|
||||
model = Mock()
|
||||
comp.before_mip(tsp_gp_h5[0], model, stats)
|
||||
comp.before_mip(h5[0], model, stats)
|
||||
|
||||
# Should call predict with correct args
|
||||
clf.predict.assert_called()
|
||||
|
||||
14
tests/fixtures/gen_tsp.py
vendored
14
tests/fixtures/gen_tsp.py
vendored
@@ -7,6 +7,7 @@ from miplearn.collectors.basic import BasicCollector
|
||||
from miplearn.io import write_pkl_gz
|
||||
from miplearn.problems.tsp import (
|
||||
TravelingSalesmanGenerator,
|
||||
TravelingSalesmanPerturber,
|
||||
build_tsp_model_gurobipy,
|
||||
build_tsp_model_pyomo,
|
||||
)
|
||||
@@ -16,12 +17,19 @@ 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,
|
||||
gamma=uniform(loc=1.0, scale=0.0),
|
||||
round=True,
|
||||
)
|
||||
|
||||
data = gen.generate(3)
|
||||
# Generate a reference instance with fixed cities
|
||||
reference_instance = gen.generate(1)[0]
|
||||
|
||||
# Generate perturbed instances with same cities but different distance scaling
|
||||
perturber = TravelingSalesmanPerturber(
|
||||
gamma=uniform(loc=1.0, scale=0.25),
|
||||
round=True,
|
||||
)
|
||||
data = perturber.perturb(reference_instance, 3)
|
||||
|
||||
params = {"seed": 42, "threads": 1}
|
||||
|
||||
|
||||
BIN
tests/fixtures/tsp-gp-n20-00000.h5
vendored
BIN
tests/fixtures/tsp-gp-n20-00000.h5
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00000.mps.gz
vendored
BIN
tests/fixtures/tsp-gp-n20-00000.mps.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00000.pkl.gz
vendored
BIN
tests/fixtures/tsp-gp-n20-00000.pkl.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00001.h5
vendored
BIN
tests/fixtures/tsp-gp-n20-00001.h5
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00001.mps.gz
vendored
BIN
tests/fixtures/tsp-gp-n20-00001.mps.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00001.pkl.gz
vendored
BIN
tests/fixtures/tsp-gp-n20-00001.pkl.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00002.h5
vendored
BIN
tests/fixtures/tsp-gp-n20-00002.h5
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00002.mps.gz
vendored
BIN
tests/fixtures/tsp-gp-n20-00002.mps.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-gp-n20-00002.pkl.gz
vendored
BIN
tests/fixtures/tsp-gp-n20-00002.pkl.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00000.h5
vendored
BIN
tests/fixtures/tsp-pyo-n20-00000.h5
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00000.mps.gz
vendored
BIN
tests/fixtures/tsp-pyo-n20-00000.mps.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00000.pkl.gz
vendored
BIN
tests/fixtures/tsp-pyo-n20-00000.pkl.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00001.h5
vendored
BIN
tests/fixtures/tsp-pyo-n20-00001.h5
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00001.mps.gz
vendored
BIN
tests/fixtures/tsp-pyo-n20-00001.mps.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00001.pkl.gz
vendored
BIN
tests/fixtures/tsp-pyo-n20-00001.pkl.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00002.h5
vendored
BIN
tests/fixtures/tsp-pyo-n20-00002.h5
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00002.mps.gz
vendored
BIN
tests/fixtures/tsp-pyo-n20-00002.mps.gz
vendored
Binary file not shown.
BIN
tests/fixtures/tsp-pyo-n20-00002.pkl.gz
vendored
BIN
tests/fixtures/tsp-pyo-n20-00002.pkl.gz
vendored
Binary file not shown.
@@ -17,56 +17,30 @@ def test_tsp_generator() -> None:
|
||||
gen = TravelingSalesmanGenerator(
|
||||
x=uniform(loc=0.0, scale=1000.0),
|
||||
y=uniform(loc=0.0, scale=1000.0),
|
||||
n=randint(low=3, high=4),
|
||||
n=randint(low=5, high=6),
|
||||
gamma=uniform(loc=1.0, scale=0.25),
|
||||
fix_cities=True,
|
||||
round=True,
|
||||
)
|
||||
data = gen.generate(2)
|
||||
data = gen.generate(1)
|
||||
assert data[0].distances.tolist() == [
|
||||
[0.0, 591.0, 996.0],
|
||||
[591.0, 0.0, 765.0],
|
||||
[996.0, 765.0, 0.0],
|
||||
]
|
||||
assert data[1].distances.tolist() == [
|
||||
[0.0, 556.0, 853.0],
|
||||
[556.0, 0.0, 779.0],
|
||||
[853.0, 779.0, 0.0],
|
||||
[0.0, 525.0, 950.0, 392.0, 382.0],
|
||||
[525.0, 0.0, 752.0, 761.0, 178.0],
|
||||
[950.0, 752.0, 0.0, 809.0, 721.0],
|
||||
[392.0, 761.0, 809.0, 0.0, 700.0],
|
||||
[382.0, 178.0, 721.0, 700.0, 0.0],
|
||||
]
|
||||
|
||||
|
||||
def test_tsp() -> None:
|
||||
data = TravelingSalesmanData(
|
||||
n_cities=6,
|
||||
distances=squareform(
|
||||
pdist(
|
||||
[
|
||||
[0.0, 0.0],
|
||||
[1.0, 0.0],
|
||||
[2.0, 0.0],
|
||||
[3.0, 0.0],
|
||||
[0.0, 1.0],
|
||||
[3.0, 1.0],
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
model = build_tsp_model_gurobipy(data)
|
||||
model = build_tsp_model_gurobipy(data[0])
|
||||
model.optimize()
|
||||
assert model.inner.getAttr("x", model.inner.getVars()) == [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user