Python implementation of a heuristic solver for the RCPSP group project.
The repository now also includes an exact branch-and-bound solver for proving optimality
- Chai Yi Khuen (01469570)
- Chong Wei Choon (01485701)
- Lau Wei Bin (01469625)
- Win Lei Thawdar (01513787)
- Yeo Ben Shin (01479845)
scripts/solvers/rcpsp_solver.py: heuristic RCPSP solver for one instancescripts/solvers/eval_fast.py: fast batch evaluator for the heuristic solverscripts/solvers/optimal_rcpsp.py: exact branch-and-bound solver for one instancescripts/solvers/batch_optimal_updated.py: batch runner for the exact solver ondata/updatedscripts/solvers/compare_solvers.py: runs both solvers and writes a comparison CSV
scripts/psplib/download_psplib.py: downloads and extracts held-out PSPLIB archivesscripts/psplib/convert_psplib_sm.py: flattens extracted.smarchives into instance foldersscripts/psplib/eval_psplib.py: evaluates the solver on held-out PSPLIB instancesscripts/psplib/summarize_psplib.py: summarizes held-out PSPLIB results against references
helpers/parser.py: parses.SCH/.sminstance fileshelpers/solver.py: heuristic search and schedule constructionhelpers/exact_solver.py: exact branch-and-bound search enginehelpers/validator.py: schedule validation and output formattinghelpers/instance.py: RCPSP instance data modelhelpers/helper.py: low-level feasibility and scheduling helpers
data/updated/: current benchmark instances used by the main solver workflowsdata/updated/sm_j10/: updated J10 instances with 10 real jobs plus source/sinkdata/updated/sm_j20/: updated J20 instances with 20 real jobs plus source/sinkdata/old/: older benchmark copy kept for reference onlyheldout/psplib/: downloaded PSPLIB held-out data and generated evaluation artifactsheldout/psplib/downloads/: downloaded archivesheldout/psplib/raw/: extracted raw.smfilesheldout/psplib/reference/: PSPLIB reference files and boundsheldout/psplib/instances/: flattened instance folders generated from the raw archivesheldout/psplib/manifests/: manifest files produced during conversionheldout/psplib/results/: JSONL or other evaluation outputs for held-out PSPLIB runs
data_analysis/: notebooks for comparisons and experiment writeupsdata_analysis/comparsion.ipynb: quick notebook comparing two J20 runsdata_analysis/experiment_1.ipynb: heuristic vs. optimal solver analysis notebookdata_analysis/experiment_2.ipynb: tradeoff analysis notebook across dataset sizesresults/: generated CSV files, plots, and solver outputsproject_instructions/: project PDFs and submission guidelines
Run command:
python3 scripts/solvers/rcpsp_solver.py data/updated/sm_j10/PSP1.SCHExpected output (single line of comma-separated start times for real jobs):
0,0,2,5,7,8,13,15,20,22
Optional flags:
--validate: validate schedule before printing--show_makespan: print makespan to stderr--output PATH: save solver output to file--legacy_output: print onejob start_timeper line
Example with optional flags:
python3 scripts/solvers/rcpsp_solver.py data/updated/sm_j10/PSP1.SCH --validate --show_makespanExpected output pattern:
Makespan: 25
0,0,2,5,7,8,13,15,20,22
Run command (full evaluation set):
python3 scripts/solvers/eval_fast.py --fullOptional flags:
--time_limit FLOAT: per-instance heuristic runtime budget--seed INT: random seed--j10_only: evaluate onlydata/updated/sm_j10--j20_only: evaluate onlydata/updated/sm_j20
Example with optional flags:
python3 scripts/solvers/eval_fast.py --full --time_limit 2.0 --j10_onlyExpected output pattern:
sm_j10/PSP1.SCH VALID mk= 25 t=0.03s OK
sm_j10/PSP2.SCH VALID mk= 45 t=0.04s OK
...
METRIC: total_makespan=XXXXX valid=YYY unsolved=Z invalid=W infeasible=V total=T time=SS.Ss
Run command:
python3 scripts/solvers/optimal_rcpsp.py data/updated/sm_j10/PSP1.SCH --validateExpected output pattern:
Instance : PSP1.SCH
Status : OK
Makespan : 25
Jobs : 10
Nodes : 22657
Pruned : 35
Time(s) : 0.47XX
Schedule : 0,0,2,5,7,8,13,15,20,22
Optional flags:
--time_limit FLOAT: exact-search timeout per instance (default60.0seconds)--heuristic_time FLOAT: time used to build initial incumbent--progress_seconds FLOAT: print progress every N seconds--validate: validate schedule--save_dir PATH: save schedule files--strict_exit: non-zero exit code if any instance errors out
Example with optional flags:
python3 scripts/solvers/optimal_rcpsp.py data/updated/sm_j20/PSP10.SCH --validate --time_limit 60 --progress_seconds 1Expected output pattern:
[progress] instance=PSP10.SCH elapsed=1.0s nodes=61440 pruned=6483 best=53
...
Instance : PSP10.SCH
Status : TIMEOUT
Makespan : 53
Jobs : 20
Nodes : NNNNN
Pruned : MMMM
Time(s) : 60.0XXX
Schedule : 53,0,12,...
Note: the exact solver stops at 60 seconds by default to prevent computational overload.
Run command:
python3 scripts/solvers/batch_optimal_updated.py --validateOptional flags:
--root PATH: root folder to scan recursively (defaultdata/updated)--time_limit FLOAT: per-instance exact timeout (default60.0)--heuristic_time FLOAT: incumbent generation time--progress_seconds FLOAT: per-instance progress print interval--seed INT: base random seed--save_dir PATH: save per-instance schedules--csv PATH: summary CSV path--max_instances INT: run only first N instances (smoke test)
Example with optional flags:
python3 scripts/solvers/batch_optimal_updated.py --time_limit 60 --progress_seconds 2 --validate --csv results/optimal_updated_summary.csvExpected output pattern:
INSTANCE STATUS MK JOBS NODES PRUNED TIME(s)
------------ ------- -- ---- ------ ------ -------
PSP1.SCH OK 25 10 22657 35 0.4897
PSP10.SCH TIMEOUT 53 20 123456 12000 60.0123
...
Generate a comprehensive comparison CSV containing results from both solvers on the full dataset.
To evaluate solution quality, we compare our heuristic solver against the exact branch-and-bound solver on the J10 and J20 test sets.
- Heuristic solver timeout:
15sper instance - Optimal solver timeout:
60sper instance
The exact branch-and-bound solver has factorial worst-case behavior (often described as exploring job-order combinations with complexity on the order of O(n!)), so full exhaustive runs are computationally expensive. Because of runtime constraints, we did not extend the heuristic budget to 30s and instead used 15s for the heuristic and 60s for the optimal baseline.
For each instance, we record:
- heuristic makespan and runtime
- optimal makespan and runtime
- failure cases for both solvers
Failure cases are tracked in the CSV as *_failure_reason fields. In particular, for the optimal solver:
TIMEOUTmeans the solver consumed the full60sbudget without finishing proof of optimalityINFEASIBLEmeans no feasible schedule found- other non-empty values indicate invalid schedule or runtime error
Run command:
python3 scripts/solvers/compare_solvers.py --output results/solver_comparison.csvThis script processes all instances under data/updated with:
- Heuristic solver: 1.0 second time limit per instance
- Optimal solver: 60.0 second time limit per instance
Optional flags:
--root PATH: root folder containing instances (defaultdata/updated)--heuristic_time FLOAT: time limit for heuristic solver (default1.0)--optimal_time FLOAT: time limit for optimal solver (default60.0)--output PATH: output CSV path (defaultresults/solver_comparison.csv)--seed INT: random seed (default0)--progress: print progress to stderr
Example with custom time limits:
python3 scripts/solvers/compare_solvers.py \
--heuristic_time 1.0 \
--optimal_time 60.0 \
--output results/my_comparison.csv \
--progressOutput CSV format:
job_instance,num_jobs,heuristic_makespan,heuristic_time,heuristic_failure_reason,optimal_makespan,optimal_time,optimal_failure_reason
PSP1.SCH,10,25,1.0000,,25,0.4759,
PSP10.SCH,10,28,1.0001,,28,0.2002,
PSP100.SCH,10,39,1.0001,,39,0.2838,Columns:
job_instance: path to instance file (relative to root)num_jobs: number of real jobs (excluding source/sink)heuristic_makespan: makespan from heuristic solver, or blank if failedheuristic_time: wall-clock time for heuristic solverheuristic_failure_reason: empty if successful, otherwiseTIMEOUT,INFEASIBLE,INVALID_SCHEDULE, or error messageoptimal_makespan: makespan from exact solver, or blank if failedoptimal_time: wall-clock time for exact solveroptimal_failure_reason: empty if successful, otherwiseTIMEOUT,INFEASIBLE,INVALID_SCHEDULE, or error message
After generating the comparison CSV, open and run experiment_1.ipynb to reproduce Experiment 1 plots and summary analysis.
Suggested flow:
- Generate CSV:
- Open
experiment_1.ipynb. - Update the input CSV path in the notebook if needed (for example
results/my_comparison.csv). - Run all cells to produce quality comparison figures and aggregate metrics.
Analyse runtime versus solution quality by running the heuristic solver at timeouts
0.1s, 1s, 5s, and 10s, and comparing each to the 15s configuration.
The 15s configuration is used as the baseline in this experiment because it showed
strong performance in Experiment 1 (including outperforming the 60s optimal solver
under our evaluation setting).
In addition to local j10 and j20 datasets, this experiment includes held-out
PSPLIB groups j30, j60, j90, and j120 to evaluate larger problem sizes.
For each timeout configuration, we record makespan values for comparison.
Run eval_fast.py once per time budget:
python3 eval_fast.py --full --output --time_limit 0.1
python3 eval_fast.py --full --output --time_limit 1.0
python3 eval_fast.py --full --output --time_limit 5.0
python3 eval_fast.py --full --output --time_limit 10.0
python3 eval_fast.py --full --output --time_limit 15.0Expected outputs:
results/j10/0.1.csv,results/j10/1.0.csv, ...,results/j10/15.0.csvresults/j20/0.1.csv,results/j20/1.0.csv, ...,results/j20/15.0.csv
- Download and unpack the external PSPLIB single-mode archives plus their reference files using:
python3 scripts/psplib/download_psplib.py- Flatten the extracted
.smfiles into per-set folders and generate manifests:
python3 scripts/psplib/convert_psplib_sm.py --copyRun scripts/psplib/eval_psplib.py per dataset and budget (use --output to write CSV):
python3 scripts/psplib/eval_psplib.py --set j30 --time_limit 0.1 --output
python3 scripts/psplib/eval_psplib.py --set j30 --time_limit 1.0 --output
python3 scripts/psplib/eval_psplib.py --set j30 --time_limit 5.0 --output
python3 scripts/psplib/eval_psplib.py --set j30 --time_limit 10.0 --output
python3 scripts/psplib/eval_psplib.py --set j30 --time_limit 15.0 --output
python3 scripts/psplib/eval_psplib.py --set j60 --time_limit 0.1 --output
python3 scripts/psplib/eval_psplib.py --set j60 --time_limit 1.0 --output
python3 scripts/psplib/eval_psplib.py --set j60 --time_limit 5.0 --output
python3 scripts/psplib/eval_psplib.py --set j60 --time_limit 10.0 --output
python3 scripts/psplib/eval_psplib.py --set j60 --time_limit 15.0 --output
python3 scripts/psplib/eval_psplib.py --set j90 --time_limit 0.1 --output
python3 scripts/psplib/eval_psplib.py --set j90 --time_limit 1.0 --output
python3 scripts/psplib/eval_psplib.py --set j90 --time_limit 5.0 --output
python3 scripts/psplib/eval_psplib.py --set j90 --time_limit 10.0 --output
python3 scripts/psplib/eval_psplib.py --set j90 --time_limit 15.0 --output
python3 scripts/psplib/eval_psplib.py --set j120 --time_limit 0.1 --output
python3 scripts/psplib/eval_psplib.py --set j120 --time_limit 1.0 --output
python3 scripts/psplib/eval_psplib.py --set j120 --time_limit 5.0 --output
python3 scripts/psplib/eval_psplib.py --set j120 --time_limit 10.0 --output
python3 scripts/psplib/eval_psplib.py --set j120 --time_limit 15.0 --outputExpected outputs (examples):
results/j30/0.1.csv...results/j30/15.0.csvresults/j60/0.1.csv...results/j60/15.0.csvresults/j90/0.1.csv...results/j90/15.0.csvresults/j120/0.1.csv...results/j120/15.0.csv
Open and run all cells in experiment_2.ipynb.
Notebook pipeline:
- Import plotting/data libraries.
- Load all CSVs from
results/j10,results/j20,results/j30,results/j60,results/j90,results/j120. - Build merged comparison tables against each family's
15.0sbaseline. - Render charts (combined tradeoff line chart, heatmaps, scatter, average-gap-by-budget).
- Save generated figures.