"""
PSPLib is a library of Project Scheduling Problem (RCPSP) benchmark instances.
Origin: https://www.om-db.wi.tum.de/psplib/main.html
"""
from __future__ import annotations
import os
import pathlib
import zipfile
from typing import Any, Optional, Callable, Union, Tuple
import builtins
from cpmpy.tools.datasets.core import FileDataset
from cpmpy.tools.io.rcpsp import parse_rcpsp
[docs]
class PSPLibDataset(FileDataset): # torch.utils.data.Dataset compatible
"""
PSPlib Dataset in a PyTorch compatible format.
- Origin: https://www.om-db.wi.tum.de/psplib/main.html
- Reference: Kolisch, R., Sprecher, A. PSPLIB - A project scheduling problem library. European Journal of Operational Research, 96(1), 205-216, 1997.
To load an instance into a CPMpy model, use :func:`~cpmpy.tools.io.rcpsp.load_rcpsp`.
For examples of using a loader as a dataset ``transform``, see the
:ref:`modeling guide <modeling-datasets>`.
Arguments:
root (str): Root directory containing the psplib instances (if 'download', instances will be downloaded to this location)
variant (str): scheduling variant (only 'rcpsp' is supported for now)
family (str): family name (e.g. j30, j60, etc...)
transform (callable, optional): Optional transform to be applied on the instance data (the file path of each problem instance)
target_transform (callable, optional): Optional transform to be applied on the metadata (the metadata dictionary of each problem instance)
download (bool): If True, downloads the dataset from the internet and puts it in `root` directory
"""
name = "psplib"
description = "Project Scheduling Problem Library (RCPSP) benchmark instances."
homepage = "https://www.om-db.wi.tum.de/psplib/main.html"
citation = [
"Kolisch, R., Sprecher, A. PSPLIB - A project scheduling problem library. European Journal of Operational Research, 96(1), 205-216, 1997.",
]
def __init__(self, root: str = ".", variant: str = "rcpsp", family: str = "j30",
transform: Optional[Callable] = None, target_transform: Optional[Callable] = None,
download: bool = False, **kwargs: Any):
"""
Constructor for a dataset object for PSPlib.
Raises:
ValueError: If the dataset directory does not exist and `download=False`,
or if the requested variant/family combination is not available.
"""
self.root = pathlib.Path(root)
self.variant = variant
self.family = family
self.families = dict(
rcpsp = ["j30", "j60", "j90", "j120"]
)
self.family_codes = dict(rcpsp="sm", mrcpsp="mm")
if variant != "rcpsp":
raise ValueError("Only 'rcpsp' variant is supported for now")
if family not in self.families[variant]:
raise ValueError(f"Unknown problem family. Must be any of {','.join(self.families[variant])}")
dataset_dir = self.root / self.name / self.variant / self.family
super().__init__(
dataset_dir=dataset_dir,
transform=transform, target_transform=target_transform,
download=download, extension=f".{self.family_codes[self.variant]}",
**kwargs
)
[docs]
@classmethod
def parse(cls, instance: os.PathLike) -> dict[str, Any]:
"""
Parse a PSPLIB RCPSP instance into job data and capacities.
"""
return parse_rcpsp(instance, open=cls.open)
[docs]
def categories(self) -> dict[str, Any]:
return {
"variant": self.variant,
"family": self.family
}
[docs]
def download(self):
code = self.family_codes[self.variant]
url = "https://www.om-db.wi.tum.de/psplib/"
target = f"download_dataset.php?set={self.family}&mode={code}&format=zip"
target_download_path = self.root / f"{self.family}.{code}.zip"
print(f"Downloading PSPLib {self.variant} {self.family} instances from www.om-db.wi.tum.de")
try:
target_download_path = self._download_file(url, target, destination=str(target_download_path), desc=f"{self.family}.{code}.zip")
except ValueError as e:
raise ValueError(f"No dataset available for variant {self.variant} and family {self.family}. Error: {str(e)}")
# make directory and extract files
with zipfile.ZipFile(target_download_path, 'r') as zip_ref:
# Create track folder in root directory, parents=True ensures recursive creation
self.dataset_dir.mkdir(parents=True, exist_ok=True)
# Extract files
for file_info in zip_ref.infolist():
# Extract file to family_dir, removing main_folder/track prefix
filename = pathlib.Path(file_info.filename).name
with zip_ref.open(file_info) as source, open(self.dataset_dir / filename, 'wb') as target:
target.write(source.read())
# Clean up the zip file
target_download_path.unlink()
if __name__ == "__main__":
dataset = PSPLibDataset(variant="rcpsp", family="j30", download=True)
print("Dataset size:", len(dataset))
print("Instance 0:", dataset[0])
print("Instance 'j301_1':", dataset["j301_1"])