Skip to content

Commit f181785

Browse files
authored
Write instances and solutions (#109)
1 parent 3f864c9 commit f181785

8 files changed

+443
-4
lines changed

README.md

+75-4
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@
44
[![codecov](https://codecov.io/gh/leonlan/VRPLIB/branch/master/graph/badge.svg?token=X0X66LBNZ7)](https://codecov.io/gh/leonlan/VRPLIB)
55

66
`vrplib` is a Python package for working with Vehicle Routing Problem (VRP) instances. The main features are:
7-
- reading VRPLIB and Solomon instances and solutions, and
7+
- reading VRPLIB and Solomon instances and solutions,
8+
- writing VRPLIB-style instances and solutions, and
89
- downloading instances and best known solutions from [CVRPLIB](http://vrp.atd-lab.inf.puc-rio.br/index.php/en/).
910

11+
## Outline
12+
- [Installation](#installation)
13+
- [Example usage](#example-usage)
14+
- [Documentation](#documentation)
15+
1016
## Installation
11-
`vrplib` works with Python 3.8+ and only depends on `numpy`.
17+
`vrplib` works with Python 3.8+ and only depends on `numpy`. It may be installed in the usual way as
1218

1319
```shell
1420
pip install vrplib
1521
```
1622

1723
## Example usage
18-
### Reading instances and solutions
24+
### Reading files
1925
```python
2026
import vrplib
2127

@@ -38,7 +44,72 @@ dict_keys(['routes', 'cost'])
3844
```
3945

4046

41-
### Downloading instances from CVRPLIB
47+
### Writing files
48+
The functions `write_instance` and `write_solution` provide a simple interface to writing instances and solutions in VRPLIB-style:
49+
- `write_instance` adds indices to data sections when necessary (`EDGE_WEIGHT_SECTION` and `DEPOT_SECTION` are excluded).
50+
- `write_solution` adds the `Route #{idx}` prefix to routes.
51+
52+
Note that these functions do not validate instances: it is up to the user to write correct VRPLIB-style files.
53+
54+
#### Instances
55+
``` python
56+
import vrplib
57+
58+
instance_loc = "instance.vrp"
59+
instance_data = {
60+
"NAME": "instance",
61+
"TYPE": "CVRP",
62+
"VEHICLES": 2,
63+
"DIMENSION": 1,
64+
"CAPACITY": 1,
65+
"EDGE_WEIGHT_TYPE": "EUC_2D",
66+
"NODE_COORD_SECTION": [[250, 250], [500, 500]],
67+
"DEMAND_SECTION": [1, 1],
68+
"DEPOT_SECTION": [1],
69+
}
70+
71+
vrplib.write_instance(instance_loc, instance_data)
72+
```
73+
74+
```
75+
NAME: instance
76+
TYPE: CVRP
77+
VEHICLES: 2
78+
DIMENSION: 1
79+
CAPACITY: 1
80+
EDGE_WEIGHT_TYPE: EUC_2D
81+
NODE_COORD_SECTION
82+
1 250 250
83+
2 500 500
84+
DEMAND_SECTION
85+
1 1
86+
2 1
87+
DEPOT_SECTION
88+
1
89+
EOF
90+
```
91+
92+
#### Solutions
93+
``` python
94+
import vrplib
95+
96+
solution_loc = "solution.sol"
97+
routes = [[1], [2, 3], [4, 5, 6]]
98+
solution_data = {"Cost": 42, "Vehicle types": [1, 2, 3]}
99+
100+
vrplib.write_solution(solution_loc, routes, solution_data)
101+
```
102+
103+
``` { .html }
104+
Route #1: 1
105+
Route #2: 2 3
106+
Route #3: 4 5 6
107+
Cost: 42
108+
Vehicle types: [1, 2, 3]
109+
```
110+
111+
112+
### Downloading from CVRPLIB
42113
``` python
43114
import vrplib
44115

tests/write/__init__.py

Whitespace-only changes.

tests/write/test_write_instance.py

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import numpy as np
2+
from numpy.testing import assert_equal
3+
from pytest import mark
4+
5+
from vrplib import write_instance
6+
7+
8+
@mark.parametrize(
9+
"key, value, desired",
10+
(
11+
["name", "Instance", "name: Instance"], # string
12+
["DIMENSION", 100, "DIMENSION: 100"], # int
13+
["VEHICLES", -10, "VEHICLES: -10"], # negative
14+
["CAPACITY", 10.5, "CAPACITY: 10.5"], # float
15+
["EMPTY", "", "EMPTY: "], # empty
16+
),
17+
)
18+
def test_specifications(tmp_path, key, value, desired):
19+
"""
20+
Tests that key-value pairs where values are floats or strings are
21+
formatted as specifications.
22+
"""
23+
name = "specifications"
24+
instance = {key: value}
25+
write_instance(tmp_path / name, instance)
26+
27+
desired = "\n".join([desired, "EOF", ""])
28+
with open(tmp_path / name, "r") as fh:
29+
assert_equal(fh.read(), desired)
30+
31+
32+
@mark.parametrize(
33+
"key, value, desired",
34+
(
35+
# 1-dimensional list
36+
["X_SECTION", [0, 10], "\n".join(["X_SECTION", "1\t0", "2\t10"])],
37+
# 1-dimensional list with mixed int and float values
38+
["X_SECTION", [0, 10.5], "\n".join(["X_SECTION", "1\t0", "2\t10.5"])],
39+
# 1-dimensional list empty
40+
["X_SECTION", [], "\n".join(["X_SECTION"])],
41+
# 2-dimensional numpy array
42+
[
43+
"Y_SECTION",
44+
np.array([[0, 0], [1, 1]]),
45+
"\n".join(["Y_SECTION", "1\t0\t0", "2\t1\t1"]),
46+
],
47+
# 2-dimensional list empty
48+
["Y_SECTION", [[]], "\n".join(["Y_SECTION", "1\t"])],
49+
# 2-dimensional array with different row lengths
50+
# NOTE: This is currently an invalid VRPLIB format, see
51+
# https://github.com/leonlan/VRPLIB/issues/108.
52+
[
53+
"DATA_SECTION",
54+
[[1], [3, 4]],
55+
"\n".join(["DATA_SECTION", "1\t1", "2\t3\t4"]),
56+
],
57+
),
58+
)
59+
def test_sections(tmp_path, key, value, desired):
60+
"""
61+
Tests that key-value pairs where values are lists are formatted as
62+
sections.
63+
"""
64+
name = "sections"
65+
instance = {key: value}
66+
write_instance(tmp_path / name, instance)
67+
68+
with open(tmp_path / name, "r") as fh:
69+
assert_equal(fh.read(), "\n".join([desired, "EOF", ""]))
70+
71+
72+
def test_no_indices_depot_and_edge_weight_section(tmp_path):
73+
"""
74+
Tests that indices are not included when formatting depot and edge weight
75+
section.
76+
"""
77+
# Let's first test the depot section.
78+
name = "depot"
79+
instance = {"DEPOT_SECTION": [1, 2]}
80+
write_instance(tmp_path / name, instance)
81+
82+
desired = "\n".join(["DEPOT_SECTION", "1", "2", "EOF", ""])
83+
with open(tmp_path / name, "r") as fh:
84+
assert_equal(fh.read(), desired)
85+
86+
# Now let's test the edge weight section.
87+
name = "edge_weight"
88+
instance = {
89+
"EDGE_WEIGHT_SECTION": [
90+
[1, 1, 2],
91+
[1, 0, 3],
92+
[1, 3, 0],
93+
]
94+
}
95+
write_instance(tmp_path / name, instance)
96+
97+
desired = "\n".join(
98+
[
99+
"EDGE_WEIGHT_SECTION",
100+
"1\t1\t2",
101+
"1\t0\t3",
102+
"1\t3\t0",
103+
"EOF",
104+
"",
105+
]
106+
)
107+
with open(tmp_path / name, "r") as fh:
108+
assert_equal(fh.read(), desired)
109+
110+
111+
def test_small_instance_example(tmp_path):
112+
"""
113+
Tests if writing a small instance yields the correct result.
114+
"""
115+
name = "C101"
116+
instance = {
117+
"NAME": name,
118+
"TYPE": "VRPTW",
119+
"DIMENSION": 4,
120+
"CAPACITY": 200,
121+
"NODE_COORD_SECTION": [
122+
[40, 50],
123+
[45, 68],
124+
[45, 70],
125+
[42, 66],
126+
],
127+
"DEMAND_SECTION": [0, 10, 30, 10],
128+
"DEPOT_SECTION": [1],
129+
}
130+
131+
write_instance(tmp_path / name, instance)
132+
133+
desired = "\n".join(
134+
[
135+
"NAME: C101",
136+
"TYPE: VRPTW",
137+
"DIMENSION: 4",
138+
"CAPACITY: 200",
139+
"NODE_COORD_SECTION",
140+
"1\t40\t50",
141+
"2\t45\t68",
142+
"3\t45\t70",
143+
"4\t42\t66",
144+
"DEMAND_SECTION",
145+
"1\t0",
146+
"2\t10",
147+
"3\t30",
148+
"4\t10",
149+
"DEPOT_SECTION",
150+
"1",
151+
"EOF",
152+
"",
153+
]
154+
)
155+
156+
with open(tmp_path / name, "r") as fh:
157+
assert_equal(fh.read(), desired)

tests/write/test_write_solution.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from numpy.testing import assert_equal, assert_raises
2+
from pytest import mark
3+
4+
from vrplib import write_solution
5+
6+
7+
@mark.parametrize(
8+
"routes, desired",
9+
[
10+
([[1, 2]], "Route #1: 1 2"),
11+
([[1, 2], [42, 9]], "Route #1: 1 2\nRoute #2: 42 9"),
12+
],
13+
)
14+
def test_write_routes(tmp_path, routes, desired):
15+
"""
16+
Tests the writing of a solution with routes.
17+
"""
18+
name = "test.sol"
19+
write_solution(tmp_path / name, routes)
20+
21+
with open(tmp_path / name, "r") as fh:
22+
assert_equal(fh.read(), desired + "\n")
23+
24+
25+
def test_raise_empty_routes(tmp_path):
26+
"""
27+
Tests that an error is raised if a route is empty.
28+
"""
29+
name = "test.sol"
30+
31+
with assert_raises(ValueError):
32+
write_solution(tmp_path / name, [[]])
33+
34+
with assert_raises(ValueError):
35+
write_solution(tmp_path / name, [[1], []])
36+
37+
38+
@mark.parametrize(
39+
"data, desired",
40+
[
41+
({"Cost": 100}, "Cost: 100"), # int
42+
({"Time": 123.45}, "Time: 123.45"), # float
43+
({"Distance": -1}, "Distance: -1"), # negative int
44+
({"name": "test.sol"}, "name: test.sol"), # string
45+
({"Vehicle types": [1, 2, 3]}, "Vehicle types: [1, 2, 3]"), # list
46+
({"Vehicle types": (1, 3)}, "Vehicle types: (1, 3)"), # tuple
47+
],
48+
)
49+
def test_format_other_data(tmp_path, data, desired):
50+
name = "test.sol"
51+
routes = [[1]]
52+
write_solution(tmp_path / name, routes, data)
53+
54+
with open(tmp_path / name, "r") as fh:
55+
text = "Route #1: 1" + "\n" + desired + "\n"
56+
assert_equal(fh.read(), text)
57+
58+
59+
def test_small_example(tmp_path):
60+
"""
61+
Tests the writing of a small example.
62+
"""
63+
name = "test.sol"
64+
routes = [[1, 2], [3, 4], [5]]
65+
data = {"Cost": 100, "Time": 123.45, "name": name}
66+
67+
write_solution(tmp_path / name, routes, data)
68+
69+
desired = "\n".join(
70+
[
71+
"Route #1: 1 2",
72+
"Route #2: 3 4",
73+
"Route #3: 5",
74+
"Cost: 100",
75+
"Time: 123.45",
76+
"name: test.sol",
77+
"",
78+
]
79+
)
80+
81+
with open(tmp_path / name, "r") as fh:
82+
assert_equal(fh.read(), desired)

vrplib/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .download import download_instance, download_solution, list_names
22
from .read import read_instance, read_solution
3+
from .write import write_instance, write_solution

vrplib/write/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .write_instance import write_instance
2+
from .write_solution import write_solution

0 commit comments

Comments
 (0)