Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading