diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 321b2dc4..8bdffa0d 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -40,13 +40,15 @@ jobs: venv-${{ runner.os }}- - name: Install solver dependencies + env: + GAMSPY_LICENSE: ${{ secrets.GAMSPY_LICENSE }} run: ./scripts/ci-setup-solvers.sh - name: Run checks shell: bash # required for the source command run: | # change to "./gradlew check --info" for more debugging output. source /opt/conda/bin/activate gams - ./gradlew check --info + ./gradlew check - name: Archive test reports if: always() @@ -81,4 +83,4 @@ jobs: command_timeout: 60m script: | echo "Deploying ${{ github.event.repository.name }} to '${{ github.ref_name }}' environment" - sh /root/ProvideQ/${{ github.event.repository.name }}/deploy.sh ${{ github.ref_name }} \ No newline at end of file + sh /root/ProvideQ/${{ github.event.repository.name }}/deploy.sh ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index f20019d1..10037ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ HELP.md *.DS_Store .gradle +.gamspy_license build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ diff --git a/build.gradle b/build.gradle index 510fe67d..15a41002 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,12 @@ jar { // inject JUnit into test task tasks.named('test') { useJUnitPlatform() + + // Show test output (System.out.println, etc) + testLogging { + showStandardStreams = true + events "passed", "skipped", "failed" + } } tasks.withType(JavaCompile).configureEach { diff --git a/scripts/install-python-dependencies.py b/scripts/install-python-dependencies.py index 1f8ad00e..3e2a0733 100644 --- a/scripts/install-python-dependencies.py +++ b/scripts/install-python-dependencies.py @@ -14,6 +14,23 @@ os.path.join(root_dir, 'solvers'), ] +def get_gamspy_license(): + # 1. Environment variable (GitHub Actions) + license_key = os.environ.get("GAMSPY_LICENSE") + if license_key: + return license_key.strip() + + # 2. Local file (ignored by git) + license_file = os.path.join(root_dir, ".gamspy_license") + if os.path.exists(license_file): + with open(license_file) as f: + return f.read().strip() + + raise RuntimeError( + "No GAMSPY license found. " + "Set the GAMSPY_LICENSE environment variable or create a .gamspy_license file." + ) + exitCode = 0 for base_dir in base_dirs: for root, dirs, files in os.walk(base_dir): @@ -24,6 +41,7 @@ # Iterate over problem directory (knapsack, tsp, etc.) for solver_name in os.listdir(framework_dir): solver_dir = os.path.join(framework_dir, solver_name) + # If folder filter is specified, only process matching folders if folder_filter and folder_filter not in solver_dir: continue @@ -31,18 +49,61 @@ req_file = os.path.join(solver_dir, 'requirements.txt') if os.path.exists(req_file): venv_name = f"{os.path.basename(root)}_{framework_name}_{solver_name}" - print(f"Setting up virtual environment '{venv_name}' for {solver_dir}...") + + print(f"Setting up virtual environment '{venv_name}'") try: venv_path = os.path.join('venv', venv_name) - subprocess.run(['python', '-m', 'venv', venv_path], check=True) + subprocess.run(['python', '-m', 'venv', venv_path], check=True, capture_output=True) if platform.system() == 'Windows': pip_executable = os.path.join(venv_path, 'Scripts', 'pip.exe') + python_executable = os.path.join(venv_path, 'Scripts', 'python.exe') else: pip_executable = os.path.join(venv_path, 'bin', 'pip') - subprocess.run([pip_executable, 'install', '-r', req_file], check=True) + python_executable = os.path.join(venv_path, 'bin', 'python') + + # install dependencies from requirements.txt + subprocess.run([pip_executable, 'install', '-r', req_file], check=True, capture_output=True) + + # install GAMSPy license if this is a GAMS environment + if "gams" in venv_name.lower(): + license_key = get_gamspy_license() + + print("Installing GAMSPy license and scip solver ...") + subprocess.run( + [ + python_executable, + "-m", + "gamspy", + "install", + "license", + license_key, + ], + check=True, + capture_output=True, + ) + + subprocess.run( + [ + python_executable, + "-m", + "gamspy", + "install", + "solver", + "scip" + ], + check=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: - print(f"Error setting up virtual environment for {solver_dir}: {e}") + print(f"Error setting up virtual environment '{venv_name}' for {solver_dir}:") + if e.stdout: + print("STDOUT:") + print(e.stdout.decode() if isinstance(e.stdout, bytes) else e.stdout) + if e.stderr: + print("STDERR:") + print(e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr) exitCode = 1 # let pipeline fail if there was an error in the venv setup. -exit(exitCode) \ No newline at end of file +exit(exitCode) diff --git a/solvers/gams/python/requirements.txt b/solvers/gams/python/requirements.txt index b1b37424..c2608687 100644 --- a/solvers/gams/python/requirements.txt +++ b/solvers/gams/python/requirements.txt @@ -1,3 +1,4 @@ -# This file describes the python package requirements for all GAMS solvers. -# We only build one venv for all of GAMS, not for every GAMS solver. +# This file contains of the python dependencies required in the .gms files +# GamsPy solvers are handles extra. networkx +gamspy == 1.23.1 diff --git a/solvers/gams/qubo/solver_lp_qubo.py b/solvers/gams/qubo/solver_lp_qubo.py index 7c8650b6..e1d6f073 100644 --- a/solvers/gams/qubo/solver_lp_qubo.py +++ b/solvers/gams/qubo/solver_lp_qubo.py @@ -71,7 +71,8 @@ def parse_lp_to_df(filepath="unsplittable_model.lp"): ) print("Solving model natively...") -qubo_model.solve(solver="CPLEX", output=sys.stdout, options=gp.Options(time_limit=60)) +qubo_model.solve(solver="SCIP", solver_options={"lp/solver": '"highs"'}, + output=sys.stdout, options=gp.Options(time_limit=60)) print(f"\n--- Solver Status: {qubo_model.status} ---") print(f"--- Objective Value: {qubo_model.objective_value} ---") @@ -88,4 +89,4 @@ def parse_lp_to_df(filepath="unsplittable_model.lp"): with open(output_path, "w") as f: json.dump(solution_dict, f, indent=4) -print(f"--- Extracted {len(solution_dict)} active variables to {output_path} ---") \ No newline at end of file +print(f"--- Extracted {len(solution_dict)} active variables to {output_path} ---") diff --git a/solvers/gams/unsplittable-mcf/solver_classical.py b/solvers/gams/unsplittable-mcf/solver_classical.py index 55457e8f..3f14552f 100644 --- a/solvers/gams/unsplittable-mcf/solver_classical.py +++ b/solvers/gams/unsplittable-mcf/solver_classical.py @@ -26,4 +26,4 @@ model.build_equations_and_model() print("\n--- Solving Classical MIP with CPLEX ---") -model.solve_classical(solver="CPLEX", output_html=output_path) +model.solve_classical(solver="SCIP", output_html=output_path) diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/GamsQuboSolver.java b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/GamsQuboSolver.java index 960195df..49522b69 100644 --- a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/GamsQuboSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/GamsQuboSolver.java @@ -33,13 +33,13 @@ public GamsQuboSolver( @Override public String getName() { - return "(GAMS) CPLEX Solver for QUBOs"; + return "(GAMS) SCIP Solver for QUBOs"; } @Override public String getDescription() { return "Solves QUBO problems by transforming it into a MIP," - + "which is then solved with CPLEX. Implementation is done in GAMS"; + + "which is then solved with SCIP. Implementation is done in GAMS"; } @Override diff --git a/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfClassicalSolver.java b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfClassicalSolver.java index 6e298aca..4daa51cf 100644 --- a/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfClassicalSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfClassicalSolver.java @@ -34,12 +34,12 @@ public GamsUnsplittableMcfClassicalSolver( @Override public String getName() { - return "(GAMS) CPLEX Classical MCF Solver"; + return "(GAMS) SCIP Classical MCF Solver"; } @Override public String getDescription() { - return "Solves the Unsplittable Multi Commodity Flow problem using GAMSPy with CPLEX. " + return "Solves the Unsplittable Multi Commodity Flow problem using GAMSPy and SCIP. " + "Builds a time-expanded network model and finds optimal flow routes minimizing " + "delay and slack penalties."; } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java b/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java index 2d2abb1f..b33b80f0 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java @@ -209,6 +209,7 @@ public static ProblemDto trySolveFor( // print error output if something went wrong if (problemDto.getState() != ProblemState.SOLVED || problemDto.getSolution().getStatus() != SolutionStatus.SOLVED) { + System.out.println("Testcase failed. Printing debugging info:"); System.out.println(builder); }