Skip to content

Commit

Permalink
RCPSP/max format (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
leonlan authored Jan 6, 2025
1 parent 465e189 commit 70c6b68
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 16 deletions.
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This library implements parsers for various project scheduling benchmark instanc
- Resource-Constrained Project Scheduling Problem (RCPSP)
- Multi-Mode Resource-Constrained Project Scheduling Problem (MMRCPSP)
- Resource-Constrained Multi Project Scheduling Problem (RCMPSP)
- Resource-Constrained Project Scheduling Problem with Minimal and Maximal Time Lags (RCPSP/max)

`psplib` only depends on `numpy` and can be installed in the usual way:

Expand Down Expand Up @@ -36,22 +37,23 @@ pip install psplib
32

>>> instance.activities
[Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[1, 2, 3], name=''),
Activity(modes=[Mode(duration=8, demands=[4, 0, 0, 0])], successors=[5, 10, 14], name=''),
[Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[1, 2, 3], delays=None, name=''),
Activity(modes=[Mode(duration=8, demands=[4, 0, 0, 0])], successors=[5, 10, 14], delays=None, name=''),
...,
Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[], name='')]
Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[], delays=None, name='')]
```

All parsers return an instance of the [`ProjectInstance`](https://github.com/PyJobShop/PSPLIB/blob/main/psplib/ProjectInstance.py) class, which is an instance representation of the multi-project, multi-mode, resource-constrained project scheduling problem (MP-MM-RCPSP).
All parsers return an instance of the [`ProjectInstance`](https://github.com/PyJobShop/PSPLIB/blob/main/psplib/ProjectInstance.py) class.

## Instance formats

`psplib` implements parsers for three commonly used instance formats, listed below.
`psplib` implements parsers for commonly used project scheduling instance formats, listed below.
To parse a specific instance format, set the `instance_format` argument in `parse`.

1. **PSPLIB format**: used by the [PSPLIB](https://www.om-db.wi.tum.de/psplib/) library to describe RCPSP and MMRCPSP instances.
2. **Patterson format**: used for RCPSP instances, mostly used by the [OR&S](https://www.projectmanagement.ugent.be/research/data) library. See [this](http://www.p2engine.com/p2reader/patterson_format) website for more details.
3. **MPLIB format**: used for RCMPSP instances from the [MPLIB](https://www.projectmanagement.ugent.be/research/data) library.
1. `psplib`: The **PSPLIB format** is used by the [PSPLIB](https://www.om-db.wi.tum.de/psplib/) library to describe RCPSP and MMRCPSP instances.
2. `patterson`: The **Patterson format**: used for RCPSP instances, mostly used by the [OR&S](https://www.projectmanagement.ugent.be/research/data) library. See [this](http://www.p2engine.com/p2reader/patterson_format) website for more details.
3. `mplib`: The **MPLIB format** is used for RCMPSP instances from the [MPLIB](https://www.projectmanagement.ugent.be/research/data) library.
4. `rcpsp_max`: The **RCPSP/max format** is used for RCPSP/max instances from [TU Clausthal](https://www.wiwi.tu-clausthal.de/en/ueber-uns/abteilungen/betriebswirtschaftslehre-insbesondere-produktion-und-logistik/research/research-areas/project-generator-progen/max-and-psp/max-library/single-mode-project-duration-problem-rcpsp/max).

## Instance databases

Expand All @@ -60,3 +62,5 @@ The following websites host widely-used project scheduling benchmark instances.
- [PSPLIB](https://www.om-db.wi.tum.de/psplib/) contains different problem sets for various types of resource constrained project scheduling problems as well as optimal and heuristic solutions.

- [OR&S project database](https://www.projectmanagement.ugent.be/research/data) is the research data website of the Operations Research and Scheduling (OR&S) Research group of the Faculty of Economics and Business Administration at Ghent University (Belgium). OR&S is very active in the field of project scheduling and has published instances for many project scheduling variants.

- [TU Clausthal](https://www.wiwi.tu-clausthal.de/ueber-uns/abteilungen/betriebswirtschaftslehre-insbesondere-produktion-und-logistik/forschung-und-transfer/schwerpunkte/projekt-generator) provides RCPSP/max benchmark instances.
16 changes: 15 additions & 1 deletion psplib/ProjectInstance.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Optional


@dataclass
Expand Down Expand Up @@ -46,15 +47,25 @@ class Activity:
The processing modes of this activity.
successors
The indices of successor activities.
delays
The delay for each successor activity. If delays are specified, then
the length of this list must be equal to the length of `successors`.
Delays are used for RCPSP/max instances, where the precedence
relationship is defined as ``start(pred) + delay <= start(succ)``.
name
Optional name of the activity to identify this activity. This is
helpful to map this activity back to the original problem instance.
"""

modes: list[Mode]
successors: list[int]
delays: Optional[list[int]] = None
name: str = ""

def __post_init__(self):
if self.delays and len(self.delays) != len(self.successors):
raise ValueError("Length of successors and delays must be equal.")

@property
def num_modes(self):
return len(self.modes)
Expand All @@ -66,6 +77,9 @@ class Project:
A project is a collection of activities that share a common release date
and the project is considered finished when all activities are completed.
Mainly used in multi-project instances. In regular project scheduling
instances, there is only one project that contains all activities.
Parameters
----------
activities
Expand All @@ -85,7 +99,7 @@ def num_activities(self):
@dataclass
class ProjectInstance:
"""
Multi-project multi-mode resource-constrained project scheduling instance.
The project scheduling instance.
"""

resources: list[Resource]
Expand Down
1 change: 1 addition & 0 deletions psplib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
from .parse_mplib import parse_mplib as parse_mplib
from .parse_patterson import parse_patterson as parse_patterson
from .parse_psplib import parse_psplib as parse_psplib
from .parse_rcpsp_max import parse_rcpsp_max as parse_rcpsp_max
from .ProjectInstance import ProjectInstance as ProjectInstance
3 changes: 3 additions & 0 deletions psplib/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .parse_mplib import parse_mplib
from .parse_patterson import parse_patterson
from .parse_psplib import parse_psplib
from .parse_rcpsp_max import parse_rcpsp_max
from .ProjectInstance import ProjectInstance


Expand Down Expand Up @@ -32,5 +33,7 @@ def parse(
return parse_patterson(loc)
elif instance_format == "mplib":
return parse_mplib(loc)
if instance_format == "rcpsp_max":
return parse_rcpsp_max(loc)

raise ValueError(f"Unknown instance format: {instance_format}")
3 changes: 1 addition & 2 deletions psplib/parse_mplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def parse_mplib(loc: Union[str, Path]) -> ProjectInstance:
The parsed instance.
"""
with open(loc, "r") as fh:
# Strip all lines and ignore all empty lines.
lines = iter(line.strip() for line in fh.readlines() if line.strip())

num_projects = int(next(lines))
Expand Down Expand Up @@ -50,7 +49,7 @@ def parse_mplib(loc: Union[str, Path]) -> ProjectInstance:
mode = Mode(duration, demands)
name = f"{project_idx}:{activity_idx}" # original activity id
id2idx[name] = len(activities)
activities.append(Activity([mode], successors, name)) # type: ignore
activities.append(Activity([mode], successors, name=name)) # type: ignore

for activity in activities:
# Map the successors ids from {project_idx:activity_idx}, to the
Expand Down
1 change: 0 additions & 1 deletion psplib/parse_patterson.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def parse_patterson(loc: Union[str, Path]) -> ProjectInstance:
The parsed project instance.
"""
with open(loc, "r") as fh:
# Strip all lines and ignore all empty lines.
lines = iter(line.strip() for line in fh.readlines() if line.strip())

num_activities, num_resources = map(int, next(lines).split())
Expand Down
46 changes: 46 additions & 0 deletions psplib/parse_rcpsp_max.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from pathlib import Path
from typing import Union

from .ProjectInstance import Activity, Mode, Project, ProjectInstance, Resource


def parse_rcpsp_max(loc: Union[str, Path]) -> ProjectInstance:
"""
Parses the RCPSP/max instance format.
Parameters
----------
loc
The location of the instance.
Returns
-------
ProjectInstance
The parsed project instance.
"""

with open(loc, "r") as fh:
lines = iter(line.strip() for line in fh.readlines() if line.strip())

num_activities, num_renewables, *_ = map(int, next(lines).split())
num_activities += 2 # source and target
activities = []

succ_lines = [next(lines).split() for _ in range(num_activities)]
activity_lines = [next(lines).split() for _ in range(num_activities)]

for idx in range(num_activities):
_, _, num_successors, *succ = succ_lines[idx]
successors = list(map(int, succ[: int(num_successors)]))
delays = [int(val.strip("[]")) for val in succ[int(num_successors) :]]

_, _, duration, *demands = map(int, activity_lines[idx])

activity = Activity([Mode(duration, demands)], successors, delays)
activities.append(activity)

capacities = map(int, next(lines).split())
resources = [Resource(capacity, renewable=True) for capacity in capacities]
projects = [Project(list(range(num_activities)))]

return ProjectInstance(resources, activities, projects)
26 changes: 26 additions & 0 deletions tests/data/UBO10_01.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
10 5 0 0
0 1 4 3 2 1 8 [0] [0] [0] [0]
1 1 1 10 [2]
2 1 3 4 11 7 [5] [9] [0]
3 1 1 9 [3]
4 1 2 11 5 [6] [4]
5 1 2 11 6 [9] [-5]
6 1 3 5 7 11 [-4] [-4] [10]
7 1 2 8 11 [-4] [5]
8 1 2 11 7 [7] [-2]
9 1 1 11 [7]
10 1 2 11 1 [5] [-3]
11 1 0
0 1 0 0 0 0 0 0
1 1 2 5 7 8 4 6
2 1 9 10 8 0 8 10
3 1 6 9 9 0 4 5
4 1 6 0 8 0 5 5
5 1 9 0 8 6 3 4
6 1 10 8 9 4 9 9
7 1 5 6 3 0 6 9
8 1 7 6 8 2 0 10
9 1 7 0 8 10 3 0
10 1 5 0 8 0 10 0
11 1 0 0 0 0 0 0
10 10 10 10 10
1 change: 1 addition & 0 deletions tests/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
("data/Jall1_1.mm", "psplib"),
("data/RG300_1.rcp", "patterson"),
("data/MPLIB1_Set1_0.rcmp", "mplib"),
("data/UBO10_01.sch", "rcpsp_max"),
],
)
def test_parse(loc, instance_format):
Expand Down
2 changes: 2 additions & 0 deletions tests/test_parse_mplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_mplib_set1():

# Successors are 1:2, 1:3, 1:4 -> activity indices 1, 2, 3
assert_equal(activity.successors, [1, 2, 3])
assert_equal(activity.delays, None)

assert_equal(activity.num_modes, 1)
assert_equal(activity.modes[0].demands, [0, 0, 0, 0])
Expand Down Expand Up @@ -60,6 +61,7 @@ def test_mplib_set2():
# Successors are 10:14 10:13 10:12 10:9.
successors = [(9 * 52) + idx - 1 for idx in [14, 13, 12, 9]]
assert_equal(activity.successors, successors)
assert_equal(activity.delays, None)

assert_equal(activity.num_modes, 1)
assert_equal(activity.modes[0].demands, [8, 4, 3, 5, 1])
Expand Down
1 change: 1 addition & 0 deletions tests/test_parse_patterson.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_rg300():
)
successors = list(map(int, successors.split(", ")))
assert_equal(activity.successors, successors)
assert_equal(activity.delays, None)

assert_equal(activity.num_modes, 1)
assert_equal(activity.modes[0].demands, [0, 1, 0, 0])
Expand Down
2 changes: 2 additions & 0 deletions tests/test_parse_psplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def test_instance_single_mode():
activity = instance.activities[1] # second activity (jobnr. 2)
successors = [4, 8]
assert_equal(activity.successors, successors)
assert_equal(activity.delays, None)

assert_equal(activity.num_modes, 1)
assert_equal(activity.modes[0].duration, 2)
Expand Down Expand Up @@ -53,6 +54,7 @@ def test_instance_mmlib():
activity = instance.activities[1] # second activity (jobnr. 2)
successors = [50, 49, 47, 24, 22, 20, 19, 17, 16, 13]
assert_equal(activity.successors, successors)
assert_equal(activity.delays, None)

assert_equal(activity.num_modes, 3)
assert_equal(activity.modes[0].duration, 2)
Expand Down
32 changes: 32 additions & 0 deletions tests/test_parse_rcpsp_max.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from numpy.testing import assert_equal

from psplib import parse_rcpsp_max

from .utils import relative


def test_ubo10():
"""
Tests that the instance ``UBO10_01.rcpsp`` is correctly parsed.
"""
instance = parse_rcpsp_max(relative("data/UBO10_01.sch"))

capacities = [res.capacity for res in instance.resources]
renewables = [res.renewable for res in instance.resources]

assert_equal(instance.num_resources, 5)
assert_equal(capacities, [10, 10, 10, 10, 10])
assert_equal(renewables, [True, True, True, True, True])

assert_equal(instance.num_projects, 1)
assert_equal(instance.projects[0].num_activities, 12)
assert_equal(instance.num_activities, 12)

activity = instance.activities[2] # third activity

assert_equal(activity.successors, [4, 11, 7])
assert_equal(activity.delays, [5, 9, 0])

assert_equal(activity.num_modes, 1)
assert_equal(activity.modes[0].demands, [10, 8, 0, 8, 10])
assert_equal(activity.modes[0].duration, 9)
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 70c6b68

Please sign in to comment.