|
2 | 2 | import gzip |
3 | 3 | import json |
4 | 4 | import os |
| 5 | +import re |
5 | 6 | import shutil |
6 | 7 | import statistics |
7 | 8 | import subprocess |
8 | 9 | import sys |
| 10 | +import time |
9 | 11 | from pathlib import Path |
10 | 12 |
|
11 | 13 | import requests |
@@ -229,17 +231,101 @@ def benchmark_solver(input_file, solver_name, timeout): |
229 | 231 | return metrics |
230 | 232 |
|
231 | 233 |
|
| 234 | +def get_highs_binary_version(): |
| 235 | + """Get the version of the HiGHS binary from the --version command""" |
| 236 | + highs_binary = "/opt/highs/bin/highs" |
| 237 | + |
| 238 | + try: |
| 239 | + result = subprocess.run( |
| 240 | + [highs_binary, "--version"], |
| 241 | + capture_output=True, |
| 242 | + text=True, |
| 243 | + check=True, |
| 244 | + encoding="utf-8", |
| 245 | + ) |
| 246 | + |
| 247 | + version_match = re.search(r"HiGHS version (\d+\.\d+\.\d+)", result.stdout) |
| 248 | + if version_match: |
| 249 | + return version_match.group(1) |
| 250 | + |
| 251 | + return "unknown" |
| 252 | + except Exception as e: |
| 253 | + print(f"Error getting HiGHS binary version: {str(e)}") |
| 254 | + return "unknown" |
| 255 | + |
| 256 | + |
| 257 | +def benchmark_highs_binary(): |
| 258 | + """ |
| 259 | + Run a reference benchmark using the pre-installed HiGHS binary |
| 260 | + """ |
| 261 | + reference_model = "/benchmark-test-model.lp" |
| 262 | + highs_binary = "/opt/highs/bin/highs" |
| 263 | + |
| 264 | + command = [ |
| 265 | + highs_binary, |
| 266 | + reference_model, |
| 267 | + ] |
| 268 | + |
| 269 | + # Run the command and capture the output |
| 270 | + start_time = time.time() |
| 271 | + result = subprocess.run( |
| 272 | + command, |
| 273 | + capture_output=True, |
| 274 | + text=True, |
| 275 | + check=False, |
| 276 | + encoding="utf-8", |
| 277 | + ) |
| 278 | + runtime = time.time() - start_time |
| 279 | + if result.returncode != 0: |
| 280 | + print( |
| 281 | + f"ERROR running reference benchmark. Captured output:\n{result.stdout}\n{result.stderr}" |
| 282 | + ) |
| 283 | + metrics = { |
| 284 | + "status": "ER", |
| 285 | + "condition": "Error", |
| 286 | + "objective": None, |
| 287 | + "runtime": runtime, |
| 288 | + "duality_gap": None, |
| 289 | + "max_integrality_violation": None, |
| 290 | + } |
| 291 | + else: |
| 292 | + # Parse HiGHS output to extract objective value |
| 293 | + objective = None |
| 294 | + for line in result.stdout.splitlines(): |
| 295 | + if "Objective value" in line: |
| 296 | + try: |
| 297 | + objective = float(line.split(":")[-1].strip()) |
| 298 | + except (ValueError, IndexError): |
| 299 | + pass |
| 300 | + |
| 301 | + metrics = { |
| 302 | + "status": "OK", |
| 303 | + "condition": "Optimal", |
| 304 | + "objective": objective, |
| 305 | + "runtime": runtime, |
| 306 | + "memory": "N/A", |
| 307 | + "duality_gap": None, # Not available from command line output |
| 308 | + "max_integrality_violation": None, # Not available from command line output |
| 309 | + } |
| 310 | + |
| 311 | + return metrics |
| 312 | + |
| 313 | + |
232 | 314 | def main( |
233 | 315 | benchmark_yaml_path, |
234 | 316 | solvers, |
235 | 317 | year=None, |
236 | 318 | iterations=1, |
237 | 319 | timeout=10 * 60, |
| 320 | + reference_interval=0, # Default: disabled |
238 | 321 | override=True, |
239 | 322 | ): |
240 | 323 | size_categories = None # TODO add this to CLI args |
241 | 324 | results = {} |
242 | 325 |
|
| 326 | + # Track the last time we ran the reference benchmark |
| 327 | + last_reference_run = 0 |
| 328 | + |
243 | 329 | # Load benchmarks from YAML file |
244 | 330 | with open(benchmark_yaml_path, "r") as file: |
245 | 331 | yaml_content = yaml.safe_load(file) |
@@ -360,24 +446,65 @@ def main( |
360 | 446 | results[(benchmark["name"], benchmark["size"], solver, solver_version)] = ( |
361 | 447 | metrics |
362 | 448 | ) |
| 449 | + |
| 450 | + # Check if we should run the reference benchmark based on the interval |
| 451 | + if reference_interval > 0: |
| 452 | + current_time = time.time() |
| 453 | + time_since_last_run = current_time - last_reference_run |
| 454 | + |
| 455 | + if last_reference_run == 0 or time_since_last_run >= int( |
| 456 | + reference_interval |
| 457 | + ): |
| 458 | + print( |
| 459 | + f"Running reference benchmark with HiGHS binary (interval: {reference_interval}s)...", |
| 460 | + flush=True, |
| 461 | + ) |
| 462 | + reference_metrics = benchmark_highs_binary() |
| 463 | + |
| 464 | + # Add required fields to reference metrics |
| 465 | + reference_metrics["size"] = "reference" |
| 466 | + reference_metrics["solver"] = "highs-binary" |
| 467 | + reference_metrics["solver_version"] = get_highs_binary_version() |
| 468 | + reference_metrics["solver_release_year"] = "N/A" |
| 469 | + |
| 470 | + # Record reference benchmark results |
| 471 | + write_csv_row(results_csv, "reference-benchmark", reference_metrics) |
| 472 | + |
| 473 | + # Update the last reference run time |
| 474 | + last_reference_run = current_time |
| 475 | + else: |
| 476 | + print( |
| 477 | + f"Skipping reference benchmark (last run {time_since_last_run:.1f}s ago, interval: {reference_interval}s)", |
| 478 | + flush=True, |
| 479 | + ) |
| 480 | + |
363 | 481 | return results |
364 | 482 |
|
365 | 483 |
|
366 | 484 | if __name__ == "__main__": |
367 | | - # Check for benchmark file argument and optional year and override arguments |
| 485 | + # Check for benchmark file argument and optional year, override, and reference interval arguments |
368 | 486 | if len(sys.argv) < 3: |
369 | 487 | raise ValueError( |
370 | | - "Usage: python run_benchmarks.py <path_to_benchmarks.yaml> [<year>] [<override>]" |
| 488 | + "Usage: python run_benchmarks.py <path_to_benchmarks.yaml> [<year>] [<override>] [<reference_interval>]" |
371 | 489 | ) |
372 | 490 | sys.exit(1) |
373 | 491 |
|
374 | 492 | benchmark_yaml_path = sys.argv[1] |
375 | 493 | year = sys.argv[2] if len(sys.argv) > 2 else None |
376 | 494 | override = sys.argv[3].lower() == "true" if len(sys.argv) > 3 else True |
| 495 | + reference_interval = ( |
| 496 | + int(sys.argv[4]) if len(sys.argv) > 4 else 0 |
| 497 | + ) # Default: disabled |
377 | 498 |
|
378 | 499 | # solvers = ["highs", "glpk"] # For dev and testing |
379 | 500 | solvers = ["highs", "scip", "cbc", "glpk"] # For production |
380 | 501 |
|
381 | | - main(benchmark_yaml_path, solvers, year, override=override) |
| 502 | + main( |
| 503 | + benchmark_yaml_path, |
| 504 | + solvers, |
| 505 | + year, |
| 506 | + reference_interval=reference_interval, |
| 507 | + override=override, |
| 508 | + ) |
382 | 509 | # Print a message indicating completion |
383 | 510 | print("Benchmarking complete.") |
0 commit comments