Skip to content

Commit

Permalink
Write instances and solutions (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
leonlan committed Oct 6, 2023
1 parent 3f864c9 commit f181785
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 4 deletions.
79 changes: 75 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
[![codecov](https://codecov.io/gh/leonlan/VRPLIB/branch/master/graph/badge.svg?token=X0X66LBNZ7)](https://codecov.io/gh/leonlan/VRPLIB)

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

## Outline
- [Installation](#installation)
- [Example usage](#example-usage)
- [Documentation](#documentation)

## Installation
`vrplib` works with Python 3.8+ and only depends on `numpy`.
`vrplib` works with Python 3.8+ and only depends on `numpy`. It may be installed in the usual way as

```shell
pip install vrplib
```

## Example usage
### Reading instances and solutions
### Reading files
```python
import vrplib

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


### Downloading instances from CVRPLIB
### Writing files
The functions `write_instance` and `write_solution` provide a simple interface to writing instances and solutions in VRPLIB-style:
- `write_instance` adds indices to data sections when necessary (`EDGE_WEIGHT_SECTION` and `DEPOT_SECTION` are excluded).
- `write_solution` adds the `Route #{idx}` prefix to routes.

Note that these functions do not validate instances: it is up to the user to write correct VRPLIB-style files.

#### Instances
``` python
import vrplib

instance_loc = "instance.vrp"
instance_data = {
"NAME": "instance",
"TYPE": "CVRP",
"VEHICLES": 2,
"DIMENSION": 1,
"CAPACITY": 1,
"EDGE_WEIGHT_TYPE": "EUC_2D",
"NODE_COORD_SECTION": [[250, 250], [500, 500]],
"DEMAND_SECTION": [1, 1],
"DEPOT_SECTION": [1],
}

vrplib.write_instance(instance_loc, instance_data)
```

```
NAME: instance
TYPE: CVRP
VEHICLES: 2
DIMENSION: 1
CAPACITY: 1
EDGE_WEIGHT_TYPE: EUC_2D
NODE_COORD_SECTION
1 250 250
2 500 500
DEMAND_SECTION
1 1
2 1
DEPOT_SECTION
1
EOF
```

#### Solutions
``` python
import vrplib

solution_loc = "solution.sol"
routes = [[1], [2, 3], [4, 5, 6]]
solution_data = {"Cost": 42, "Vehicle types": [1, 2, 3]}

vrplib.write_solution(solution_loc, routes, solution_data)
```

``` { .html }
Route #1: 1
Route #2: 2 3
Route #3: 4 5 6
Cost: 42
Vehicle types: [1, 2, 3]
```


### Downloading from CVRPLIB
``` python
import vrplib

Expand Down
Empty file added tests/write/__init__.py
Empty file.
157 changes: 157 additions & 0 deletions tests/write/test_write_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import numpy as np
from numpy.testing import assert_equal
from pytest import mark

from vrplib import write_instance


@mark.parametrize(
"key, value, desired",
(
["name", "Instance", "name: Instance"], # string
["DIMENSION", 100, "DIMENSION: 100"], # int
["VEHICLES", -10, "VEHICLES: -10"], # negative
["CAPACITY", 10.5, "CAPACITY: 10.5"], # float
["EMPTY", "", "EMPTY: "], # empty
),
)
def test_specifications(tmp_path, key, value, desired):
"""
Tests that key-value pairs where values are floats or strings are
formatted as specifications.
"""
name = "specifications"
instance = {key: value}
write_instance(tmp_path / name, instance)

desired = "\n".join([desired, "EOF", ""])
with open(tmp_path / name, "r") as fh:
assert_equal(fh.read(), desired)


@mark.parametrize(
"key, value, desired",
(
# 1-dimensional list
["X_SECTION", [0, 10], "\n".join(["X_SECTION", "1\t0", "2\t10"])],
# 1-dimensional list with mixed int and float values
["X_SECTION", [0, 10.5], "\n".join(["X_SECTION", "1\t0", "2\t10.5"])],
# 1-dimensional list empty
["X_SECTION", [], "\n".join(["X_SECTION"])],
# 2-dimensional numpy array
[
"Y_SECTION",
np.array([[0, 0], [1, 1]]),
"\n".join(["Y_SECTION", "1\t0\t0", "2\t1\t1"]),
],
# 2-dimensional list empty
["Y_SECTION", [[]], "\n".join(["Y_SECTION", "1\t"])],
# 2-dimensional array with different row lengths
# NOTE: This is currently an invalid VRPLIB format, see
# https://github.com/leonlan/VRPLIB/issues/108.
[
"DATA_SECTION",
[[1], [3, 4]],
"\n".join(["DATA_SECTION", "1\t1", "2\t3\t4"]),
],
),
)
def test_sections(tmp_path, key, value, desired):
"""
Tests that key-value pairs where values are lists are formatted as
sections.
"""
name = "sections"
instance = {key: value}
write_instance(tmp_path / name, instance)

with open(tmp_path / name, "r") as fh:
assert_equal(fh.read(), "\n".join([desired, "EOF", ""]))


def test_no_indices_depot_and_edge_weight_section(tmp_path):
"""
Tests that indices are not included when formatting depot and edge weight
section.
"""
# Let's first test the depot section.
name = "depot"
instance = {"DEPOT_SECTION": [1, 2]}
write_instance(tmp_path / name, instance)

desired = "\n".join(["DEPOT_SECTION", "1", "2", "EOF", ""])
with open(tmp_path / name, "r") as fh:
assert_equal(fh.read(), desired)

# Now let's test the edge weight section.
name = "edge_weight"
instance = {
"EDGE_WEIGHT_SECTION": [
[1, 1, 2],
[1, 0, 3],
[1, 3, 0],
]
}
write_instance(tmp_path / name, instance)

desired = "\n".join(
[
"EDGE_WEIGHT_SECTION",
"1\t1\t2",
"1\t0\t3",
"1\t3\t0",
"EOF",
"",
]
)
with open(tmp_path / name, "r") as fh:
assert_equal(fh.read(), desired)


def test_small_instance_example(tmp_path):
"""
Tests if writing a small instance yields the correct result.
"""
name = "C101"
instance = {
"NAME": name,
"TYPE": "VRPTW",
"DIMENSION": 4,
"CAPACITY": 200,
"NODE_COORD_SECTION": [
[40, 50],
[45, 68],
[45, 70],
[42, 66],
],
"DEMAND_SECTION": [0, 10, 30, 10],
"DEPOT_SECTION": [1],
}

write_instance(tmp_path / name, instance)

desired = "\n".join(
[
"NAME: C101",
"TYPE: VRPTW",
"DIMENSION: 4",
"CAPACITY: 200",
"NODE_COORD_SECTION",
"1\t40\t50",
"2\t45\t68",
"3\t45\t70",
"4\t42\t66",
"DEMAND_SECTION",
"1\t0",
"2\t10",
"3\t30",
"4\t10",
"DEPOT_SECTION",
"1",
"EOF",
"",
]
)

with open(tmp_path / name, "r") as fh:
assert_equal(fh.read(), desired)
82 changes: 82 additions & 0 deletions tests/write/test_write_solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from numpy.testing import assert_equal, assert_raises
from pytest import mark

from vrplib import write_solution


@mark.parametrize(
"routes, desired",
[
([[1, 2]], "Route #1: 1 2"),
([[1, 2], [42, 9]], "Route #1: 1 2\nRoute #2: 42 9"),
],
)
def test_write_routes(tmp_path, routes, desired):
"""
Tests the writing of a solution with routes.
"""
name = "test.sol"
write_solution(tmp_path / name, routes)

with open(tmp_path / name, "r") as fh:
assert_equal(fh.read(), desired + "\n")


def test_raise_empty_routes(tmp_path):
"""
Tests that an error is raised if a route is empty.
"""
name = "test.sol"

with assert_raises(ValueError):
write_solution(tmp_path / name, [[]])

with assert_raises(ValueError):
write_solution(tmp_path / name, [[1], []])


@mark.parametrize(
"data, desired",
[
({"Cost": 100}, "Cost: 100"), # int
({"Time": 123.45}, "Time: 123.45"), # float
({"Distance": -1}, "Distance: -1"), # negative int
({"name": "test.sol"}, "name: test.sol"), # string
({"Vehicle types": [1, 2, 3]}, "Vehicle types: [1, 2, 3]"), # list
({"Vehicle types": (1, 3)}, "Vehicle types: (1, 3)"), # tuple
],
)
def test_format_other_data(tmp_path, data, desired):
name = "test.sol"
routes = [[1]]
write_solution(tmp_path / name, routes, data)

with open(tmp_path / name, "r") as fh:
text = "Route #1: 1" + "\n" + desired + "\n"
assert_equal(fh.read(), text)


def test_small_example(tmp_path):
"""
Tests the writing of a small example.
"""
name = "test.sol"
routes = [[1, 2], [3, 4], [5]]
data = {"Cost": 100, "Time": 123.45, "name": name}

write_solution(tmp_path / name, routes, data)

desired = "\n".join(
[
"Route #1: 1 2",
"Route #2: 3 4",
"Route #3: 5",
"Cost: 100",
"Time: 123.45",
"name: test.sol",
"",
]
)

with open(tmp_path / name, "r") as fh:
assert_equal(fh.read(), desired)
1 change: 1 addition & 0 deletions vrplib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .download import download_instance, download_solution, list_names
from .read import read_instance, read_solution
from .write import write_instance, write_solution
2 changes: 2 additions & 0 deletions vrplib/write/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .write_instance import write_instance
from .write_solution import write_solution

0 comments on commit f181785

Please sign in to comment.