diff --git a/README.md b/README.md index 45654a9..f0ed132 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ This library implements parsers for various project scheduling benchmark instances, including: - Resource-Constrained Project Scheduling Problem (RCPSP) -- Multi-Mode Resource-Constrained Project Scheduling Problem (MMRCPSP) -- Resource-Constrained Project Scheduling Problem with Minimal and Maximal Time Lags (RCPSP/max) +- Multi-Mode RCPSP (MMRCPSP) +- RCPSP with Minimal and Maximal Time Lags (RCPSP/max) - Resource-Constrained Multi Project Scheduling Problem (RCMPSP) +- RCPSP with flexible project structure (RCPSP-PS) and RCPSP with alternative subgraphs (RCPSP-AS) `psplib` has no dependencies and can be installed in the usual way: @@ -47,6 +48,11 @@ To parse a specific instance format, set the `instance_format` argument in `pars 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. `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). 4. `mplib`: The **MPLIB format** is used for RCMPSP instances from the [MPLIB](https://www.projectmanagement.ugent.be/research/data) library. +5. `rcpsp_ps`: The **RCPSP-PS format** is the format used by [Van der Beek et al. (2024)](https://www.sciencedirect.com/science/article/pii/S0377221724008269). +Specifically, we included an extra line that defines for each task whether it is optional or not. +6. `aslib`: The **ASLIB format** is the format used by RCPSP-AS instances from the ASLIB instance set at [OR&S project database](https://www.projectmanagement.ugent.be/research/data). +ASLIB consist of three different parts (a, b, c). +To use this parser, you have to merge parts (a) and (b) into a single file - part (c) is not parsed. ## Instance databases diff --git a/data/aslib0_0.rcp b/data/aslib0_0.rcp new file mode 100644 index 0000000..d90416f --- /dev/null +++ b/data/aslib0_0.rcp @@ -0,0 +1,251 @@ +122 5 +10 10 10 10 10 + +0 0 0 0 0 0 5 2 14 26 38 50 +0 0 0 0 0 0 6 3 4 5 6 7 8 +1 0 0 1 0 0 2 10 9 +1 0 0 5 0 0 2 10 9 +7 0 0 4 0 0 2 10 9 +1 0 0 4 0 0 2 10 9 +5 0 0 3 0 0 2 10 9 +6 0 0 1 0 0 1 9 +3 0 0 1 0 0 2 12 11 +8 0 0 2 0 0 2 12 11 +3 0 0 2 0 0 1 13 +9 0 0 2 0 0 1 13 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 6 15 16 18 19 20 24 +6 0 0 0 0 1 2 23 22 +5 0 0 0 0 3 1 17 +9 0 0 0 0 2 1 21 +5 0 0 0 0 2 1 21 +2 0 0 0 0 5 1 21 +9 0 0 0 0 2 1 21 +1 0 0 0 0 2 1 25 +4 0 0 0 0 2 1 25 +1 0 0 0 0 4 1 25 +1 0 0 0 0 2 1 25 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 6 27 28 29 30 32 36 +10 0 0 0 1 0 3 35 34 33 +2 0 0 0 2 0 2 34 31 +8 0 0 0 2 0 2 34 33 +1 0 0 0 1 0 1 31 +1 0 0 0 4 0 1 33 +3 0 0 0 5 0 1 33 +4 0 0 0 4 0 1 37 +2 0 0 0 1 0 1 37 +8 0 0 0 3 0 1 37 +9 0 0 0 2 0 1 37 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 3 39 40 43 +4 0 0 0 1 0 3 48 42 41 +8 0 0 0 3 0 3 47 44 41 +5 0 0 0 3 0 2 46 45 +5 0 0 0 3 0 2 47 46 +10 0 0 0 1 0 1 44 +6 0 0 0 2 0 1 45 +2 0 0 0 4 0 1 49 +2 0 0 0 2 0 1 49 +3 0 0 0 2 0 1 49 +10 0 0 0 4 0 1 49 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 4 51 52 53 56 +5 1 0 0 0 0 4 60 59 58 54 +10 2 0 0 0 0 4 59 58 55 54 +2 2 0 0 0 0 3 60 59 57 +9 4 0 0 0 0 1 57 +2 2 0 0 0 0 1 57 +6 1 0 0 0 0 1 58 +1 3 0 0 0 0 1 61 +4 1 0 0 0 0 1 61 +7 4 0 0 0 0 1 61 +10 5 0 0 0 0 1 61 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 2 63 75 +0 0 0 0 0 0 3 64 65 69 +5 0 0 1 0 0 3 68 67 66 +6 0 0 2 0 0 3 68 67 66 +5 0 0 2 0 0 4 73 72 71 70 +3 0 0 3 0 0 2 72 70 +9 0 0 5 0 0 2 71 70 +1 0 0 3 0 0 2 71 70 +4 0 0 4 0 0 1 74 +5 0 0 2 0 0 1 74 +3 0 0 1 0 0 1 74 +10 0 0 2 0 0 1 74 +0 0 0 0 0 0 1 87 +0 0 0 0 0 0 6 76 78 80 83 84 85 +6 0 0 0 0 1 1 77 +3 0 0 0 0 3 2 82 81 +7 0 0 0 0 2 1 79 +9 0 0 0 0 1 1 81 +6 0 0 0 0 5 1 81 +4 0 0 0 0 1 1 86 +10 0 0 0 0 3 1 86 +7 0 0 0 0 1 1 86 +2 0 0 0 0 3 1 86 +8 0 0 0 0 5 1 86 +0 0 0 0 0 0 1 87 +0 0 0 0 0 0 5 88 89 90 91 92 +1 0 0 0 1 0 4 97 96 95 93 +10 0 0 0 3 0 3 97 95 94 +1 0 0 0 3 0 3 96 95 94 +4 0 0 0 3 0 2 96 93 +2 0 0 0 3 0 2 95 93 +10 0 0 0 1 0 1 94 +3 0 0 0 4 0 1 98 +10 0 0 0 2 0 1 98 +7 0 0 0 3 0 1 98 +4 0 0 0 2 0 1 98 +0 0 0 0 0 0 1 99 +0 0 0 0 0 0 7 100 102 103 106 107 108 109 +3 0 0 0 1 0 1 101 +7 0 0 0 2 0 2 105 104 +9 0 0 0 4 0 1 104 +8 0 0 0 2 0 1 104 +8 0 0 0 2 0 1 110 +1 0 0 0 3 0 1 110 +7 0 0 0 3 0 1 110 +6 0 0 0 3 0 1 110 +10 0 0 0 2 0 1 110 +2 0 0 0 3 0 1 110 +0 0 0 0 0 0 1 111 +0 0 0 0 0 0 3 112 113 114 +10 0 0 0 0 1 4 121 120 118 115 +3 0 0 0 0 3 3 120 119 115 +6 0 0 0 0 3 3 119 117 116 +9 0 0 0 0 2 1 117 +5 0 0 0 0 2 1 118 +8 0 0 0 0 3 1 122 +8 0 0 0 0 3 1 122 +7 0 0 0 0 3 1 122 +8 0 0 0 0 3 1 122 +2 0 0 0 0 2 1 122 +0 0 0 0 0 0 0 +0.250000 0.000000 0.000000 +2 +5 2 3 4 5 6 +2 7 8 +1 1 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 1 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 diff --git a/data/rcpsp_ps.txt b/data/rcpsp_ps.txt new file mode 100644 index 0000000..dede5c8 --- /dev/null +++ b/data/rcpsp_ps.txt @@ -0,0 +1,546 @@ +136 4 0 +10 10 10 10 + +0 0 0 0 0 +1 2 1 2 +2 1 2 + +0 0 0 0 0 +2 1 3 1 4 +2 3 4 + +0 0 0 0 0 +1 2 11 12 +2 11 12 + +9 0 0 1 1 +1 1 5 +1 5 + +8 9 9 9 0 +1 1 43 +1 43 + +3 1 1 5 9 +1 3 21 22 6 +3 21 22 6 + +0 0 0 0 0 +2 1 7 1 8 +2 7 8 + +9 0 0 1 1 +1 1 9 +1 9 + +8 9 9 9 0 +1 1 10 +1 10 + +3 1 1 5 9 +1 1 10 +1 10 + +0 0 0 0 0 +1 1 32 +1 32 + +0 0 0 0 0 +2 1 13 1 14 +2 13 14 + +0 0 0 0 0 +2 1 17 1 18 +2 17 18 + +6 1 0 1 1 +1 1 15 +1 15 + +1 9 5 9 8 +1 1 15 +1 15 + +1 5 0 0 6 +1 1 16 +1 16 + +0 0 0 0 0 +1 1 23 +1 23 + +9 0 0 1 1 +1 1 19 +1 19 + +8 9 9 9 0 +1 1 20 +1 20 + +3 1 1 5 9 +1 1 20 +1 20 + +0 0 0 0 0 +1 1 23 +1 23 + +0 0 0 0 0 +2 1 24 1 25 +2 24 25 + +0 0 0 0 0 +1 1 28 +1 28 + +0 0 0 0 0 +1 1 33 +1 33 + +9 0 0 1 1 +1 1 26 +1 26 + +8 9 9 9 0 +1 1 27 +1 27 + +3 1 1 5 9 +1 1 27 +1 27 + +0 0 0 0 0 +1 1 32 +1 32 + +2 1 0 5 1 +2 1 29 1 30 +2 29 30 + +2 7 1 0 8 +1 1 31 +1 31 + +7 7 9 0 6 +1 1 31 +1 31 + +0 0 0 0 0 +1 1 32 +1 32 + +0 0 0 0 0 +1 1 43 +1 43 + +2 1 0 5 1 +2 1 34 1 35 +2 34 35 + +2 7 1 0 8 +1 3 36 37 38 +3 36 37 38 + +7 7 9 0 6 +1 1 69 +1 69 + +0 0 0 0 0 +1 1 39 +1 39 + +0 0 0 0 0 +2 1 44 1 45 +2 44 45 + +0 0 0 0 0 +2 1 47 1 48 +2 47 48 + +2 1 0 5 1 +2 1 40 1 41 +2 40 41 + +2 7 1 0 8 +1 1 42 +1 42 + +7 7 9 0 6 +1 1 42 +1 42 + +0 0 0 0 0 +1 1 68 +1 68 + +0 0 0 0 0 +1 3 51 52 53 +3 51 52 53 + +9 0 0 1 1 +1 1 46 +1 46 + +8 9 9 9 0 +1 1 66 +1 66 + +3 1 1 5 9 +1 1 66 +1 66 + +6 1 0 1 1 +1 1 49 +1 49 + +1 9 5 9 8 +1 1 49 +1 49 + +1 5 0 0 6 +1 1 50 +1 50 + +0 0 0 0 0 +1 1 68 +1 68 + +0 0 0 0 0 +2 1 54 1 55 +2 54 55 + +0 0 0 0 0 +1 1 58 +1 58 + +0 0 0 0 0 +2 1 62 1 63 +2 62 63 + +9 0 0 1 1 +1 1 56 +1 56 + +8 9 9 9 0 +1 1 57 +1 57 + +3 1 1 5 9 +1 1 57 +1 57 + +0 0 0 0 0 +1 1 67 +1 67 + +2 1 0 5 1 +2 1 59 1 60 +2 59 60 + +2 7 1 0 8 +1 1 61 +1 61 + +7 7 9 0 6 +1 1 61 +1 61 + +0 0 0 0 0 +1 1 67 +1 67 + +6 1 0 1 1 +1 1 64 +1 64 + +1 9 5 9 8 +1 1 64 +1 64 + +1 5 0 0 6 +1 1 65 +1 65 + +0 0 0 0 0 +1 1 67 +1 67 + +0 0 0 0 0 +1 1 68 +1 68 + +0 0 0 0 0 +1 1 70 +1 70 + +0 0 0 0 0 +1 1 69 +1 69 + +0 0 0 0 0 +1 1 70 +1 70 + +0 0 0 0 0 +1 2 72 71 +2 72 71 + +0 0 0 0 0 +1 2 73 93 +2 73 93 + +0 0 0 0 0 +1 3 80 78 79 +3 78 79 80 + +0 0 0 0 0 +1 1 74 +1 74 + +2 1 0 5 1 +2 1 75 1 76 +2 75 76 + +2 7 1 0 8 +1 1 77 +1 77 + +7 7 9 0 6 +1 1 77 +1 77 + +0 0 0 0 0 +1 1 99 +1 99 + +0 0 0 0 0 +2 1 81 1 82 +2 81 82 + +0 0 0 0 0 +2 1 85 1 86 +2 85 86 + +0 0 0 0 0 +2 1 89 1 90 +2 89 90 + +6 1 0 1 1 +1 1 83 +1 83 + +1 9 5 9 8 +1 1 83 +1 83 + +1 5 0 0 6 +1 1 84 +1 84 + +0 0 0 0 0 +1 1 94 +1 94 + +6 1 0 1 1 +1 1 87 +1 87 + +1 9 5 9 8 +1 1 87 +1 87 + +1 5 0 0 6 +1 1 88 +1 88 + +0 0 0 0 0 +1 1 94 +1 94 + +9 0 0 1 1 +1 1 91 +1 91 + +8 9 9 9 0 +1 1 92 +1 92 + +3 1 1 5 9 +1 1 92 +1 92 + +0 0 0 0 0 +1 1 94 +1 94 + +0 0 0 0 0 +2 1 95 1 96 +2 95 96 + +0 0 0 0 0 +2 1 100 1 101 +2 100 101 + +6 1 0 1 1 +1 1 97 +1 97 + +1 9 5 9 8 +1 1 97 +1 97 + +1 5 0 0 6 +1 1 98 +1 98 + +0 0 0 0 0 +1 1 99 +1 99 + +0 0 0 0 0 +1 1 104 +1 104 + +6 1 0 1 1 +1 1 102 +1 102 + +1 9 5 9 8 +1 1 102 +1 102 + +1 5 0 0 6 +1 1 103 +1 103 + +0 0 0 0 0 +1 2 105 108 +2 108 105 + +2 1 0 5 1 +1 3 106 107 114 +3 106 107 114 + +0 0 0 0 0 +1 1 113 +1 113 + +0 0 0 0 0 +1 1 116 +1 116 + +0 0 0 0 0 +1 1 120 +1 120 + +0 0 0 0 0 +1 1 109 +1 109 + +2 1 0 5 1 +2 1 110 1 111 +2 110 111 + +2 7 1 0 8 +1 1 112 +1 112 + +7 7 9 0 6 +1 1 112 +1 112 + +0 0 0 0 0 +1 1 132 +1 132 + +2 1 0 5 1 +2 1 128 1 115 +2 128 115 + +0 0 0 0 0 +1 1 124 +1 124 + +7 7 9 0 6 +1 1 130 +1 130 + +2 1 0 5 1 +2 1 117 1 118 +2 117 118 + +2 7 1 0 8 +1 1 119 +1 119 + +7 7 9 0 6 +1 1 119 +1 119 + +0 0 0 0 0 +1 1 129 +1 129 + +2 1 0 5 1 +2 1 121 1 122 +2 121 122 + +2 7 1 0 8 +1 1 123 +1 123 + +7 7 9 0 6 +1 1 123 +1 123 + +0 0 0 0 0 +1 1 129 +1 129 + +2 1 0 5 1 +2 1 125 1 126 +2 125 126 + +2 7 1 0 8 +1 1 127 +1 127 + +7 7 9 0 6 +1 1 127 +1 127 + +0 0 0 0 0 +1 1 129 +1 129 + +2 7 1 0 8 +1 1 130 +1 130 + +0 0 0 0 0 +2 1 131 1 133 +2 131 133 + +0 0 0 0 0 +1 1 132 +1 132 + +2 7 1 0 8 +1 1 134 +1 134 + +0 0 0 0 0 +1 1 135 +1 135 + +7 7 9 0 6 +1 1 134 +1 134 + +0 0 0 0 0 +1 1 135 +1 135 + +0 0 0 0 0 +0 +0 diff --git a/example.ipynb b/example.ipynb index 224182f..2eec68c 100644 --- a/example.ipynb +++ b/example.ipynb @@ -19,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 1, "id": "4fec4d5b-df0f-48c4-9e55-602203a89176", "metadata": {}, "outputs": [], @@ -45,7 +45,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 2, "id": "0530bec2-580c-4845-a1f7-8f94d69a5f40", "metadata": {}, "outputs": [], @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 3, "id": "bfbca94a-720a-4873-b1b7-3c0a52d2a5b0", "metadata": {}, "outputs": [ @@ -73,7 +73,7 @@ "(4, 32)" ] }, - "execution_count": 54, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -92,7 +92,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 4, "id": "e2f6acd6-f529-4833-b479-e00550584820", "metadata": {}, "outputs": [ @@ -105,7 +105,7 @@ " Resource(capacity=12, renewable=True)]" ] }, - "execution_count": 55, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -124,20 +124,20 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 5, "id": "8a1235ac-d44f-4d01-9081-dfb7778c2fc7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[1, 2, 3], delays=None, name=''),\n", - " Activity(modes=[Mode(duration=8, demands=[4, 0, 0, 0])], successors=[5, 10, 14], delays=None, name=''),\n", - " Activity(modes=[Mode(duration=4, demands=[10, 0, 0, 0])], successors=[6, 7, 12], delays=None, name=''),\n", - " Activity(modes=[Mode(duration=6, demands=[0, 0, 0, 3])], successors=[4, 8, 9], delays=None, name='')]" + "[Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[1, 2, 3], delays=None, optional=False, selection_groups=[], name=''),\n", + " Activity(modes=[Mode(duration=8, demands=[4, 0, 0, 0])], successors=[5, 10, 14], delays=None, optional=False, selection_groups=[], name=''),\n", + " Activity(modes=[Mode(duration=4, demands=[10, 0, 0, 0])], successors=[6, 7, 12], delays=None, optional=False, selection_groups=[], name=''),\n", + " Activity(modes=[Mode(duration=6, demands=[0, 0, 0, 3])], successors=[4, 8, 9], delays=None, optional=False, selection_groups=[], name='')]" ] }, - "execution_count": 56, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -156,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 6, "id": "33dec608-c15c-4b7b-894d-0a6f67d8dafc", "metadata": {}, "outputs": [ @@ -166,7 +166,7 @@ "[Mode(duration=0, demands=[0, 0, 0, 0])]" ] }, - "execution_count": 57, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -194,7 +194,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 7, "id": "7c44dee1-a7d8-4c5f-9d5c-9dc119bb597d", "metadata": {}, "outputs": [], @@ -220,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 8, "id": "2a0012ed-f019-4aaf-85b3-93241d380401", "metadata": {}, "outputs": [], @@ -238,20 +238,20 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 9, "id": "6cdb8e24-587f-4b4b-b836-d808a3ecbe3e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0, 0])], successors=[3, 2, 1, 8], delays=[0, 0, 0, 0], name=''),\n", - " Activity(modes=[Mode(duration=2, demands=[5, 7, 8, 4, 6])], successors=[10], delays=[2], name=''),\n", - " Activity(modes=[Mode(duration=9, demands=[10, 8, 0, 8, 10])], successors=[4, 11, 7], delays=[5, 9, 0], name=''),\n", - " Activity(modes=[Mode(duration=6, demands=[9, 9, 0, 4, 5])], successors=[9], delays=[3], name='')]" + "[Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0, 0])], successors=[3, 2, 1, 8], delays=[0, 0, 0, 0], optional=False, selection_groups=[], name=''),\n", + " Activity(modes=[Mode(duration=2, demands=[5, 7, 8, 4, 6])], successors=[10], delays=[2], optional=False, selection_groups=[], name=''),\n", + " Activity(modes=[Mode(duration=9, demands=[10, 8, 0, 8, 10])], successors=[4, 11, 7], delays=[5, 9, 0], optional=False, selection_groups=[], name=''),\n", + " Activity(modes=[Mode(duration=6, demands=[9, 9, 0, 4, 5])], successors=[9], delays=[3], optional=False, selection_groups=[], name='')]" ] }, - "execution_count": 61, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -278,7 +278,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 10, "id": "3280aeae-0bcd-4faa-8649-6a8b64db4e5e", "metadata": {}, "outputs": [], @@ -288,7 +288,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 11, "id": "f3d65f88-1840-4665-abcb-3d1fd3ae46da", "metadata": {}, "outputs": [ @@ -298,7 +298,7 @@ "(4, 372, 6)" ] }, - "execution_count": 64, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -317,7 +317,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 12, "id": "67e3f2ad-8aed-47fe-9a43-cbe7159a585d", "metadata": {}, "outputs": [ @@ -328,7 +328,7 @@ " Project(activities=[62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123], release_date=0)]" ] }, - "execution_count": 68, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -336,6 +336,227 @@ "source": [ "instance.projects[:2]" ] + }, + { + "cell_type": "markdown", + "id": "0d50356a-172d-4086-95e0-afd838ea7c56", + "metadata": {}, + "source": [ + "## RCPSP-PS and ASLIB format\n", + "In the RCPSP with flexible project structures (RCPSP-PS), there are choices about which activities to schedule.\n", + "\n", + "1. Activities can be optional - they don't all need to be scheduled\n", + "2. Activities each have a set of associated selection groups\n", + "3. If an activity is scheduled, then for each selection group, exactly one activity must also be scheduled" + ] + }, + { + "cell_type": "markdown", + "id": "ba6e61af-bd49-4871-b932-a78e23d24e0b", + "metadata": {}, + "source": [ + "Our library support two common RCPSP-PS formats." + ] + }, + { + "cell_type": "markdown", + "id": "a915ad69-962b-43f4-bc99-002739bb7eb8", + "metadata": {}, + "source": [ + "### RCPSP-PS" + ] + }, + { + "cell_type": "markdown", + "id": "68832a0e-eca0-407e-ad3b-dad79bc72cc4", + "metadata": {}, + "source": [ + "The \"RCPSP-PS\" format is the format used by [Van der Beek et al. (2024)](https://www.sciencedirect.com/science/article/pii/S0377221724008269)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c7b74459-6107-4e88-a729-7f0cc7d7f76f", + "metadata": {}, + "outputs": [], + "source": [ + "instance = parse(\"data/rcpsp_ps.txt\", instance_format=\"rcpsp_ps\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "bebe5e60-c84a-45c4-a9ef-9d7f34eed561", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(4, 136)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(instance.num_resources, instance.num_activities)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "990c6936-cbf2-47c4-823d-35fbdc2c240f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[1, 2], delays=None, optional=False, selection_groups=[[1, 2]], name='')" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "activity = instance.activities[0] # source\n", + "activity" + ] + }, + { + "cell_type": "markdown", + "id": "529e7add-9c25-4a41-b44b-4fda01f2f5d0", + "metadata": {}, + "source": [ + "The source activity must always be present (`optional=False`).\n", + "It has one selection group consisting of activities 1 and 2.\n", + "This means that either activity 1 or activity 2 must be scheduled." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1cb5b8c1-a6f3-4297-a327-fb2b4f05a927", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0])], successors=[3, 4], delays=None, optional=True, selection_groups=[[3], [4]], name='')" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "activity = instance.activities[1]\n", + "activity" + ] + }, + { + "cell_type": "markdown", + "id": "a955cdd4-fb27-491f-8ebd-d26737c9c9d7", + "metadata": {}, + "source": [ + "This activity has two selection groups, one with activity 3, and the other with activity 4.\n", + "If this activity is scheduled, then both activity 3 and activity must be scheduled." + ] + }, + { + "cell_type": "markdown", + "id": "01d7ae4f-58c3-445b-899c-2919a0fd3a79", + "metadata": {}, + "source": [ + "### ASLIB\n", + "The \"ASLIB\" format is the format used for RCPSP with alternative subgraph (RCPSP-AS) instances at https://www.projectmanagement.ugent.be/research/project_scheduling/rcpspas.\n", + "\n", + "Like the RCPSP-PS, the RCPSP-AS also has optional tasks and selection groups, but they use slightly different concepts (branches and subgraphs). \n", + "\n", + "**Important**: RCPSP-AS instances from the ASLIB instance set contain multiple file parts (a, b, c).\n", + "Our library parses _merged part (a) and (b)_ files, meaning that you have to manually concatenate files first.\n", + "See the instance file for details." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "e9a53c91-5102-4a7a-8768-24fa645370c2", + "metadata": {}, + "outputs": [], + "source": [ + "instance = parse(\"data/aslib0_0.rcp\", instance_format=\"aslib\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "26e640fe-bac2-41dc-8761-f645d144dc74", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(5, 122)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(instance.num_resources, instance.num_activities)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "21d245eb-c3b4-4717-9597-f1850e3f9689", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0, 0])], successors=[1, 13, 25, 37, 49], delays=None, optional=False, selection_groups=[[1, 13, 25, 37, 49]], name='')" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "activity = instance.activities[0] # source\n", + "activity" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "889d69b0-b492-4423-9d9d-64bac9875141", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Activity(modes=[Mode(duration=0, demands=[0, 0, 0, 0, 0])], successors=[2, 3, 4, 5, 6, 7], delays=None, optional=True, selection_groups=[[2], [3], [4], [5], [6], [7]], name='')" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "activity = instance.activities[1]\n", + "activity" + ] } ], "metadata": { diff --git a/formats/README.md b/formats/README.md new file mode 100644 index 0000000..2ea616b --- /dev/null +++ b/formats/README.md @@ -0,0 +1,3 @@ +# Instance formats + +This folder contains instructions for each instance format. diff --git a/formats/aslib.md b/formats/aslib.md new file mode 100644 index 0000000..90ee97a --- /dev/null +++ b/formats/aslib.md @@ -0,0 +1,5 @@ +# ASLIB + +ASLIB files contain two parts (A) and (B). +See https://www.projectmanagement.ugent.be/research/project_scheduling/rcpspas for details. +The `psplib` library expects a single file which includes the merged parts of (A) and (B). diff --git a/formats/rcpsp_ps.md b/formats/rcpsp_ps.md new file mode 100644 index 0000000..900ce8c --- /dev/null +++ b/formats/rcpsp_ps.md @@ -0,0 +1,57 @@ +Note: taken from https://data.4tu.nl/articles/dataset/Instances_and_file_format_for_the_Resource_Constrained_Project_Scheduling_Problem_with_a_flexible_Project_Structure/21106768 + +---- + +The input file format is as following (# of means number of) +Counting starts at 0 + + [# of activities] [# of renewable resources] [#number of nonrewable resources] + [resource availabilities] + + [duration] [resource requirements per resource type] + [# of selection-groups] [[# of or selection successors] [selection group successor1] [selection group successor2]...] (repeat) + [# of time precedence successors] [successor1] [successor2] ... + + + + +To give a small example: + + 5 1 0 + 10 + + 0 0 + 2 2 1 2 1 3 + 3 1 2 3 + + 2 5 + 1 1 4 + 1 4 + + 2 6 + 1 1 4 + 1 4 + + 3 10 + 1 1 4 + 1 4 + + 4 0 + 0 + 0 + +This means: + + 5 1 0 + 10 6 8 2 + 1.05 1.06 + +In total 5 activities, with 1 renewable resource (with availability 10), and 0 nonrenewable resources + + 0 0 + 2 2 1 2 1 4 + 3 1 2 3 + +First line: Activity 0 with a duration of 0 and no resource requirements +Second line: Two selection-groups. The first one has two successor nodes +Third line: Three time successors: Activities 1 2 and 3 diff --git a/psplib/ProjectInstance.py b/psplib/ProjectInstance.py index 5e8abf3..d483298 100644 --- a/psplib/ProjectInstance.py +++ b/psplib/ProjectInstance.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional @@ -52,6 +52,12 @@ class Activity: 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)``. + optional + Whether this activity is optional or not. Default ``False``. + selection_groups + The selection groups of this activity. If the current activity is + scheduled, then for each group, exactly one activity must be scheduled. + This is used for RCPSP-PS instances. Default is an empty list. name Optional name of the activity to identify this activity. This is helpful to map this activity back to the original problem instance. @@ -60,6 +66,8 @@ class Activity: modes: list[Mode] successors: list[int] delays: Optional[list[int]] = None + optional: bool = False + selection_groups: list[list[int]] = field(default_factory=list) name: str = "" def __post_init__(self): diff --git a/psplib/__init__.py b/psplib/__init__.py index 4af003b..7193308 100644 --- a/psplib/__init__.py +++ b/psplib/__init__.py @@ -1,6 +1,8 @@ from .parse import parse as parse +from .parse_aslib import parse_aslib as parse_aslib 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 .parse_rcpsp_ps import parse_rcpsp_ps as parse_rcpsp_ps from .ProjectInstance import ProjectInstance as ProjectInstance diff --git a/psplib/parse.py b/psplib/parse.py index 9e072c1..dd1bb59 100644 --- a/psplib/parse.py +++ b/psplib/parse.py @@ -1,10 +1,12 @@ from pathlib import Path from typing import Union +from .parse_aslib import parse_aslib 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 .parse_rcpsp_ps import parse_rcpsp_ps from .ProjectInstance import ProjectInstance @@ -35,5 +37,9 @@ def parse( return parse_rcpsp_max(loc) elif instance_format == "mplib": return parse_mplib(loc) + elif instance_format == "rcpsp_ps": + return parse_rcpsp_ps(loc) + elif instance_format == "aslib": + return parse_aslib(loc) raise ValueError(f"Unknown instance format: {instance_format}") diff --git a/psplib/parse_aslib.py b/psplib/parse_aslib.py new file mode 100644 index 0000000..73f0ada --- /dev/null +++ b/psplib/parse_aslib.py @@ -0,0 +1,224 @@ +from collections import defaultdict, deque +from dataclasses import dataclass +from itertools import chain +from pathlib import Path +from typing import Union + +from .ProjectInstance import ( + Activity, + Mode, + Project, + ProjectInstance, + Resource, +) + + +@dataclass +class AlternativeSubgraph: + """ + Represents a single alternative subgraph in an ASLIB instance. + + Parameters + ---------- + branches + A list of branches in the subgraph. Each branch is a list of activity + indices that are part of the branch. + """ + + branches: list[list[int]] + + +def _parse_part_a(lines): + """ + Part (a) of ASLIB instance is formatted as Patterson instance. + """ + num_activities, num_resources = map(int, next(lines).split()) + + # Instances without resources do not have an availability line. + capacities = list(map(int, next(lines).split())) if num_resources else [] + resources = [Resource(capacity=cap, renewable=True) for cap in capacities] + + activities = [] + for _ in range(num_activities): + values = map(int, next(lines).split()) + duration = int(next(values)) + demands = [int(next(values)) for _ in range(num_resources)] + num_successors = int(next(values)) + successors = [int(next(values)) - 1 for _ in range(num_successors)] + activities.append(Activity([Mode(duration, demands)], successors)) + + return resources, activities + + +def _parse_part_b(lines) -> list[AlternativeSubgraph]: + """ + Part (b) of ASLIB instance results in alternative subgraphs. + """ + pct_flex, pct_nested, pct_linked = map(float, next(lines).split()) + num_subgraphs = int(next(lines)) + total_branches = 1 # first branch is always the dummy branch + subgraphs = [] + + for _ in range(num_subgraphs): + num_branches, *branch_idcs = map(int, next(lines).split()) + total_branches += num_branches + branch_idcs = [idx - 1 for idx in branch_idcs] + subgraphs.append(branch_idcs) + + branches: list[list[int]] = [[] for _ in range(total_branches)] + for activity, line in enumerate(lines): + num_braches, *branch_idcs = map(int, line.split()) + for idx in branch_idcs: + branches[idx - 1].append(activity) + + # Return the alternative subgraphs, with the first subgraph containing the + # fixed activities (an activity belongs to node branch 0 if it is fixed). + result = [AlternativeSubgraph([branches[0]])] + result += [ + AlternativeSubgraph([branches[idx] for idx in branch_idcs]) + for branch_idcs in subgraphs + ] + + return result + + +def parse_aslib(loc: Union[str, Path]) -> ProjectInstance: + """ + Parses an ASLIB-formatted instance from a file. This format is used for + RCPSP instances with alternative subgraphs. + + Note + ---- + This function parses files that combine both "a" and "b" part files from + the ASLIB instance. You have to manually create such instances first! + + 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()) + + resources, activities = _parse_part_a(lines) + subgraphs = _parse_part_b(lines) + + # With the already parsed activities and alternative subgraph data, + # we add optional and selections groups data to the activities. + activities = _make_optional_activities(activities, subgraphs) + + project = Project(list(range(len(activities)))) + return ProjectInstance(resources, activities, [project]) + + +class DiGraph: + """ + Simple directed graph implementation to replace networkx.DiGraph. + """ + + def __init__(self): + self.adj: dict[int, list[int]] = defaultdict(list) + self.nodes = set() + + def add_node(self, node: int): + self.nodes.add(node) + + def add_edge(self, u: int, v: int): + self.adj[u].append(v) + self.nodes.add(u) + self.nodes.add(v) + + def topological_sort(self) -> list[int]: + """ + Returns a topological ordering of the graph's nodes. + """ + in_degree = {node: 0 for node in self.nodes} + for u in self.adj: + for v in self.adj[u]: + in_degree[v] += 1 + + queue = deque(node for node in self.nodes if in_degree[node] == 0) + order = [] + while queue: + node = queue.popleft() + order.append(node) + for neighbor in self.adj[node]: + in_degree[neighbor] -= 1 + if in_degree[neighbor] == 0: + queue.append(neighbor) + + return order + + @classmethod + def from_activities(cls, activities: list[Activity]): + G = cls() + for idx, activity in enumerate(activities): + G.add_node(idx) + for succ in activity.successors: + G.add_edge(idx, succ) + + return G + + +def _make_optional_activities( + activities: list[Activity], + subgraphs: list[AlternativeSubgraph], +) -> list[Activity]: + """ + Adds optional and selection group data to activities based on alternative + subgraph data. Because the activity graphs are directed acylcic, we can + transform subgraphs into selection groups. + """ + G = DiGraph.from_activities(activities) + order = G.topological_sort() + + is_fixed = subgraphs[0].branches[0] # first subgraph contains fixed nodes + alternatives = subgraphs[1:] + all_branching = [] # idcs of branching activities + groups = defaultdict(list) + + for subgraph in alternatives: + # The branching activities are the lowest-indexed activities in each + # branch. This works because we have a directed acyclic graph. + branching = [min(b, key=order.index) for b in subgraph.branches] + arcs = [(u, v) for u in G.adj for v in G.adj[u] if v in branching] + + # The principal activity is the sole activity that goes to the + # branching activities. + nodes = {u for (u, _) in arcs} + assert len(nodes) == 1 # should be only 1 principal activity + principal = nodes.pop() + + all_branching.extend(branching) + groups[principal].append(branching) + + # For all remaining edges, we add another unit selection group if + # v is not a branching activity. Edges with v as a branching activity + # are already covered by the groups above. + for u in G.adj: + for v in G.adj[u]: + if v not in all_branching: + groups[u].append([v]) + + # Create new activities with optional and selection group data. + new = [] + for idx, activity in enumerate(activities): + activity = Activity( + modes=activity.modes, + successors=activity.successors, + optional=idx not in is_fixed, + selection_groups=groups[idx], + ) + new.append(activity) + + for activity in new: + # Check: In RCPSP-AS, timing successors are also selection successors. + select_succ = list(chain(*activity.selection_groups)) + assert sorted(select_succ) == sorted(activity.successors) + + return new diff --git a/psplib/parse_rcpsp_ps.py b/psplib/parse_rcpsp_ps.py new file mode 100644 index 0000000..fd276c6 --- /dev/null +++ b/psplib/parse_rcpsp_ps.py @@ -0,0 +1,44 @@ +from pathlib import Path +from typing import Union + +from .ProjectInstance import Activity, Mode, Project, ProjectInstance, Resource + + +def parse_rcpsp_ps(instance_loc: Union[str, Path]) -> ProjectInstance: + """ + Parses a RCPSP-PS formatted instance from Van der Beek et al. (2024). + """ + with open(instance_loc, "r") as fh: + lines = iter(line.strip() for line in fh.readlines() if line.strip()) + + num_activities, num_renewable, _ = map(int, next(lines).split()) + capacities = list(map(int, next(lines).split())) + + resources = [ + Resource(capacity, idx < num_renewable) # resources are ordered + for idx, capacity in enumerate(capacities) + ] + activities = [] + + for idx in range(num_activities): + duration, *demands = map(int, next(lines).split()) + line = map(int, next(lines).split()) + + groups = [] + num_groups = next(line) + for _ in range(num_groups): + num_successors = next(line) + groups.append([next(line) for _ in range(num_successors)]) + + num_successors, *successors = map(int, next(lines).split()) + activities.append( + Activity( + [Mode(duration, demands)], + successors, + optional=idx > 0, # source activity is not optional + selection_groups=groups, + ) + ) + + project = Project(list(range(num_activities))) + return ProjectInstance(resources, activities, [project]) diff --git a/tests/data/aslib0_0.rcp b/tests/data/aslib0_0.rcp new file mode 100644 index 0000000..d90416f --- /dev/null +++ b/tests/data/aslib0_0.rcp @@ -0,0 +1,251 @@ +122 5 +10 10 10 10 10 + +0 0 0 0 0 0 5 2 14 26 38 50 +0 0 0 0 0 0 6 3 4 5 6 7 8 +1 0 0 1 0 0 2 10 9 +1 0 0 5 0 0 2 10 9 +7 0 0 4 0 0 2 10 9 +1 0 0 4 0 0 2 10 9 +5 0 0 3 0 0 2 10 9 +6 0 0 1 0 0 1 9 +3 0 0 1 0 0 2 12 11 +8 0 0 2 0 0 2 12 11 +3 0 0 2 0 0 1 13 +9 0 0 2 0 0 1 13 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 6 15 16 18 19 20 24 +6 0 0 0 0 1 2 23 22 +5 0 0 0 0 3 1 17 +9 0 0 0 0 2 1 21 +5 0 0 0 0 2 1 21 +2 0 0 0 0 5 1 21 +9 0 0 0 0 2 1 21 +1 0 0 0 0 2 1 25 +4 0 0 0 0 2 1 25 +1 0 0 0 0 4 1 25 +1 0 0 0 0 2 1 25 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 6 27 28 29 30 32 36 +10 0 0 0 1 0 3 35 34 33 +2 0 0 0 2 0 2 34 31 +8 0 0 0 2 0 2 34 33 +1 0 0 0 1 0 1 31 +1 0 0 0 4 0 1 33 +3 0 0 0 5 0 1 33 +4 0 0 0 4 0 1 37 +2 0 0 0 1 0 1 37 +8 0 0 0 3 0 1 37 +9 0 0 0 2 0 1 37 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 3 39 40 43 +4 0 0 0 1 0 3 48 42 41 +8 0 0 0 3 0 3 47 44 41 +5 0 0 0 3 0 2 46 45 +5 0 0 0 3 0 2 47 46 +10 0 0 0 1 0 1 44 +6 0 0 0 2 0 1 45 +2 0 0 0 4 0 1 49 +2 0 0 0 2 0 1 49 +3 0 0 0 2 0 1 49 +10 0 0 0 4 0 1 49 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 4 51 52 53 56 +5 1 0 0 0 0 4 60 59 58 54 +10 2 0 0 0 0 4 59 58 55 54 +2 2 0 0 0 0 3 60 59 57 +9 4 0 0 0 0 1 57 +2 2 0 0 0 0 1 57 +6 1 0 0 0 0 1 58 +1 3 0 0 0 0 1 61 +4 1 0 0 0 0 1 61 +7 4 0 0 0 0 1 61 +10 5 0 0 0 0 1 61 +0 0 0 0 0 0 1 62 +0 0 0 0 0 0 2 63 75 +0 0 0 0 0 0 3 64 65 69 +5 0 0 1 0 0 3 68 67 66 +6 0 0 2 0 0 3 68 67 66 +5 0 0 2 0 0 4 73 72 71 70 +3 0 0 3 0 0 2 72 70 +9 0 0 5 0 0 2 71 70 +1 0 0 3 0 0 2 71 70 +4 0 0 4 0 0 1 74 +5 0 0 2 0 0 1 74 +3 0 0 1 0 0 1 74 +10 0 0 2 0 0 1 74 +0 0 0 0 0 0 1 87 +0 0 0 0 0 0 6 76 78 80 83 84 85 +6 0 0 0 0 1 1 77 +3 0 0 0 0 3 2 82 81 +7 0 0 0 0 2 1 79 +9 0 0 0 0 1 1 81 +6 0 0 0 0 5 1 81 +4 0 0 0 0 1 1 86 +10 0 0 0 0 3 1 86 +7 0 0 0 0 1 1 86 +2 0 0 0 0 3 1 86 +8 0 0 0 0 5 1 86 +0 0 0 0 0 0 1 87 +0 0 0 0 0 0 5 88 89 90 91 92 +1 0 0 0 1 0 4 97 96 95 93 +10 0 0 0 3 0 3 97 95 94 +1 0 0 0 3 0 3 96 95 94 +4 0 0 0 3 0 2 96 93 +2 0 0 0 3 0 2 95 93 +10 0 0 0 1 0 1 94 +3 0 0 0 4 0 1 98 +10 0 0 0 2 0 1 98 +7 0 0 0 3 0 1 98 +4 0 0 0 2 0 1 98 +0 0 0 0 0 0 1 99 +0 0 0 0 0 0 7 100 102 103 106 107 108 109 +3 0 0 0 1 0 1 101 +7 0 0 0 2 0 2 105 104 +9 0 0 0 4 0 1 104 +8 0 0 0 2 0 1 104 +8 0 0 0 2 0 1 110 +1 0 0 0 3 0 1 110 +7 0 0 0 3 0 1 110 +6 0 0 0 3 0 1 110 +10 0 0 0 2 0 1 110 +2 0 0 0 3 0 1 110 +0 0 0 0 0 0 1 111 +0 0 0 0 0 0 3 112 113 114 +10 0 0 0 0 1 4 121 120 118 115 +3 0 0 0 0 3 3 120 119 115 +6 0 0 0 0 3 3 119 117 116 +9 0 0 0 0 2 1 117 +5 0 0 0 0 2 1 118 +8 0 0 0 0 3 1 122 +8 0 0 0 0 3 1 122 +7 0 0 0 0 3 1 122 +8 0 0 0 0 3 1 122 +2 0 0 0 0 2 1 122 +0 0 0 0 0 0 0 +0.250000 0.000000 0.000000 +2 +5 2 3 4 5 6 +2 7 8 +1 1 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 2 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 3 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 4 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 5 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 6 +1 1 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 7 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 8 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 +1 1 diff --git a/tests/data/rcpsp_ps.txt b/tests/data/rcpsp_ps.txt new file mode 100644 index 0000000..dede5c8 --- /dev/null +++ b/tests/data/rcpsp_ps.txt @@ -0,0 +1,546 @@ +136 4 0 +10 10 10 10 + +0 0 0 0 0 +1 2 1 2 +2 1 2 + +0 0 0 0 0 +2 1 3 1 4 +2 3 4 + +0 0 0 0 0 +1 2 11 12 +2 11 12 + +9 0 0 1 1 +1 1 5 +1 5 + +8 9 9 9 0 +1 1 43 +1 43 + +3 1 1 5 9 +1 3 21 22 6 +3 21 22 6 + +0 0 0 0 0 +2 1 7 1 8 +2 7 8 + +9 0 0 1 1 +1 1 9 +1 9 + +8 9 9 9 0 +1 1 10 +1 10 + +3 1 1 5 9 +1 1 10 +1 10 + +0 0 0 0 0 +1 1 32 +1 32 + +0 0 0 0 0 +2 1 13 1 14 +2 13 14 + +0 0 0 0 0 +2 1 17 1 18 +2 17 18 + +6 1 0 1 1 +1 1 15 +1 15 + +1 9 5 9 8 +1 1 15 +1 15 + +1 5 0 0 6 +1 1 16 +1 16 + +0 0 0 0 0 +1 1 23 +1 23 + +9 0 0 1 1 +1 1 19 +1 19 + +8 9 9 9 0 +1 1 20 +1 20 + +3 1 1 5 9 +1 1 20 +1 20 + +0 0 0 0 0 +1 1 23 +1 23 + +0 0 0 0 0 +2 1 24 1 25 +2 24 25 + +0 0 0 0 0 +1 1 28 +1 28 + +0 0 0 0 0 +1 1 33 +1 33 + +9 0 0 1 1 +1 1 26 +1 26 + +8 9 9 9 0 +1 1 27 +1 27 + +3 1 1 5 9 +1 1 27 +1 27 + +0 0 0 0 0 +1 1 32 +1 32 + +2 1 0 5 1 +2 1 29 1 30 +2 29 30 + +2 7 1 0 8 +1 1 31 +1 31 + +7 7 9 0 6 +1 1 31 +1 31 + +0 0 0 0 0 +1 1 32 +1 32 + +0 0 0 0 0 +1 1 43 +1 43 + +2 1 0 5 1 +2 1 34 1 35 +2 34 35 + +2 7 1 0 8 +1 3 36 37 38 +3 36 37 38 + +7 7 9 0 6 +1 1 69 +1 69 + +0 0 0 0 0 +1 1 39 +1 39 + +0 0 0 0 0 +2 1 44 1 45 +2 44 45 + +0 0 0 0 0 +2 1 47 1 48 +2 47 48 + +2 1 0 5 1 +2 1 40 1 41 +2 40 41 + +2 7 1 0 8 +1 1 42 +1 42 + +7 7 9 0 6 +1 1 42 +1 42 + +0 0 0 0 0 +1 1 68 +1 68 + +0 0 0 0 0 +1 3 51 52 53 +3 51 52 53 + +9 0 0 1 1 +1 1 46 +1 46 + +8 9 9 9 0 +1 1 66 +1 66 + +3 1 1 5 9 +1 1 66 +1 66 + +6 1 0 1 1 +1 1 49 +1 49 + +1 9 5 9 8 +1 1 49 +1 49 + +1 5 0 0 6 +1 1 50 +1 50 + +0 0 0 0 0 +1 1 68 +1 68 + +0 0 0 0 0 +2 1 54 1 55 +2 54 55 + +0 0 0 0 0 +1 1 58 +1 58 + +0 0 0 0 0 +2 1 62 1 63 +2 62 63 + +9 0 0 1 1 +1 1 56 +1 56 + +8 9 9 9 0 +1 1 57 +1 57 + +3 1 1 5 9 +1 1 57 +1 57 + +0 0 0 0 0 +1 1 67 +1 67 + +2 1 0 5 1 +2 1 59 1 60 +2 59 60 + +2 7 1 0 8 +1 1 61 +1 61 + +7 7 9 0 6 +1 1 61 +1 61 + +0 0 0 0 0 +1 1 67 +1 67 + +6 1 0 1 1 +1 1 64 +1 64 + +1 9 5 9 8 +1 1 64 +1 64 + +1 5 0 0 6 +1 1 65 +1 65 + +0 0 0 0 0 +1 1 67 +1 67 + +0 0 0 0 0 +1 1 68 +1 68 + +0 0 0 0 0 +1 1 70 +1 70 + +0 0 0 0 0 +1 1 69 +1 69 + +0 0 0 0 0 +1 1 70 +1 70 + +0 0 0 0 0 +1 2 72 71 +2 72 71 + +0 0 0 0 0 +1 2 73 93 +2 73 93 + +0 0 0 0 0 +1 3 80 78 79 +3 78 79 80 + +0 0 0 0 0 +1 1 74 +1 74 + +2 1 0 5 1 +2 1 75 1 76 +2 75 76 + +2 7 1 0 8 +1 1 77 +1 77 + +7 7 9 0 6 +1 1 77 +1 77 + +0 0 0 0 0 +1 1 99 +1 99 + +0 0 0 0 0 +2 1 81 1 82 +2 81 82 + +0 0 0 0 0 +2 1 85 1 86 +2 85 86 + +0 0 0 0 0 +2 1 89 1 90 +2 89 90 + +6 1 0 1 1 +1 1 83 +1 83 + +1 9 5 9 8 +1 1 83 +1 83 + +1 5 0 0 6 +1 1 84 +1 84 + +0 0 0 0 0 +1 1 94 +1 94 + +6 1 0 1 1 +1 1 87 +1 87 + +1 9 5 9 8 +1 1 87 +1 87 + +1 5 0 0 6 +1 1 88 +1 88 + +0 0 0 0 0 +1 1 94 +1 94 + +9 0 0 1 1 +1 1 91 +1 91 + +8 9 9 9 0 +1 1 92 +1 92 + +3 1 1 5 9 +1 1 92 +1 92 + +0 0 0 0 0 +1 1 94 +1 94 + +0 0 0 0 0 +2 1 95 1 96 +2 95 96 + +0 0 0 0 0 +2 1 100 1 101 +2 100 101 + +6 1 0 1 1 +1 1 97 +1 97 + +1 9 5 9 8 +1 1 97 +1 97 + +1 5 0 0 6 +1 1 98 +1 98 + +0 0 0 0 0 +1 1 99 +1 99 + +0 0 0 0 0 +1 1 104 +1 104 + +6 1 0 1 1 +1 1 102 +1 102 + +1 9 5 9 8 +1 1 102 +1 102 + +1 5 0 0 6 +1 1 103 +1 103 + +0 0 0 0 0 +1 2 105 108 +2 108 105 + +2 1 0 5 1 +1 3 106 107 114 +3 106 107 114 + +0 0 0 0 0 +1 1 113 +1 113 + +0 0 0 0 0 +1 1 116 +1 116 + +0 0 0 0 0 +1 1 120 +1 120 + +0 0 0 0 0 +1 1 109 +1 109 + +2 1 0 5 1 +2 1 110 1 111 +2 110 111 + +2 7 1 0 8 +1 1 112 +1 112 + +7 7 9 0 6 +1 1 112 +1 112 + +0 0 0 0 0 +1 1 132 +1 132 + +2 1 0 5 1 +2 1 128 1 115 +2 128 115 + +0 0 0 0 0 +1 1 124 +1 124 + +7 7 9 0 6 +1 1 130 +1 130 + +2 1 0 5 1 +2 1 117 1 118 +2 117 118 + +2 7 1 0 8 +1 1 119 +1 119 + +7 7 9 0 6 +1 1 119 +1 119 + +0 0 0 0 0 +1 1 129 +1 129 + +2 1 0 5 1 +2 1 121 1 122 +2 121 122 + +2 7 1 0 8 +1 1 123 +1 123 + +7 7 9 0 6 +1 1 123 +1 123 + +0 0 0 0 0 +1 1 129 +1 129 + +2 1 0 5 1 +2 1 125 1 126 +2 125 126 + +2 7 1 0 8 +1 1 127 +1 127 + +7 7 9 0 6 +1 1 127 +1 127 + +0 0 0 0 0 +1 1 129 +1 129 + +2 7 1 0 8 +1 1 130 +1 130 + +0 0 0 0 0 +2 1 131 1 133 +2 131 133 + +0 0 0 0 0 +1 1 132 +1 132 + +2 7 1 0 8 +1 1 134 +1 134 + +0 0 0 0 0 +1 1 135 +1 135 + +7 7 9 0 6 +1 1 134 +1 134 + +0 0 0 0 0 +1 1 135 +1 135 + +0 0 0 0 0 +0 +0 diff --git a/tests/test_parse.py b/tests/test_parse.py index 7fbd475..00c2fdd 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -11,6 +11,7 @@ ("data/RG300_1.rcp", "patterson"), ("data/MPLIB1_Set1_0.rcmp", "mplib"), ("data/UBO10_01.sch", "rcpsp_max"), + ("data/rcpsp_ps.txt", "rcpsp_ps"), ], ) def test_parse(loc, instance_format): diff --git a/tests/test_parse_aslib.py b/tests/test_parse_aslib.py new file mode 100644 index 0000000..409806f --- /dev/null +++ b/tests/test_parse_aslib.py @@ -0,0 +1,42 @@ +from numpy.testing import assert_equal + +from psplib import parse_aslib + +from .utils import relative + + +def test_aslib0_0(): + """ + Tests that the instance ``aslib0_0.rcp`` is correctly parsed. + """ + instance = parse_aslib(relative("data/aslib0_0.rcp")) + assert_equal(instance.num_resources, 5) + + capacities = [res.capacity for res in instance.resources] + renewables = [res.renewable for res in instance.resources] + + assert_equal(capacities, [10, 10, 10, 10, 10]) + assert_equal(renewables, [True, True, True, True, True]) + + assert_equal(instance.num_activities, 122) + + activity = instance.activities[0] # source + assert_equal(activity.successors, [1, 13, 25, 37, 49]) + assert_equal(activity.optional, False) # source always present + assert_equal(activity.selection_groups, [[1, 13, 25, 37, 49]]) + + assert_equal(activity.num_modes, 1) + assert_equal(activity.modes[0].demands, [0, 0, 0, 0, 0]) + assert_equal(activity.modes[0].duration, 0) + + activity = instance.activities[2] + assert_equal(activity.successors, [9, 8]) + assert_equal(activity.optional, True) + assert_equal(activity.selection_groups, [[9], [8]]) + + assert_equal(activity.num_modes, 1) + assert_equal(activity.modes[0].demands, [0, 0, 1, 0, 0]) + assert_equal(activity.modes[0].duration, 1) + + assert_equal(instance.num_projects, 1) + assert_equal(instance.projects[0].num_activities, 122) diff --git a/tests/test_parse_mplib.py b/tests/test_parse_mplib.py index 3fc9738..1ce5998 100644 --- a/tests/test_parse_mplib.py +++ b/tests/test_parse_mplib.py @@ -18,11 +18,6 @@ def test_mplib_set1(): assert_equal(capacities, [56, 56, 56, 56]) assert_equal(renewables, [True, True, True, True]) - assert_equal(instance.num_projects, 6) - for project in instance.projects: - assert_equal(project.num_activities, 62) - assert_equal(project.release_date, 0) - assert_equal(instance.num_activities, 6 * 62) activity = instance.activities[0] @@ -35,6 +30,11 @@ def test_mplib_set1(): assert_equal(activity.modes[0].demands, [0, 0, 0, 0]) assert_equal(activity.modes[0].duration, 0) + assert_equal(instance.num_projects, 6) + for project in instance.projects: + assert_equal(project.num_activities, 62) + assert_equal(project.release_date, 0) + def test_mplib_set2(): """ @@ -49,11 +49,6 @@ def test_mplib_set2(): assert_equal(capacities, [48, 48, 46, 50, 48]) assert_equal(renewables, [True, True, True, True, True]) - assert_equal(instance.num_projects, 10) - for project in instance.projects: - assert_equal(project.num_activities, 52) - assert_equal(project.release_date, 0) - assert_equal(instance.num_activities, 10 * 52) activity = instance.activities[-51] # second activity of last project @@ -66,3 +61,8 @@ def test_mplib_set2(): assert_equal(activity.num_modes, 1) assert_equal(activity.modes[0].demands, [8, 4, 3, 5, 1]) assert_equal(activity.modes[0].duration, 7) + + assert_equal(instance.num_projects, 10) + for project in instance.projects: + assert_equal(project.num_activities, 52) + assert_equal(project.release_date, 0) diff --git a/tests/test_parse_patterson.py b/tests/test_parse_patterson.py index dc33c82..5fd06c0 100644 --- a/tests/test_parse_patterson.py +++ b/tests/test_parse_patterson.py @@ -18,8 +18,6 @@ def test_rg300(): assert_equal(capacities, [10, 10, 10, 10]) assert_equal(renewables, [True, True, True, True]) - assert_equal(instance.num_projects, 1) - assert_equal(instance.projects[0].num_activities, 302) assert_equal(instance.num_activities, 302) activity = instance.activities[1] # second activity @@ -36,3 +34,6 @@ def test_rg300(): assert_equal(activity.num_modes, 1) assert_equal(activity.modes[0].demands, [0, 1, 0, 0]) assert_equal(activity.modes[0].duration, 3) + + assert_equal(instance.num_projects, 1) + assert_equal(instance.projects[0].num_activities, 302) diff --git a/tests/test_parse_psplib.py b/tests/test_parse_psplib.py index 7d1b986..c0ee3e5 100644 --- a/tests/test_parse_psplib.py +++ b/tests/test_parse_psplib.py @@ -18,8 +18,6 @@ def test_instance_single_mode(): assert_equal(capacities, [12, 9, 37, 53]) assert_equal(renewables, [True, True, False, False]) - assert_equal(instance.num_projects, 1) - assert_equal(instance.projects[0].num_activities, 18) assert_equal(instance.num_activities, 18) activity = instance.activities[1] # second activity (jobnr. 2) @@ -31,6 +29,9 @@ def test_instance_single_mode(): assert_equal(activity.modes[0].duration, 2) assert_equal(activity.modes[0].demands, [0, 4, 8, 0]) + assert_equal(instance.num_projects, 1) + assert_equal(instance.projects[0].num_activities, 18) + def test_instance_mmlib(): """ @@ -46,9 +47,6 @@ def test_instance_mmlib(): assert_equal(capacities, [33, 33, 247, 248]) assert_equal(renewables, [True, True, False, False]) - assert_equal(instance.num_projects, 1) - assert_equal(instance.projects[0].num_activities, 52) - assert_equal(instance.num_activities, 52) activity = instance.activities[1] # second activity (jobnr. 2) @@ -63,3 +61,6 @@ def test_instance_mmlib(): assert_equal(activity.modes[1].demands, [5, 5, 2, 6]) assert_equal(activity.modes[2].duration, 4) assert_equal(activity.modes[2].demands, [4, 5, 2, 6]) + + assert_equal(instance.num_projects, 1) + assert_equal(instance.projects[0].num_activities, 52) diff --git a/tests/test_parse_rcpsp_max.py b/tests/test_parse_rcpsp_max.py index 41512e5..e7637f6 100644 --- a/tests/test_parse_rcpsp_max.py +++ b/tests/test_parse_rcpsp_max.py @@ -18,15 +18,15 @@ def test_ubo10(): 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) + + assert_equal(instance.num_projects, 1) + assert_equal(instance.projects[0].num_activities, 12) diff --git a/tests/test_parse_rcpsp_ps.py b/tests/test_parse_rcpsp_ps.py new file mode 100644 index 0000000..9def29c --- /dev/null +++ b/tests/test_parse_rcpsp_ps.py @@ -0,0 +1,42 @@ +from numpy.testing import assert_equal + +from psplib import parse_rcpsp_ps + +from .utils import relative + + +def test_rcpsp_ps(): + """ + Tests that the instance ``rcpsp_ps.txt`` is correctly parsed. + """ + instance = parse_rcpsp_ps(relative("data/rcpsp_ps.txt")) + + assert_equal(instance.num_resources, 4) + + capacities = [res.capacity for res in instance.resources] + renewables = [res.renewable for res in instance.resources] + + assert_equal(capacities, [10, 10, 10, 10]) + assert_equal(renewables, [True, True, True, True]) + + assert_equal(instance.num_activities, 136) + + activity = instance.activities[0] # first activity (source) + assert_equal(activity.successors, [1, 2]) + assert_equal(activity.optional, False) # source always present + assert_equal(activity.selection_groups, [[1, 2]]) + + assert_equal(activity.num_modes, 1) + assert_equal(activity.modes[0].demands, [0, 0, 0, 0]) + assert_equal(activity.modes[0].duration, 0) + + activity = instance.activities[3] # fourth activity + assert_equal(activity.successors, [5]) + assert_equal(activity.selection_groups, [[5]]) + + assert_equal(activity.num_modes, 1) + assert_equal(activity.modes[0].demands, [0, 0, 1, 1]) + assert_equal(activity.modes[0].duration, 9) + + assert_equal(instance.num_projects, 1) + assert_equal(instance.projects[0].num_activities, 136)