diff --git a/.github/workflows/ci-cd-develop.yml b/.github/workflows/ci-cd.yml similarity index 76% rename from .github/workflows/ci-cd-develop.yml rename to .github/workflows/ci-cd.yml index da4d67d9..321b2dc4 100644 --- a/.github/workflows/ci-cd-develop.yml +++ b/.github/workflows/ci-cd.yml @@ -46,7 +46,7 @@ jobs: 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 + ./gradlew check --info - name: Archive test reports if: always() @@ -65,16 +65,20 @@ jobs: deploy-staging: runs-on: ubuntu-latest needs: check - if: ${{ github.ref == 'refs/heads/develop' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }} steps: - - name: Clone repo (shallow) + - name: Cloning repo uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Push to dokku - uses: dokku/github-action@master + + - name: Deploy via SSH and Docker Compose + uses: appleboy/ssh-action@v1.2.5 with: - branch: develop - # only extracting the server address to a secret avoids leaking it in the logs - git_remote_url: ssh://dokku@${{ secrets.DOKKU_SERVER_ADDRESS }}/toolbox-backend-staging - ssh_private_key: ${{ secrets.DOKKU_DEPLOYMENT_KEY }} + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + 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 diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml deleted file mode 100644 index cc65e593..00000000 --- a/.github/workflows/deploy-main.yml +++ /dev/null @@ -1,24 +0,0 @@ -# adapted from https://github.com/dokku/github-action/blob/db5e3b84461e5e73c56d8b0f6a67aab0df25256c/example-workflows/simple.yml -name: 'deploy-main' - -on: - push: - branches: - - main - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Cloning repo - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Push to dokku - uses: dokku/github-action@master - with: - branch: main - # only extracting the server address to a secret avoids leaking it in the logs - git_remote_url: ssh://dokku@${{ secrets.DOKKU_SERVER_ADDRESS }}/toolbox-backend - ssh_private_key: ${{ secrets.DOKKU_DEPLOYMENT_KEY }} diff --git a/.gitignore b/.gitignore index 5b23f68f..f20019d1 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,10 @@ __pycache__/ # Software license key for GAMS, used for a docker container with GAMS included gamslice.txt -# Listing files compiled from our GAMS scripts +# Don't include GAMS Data eXchange (GDX) +*.gdx + +# Listing files compiled from our GAMS scripts files, which are generated when running GAMS solvers *.lst *.op2 solvers/gams/**/225a diff --git a/CITATION.bib b/CITATION.bib new file mode 100644 index 00000000..49353be2 --- /dev/null +++ b/CITATION.bib @@ -0,0 +1,8 @@ +@inproceedings{eichhorn2025provideq, + title={Provideq: A quantum optimization toolbox}, + author={Eichhorn, Domenik and Poser, Nick and Schweikart, Maximilian and Schaefer, Ina}, + booktitle={2025 IEEE International Conference on Quantum Software (QSW)}, + pages={206--214}, + year={2025}, + organization={IEEE} +} diff --git a/Dockerfile b/Dockerfile index 1bc8bacd..280d686f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,14 @@ RUN jlink \ FROM debian:bookworm-slim AS runner WORKDIR /app +# Install system build tools, needed to build solver dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + gcc \ + g++ \ + python3-dev \ + && rm -rf /var/lib/apt/lists/* + COPY scripts scripts # Install GAMS with conda python environment diff --git a/README.md b/README.md index 1c896dfc..70742532 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Build](https://img.shields.io/github/actions/workflow/status/ProvideQ/toolbox-server/deploy-main.yml?style=for-the-badge)](https://github.com/ProvideQ/toolbox-server/actions/workflows/deploy-main.yml) +[![Build](https://img.shields.io/github/actions/workflow/status/ProvideQ/toolbox-server/ci-cd.yml?branch=main&style=for-the-badge +)](https://github.com/ProvideQ/toolbox-server/actions/workflows/ci-cd.yml) [![Release](https://img.shields.io/github/v/release/ProvideQ/toolbox-server?style=for-the-badge)](https://github.com/ProvideQ/toolbox-server/releases/) # ProvideQ Toolbox Server @@ -86,6 +87,6 @@ To use this, enable GitHub Actions and configure the following secrets in the Gi 3. Set `DOKKU_DEPLOYMENT_KEY` to the private key generated in step i. ## License -Copyright (c) 2022 - 2023 ProvideQ +Copyright (c) 2022 - 2026 ProvideQ This project is available under the [MIT License](./LICENSE). diff --git a/build.gradle b/build.gradle index dcad0d47..510fe67d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,12 +6,16 @@ plugins { } group = 'edu.kit.provideq' -version = '0.4.1' -sourceCompatibility = '17' +version = '0.5.0' + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} repositories { mavenCentral() - maven { url 'https://jitpack.io' } + maven { url = 'https://jitpack.io' } } dependencies { @@ -35,9 +39,6 @@ tasks.named('test') { useJUnitPlatform() } -// set Java language level -targetCompatibility = JavaVersion.VERSION_17 - tasks.withType(JavaCompile).configureEach { // treat "unchecked" warnings as errors options.compilerArgs << '-Xlint:unchecked' diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 3a27f5aa..dde68653 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -74,7 +74,7 @@ + value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_WHILE"/> 1 else None + base_dirs = [ os.path.join(root_dir, 'demonstrators'), os.path.join(root_dir, 'solvers'), @@ -19,13 +24,17 @@ # 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 + 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}...") try: venv_path = os.path.join('venv', venv_name) - subprocess.run(['python3', '-m', 'venv', venv_path], check=True) + subprocess.run(['python', '-m', 'venv', venv_path], check=True) if platform.system() == 'Windows': pip_executable = os.path.join(venv_path, 'Scripts', 'pip.exe') else: diff --git a/scripts/setup-gams.sh b/scripts/setup-gams.sh index 8ce91fca..289f8181 100755 --- a/scripts/setup-gams.sh +++ b/scripts/setup-gams.sh @@ -19,7 +19,7 @@ apt-get install curl --yes # iisaa/gams-docker (https://github.com/iiasa/gams-docker/blob/master/Dockerfile, GPL-3.0 licensed) # Configure the GAMS version here -GAMS_VERSION_RELEASE_MAJOR=49.3 +GAMS_VERSION_RELEASE_MAJOR=52.5 GAMS_VERSION_HOTFIX=0 # download the self-extracting archive to /opt/gams/gams.exe and run/extract it diff --git a/solvers/_utility/parse_qubo_from_lp.py b/solvers/_utility/parse_qubo_from_lp.py new file mode 100644 index 00000000..dbd5258c --- /dev/null +++ b/solvers/_utility/parse_qubo_from_lp.py @@ -0,0 +1,34 @@ +def parse_qubo_from_lp(model) -> tuple[list[str], dict[tuple[int, int], float], dict[int, float], float]: + vars = [x.VarName for x in model.getVars() if x.VType == "B"] + + obj = model.getObjective() + + quadratic_terms = { + (vars.index(term[1].VarName), vars.index(term[2].VarName)): term[0] + for term in obj.quadTerms() + } + + linear_terms = { + vars.index(var.VarName): coeff + for coeff, var in obj.linTerms() + if var.VType == "B" + } + + # Detect constant terms from non-binary variables + # A variable is considered a constant if: + # 1. Its LB equals its UB (fixed variable), OR + # 2. Its name can be parsed as an integer + constant_offset = obj.getLinExpr().getConstant() + + # Extract constant contributions + constant_contributions = [ + coeff * lb if lb == ub else ( + coeff * int(var.VarName) if var.VarName.isdigit() else 0 + ) + for coeff, var in obj.getLinExpr().linTerms() + for lb, ub in [(var.getAttr('LB'), var.getAttr('UB'))] + ] + + constant_offset += sum(constant_contributions) + + return vars, quadratic_terms, linear_terms, constant_offset diff --git a/solvers/circuitprocessing/circuitexecution/executor.py b/solvers/circuitprocessing/circuitexecution/executor.py new file mode 100644 index 00000000..379288f1 --- /dev/null +++ b/solvers/circuitprocessing/circuitexecution/executor.py @@ -0,0 +1,40 @@ +import sys +from pytket.qasm import circuit_from_qasm_str + +input_path = sys.argv[1] +num_runs = int(sys.argv[2]) +backend_name = sys.argv[3] + +with open(input_path, 'r') as input_file: + text = input_file.read() + +try: + circuit = circuit_from_qasm_str(text) +except Exception as e: + print("Was not able to convert to OpenQASM: ", e) + sys.exit(1) + +if backend_name == "aer": + from pytket.extensions.qiskit import AerBackend + backend = AerBackend() +elif backend_name == "qulacs": + from pytket.extensions.qulacs import QulacsBackend + backend = QulacsBackend() +elif backend_name == "aer_noisy": + from pytket.extensions.qiskit import AerBackend + from qiskit_aer.noise import NoiseModel + from qiskit_aer.noise.errors import depolarizing_error + # https://docs.quantinuum.com/tket/user-guide/manual/manual_noise.html + noise_model = NoiseModel() + noise_model.add_readout_error([[0.9, 0.1], [0.1, 0.9]], [0]) + noise_model.add_readout_error([[0.95, 0.05], [0.05, 0.95]], [1]) + noise_model.add_quantum_error(depolarizing_error(0.1, 2), ["cx"], [0, 1]) + backend = AerBackend(noise_model) +else: + print(f"Unknown backend: {backend_name}", file=sys.stderr) + sys.exit(1) + +c = backend.get_compiled_circuit(circuit) +handle = backend.process_circuit(c, n_shots=num_runs) +counts = backend.get_result(handle).get_counts() +print(counts) diff --git a/solvers/circuitprocessing/circuitexecution/requirements.txt b/solvers/circuitprocessing/circuitexecution/requirements.txt new file mode 100644 index 00000000..2ef4b7c2 --- /dev/null +++ b/solvers/circuitprocessing/circuitexecution/requirements.txt @@ -0,0 +1,3 @@ +pytket == 2.18.0 +pytket-qiskit == 0.77.0 +# pytket-qulacs \ No newline at end of file diff --git a/solvers/circuitprocessing/circuitoptimizing/decompose-multi-cx/decompose_multi_cx_optimizer.py b/solvers/circuitprocessing/circuitoptimizing/decompose-multi-cx/decompose_multi_cx_optimizer.py new file mode 100644 index 00000000..3378a019 --- /dev/null +++ b/solvers/circuitprocessing/circuitoptimizing/decompose-multi-cx/decompose_multi_cx_optimizer.py @@ -0,0 +1,25 @@ +import sys +from pytket.qasm import circuit_from_qasm_str, circuit_to_qasm_str +from pytket.predicates import CompilationUnit +from pytket.passes import DecomposeMultiQubitsCX + +input_path = sys.argv[1] + +# read input from file +with open(input_path, 'r') as input_file: + text = input_file.read() + +input_circuit = text + +try: + circuit = circuit_from_qasm_str(input_circuit) +except Exception as e: + print("Was not able to convert to OpenQASM: ", e) + sys.exit(1) + +pass1 = DecomposeMultiQubitsCX() +cu = CompilationUnit(circuit) +pass1.apply(cu) + +print(circuit_to_qasm_str(cu.circuit)) + diff --git a/solvers/circuitprocessing/circuitoptimizing/remove-redundancies/remove_redundancies_optimizer.py b/solvers/circuitprocessing/circuitoptimizing/remove-redundancies/remove_redundancies_optimizer.py new file mode 100644 index 00000000..a238a84a --- /dev/null +++ b/solvers/circuitprocessing/circuitoptimizing/remove-redundancies/remove_redundancies_optimizer.py @@ -0,0 +1,24 @@ +import sys +from pytket.qasm import circuit_from_qasm_str, circuit_to_qasm_str +from pytket.predicates import CompilationUnit +from pytket.passes import RemoveRedundancies + +input_path = sys.argv[1] + +# read input from file +with open(input_path, 'r') as input_file: + text = input_file.read() + +input_circuit = text + +try: + circuit = circuit_from_qasm_str(input_circuit) +except Exception as e: + print("Was not able to convert to OpenQASM: ", e) + sys.exit(1) + +pass1 = RemoveRedundancies() +cu = CompilationUnit(circuit) +pass1.apply(cu) + +print(circuit_to_qasm_str(cu.circuit)) \ No newline at end of file diff --git a/solvers/circuitprocessing/circuitoptimizing/requirements.txt b/solvers/circuitprocessing/circuitoptimizing/requirements.txt new file mode 100644 index 00000000..30683d5a --- /dev/null +++ b/solvers/circuitprocessing/circuitoptimizing/requirements.txt @@ -0,0 +1 @@ +pytket == 2.18.0 \ No newline at end of file diff --git a/solvers/custom/hs-knapsack/knapsack.py b/solvers/custom/hs-knapsack/knapsack.py index e1863aa8..5d76a53f 100644 --- a/solvers/custom/hs-knapsack/knapsack.py +++ b/solvers/custom/hs-knapsack/knapsack.py @@ -4,7 +4,7 @@ from knapsack01 import HSKnapsack if len(sys.argv) != 3: - raise TypeError('This script expects exactly 2 arguments. Input file (argument 1) and output file (argument 2).') + raise ValueError('This script expects exactly 2 arguments. Input file (argument 1) and output file (argument 2).') input_path = sys.argv[1] output_path = sys.argv[2] diff --git a/solvers/dwave/qubo/main.py b/solvers/dwave/qubo/main.py index 050fb3ec..edc32d13 100644 --- a/solvers/dwave/qubo/main.py +++ b/solvers/dwave/qubo/main.py @@ -1,14 +1,22 @@ import argparse import os +import sys from datetime import datetime from typing import Literal +import gurobipy as gp -from dimod import BINARY, BinaryQuadraticModel, binary, constrained, lp +# Import parse_qubo_from_lp from sibling _utility directory +# Add the parent (solvers) directory to enable the import +script_dir = os.path.dirname(os.path.abspath(__file__)) +solvers_dir = os.path.join(script_dir, '..', '..') +sys.path.insert(0, solvers_dir) + +from _utility.parse_qubo_from_lp import parse_qubo_from_lp + +from dimod import BINARY, BinaryQuadraticModel from dimod.serialization import coo -from dwave.cloud import Client from solver import solve_with - def main(): parser = argparse.ArgumentParser( prog="DWave QUBO solver", @@ -32,21 +40,12 @@ def main(): bqm = None if bqm is None: - with open(args.file) as problem: - cqm = lp.load(problem) - converted, _ = constrained.cqm_to_bqm(cqm) - - linear_conv = { - (int(str(x)[1:])): converted.linear[x] for x in converted.linear - } - quad_conv = { - (int(str(x)[1:]), int(str(y)[1:])): converted.quadratic[(x, y)] - for x, y in converted.quadratic - } - - bqm = BinaryQuadraticModel(linear_conv, quad_conv, converted.offset, BINARY) - if len(bqm.quadratic) == 0: - bqm = None + model = gp.read(args.file) + (vars, quadratic_terms, linear_terms, constant_offset) = parse_qubo_from_lp(model) + + bqm = BinaryQuadraticModel(linear_terms, quadratic_terms, constant_offset, BINARY) + if len(bqm.quadratic) == 0: + bqm = None filename = os.path.basename(args.file) diff --git a/solvers/dwave/qubo/requirements.txt b/solvers/dwave/qubo/requirements.txt index 694e1464..dc8328b4 100644 --- a/solvers/dwave/qubo/requirements.txt +++ b/solvers/dwave/qubo/requirements.txt @@ -1,2 +1,3 @@ # This file describes the python package requirements for the Dwave Qubo solver -dwave-ocean-sdk == 6.4.1 +dwave-ocean-sdk == 9.3.0 +gurobipy == 13.0.2 \ No newline at end of file diff --git a/solvers/gams/qubo/requirements.txt b/solvers/gams/qubo/requirements.txt new file mode 100644 index 00000000..9b8fbaef --- /dev/null +++ b/solvers/gams/qubo/requirements.txt @@ -0,0 +1,3 @@ +# This file describes the python package requirements for the GAMS Qubo solver +gamspy == 1.23.1 +gurobipy == 13.0.2 \ No newline at end of file diff --git a/solvers/gams/qubo/solver_lp_qubo.py b/solvers/gams/qubo/solver_lp_qubo.py new file mode 100644 index 00000000..7c8650b6 --- /dev/null +++ b/solvers/gams/qubo/solver_lp_qubo.py @@ -0,0 +1,91 @@ +import gamspy as gp +import gurobipy +import pandas as pd +import json +import os +import re +import sys + +# Import parse_qubo_from_lp from sibling _utility directory +# Add the parent (solvers) directory to enable the import +script_dir = os.path.dirname(os.path.abspath(__file__)) +solvers_dir = os.path.join(script_dir, '..', '..') +sys.path.insert(0, solvers_dir) + +from _utility.parse_qubo_from_lp import parse_qubo_from_lp + +arg_count = len(sys.argv) - 1 +if arg_count != 2: + raise TypeError(f'This script expects exactly 2 arguments but got {arg_count}. Input file (argument 1) and output file (argument 2).') + +input_path = sys.argv[1] +output_path = sys.argv[2] + +def parse_lp_to_df(filepath="unsplittable_model.lp"): + """Parses the LP file to extract the Q matrix coefficients and constant.""" + + q_records = [] + + model = gurobipy.read(filepath) + (vars, quadratic_terms, linear_terms, constant_offset) = parse_qubo_from_lp(model) + + print(quadratic_terms) + for ((var1, var2), coeff) in quadratic_terms.items(): + # restore the original name of the variables + q_records.append({"i": vars[var1], "j": vars[var2], "value": coeff}) + + q_df = pd.DataFrame(q_records) + print("q_records", q_records) + print("q_df", q_df) + + return q_df, constant_offset + + +q_df, constant = parse_lp_to_df(input_path) + +m = gp.Container() + +# Dynamically find all unique variables present in the parsed LP matrix +unique_vars = set(q_df["i"].unique()).union(set(q_df["j"].unique())) +print("uniqvar", unique_vars) + +# Sort variables numerically regardless of character prefix (e.g., x1, x2, ... x46) +b_idx = sorted(list(unique_vars), key=lambda x: int("".join(filter(str.isdigit, x)))) + +i = gp.Set(m, name="i", records=b_idx) +alias_j = gp.Alias(m, name="j", alias_with=i) + +Q = gp.Parameter(m, name="Q", domain=[i, alias_j], records=q_df) + +x = gp.Variable(m, name="x", domain=[i], type="binary") + +# Standard CPLEX LP format implicitly scales the quadratic block inside [...] by 0.5. +obj_expr = 0.5 * gp.Sum((i, alias_j), Q[i, alias_j] * x[i] * x[alias_j]) + constant + +z = gp.Variable(m, name="z", type="free") +eq = gp.Equation(m, name="eq") +eq[...] = z == obj_expr + +qubo_model = gp.Model( + m, name="qubo", equations=[eq], problem="MIQCP", sense="min", objective=z +) + +print("Solving model natively...") +qubo_model.solve(solver="CPLEX", 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} ---") + +solution_dict = {} + +if x.records is not None and not x.records.empty: + for _, row in x.records.iterrows(): + val = row["level"] + if val > 0.5: + var_name = row["i"] + solution_dict[var_name] = float(round(val)) + +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 diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/__init__.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/__init__.py new file mode 100644 index 00000000..fdf87e23 --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/__init__.py @@ -0,0 +1,3 @@ +from gamspy_qubo.qubo import Qubo + +__all__ = ["Qubo"] diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/_utils.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/_utils.py new file mode 100644 index 00000000..a32bed38 --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/_utils.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +import re + +import numpy as np +import pandas as pd +from gamspy import SolveStatus +from gamspy.exceptions import GamspyException, ValidationError + + +def validate_value(value: int, allowed_values: list[int], param_name: str) -> int: + if value not in allowed_values: + raise ValueError(f"{param_name} must be one of {allowed_values}") + return value + + +def check_row_entries(df: pd.DataFrame) -> pd.DataFrame: + """ + Helper function to filter DataFrame having either 0 or 1 entries in each row. + Args: + df: A Pandas DataFrame. + + Returns: + Filtered DataFrame with rows having either 0 or 1 + """ + row_contains_only_0s_or_1s = df.isin([0, 1]).all(axis=1) + return df[row_contains_only_0s_or_1s] + + +def fetch_quadratic_coeff(raw_df: pd.DataFrame, bin_vars: list) -> np.ndarray: + """ + Helper function to convert the original Q matrix of the problem to a symmetric matrix + + Args: + raw_df: Original problem Q data in a pd.DataFrame + + Returns: + Numpy Q matrix + """ + raw_df["value"] /= 2 + mask = raw_df["j_1"].astype(str) == raw_df["j_2"].astype(str) + filtered_quad = raw_df.loc[mask, :].copy() + raw_df = raw_df.loc[~mask].copy() + diag_quad = filtered_quad.reset_index(drop=True) + quad = raw_df.copy(deep=True) + quad["j_1"], quad["j_2"] = raw_df["j_2"], raw_df["j_1"] + quad = pd.concat([raw_df, quad, diag_quad], axis=0) + quad = quad.pivot(index="j_1", columns="j_2", values="value").fillna(0) + quad = quad.reindex(labels=bin_vars, axis="index") + quad = quad.reindex(labels=bin_vars, axis="columns") + return quad.to_numpy() + + +def var_contribution( + A: pd.DataFrame, vars: dict, cons: list | None = None +) -> np.ndarray: + """ + Helper function to calculate the contribution of given variables + in a constraint or set of constraints + + Args: + A: df of coefficients + vars: contributing variables + cons: participating constraints + + Returns: + np.ndarray of Total contribution of all variables for that constraint + """ + cons = slice(None) if cons is None else cons # type: ignore + coeffs_of_vars_in_constraint = A.loc[cons, vars.keys()].to_numpy() # type: ignore + lb_var_levels = np.array(list(vars.values())).reshape((len(vars), 1)) + if coeffs_of_vars_in_constraint.size > 0: + return coeffs_of_vars_in_constraint @ lb_var_levels + + return np.array([0]) + + +def modify_matrix( + b_vec: np.ndarray, + rhs: float, + slacks: np.ndarray, + A_coeff: pd.DataFrame, + ele: pd.Series, + nslacks: int, +) -> tuple[np.ndarray, pd.DataFrame, int]: + """ + Helper function to update the original "A" matrix of coeffs + + Args: + b_vec: The n*1 vector + rhs : The Right hand side of a constraint + slacks: result of gen_slacks() + A_coeff: "A" matrix + ele: constraint + nslacks: number of slacks + + Returns: + updated b_vec, A_coeff and number of slacks + """ + con_index = ele.i + slack_names = [ + f"slack_{con_index}_{i}" for i in range(nslacks + 1, nslacks + len(slacks) + 1) + ] + new_cols = pd.DataFrame( + 0, index=A_coeff.index, columns=slack_names, dtype=slacks.dtype + ) + new_cols.loc[con_index, slack_names] = slacks + A_coeff = pd.concat([A_coeff, new_cols], axis=1) + nslacks += len(slacks) + + return np.append(b_vec, [rhs]), A_coeff, nslacks + + +def gen_slacks(var_range: float) -> np.ndarray: + """ + Helper function to generate slacks depending on the range of variables or rhs. + + Note: `var_range` cannot be more than 1e4. This hard limit prevents the creation of + excessive auxiliary binary variables and maintains model performance + during the binarization of slacks or integer variables. + + Args: + var_range: upper bound of variable + + Returns: + Numpy array containing slack coefficients + + example: + if var_range=5, then gen_slacks(5) returns [1, 2, 2] + """ + if var_range >= 1e4: + raise ValidationError( + "The Upper bound is greater than or equal to 1e+4, Quitting!" + ) + + power = int(np.log2(var_range)) if var_range > 0 else 0 + bounded_coef = var_range - (2**power - 1) + D_val = [2**i for i in range(power)] + [bounded_coef] + return np.array(D_val) + + +def get_lhs_bounds(ele: pd.DataFrame) -> tuple[float, float]: + """ + Helper function to find the bounds of a constraint + + Args: + ele: The coefficients of the constraint + + Returns: + lower_bound, upper_bound + """ + return float(ele[ele < 0].sum()), float(ele[ele > 0].sum()) + + +def check_convexity(Q: np.ndarray) -> str: + eigenvalues = np.linalg.eigvals(Q) + + if np.all(eigenvalues > 0): + return "Function is strictly convex." + elif np.all(eigenvalues >= 0): + return "Function is convex." + else: + return "Function is not convex." + + +def qubo_to_ising(Q: dict, offset: float = 0.0) -> tuple[dict, dict, float]: + h = {} # type: ignore + J = {} + linear_offset = 0.0 + quadratic_offset = 0.0 + + for (u, v), bias in Q.items(): + if u == v: + if u in h: + h[u] += 0.5 * bias + else: + h[u] = 0.5 * bias + linear_offset += bias + else: + if bias != 0.0: + J[(u, v)] = 0.25 * bias + + if u in h: + h[u] += 0.25 * bias + else: + h[u] = 0.25 * bias + + if v in h: + h[v] += 0.25 * bias + else: + h[v] = 0.25 * bias + + quadratic_offset += bias + + offset += 0.5 * linear_offset + 0.25 * quadratic_offset + + return h, J, offset + + +def qubo_to_maxcut(Q: np.ndarray) -> np.ndarray: + return -1 * np.sum(Q, axis=1) + + +def triu(Q_matrix: np.ndarray): + upper_mask = np.triu(np.ones_like(Q_matrix), k=1) # 1 above diagonal, 0 elsewhere + scaled_upper = Q_matrix * (1 + upper_mask) # Double above diagonal + return np.triu(scaled_upper) + + +def check_classical_solve(solveStatus: SolveStatus | None): + if solveStatus is None: + raise GamspyException("Solver status is None. Solve the model first.") + + if solveStatus in [ + SolveStatus.IterationInterrupt, + SolveStatus.ResourceInterrupt, + SolveStatus.UserInterrupt, + ]: + # Continue mapping incumbent solution if solve_status is one of + # [UserInterrupt, ResourceInterrupt, IterationInterrupt]. + pass + + elif solveStatus != SolveStatus.NormalCompletion: + raise GamspyException( + f"Solver did not yield NormalCompletion. Solver status = {solveStatus}" + ) + + +def parse_examiner_output(text): + if "Primal constraints satisfied" in text: + return {"feasible": True, "message": "Solution is feasible."} + + match = re.search( + r"Primal infeasible.*?Max violation:\s+(.*?):\s+(.*)", text, re.DOTALL + ) + + if match: + constraint_name = match.group(1).strip() + violation_details = match.group(2).split("\n")[0].strip() + return { + "feasible": False, + "message": f"Infeasible: {constraint_name} violates bounds ({violation_details})", + } + + return { + "feasible": False, + "message": "Examiner failed or returned unexpected format.", + } diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/__init__.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/__init__.py new file mode 100644 index 00000000..e1e21d7e --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/__init__.py @@ -0,0 +1,6 @@ +from gamspy_qubo.backend.base import baseBackend +from gamspy_qubo.backend.dwave import DwaveBackend +from gamspy_qubo.backend.kipu import KipuBackend +from gamspy_qubo.backend.load_json import JsonBackend + +__all__ = ["baseBackend", "DwaveBackend", "KipuBackend", "JsonBackend"] diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/base.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/base.py new file mode 100644 index 00000000..ac6894ae --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/base.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import importlib +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import numpy as np + import pandas as pd + from gamspy import Sense + + +class baseBackend(ABC): + """Abstract base class that provides a structure to different quantum backends.""" + + def __init__( + self, + q_matrix: np.ndarray, + q_variables: pd.Series, + q_constant: float | int, + sense: Sense, + ): + """ + :param q_matrix: QUBO Matrix + :type q_matrix: np.ndarray + :param q_variables: List of variables that are associated with the QUBO + :type q_variables: list[str] + :param q_constant: the offset penalty term + :type q_constant: float | int + :param sense: direction of optimization, i.e, Min. or Max. + :type sense: Sense + """ + self.q_matrix = q_matrix + self.q_variables = q_variables + self.q_constant = q_constant + self.sense = sense + + @abstractmethod + def solve(self, *args, **kwargs) -> pd.DataFrame: + """Solve the problem using quantum backend + + Returns: + A pandas.DataFrame that must have the following structure, + `columns = ["i", "level"]`, where `i` contains variables and + `level` is the corresponding value of the variable after solving. + """ + raise NotImplementedError + + @staticmethod + def check_dependencies(solver_name: str, packages: dict): + for pkg_name, import_path in packages.items(): + try: + importlib.import_module(import_path) + except ImportError: + raise ImportError( + f"The {solver_name} solver requires the '{pkg_name}' package. " + f"Install it with: pip install {pkg_name}" + ) from None diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/dwave.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/dwave.py new file mode 100644 index 00000000..ff87665c --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/dwave.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import importlib + +import pandas as pd + +from gamspy_qubo._utils import triu +from gamspy_qubo.backend import baseBackend + + +class DwaveBackend(baseBackend): + """ + DWave Backend based on `dimod.BinaryQuadraticModel` and `SimulatedAnnealingSampler`. + """ + + def __init__(self, q_matrix, q_variables, q_constant, sense): + super().__init__(q_matrix, q_variables, q_constant, sense) + self.check_dependencies( + "DWAVE", {"dwave-ocean-sdk": "dwave.samplers", "dimod": "dimod"} + ) + + def solve(self, *args, **kwargs) -> pd.DataFrame: + """ + Solve the model using Dwave `SimulatedAnnealingSampler` and map the solution back + to original problem. + + Users can provide keyword arguments as expected by `SimulatedAnnealingSample.sample()` + + Note: Follows the general return structure defined in `baseBackend.solve` + """ + print("\n--- Starting D-Wave (Ocean) Solve ---") + multiplier = -1 if self.sense.value == "MAX" else 1 + ut_mat = triu(self.q_matrix) + ut_mat *= multiplier + matrix_dict = {} + rows, cols = ut_mat.nonzero() + for i, j in zip(rows, cols, strict=True): + matrix_dict[(self.q_variables[i], self.q_variables[j])] = ut_mat[i, j] + + dimod = importlib.import_module("dimod") + dwave_sampler = importlib.import_module("dwave.samplers") + bqm = dimod.BinaryQuadraticModel.from_qubo( + matrix_dict, offset=float(self.q_constant) + ) + sampler = dwave_sampler.SimulatedAnnealingSampler() + kwargs.setdefault("num_reads", 100) + response = sampler.sample(bqm, **kwargs) + best_sample = response.first.sample + sol = pd.DataFrame(best_sample.items(), columns=["i", "level"]) + + return sol diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/kipu.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/kipu.py new file mode 100644 index 00000000..696cfae1 --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/kipu.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import importlib + +import pandas as pd + +from gamspy_qubo._utils import triu +from gamspy_qubo.backend import baseBackend + + +class KipuBackend(baseBackend): + """ + Kipu Backend based on `Miray Advanced` (simulator) quantum optimizer. + """ + + def __init__(self, q_matrix, q_variables, q_constant, sense): + super().__init__(q_matrix, q_variables, q_constant, sense) + self.check_dependencies("KIPU", {"planqk-service-sdk": "planqk.service.client"}) + + def _miray(self): + pass + + def _illay(self): + pass + + def solve(self, *args, **kwargs) -> pd.DataFrame: + """ + Solve the model using Kipu's `Miray` quantum optimizer and map the solution back + to original problem. + + ### Authorization + To authorize the client, you must provide the following as keyword arguments + (retrieved from your application credentials): + * `access_key_id`: Your unique application access key. + * `secret_access_key`: Your private application secret key. + + ### Configuration (kwargs) + Users can provide additional parameters to tune the solver request. + Supported arguments include: + * `shots` (int): The number of samples to collect. Default is 1000. + * `num_iterations` (int): Number of optimization iterations. Default is 3. + * `num_greedy_passes` (int): Number of greedy refinement passes. Default is 0. + + Note: Follows the general return structure defined in `baseBackend.solve` + """ + assert self.q_matrix.shape[0] <= 20, ( + f"Miray Optimizer only supports 20 qubits. This QUBO has {self.q_matrix.shape[0]} variables." + ) + + print("\n--- Starting Kipu Optimizer ---") + planq_service_client = importlib.import_module("planqk.service.client") + client = planq_service_client.PlanqkServiceClient( + service_endpoint="https://gateway.hub.kipu-quantum.com/kipu-quantum/miray-advanced-quantum-optimizer---simulator/1.0.0", + access_key_id=kwargs.get("access_key_id"), + secret_access_key=kwargs.get("secret_access_key"), + token_endpoint="https://gateway.hub.kipu-quantum.com/token", + ) + + multiplier = -1 if self.sense.value == "MAX" else 1 + ut_mat = triu(self.q_matrix) + ut_mat *= multiplier + problem = {} + rows, cols = ut_mat.nonzero() + for i, j in zip(rows, cols, strict=True): + if i == j: + problem[f"({i},)"] = ut_mat[i, j] # diagonal terms + else: + problem[f"({i}, {j})"] = ut_mat[i, j] # off-diagonal terms + problem["()"] = self.q_constant # constant + + request_defaults = {"shots": 1000, "num_iterations": 3, "num_greedy_passes": 0} + filtered_kwargs = { + key: kwargs.get(key, default_value) + for key, default_value in request_defaults.items() + } + request = { + "problem": problem, + "problem_type": "binary", + **filtered_kwargs, + } + service_exec = client.run(request) # send job + print(f"JOB ID: {service_exec.id} | Waiting...") + + result = service_exec.result() + response = result.result + _sol = { + self.q_variables[int(k)]: v for k, v in response["mapped_solution"].items() + } + sol = pd.DataFrame( + _sol.items(), + columns=["i", "level"], + ) + + return sol diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/load_json.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/load_json.py new file mode 100644 index 00000000..fe25c88b --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/backend/load_json.py @@ -0,0 +1,36 @@ +import json + +import pandas as pd + +from gamspy_qubo.backend import baseBackend + + +class JsonBackend(baseBackend): + """ + A custom backend that reads a pre-solved QUBO solution from a JSON file. + """ + + def __init__(self, q_matrix, q_variables, q_constant, sense): + super().__init__(q_matrix, q_variables, q_constant, sense) + + def solve(self, json_path="solution.json", *args, **kwargs) -> pd.DataFrame: + with open(json_path, "r") as f: + sol_dict = json.load(f) + + results = [] + # q_variables holds the QUBO binary indices (e.g., 'b1', 'b2') + for var in self.q_variables: + # Match the variable name to the JSON keys, defaulting to 0.0 if not found + json_key = var.replace("b", "x", 1) + val = sol_dict.get(json_key, 0.0) + results.append({"i": var, "level": float(val)}) + + df = pd.DataFrame(results) + + # Provide safe default bounds for the binary variables + df["marginal"] = 0.0 + df["lower"] = 0.0 + df["upper"] = 1.0 + df["scale"] = 1.0 + + return df diff --git a/solvers/gams/unsplittable-mcf/gamspy_qubo/qubo.py b/solvers/gams/unsplittable-mcf/gamspy_qubo/qubo.py new file mode 100644 index 00000000..f1d0064d --- /dev/null +++ b/solvers/gams/unsplittable-mcf/gamspy_qubo/qubo.py @@ -0,0 +1,890 @@ +import logging as log + +import gamspy as gp +import numpy as np +import pandas as pd +from gamspy.exceptions import GamspyException, ValidationError + +from gamspy_qubo import _utils +from gamspy_qubo.backend import DwaveBackend, JsonBackend, KipuBackend + +LOG_LEVEL_DICT = {0: log.WARN, 1: log.INFO, 2: log.DEBUG} +SUPPORTED_QUANTUM_BACKENDS = [ + "dwave", + "kipu", + "json", +] # append this list when adding new quantum backend + + +class Qubo(gp.Model): + """ + QUBO Reformulation for Integer Programs (IPs) modelled in GAMSPy. + + Parameters + ---------- + model: gp.Model + GAMSPy generated IP Model. + name: str | None + Name for the reformulated model. By default it is "QUBO" + penalty: int | None + Set the appropriate penalty to be used to penalize the constraints. By default it is 1 + log_on: int | None + Enable logging information. + Options are {0 = WARN, 1 = INFO, 2 = DEBUG}. + By default it is 0. + + Notes + ----- + - Currently two quantum solvers, ("dwave", "kipu"), are supported. + - The desired quantum solver can be set using the `solver` keyword argument in the `model.solve()` call. (default = "cplex") + - We first use the `CONVERT` solver to generate a standardized version of the original problem. + - One can also pass keyword arguments to the `CONVERT` solver, for. e.g., `options=gp.Options(hold_fixed_variables=True)` + """ + + def __init__( + self, + model: gp.Model, + /, + name: str = "QUBO", + penalty: int = 1, + log_on: int = 0, + **kwargs, + ): + if not isinstance(model, gp.Model): + raise ValidationError("Qubo() only accepts a >gamspy.Model< object.") + + self._og_model: gp.Model = model + self._modelName: str = name + self._sense: gp.Sense = model.sense + self.penalty: int = penalty + _work_dir = model.container.working_directory + self._container: gp.Container = self._run_convert(workdir=_work_dir, **kwargs) + self._q_container = gp.Container(working_directory=_work_dir) + self.Q: np.ndarray = np.array([]) + self.Qconst: float = 0.0 + self._TRANSFORMATION_COMPLETE = False + self._MAPPING_COMPLETE = False + + if ( + log_level := LOG_LEVEL_DICT.get( + _utils.validate_value( + log_on, allowed_values=[0, 1, 2], param_name="log_on" + ), + log.WARN, + ) + ) < log.WARN: + log.basicConfig( + filename=f"{self._og_model.name}_reformulation.log", + filemode="w", + format="%(message)s", + level=log_level, + force=True, + ) + + def __str__(self) -> str: + return ( + f"Model {self._modelName}:\n Problem Type: MIQCP\n Sense:" + f" {self._sense}\n Equations: {self._modelName}_objective" + ) + + def _run_convert(self, workdir, **kwargs) -> gp.Container: + try: + self._og_model.solve( + solver="CONVERT", + solver_options={ + "dumpgdx": f"{self._og_model.name}.gdx", + "GDXQuadratic": 1, + "GDXHessian": 1, + }, + **kwargs, + ) + except Exception as e: + raise GamspyException("Error while running the >CONVERT< operation.") from e + + return gp.Container( + load_from=f"{self._og_model.name}.gdx", + working_directory=workdir, + ) + + def _check_transformation(self): + if not self._TRANSFORMATION_COMPLETE: + raise ValidationError("Run transform first to generate the Q Matrix.") + + def transform( + self, penalty: int | None = None + ) -> tuple[gp.Parameter, gp.Set, gp.Parameter]: + """ + Method to perform the QUBO reformulation on the provided model. + + Arguments: + penalty: different penalty can be provided again to generate a new QUBO. + + Returns: + qd: gp.Parameter: the Q Matrix + qi: gp.Set: binary variables participating in the QUBO + qconst: gp.Parameter: the offset calculated based on the penalty provided. + """ + if penalty is None: + penalty = self.penalty + + self._fixed_vars_flag = False + self._lower_bounded_vars_flag = False + obj_eq_name: pd.DataFrame = self._container["iobj"].records + + if obj_eq_name is None: + raise ValidationError( + "The objective is not defined using a scalar equation. `iobj` in gdx is empty. Quitting." + ) + + obj_var: list = self._container["jobj"].records["j"].to_list() + # fetch the objective variable name + all_vars: pd.DataFrame = self._container[ + "j" + ].records # fetches all variable names + raw_a: pd.DataFrame = self._container["A"].records # A coefficients + eq_data: pd.DataFrame = self._container["e"].records # fetches equation data + + if ( + raw_a[-raw_a["i"].isin(obj_eq_name["i"].tolist())]["value"] + .mod(1) + .sum(axis=0) + > 0 + ): # floating point coeffs in objective function is accepted + raise ValidationError( + "Reformulation with Non-Integer Coefficients not possible. Quitting." + ) + + raw_a = raw_a.pivot(index="i", columns="j", values="value").fillna( + 0 + ) # arranging in a matrix + + if ( + eq_data["lower"].mod(1).sum(axis=0) > 0 + or eq_data["upper"].mod(1).sum(axis=0) > 0 + ): + raise ValidationError( + "Reformulation with Non-Integer RHS not possible. Quitting." + ) + + bin_vars = self._container[ + "jb" + ].records # fetches binary variable names, if any + int_vars = self._container[ + "ji" + ].records # fetches integer variable names, if any + bin_vars = ( + [] if bin_vars is None else bin_vars["j"].to_list() + ) # check if any bin_vars are present + int_vars = ( + [] if int_vars is None else int_vars["j"].to_list() + ) # check if any int_vars are present + self._int_vars_flag = False if len(int_vars) == 0 else True + all_var_vals = ( + self._container["x"].records + ) # get all variable values, viz., [level, marginal, lower, upper, scale] + + if ( + len(all_vars) - len(bin_vars) - len(int_vars) != 1 + ): # Continuous variables are not allowed + raise ValidationError("There are continuous variables. Quitting.") + + self._obj_eq_name = obj_eq_name["i"].to_list() + + check_quad = self._container["ANl"].records + + """ + Check if there are any fixed variables in the gdx, i.e., lb=ub=level of any variable. + If such variables exist, separate them from the list of non-fixed variables and treat them as constants in the objective function. + + We also need to check if the level of variables are set and handle them separately + """ + lower_mask = (all_var_vals["lower"] > 0) & ( + all_var_vals["lower"] != all_var_vals["upper"] + ) + fixed_mask = (all_var_vals["level"] == all_var_vals["lower"]) & ( + all_var_vals["level"] == all_var_vals["upper"] + ) + + self._vars_with_lower_bounds = dict( + all_var_vals[lower_mask][["j", "lower"]].values + ) + fixed_vars = dict(all_var_vals[fixed_mask][["j", "level"]].values) + + fixed_and_lower_bounds = {**self._vars_with_lower_bounds, **fixed_vars} + sum_fixed_obj_var_coeffs = 0.0 + + if check_quad is not None: + rawquad = self._container[ + "Q" + ].records # fetch quadratic terms from the original problem, if any. + + if self._int_vars_flag: + raise ValidationError( + "Quadratic Program with integer variables are not supported." + ) + + if any(check_quad["j"].isin(fixed_and_lower_bounds.keys())): + raise ValidationError( + "Quadratic terms with non-zero variable levels are not supported at the moment." + ) + + log.debug("Coefficient matrix: raw_a\n" + raw_a.to_string()) + log.debug("\nEquation Data: eq_data\n" + eq_data.to_string()) + log.debug("\nVariable Data: all_var_vals\n" + all_var_vals.to_string()) + + if ( + fixed_and_lower_bounds + ): # adjust the rhs of equations when level of variables > 0 + log.info( + f"\nList of variables with lower bounds:\n{self._vars_with_lower_bounds}" + ) + self._lower_bounded_vars_flag = True + contribution = _utils.var_contribution(raw_a, fixed_and_lower_bounds) + eq_data.loc[:, ["lower", "upper"]] -= contribution + if fixed_vars: + fixed_set = set(fixed_vars.keys()) + self._fixed_vars_flag = True + log.info(f"\nList of Fixed Variables:\n{fixed_vars}") + # remove the fixed variables from computation + bin_vars = [var for var in bin_vars if var not in fixed_set] + int_vars = [var for var in int_vars if var not in fixed_set] + sum_fixed_obj_var_coeffs += np.ndarray.item( + _utils.var_contribution(raw_a, fixed_vars, cons=self._obj_eq_name) + ) + raw_a.drop( + labels=list(fixed_vars.keys()), axis=1, inplace=True + ) # dropping columns from the coefficient matrix + self._fixed_var_vals: pd.DataFrame = all_var_vals[ + all_var_vals["j"].isin(fixed_vars) + ].copy(deep=True) + + log.debug( + "\nAfter removing fixed variables and adjusting for non-zero levels: raw_a\n" + + raw_a.to_string() + ) + log.debug( + "\nAfter removing fixed variables and adjusting for non-zero levels: eq_data\n" + + eq_data.to_string() + ) + + """ + Check if there exist a row in coefficient matrix with all zero values. This can happen if all vars in a constraint are fixed. + Such row is irrelevant for QUBO and can be dropped out of the matrix and set of constraints. + """ + redundant_cons = list(raw_a[raw_a.apply(abs).sum(axis=1) == 0].index) + if len(redundant_cons) > 0: + log.info(f"\nDropping these redundant constraint: \n{redundant_cons}") + raw_a.drop(redundant_cons, axis=0, inplace=True) + eq_data.drop( + eq_data[eq_data["i"].isin(redundant_cons)].index, axis=0, inplace=True + ) + + """ + If integer variables exist, convert all integers to binary with '@' as a delimiter of variable names + If Integer variables with lower bound exist, i.e., lb >=1 and lb!=ub, then convert binary variable for that range + If these variables contribute to the objective function, their lower bounds are added as a constant + """ + sum_lower_bound_of_int_vars = 0 + if self._int_vars_flag: + if self._vars_with_lower_bounds: + sum_lower_bound_of_int_vars += np.ndarray.item( + _utils.var_contribution( + raw_a, self._vars_with_lower_bounds, self._obj_eq_name + ) + ) + + int_var_vals = all_var_vals[all_var_vals["j"].isin(int_vars)] + int_to_bin_bounds = { + row["j"]: _utils.gen_slacks(row["upper"] - row["lower"]) + for _, row in int_var_vals.iterrows() + } # generate coeffs for converted binary vars + int_bin_vals = pd.DataFrame(columns=["intName", "binName", "value"]) + for var, bin_bounds in int_to_bin_bounds.items(): + for i in range(len(bin_bounds)): # naming the converted binary vars + new_row = pd.DataFrame( + { + "intName": var, + "binName": f"{var}@_bin{i}", + "value": bin_bounds[i], + }, + index=[0], + ) + int_bin_vals = ( + pd.concat([int_bin_vals, new_row], ignore_index=True) + if not int_bin_vals.empty + else new_row.copy() + ) + + self._binName_list = int_bin_vals[ + "binName" + ].to_list() # list of all converted binary variable names + log.info( + "\nInteger to Binary Mapping: int_bin_vals\n" + int_bin_vals.to_string() + ) + int_bin_vals = int_bin_vals.pivot( + index="intName", columns="binName", values="value" + ).fillna(0) # mapping each binary var to its integer var component + int_bin_vals = int_bin_vals.reindex(labels=int_vars, axis="index") + int_bin_vals = int_bin_vals.reindex( + labels=self._binName_list, axis="columns" + ) + self._int_bin_vals = int_bin_vals + + raw_a_int = raw_a[int_vars] + raw_a_int = raw_a_int.dot( + int_bin_vals + ) # updating the "A" coeff matrix with the new coeffs for converted binary vars + raw_a_rest = raw_a[obj_var + bin_vars] + raw_a = pd.concat( + [raw_a_rest, raw_a_int], axis="columns" + ) # new "A" coeff matrix + log.info("\nInteger to Binary Mapping: raw_a\n" + raw_a.to_string()) + bin_vars += self._binName_list # append the list of original binary variables with the list of converted binary variables + + cons = eq_data[-eq_data["i"].isin(self._obj_eq_name)].reset_index( + drop=True + ) # fetch only the constraints and not the objective equation + nvars = len(bin_vars) + nslacks = 0 + self._obj_var_direction = raw_a[obj_var].loc[self._obj_eq_name].to_numpy() + obj_var_coeff = raw_a[bin_vars].loc[self._obj_eq_name].to_numpy() + if self._obj_var_direction > 0: + obj_var_coeff = -1 * obj_var_coeff + obj = np.zeros((nvars, nvars)) + np.fill_diagonal(obj, obj_var_coeff) + + """ + Pre-processing in case special constraints exist + Remove the constraint from the "A" matrix and include the special penalty directly in the objective + Doing so, reduces the number of slack variables used in the final reformulation + special penalty case 1: sum(x_i | 1 <= i <= n) <= 1 => P*sum(x_i*x_j | i < j) + special penalty case 2: x_i + x_j >= 1 => P*(1 - x_i - x_j + x_i*x_j) + """ + + # Case 1 implementation + special_cons_case_1_label = [ + ele.i for _, ele in cons.iterrows() if ele.upper == 1 and ele.lower != 1 + ] + if special_cons_case_1_label: + case1_cons = raw_a[bin_vars].loc[special_cons_case_1_label] + case1_cons = _utils.check_row_entries(case1_cons.copy()) + case1_cons_index_label = list(case1_cons.index) + case1_penalty = case1_cons.to_numpy() + if case1_penalty.size > 0: + case1_penalty = (case1_penalty.T @ case1_penalty) / 2 + np.fill_diagonal(case1_penalty, np.zeros((1, len(bin_vars)))) + else: # if there are no rows with only 0/1 entries + case1_penalty = np.zeros((nvars, nvars)) + log.debug(f"\nSpecial constraint case 1:\n{special_cons_case_1_label}") + else: + case1_cons_index_label = [] + case1_penalty = np.zeros((nvars, nvars)) + + # Case 2 implementation + special_cons_case_2_label = [ + ele.i for _, ele in cons.iterrows() if ele.lower == 1 and ele.upper != 1 + ] + if special_cons_case_2_label: + case2_cons = raw_a[bin_vars].loc[special_cons_case_2_label] + case2_cons = _utils.check_row_entries(case2_cons.copy()) + case2_cons = case2_cons[case2_cons.sum(axis=1) == 2] + case2_cons_index_label = list(case2_cons.index) + case2_penalty = case2_cons.to_numpy() + if case2_penalty.size > 0: + case2_penalty = (case2_penalty.T @ case2_penalty) / 2 + case2_diag = np.diag_indices_from(case2_penalty) + case2_penalty[case2_diag] *= -2 + else: # if there are no rows with two 1s in them + case2_penalty = np.zeros((nvars, nvars)) + log.debug(f"\nSpecial constraint case 2:\n{special_cons_case_2_label}") + + else: + case2_cons_index_label = [] + case2_penalty = np.zeros((nvars, nvars)) + + final_special_cons = case1_cons_index_label + case2_cons_index_label + final_special_penalty = case1_penalty + case2_penalty + case2_penalty_offset_factor = len(case2_cons_index_label) + + is_max = True if self._sense == gp.Sense.MAX else False + P = -1 * penalty if is_max else penalty # penalty term for classic solvers + obj += P * final_special_penalty + + cons.drop(cons[cons["i"].isin(final_special_cons)].index, axis=0, inplace=True) + raw_a.drop(final_special_cons, axis=0, inplace=True) + + A_coeff = raw_a.loc[cons["i"], bin_vars] + + self._quad_val = np.array([]) + if ( + check_quad is not None + ): # check if quadratic terms are present in the original problem + log.debug("\nRaw Q data from GDX: Q\n" + rawquad.to_string()) + rawquad_obj = rawquad[rawquad["i_0"].isin(self._obj_eq_name)].copy( + deep=True + ) + if ( + len(rawquad_obj.index) != 0 + ): # check if quadratic terms exist in the objective function + rawquad_obj.drop(["i_0"], axis=1, inplace=True) + self._quad_val = _utils.fetch_quadratic_coeff( + raw_df=rawquad_obj, bin_vars=bin_vars + ) + sum_fixed_obj_var_coeffs /= 2 + + rawquad_cons = rawquad[ + -rawquad["i_0"].isin(self._obj_eq_name) + ] # non-linear constraints without objective equation + if len(rawquad_cons.index) != 0: # non-linear constraints exists + raise ValidationError("There are non-linear constraints. Quitting.") + ### Removed the support for quadratic constraints. + + if ( + self._quad_val.size + ): # add the old quadratic terms/matrix to the new objective + log.debug( + "\nUpdate Objective by adding Q: \n" + np.array2string(self._quad_val) + ) + obj += ( + -1 * self._quad_val if self._obj_var_direction > 0 else self._quad_val + ) + log.debug("\nNew Q: \n" + np.array2string(obj)) + + b_vec = np.array([]) + log.info("\nFinal Cons: \n" + cons.to_string()) + for _, ele in cons.iterrows(): + if ele.upper == ele.lower: # equal-to type constraint + rhs = ele.lower + lhs_min_lb, lhs_max_ub = _utils.get_lhs_bounds(A_coeff.loc[ele.i]) + if (rhs - lhs_min_lb) < 0 or (lhs_max_ub - rhs) < 0: + raise ValidationError(f"Constraint is infeasible: {ele.i}") + else: + b_vec = np.append(b_vec, [rhs]) + slacks: ( + list | np.ndarray + ) = [] # do not introduce slacks for equality type constraints + + elif ele.upper == np.inf: # greater than type constraint + rhs = ele.lower + _, lhs_max_ub = _utils.get_lhs_bounds(A_coeff.loc[ele.i]) + slacks_range = lhs_max_ub - rhs + if slacks_range > 0: + slacks = -1 * _utils.gen_slacks(slacks_range) + b_vec, A_coeff, nslacks = _utils.modify_matrix( + b_vec, rhs, slacks, A_coeff, ele, nslacks + ) + elif slacks_range == 0: + b_vec = np.append(b_vec, [rhs]) + slacks = [] + else: + raise ValidationError(f"Constraint is infeasible: {ele.i}") + + else: # less-than type constraint + rhs = ele.upper + lhs_min_lb, _ = _utils.get_lhs_bounds(A_coeff.loc[ele.i]) + slacks_range = rhs - lhs_min_lb + if slacks_range > 0: + slacks = _utils.gen_slacks(slacks_range) + b_vec, A_coeff, nslacks = _utils.modify_matrix( + b_vec, rhs, slacks, A_coeff, ele, nslacks + ) + elif slacks_range == 0: + b_vec = np.append(b_vec, [rhs]) + slacks = [] + else: + raise ValidationError(f"Constraint is infeasible: {ele.i}") + + logging_a_mat = A_coeff.unstack().reset_index() + logging_a_mat = logging_a_mat[logging_a_mat[0] != 0] + log_data = { + "final_coefficient_matrix": f"\n{logging_a_mat.to_string()}", + "final_rhs": b_vec, + "constant_rhs_term": b_vec.T @ b_vec, + "case2_penalty_offset_factor": case2_penalty_offset_factor, + "fixed_variable_contribution": sum_fixed_obj_var_coeffs, + "integer_lower_bound_contribution": sum_lower_bound_of_int_vars, + } + # Single structured log entry + log.debug( + "Final optimization summary:\n" + + "\n".join( + f"{k.replace('_', ' ').title()}: {v}" for k, v in log_data.items() + ) + ) + # A matrix and b_vec are available. Now, for penalization: $(A.X - B)^{2}$ = $(A.X - B)^{T} * (A.X - B)$ + a_mat = A_coeff.to_numpy() + nvars += nslacks # increment the total number of variables by total number of slack variable used + X_diag = np.zeros((nvars, nvars)) + np.fill_diagonal(X_diag, -b_vec.T @ a_mat) + new_x = a_mat.T @ a_mat + 2 * X_diag + + newobj = np.zeros((nvars, nvars)) + newobj[: len(bin_vars), : len(bin_vars)] = ( + obj # define the new objective: Q for the qubo + ) + + self.Qconst = ( + P * b_vec.T @ b_vec + + P * case2_penalty_offset_factor + + sum_fixed_obj_var_coeffs + + sum_lower_bound_of_int_vars + ) + log.debug(f"\nPenalty: {P} | Total Offset: {self.Qconst}\n") + self.Q = newobj + P * new_x + Qdf_df: pd.DataFrame | pd.Series = pd.DataFrame( + self.Q, columns=list(A_coeff.columns), index=list(A_coeff.columns) + ) + Qdf_df = Qdf_df.unstack() + Qdf_df = Qdf_df.reset_index() + Qdf = list(Qdf_df.itertuples(index=False, name=None)) + + qconst = gp.Parameter( + self._q_container, + "qconst", + None, + self.Qconst, + description="Constant term to offset the objective value to original", + ) + if Qdf: + qi = gp.Set( + self._q_container, + "qi", + records=A_coeff.columns, + description="QUBO variables", + ) + qd = gp.Parameter( + self._q_container, + "qd", + [qi, qi], + records=Qdf, + description="Q matrix", + ) + else: + qi = gp.Set( + self._q_container, + "qi", + records=["all_variables_fixed"], + description="QUBO variables", + ) + qd = gp.Parameter( + self._q_container, + "qd", + [qi, qi], + records=[("all_variables_fixed", "all_variables_fixed", 0)], + description="Q matrix", + ) + + self._TRANSFORMATION_COMPLETE = True + return qd, qi, qconst + + def write_gdx(self, gdxName: str | None = None): + if gdxName: + self._q_container.write(gdxName) + else: + self._q_container.write(f"qout_{self._modelName}.gdx") + + def write_qubowl(self) -> None: + """ + Convenient method to produce QUBO in QUBOWL ready file format (.qs) + + Returns: + A QMat_.qs file accepted by QUBOWL. + """ + self._check_transformation() + + non_zero_indices = np.tril_indices_from(self.Q) + non_zero_values = self.Q[non_zero_indices] + with open(f"QMat_{self._modelName}.qs", "w") as fp: + fp.write(f"{self.Q.shape[0]} {len(non_zero_values)} {self.Qconst}\n") + for i, j, value in zip(*non_zero_indices, non_zero_values, strict=True): + if value != 0: + fp.write(f"{i + 1} {j + 1} {value}\n") + + def _model(self) -> None: + try: + qd, qi, qconst = self._q_container.getSymbols(["qd", "qi", "qconst"]) + except Exception as e: + raise GamspyException( + "Something went from while fetching the q symbols." + ) from e + + i = gp.Alias(self._q_container, name="i", alias_with=qi) + + j = gp.Alias(self._q_container, name="j", alias_with=qi) + + x = gp.Variable( + self._q_container, + name="x", + type="binary", + domain=i, + description="QUBO variables", + ) + + qubo_obj = gp.Sum(gp.Domain(i, j), x[i] * qd[i, j] * x[j]) + qconst + super().__init__( + container=self._q_container, + name=self._modelName, + problem="MIQCP", + sense=self._sense, + objective=qubo_obj, + ) + + def solve(self, *args, **kwargs) -> pd.DataFrame | None: + kwargs.setdefault("solver", "cplex") + self._backend = kwargs.get("solver") + optimized_variable_values = None + orig_obj_var = getattr(self._og_model, "_objective_variable", None) + if orig_obj_var is not None: + original_obj_sym = orig_obj_var.name + else: + original_obj_sym = f"{self._og_model.name}_objective_variable" + + if not self._TRANSFORMATION_COMPLETE: + self.transform() + + if f"{self._modelName}_objective" not in self._q_container.data: + self._model() + + if self._backend not in SUPPORTED_QUANTUM_BACKENDS: + try: + solved = super().solve(*args, **kwargs) + _utils.check_classical_solve(solveStatus=super().solve_status) + except Exception as e: + raise GamspyException( + f"Something went wrong while solving using >{self._backend}< backend." + ) from e + optimized_variable_values = self._q_container["x"].records + elif self._backend in SUPPORTED_QUANTUM_BACKENDS: + q_vars: pd.Series = self._q_container["qi"].records["uni"] + _initialize = { + "dwave": DwaveBackend, + "kipu": KipuBackend, + "json": JsonBackend, + } + _backend: DwaveBackend | KipuBackend | JsonBackend = _initialize[ + self._backend + ]( + q_matrix=self.Q, + q_variables=q_vars, + q_constant=self.Qconst, + sense=self._sense, + ) # type: ignore + try: + optimized_variable_values = _backend.solve(*args, **kwargs) + assert isinstance(optimized_variable_values, pd.DataFrame), ( + "backend.solve() must return a `pd.DataFrame`. " + "Refer to the return structue of `baseBackend.solve` for more details." + ) + optimized_variable_values["i"] = pd.Categorical( + optimized_variable_values["i"], + categories=q_vars.cat.categories, + ordered=True, + ) # NOTE: some solvers can shuffle the order of UELs, and + # it is critical for them to be in order for mapping them back + cols = ["marginal", "lower", "upper", "scale"] + optimized_variable_values = optimized_variable_values.reindex( + columns=optimized_variable_values.columns.tolist() + cols + ) + except Exception as e: + raise GamspyException( + f"Something went wrong while solving using >{self._backend}< backend." + ) from e + else: + raise GamspyException(f"Backend {self._backend} not supported.") + + solution = { + "optimized_vals": optimized_variable_values.sort_values("i"), + "obj_fn_sym": original_obj_sym, + } + self._map_solution(solution) + + return solved if self._backend not in SUPPORTED_QUANTUM_BACKENDS else None + + def _map_solution(self, solution) -> None: + """ + This function maps the QUBO solution to the original Problem + """ + all_vars = self._container["j"].records + optimized_vals = solution["optimized_vals"] + original_obj_sym = solution["obj_fn_sym"] + + if self._fixed_vars_flag: + self._fixed_var_vals.rename({"j": "i"}, axis=1, inplace=True) + optimized_vals = pd.concat( + [optimized_vals, self._fixed_var_vals], ignore_index=True + ) + + if self._int_vars_flag: + int_bin_map = self._int_bin_vals.unstack() + int_bin_map = int_bin_map[int_bin_map != 0].reset_index() # type: ignore + + opt_lookup = optimized_vals.set_index("i")["level"] + int_bin_map["final_level"] = int_bin_map[0] * int_bin_map["binName"].map( + opt_lookup + ) + + int_summary = int_bin_map.groupby("intName")["final_level"].sum() + + original_int_vals = self._container["x"].records + original_int_vals = original_int_vals[ + original_int_vals["j"].isin(int_summary.index) + ].copy() + + original_int_vals["level"] = original_int_vals["j"].map(int_summary) + original_int_vals.rename(columns={"j": "i"}, inplace=True) + + cols = ["i", "level", "marginal", "lower", "upper", "scale"] + original_int_vals = original_int_vals[cols] + optimized_vals = optimized_vals[ + ~optimized_vals["i"].isin(self._binName_list) + ] + optimized_vals = pd.concat( + [optimized_vals, original_int_vals], ignore_index=True + ) + + if self._vars_with_lower_bounds: + optimized_vals.loc[ + optimized_vals["i"].isin(self._vars_with_lower_bounds.keys()), + "level", + ] += list(self._vars_with_lower_bounds.values()) + + pattern = r"(?P.+)\((?P.*?)\)" + extracted = all_vars["element_text"].str.extract(pattern) + extracted["domain"] = extracted["domain"].str.replace(r"\'", "", regex=True) + extracted["uni"] = all_vars["uni"] + + merged_vals = optimized_vals.merge( + extracted, left_on="i", right_on="uni", how="inner" + ) + + for var_name, group in merged_vals.groupby("var_sym"): + target_symbol = self._og_model.container[var_name] + domain_names = [ + dom if isinstance(dom, str) else dom.name + for dom in target_symbol.domain + ] + + split_labels = group["domain"].str.split(",", expand=True) + split_labels.columns = domain_names + + attrs = ["level", "marginal", "lower", "upper", "scale"] + new_records = pd.concat( + [split_labels, group[attrs]], + axis=1, + ) + dtype_map = { + **{c: "category" for c in domain_names}, + **{c: "float" for c in attrs}, + } + new_records = new_records.astype(dtype_map) + target_symbol.records = new_records.reset_index(drop=True) + + """ + The code below is required to calculate the contribution of variables towards the objective using the new levels obtained from the QUBO solve. + Since the QUBO solve returns a different level for the objective variable when the optimal solution is not returned, for example, it includes the penalty for every constraint not satisfied. + """ + + obj_var = self._container["jobj"].records["j"].values[0] + rem_syms = all_vars[all_vars["uni"] != obj_var]["uni"].to_list() + mask = optimized_vals["i"].isin(rem_syms) + filtered_vals = optimized_vals[mask] + + x_l = filtered_vals["level"].to_numpy() + orig_syms_w_new_levels = filtered_vals.set_index("i")["level"].to_dict() + + # at the moment `quad` only contains contribution from the objective row. + quad_contribution = x_l.T @ self._quad_val @ x_l if self._quad_val.size else 0 + jacobian: pd.DataFrame = self._container["A"].records # A coefficients + jacobian = jacobian.pivot(index="i", columns="j", values="value").fillna( + 0 + ) # arranging in a matrix + linear_contribution = _utils.var_contribution( + jacobian, orig_syms_w_new_levels, cons=self._obj_eq_name + ).flatten()[0] + + total_objective_contribution = linear_contribution + quad_contribution + total_objective_contribution = ( + -1 * total_objective_contribution + if self._obj_var_direction > 0 + else total_objective_contribution + ) + + obj_var_coeff: gp.Variable = self._q_container[ + f"{self._modelName}_objective_variable" + ] + obj_var_coeff.l = total_objective_contribution + super().__setattr__( + "_objective_value", total_objective_contribution + ) # sets the value for the attribute of QUBO model instance + self._og_model.container[ + original_obj_sym + ].records = obj_var_coeff.records # sets the value for original model instance + self._MAPPING_COMPLETE = True + + @property + def qubo(self): + return self.Q + + @staticmethod + def check_convexity(Q: np.ndarray): + """ + Convenience method to check the convexity of the QUBO using eigenvalues + """ + return _utils.check_convexity(Q) + + @staticmethod + def qubo_to_ising(Q: dict, offset: float = 0.0): + """ + This is the Qubo to Ising Reformulation. Here, the variable X in {-1,1} + + Args: + Q: in a form of dict, {(i,j): val} + offset: offset from the Qubo reformulation + + Returns: + h: the bias vector \ + J: the coupling matrix\ + offset: adjusted offset for the Ising model + """ + return _utils.qubo_to_ising(Q, offset) + + @staticmethod + def qubo_to_maxcut(Q: np.ndarray): + """ + This is the Qubo to Maxcut Reformulation. This can be used for SDP procedures. + + Args: + Q: a n x n symmetric numpy matrix + + Returns: + n x 1 vector associated with the extra variable required in max cut transformation + """ + return _utils.qubo_to_maxcut(Q) + + @staticmethod + def triu(Q: np.ndarray) -> np.ndarray: + """ + Helper function to fetch the upper triangular matrix of a symmetric Q matrix + + Returns: + An upper triangular matrix + """ + return _utils.triu(Q) + + def check_feasibility(self): + import io + + stream = io.StringIO() + self._og_model.solve( + solver="examiner", + solver_options={"examineInitPoint": 1, "objvarAutoAdjust": 1}, + output=stream, + ) + result = _utils.parse_examiner_output(text=stream.getvalue()) + + if not self._MAPPING_COMPLETE: + result["WARNING"] = ( + "Model must be solved before checking feasibility of the QUBO solution." + ) + + return result diff --git a/solvers/gams/unsplittable-mcf/plot_qubo_solution.py b/solvers/gams/unsplittable-mcf/plot_qubo_solution.py new file mode 100644 index 00000000..7f040d8f --- /dev/null +++ b/solvers/gams/unsplittable-mcf/plot_qubo_solution.py @@ -0,0 +1,24 @@ +import os +import sys +from unsplittable_mcf import UnsplittableMCF + +arg_count = len(sys.argv) - 1 +if arg_count != 3: + raise TypeError( + f"This script expects exactly 3 arguments but got {arg_count}. " + "Unsplittable MCF instance file (argument 1), solution file (argument 2), and output HTML file (argument 3)." + ) + +unsplittable_mcf_path = sys.argv[1] +solution_path = sys.argv[2] +output_path = sys.argv[3] + +if os.path.exists(solution_path): + mapper = UnsplittableMCF(instance_file=unsplittable_mcf_path) + mapper.build_equations_and_model() + + mapper.plot_json_solution( + json_path=solution_path, output_html=output_path + ) +else: + print(f"Error: '{solution_path}' not found. Please run the external solver first.") diff --git a/solvers/gams/unsplittable-mcf/plot_unsplittable.py b/solvers/gams/unsplittable-mcf/plot_unsplittable.py new file mode 100644 index 00000000..bc0c584d --- /dev/null +++ b/solvers/gams/unsplittable-mcf/plot_unsplittable.py @@ -0,0 +1,376 @@ +from pyvis.network import Network +import re +import colorsys +import pandas as pd + +def _rgb_to_hex(rgb_color): + r, g, b = [int(c * 255) for c in rgb_color] + return f"#{r:02x}{g:02x}{b:02x}" + + +def generate_distinct_colors(n): + colors = [] + for i in range(n): + hue = i / (n if n > 0 else 1) + rgb = colorsys.hls_to_rgb(hue, 0.5, 0.7) + colors.append(_rgb_to_hex(rgb)) + return colors + + +def _parse_flow_input(flow_input): + """Parses list or DataFrame into a standard list of dicts.""" + flow_records = [] + + if flow_input is None: + return [] + + if isinstance(flow_input, pd.DataFrame): + # Filter for active flows if 'level' column exists + if "level" in flow_input.columns: + active_flows = flow_input[flow_input["level"] > 0.5] + else: + active_flows = flow_input + + for _, row in active_flows.iterrows(): + # Adjust indices based on GAMS domain order (v, t, v, t, o) + flow_records.append( + { + "u": row.iloc[0], + "t_u": row.iloc[1], + "v": row.iloc[2], + "t_v": row.iloc[3], + "order": row.iloc[4], + "value": 1, + } + ) + elif isinstance(flow_input, list): + flow_records = flow_input + + # Normalize time steps + for f in flow_records: + f["t_u"] = int(str(f["t_u"]).replace("t", "")) + f["t_v"] = int(str(f["t_v"]).replace("t", "")) + + return flow_records + + +def _get_node_info(node_name, time_step): + match = re.search(r"v_([a-zA-Z]+)(\d*)", str(node_name)) + if match: + style_key = match.group(1) + node_index = int(match.group(2)) if match.group(2) else 0 + label = f"{style_key}{node_index}\nt{time_step}" + if style_key == "source": + label = "Source" + if style_key == "magic": + label = "Slack" + return style_key, node_index, int(time_step), label + return "unknown", 0, int(time_step), str(node_name) + + +# --- Main Plotting Function (Single Solution) --- +def plot_mcf(filename="mcf_plot.html", flow_input=None, demand_records=None): + """Plots a single solution with detailed demand info and hover quantities.""" + if flow_input is None: + flow_input = [] + if demand_records is None and isinstance(flow_input, pd.DataFrame): + demand_records = pd.DataFrame() + + flow_records = _parse_flow_input(flow_input) + _generate_network(filename, flow_records, demand_records, mode="single") + + +# --- Comparison Function (Classical vs QUBO) --- +def plot_comparison( + filename="mcf_comparison.html", + classical_input=None, + qubo_input=None, + demand_records=None, +): + """Plots a comparison with detailed demand info and hover quantities.""" + if classical_input is None: + classical_input = [] + if qubo_input is None: + qubo_input = [] + + c_records = _parse_flow_input(classical_input) + d_records = _parse_flow_input(qubo_input) + + c_keys = set((r["u"], r["t_u"], r["v"], r["t_v"], r["order"]) for r in c_records) + d_keys = set((r["u"], r["t_u"], r["v"], r["t_v"], r["order"]) for r in d_records) + + combined_flows = [] + + # Matches (Green) + for k in c_keys.intersection(d_keys): + combined_flows.append( + { + "u": k[0], + "t_u": k[1], + "v": k[2], + "t_v": k[3], + "order": k[4], + "color": "green", + "status": "Match", + "width": 4, + } + ) + + # Classical Only (Blue) + for k in c_keys - d_keys: + combined_flows.append( + { + "u": k[0], + "t_u": k[1], + "v": k[2], + "t_v": k[3], + "order": k[4], + "color": "blue", + "status": "Classical Only", + "width": 2, + } + ) + + # QUBO Only (Red) + for k in d_keys - c_keys: + combined_flows.append( + { + "u": k[0], + "t_u": k[1], + "v": k[2], + "t_v": k[3], + "order": k[4], + "color": "red", + "status": "QUBO Only", + "width": 2, + } + ) + + print( + f"Comparison Stats: Matches={len(c_keys.intersection(d_keys))}, Classical_Only={len(c_keys-d_keys)}, QUBO_Only={len(d_keys-c_keys)}" + ) + + _generate_network(filename, combined_flows, demand_records, mode="compare") + + +# --- Internal Graph Generation Logic --- +def _generate_network(filename, flow_records, demand_records, mode="single"): + NODE_STYLES = { + "source": {"label": "Source", "level": 0}, + "fac": {"label": "Factory", "level": 1}, + "sort": {"label": "Sorting", "level": 2}, + "db": {"label": "DieBank", "level": 3}, + "asm": {"label": "Assembly", "level": 4}, + "test": {"label": "Testing", "level": 5}, + "dc": {"label": "Dist. Ctr", "level": 6}, + "magic": {"label": "Slack", "level": 7}, + } + + Y_TIME_SPACING, Y_UNIT_SPACING, X_SPACING = 120, 50, 250 + UNIT_COLORS = ["#ADD8E6", "#87CEEB", "#6495ED", "#4169E1", "#000080"] + + net = Network(height="100vh", width="100%", notebook=True, directed=True) + added_nodes = set() + edge_counts = {} + max_time_step = 0 + + # --- 1. Map Orders to Quantities --- + # We sum up the values for each order to get total flow quantity + order_qty_map = {} + if demand_records is not None and not demand_records.empty: + for _, row in demand_records.iterrows(): + # Robustly get 'o' and 'value' + o_val = row.get("o", row.iloc[0]) + val = row.get("value", row.iloc[-1]) # Value is typically last column + + # Aggregate if multiple entries per order (e.g. multiple products) + current_qty = order_qty_map.get(o_val, 0) + try: + current_qty += float(val) + except (ValueError, TypeError): + pass + order_qty_map[o_val] = current_qty + + # --- 2. Setup Colors --- + order_color_map = {} + if mode == "single": + unique_orders = sorted(list(set(f["order"] for f in flow_records))) + order_colors = generate_distinct_colors(len(unique_orders)) + order_color_map = {o: c for o, c in zip(unique_orders, order_colors)} + + # --- 3. Build Graph --- + for flow in flow_records: + u, tu, v, tv = flow["u"], flow["t_u"], flow["v"], flow["t_v"] + order = flow["order"] + max_time_step = max(max_time_step, tu, tv) + + source_id, target_id = f"{u},{tu}", f"{v},{tv}" + + # Add Nodes + for node_name, time, n_id in [(u, tu, source_id), (v, tv, target_id)]: + if n_id not in added_nodes: + key, idx, t, lbl = _get_node_info(node_name, time) + style = NODE_STYLES.get(key, {"level": 8}) + y_pos = (t * Y_TIME_SPACING) + (idx * Y_UNIT_SPACING) + if key == "source": + y_pos = 50 + + node_col = ( + UNIT_COLORS[idx % len(UNIT_COLORS)] + if key != "source" + else "#FFD700" + ) + if key == "magic": + node_col = "#A9A9A9" + + net.add_node( + n_id, + label=lbl, + level=style["level"], + x=style["level"] * X_SPACING, + y=y_pos, + color=node_col, + shape="circle", + physics=False, + ) + added_nodes.add(n_id) + + # Edge Logic + pair_key = tuple(sorted((source_id, target_id))) + edge_counts[pair_key] = edge_counts.get(pair_key, 0) + 1 + count = edge_counts[pair_key] + + smooth_opts = False + if count > 1: + smooth_opts = { + "type": "curvedCW", + "roundness": 0.2 * (count // 2) * (1 if count % 2 == 0 else -1), + } + + # Get Quantity for Hover + flow_qty = order_qty_map.get(order, "?") + + # Determine Color/Title + if mode == "compare": + color = flow.get("color", "grey") + width = flow.get("width", 2) + title = f"{flow['status']}\nOrder: {order}\nQty: {flow_qty}\n{u} -> {v}" + dashes = True if "magic" in str(u) or "magic" in str(v) else False + else: + color = order_color_map.get(order, "black") + width = 2 + dashes = True if "magic" in str(u) or "magic" in str(v) else False + if dashes: + color = "black" + title = f"Order: {order}\nQty: {flow_qty}\n{u} -> {v}" + + net.add_edge( + source_id, + target_id, + title=title, + color=color, + width=width, + dashes=dashes, + smooth=smooth_opts, + ) + + # --- 4. Visual Elements --- + # Time Guides + for t in range(max_time_step + 1): + y = t * Y_TIME_SPACING + net.add_node( + f"g_{t}_L", x=-100, y=y, label=f"t={t}", shape="text", physics=False + ) + net.add_node( + f"g_{t}_R", x=8 * X_SPACING, y=y, label=" ", shape="text", physics=False + ) + net.add_edge( + f"g_{t}_L", f"g_{t}_R", color="lightgrey", dashes=True, physics=False + ) + + # Legends + lx = (max([s["level"] for s in NODE_STYLES.values()]) + 1) * X_SPACING + ly = 0 + + if mode == "compare": + net.add_node( + "L_Title", + label="-- Comparison --", + x=lx, + y=ly, + shape="text", + font={"size": 20}, + physics=False, + ) + for lbl, col in [ + ("Match", "green"), + ("Classical Only", "blue"), + ("QUBO Only", "red"), + ]: + ly += 50 + net.add_node( + f"L_{lbl}", label=lbl, color=col, x=lx, y=ly, shape="box", physics=False + ) + else: + net.add_node( + "L_Title", + label="-- Orders --", + x=lx, + y=ly, + shape="text", + font={"size": 20}, + physics=False, + ) + for order, color in order_color_map.items(): + ly += 50 + net.add_node( + f"L_{order}", + label=f"Order {order}", + color=color, + x=lx, + y=ly, + shape="box", + physics=False, + ) + + # Demand Info (Printed on Canvas) + if demand_records is not None and not demand_records.empty: + ly += 80 + net.add_node( + "D_Title", + label="-- Demand --", + x=lx, + y=ly, + shape="text", + font={"size": 20}, + physics=False, + ) + ly += 40 + + for i, row in demand_records.iterrows(): + # Robust Extraction + o_val = row.get("o", row.iloc[0] if len(row) > 0 else "?") + p_val = row.get("p", row.iloc[1] if len(row) > 1 else None) + t_val = row.get("t", row.iloc[2] if len(row) > 2 else "?") + v_val = row.get("v", row.iloc[3] if len(row) > 3 else "?") + qty = row.get("value", row.iloc[4] if len(row) > 4 else "?") + + if p_val: + label_text = f"Order {o_val} ({p_val}): {qty}\nat {v_val} by {t_val}" + else: + label_text = f"Order {o_val}: {qty}\nat {v_val} by {t_val}" + + net.add_node( + f"Dem_{i}", + label=label_text, + x=lx, + y=ly, + shape="text", + font={"size": 14, "align": "left"}, + physics=False, + ) + ly += 50 + + net.set_options('{"physics": {"enabled": false}}') + print(f"Generating plot: {filename}") + net.show(filename) \ No newline at end of file diff --git a/solvers/gams/unsplittable-mcf/requirements.txt b/solvers/gams/unsplittable-mcf/requirements.txt new file mode 100644 index 00000000..bfaeb88b --- /dev/null +++ b/solvers/gams/unsplittable-mcf/requirements.txt @@ -0,0 +1,3 @@ +# This file describes the python package requirements for the GAMS unsplittable-mcf solver +gamspy == 1.23.1 +pyvis == 0.3.2 \ No newline at end of file diff --git a/solvers/gams/unsplittable-mcf/solver_classical.py b/solvers/gams/unsplittable-mcf/solver_classical.py new file mode 100644 index 00000000..55457e8f --- /dev/null +++ b/solvers/gams/unsplittable-mcf/solver_classical.py @@ -0,0 +1,29 @@ +""" +Classical CPLEX Solver for Unsplittable Multi Commodity Flow Problem. + +This solver uses GAMSPy to build the MCF model and solves it using CPLEX. +""" + +import sys +from unsplittable_mcf import UnsplittableMCF + +arg_count = len(sys.argv) - 1 +if arg_count != 2: + raise TypeError( + f"This script expects exactly 2 arguments but got {arg_count}. " + "Input file (argument 1) and output file (argument 2)." + ) + +input_path = sys.argv[1] +output_path = sys.argv[2] + +print(f"Loading instance from {input_path}...") + +# Create model from instance file +model = UnsplittableMCF(instance_file=input_path) + +print("Building equations and model...") +model.build_equations_and_model() + +print("\n--- Solving Classical MIP with CPLEX ---") +model.solve_classical(solver="CPLEX", output_html=output_path) diff --git a/solvers/gams/unsplittable-mcf/solver_lp_export.py b/solvers/gams/unsplittable-mcf/solver_lp_export.py new file mode 100644 index 00000000..f6c0ff1d --- /dev/null +++ b/solvers/gams/unsplittable-mcf/solver_lp_export.py @@ -0,0 +1,30 @@ +""" +LP Export for Unsplittable Multi Commodity Flow Problem. + +This solver exports the MCF model to LP format (QUBO reformulation). +""" + +import sys +from unsplittable_mcf import UnsplittableMCF + +arg_count = len(sys.argv) - 1 +if arg_count != 2: + raise TypeError( + f"This script expects exactly 2 arguments but got {arg_count}. " + "Input file (argument 1) and output file (argument 2)." + ) + +input_path = sys.argv[1] +output_path = sys.argv[2] + +print(f"Loading instance from {input_path}...") + +# Create model from instance file +model = UnsplittableMCF(instance_file=input_path) + +print("Building equations and model...") +model.build_equations_and_model() + +# Export to LP format (QUBO reformulation) +print(f"\n--- Exporting to LP format: {output_path} ---") +model.export_lp(output_path) diff --git a/solvers/gams/unsplittable-mcf/unsplittable_mcf.py b/solvers/gams/unsplittable-mcf/unsplittable_mcf.py new file mode 100644 index 00000000..b930660e --- /dev/null +++ b/solvers/gams/unsplittable-mcf/unsplittable_mcf.py @@ -0,0 +1,700 @@ +import json +import logging +import os + +import gamspy as gp +import numpy as np +import pandas as pd + + +class UnsplittableMCF: + DEFAULT_CONFIG = { + "N_PRODUCTS": 1, + "N_TIME_STEPS": 4, + "N_ORDERS": 1, + "N_FACTORIES": 1, + "N_SORT": 1, + "N_DIEBANK": 1, + "N_ASSEMBLY": 1, + "N_TEST": 1, + "N_DISTRIBUTION": 1, + "N_DEMAND": 1, + "MIN_CYCLE_TIME": 0, + "MAX_CYCLE_TIME": 1, + "MIN_CAPACITY": 10, + "MAX_CAPACITY": 15, + "MIN_LOAD_FACTOR": 1, + "MAX_LOAD_FACTOR": 2, + "PENALTY_DELAY": 5, + "PENALTY_SLACK": 40, + "PENALTY_CONSTANT": 50, + } + + def __init__(self, config_file="config.txt", instance_file=None): + self.m = gp.Container() + self.instance_data = {} + + if instance_file and os.path.exists(instance_file): + with open(instance_file, "r") as f: + self.instance_data = json.load(f) + + self.config = self.DEFAULT_CONFIG.copy() + self.config.update(self.instance_data.get("config", {})) + + self._build_base_symbols() + self._populate_from_data() + + elif config_file and os.path.exists(config_file): + self.config = self._read_config(config_file) + self._build_base_symbols() + self._generate_random_data() + + else: + raise FileNotFoundError( + "Must provide either a valid config_file or instance_file." + ) + + self._derive_network_parameters() + + def _read_config(self, filename): + cfg = self.DEFAULT_CONFIG.copy() + + with open(filename, "r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, val = line.split("=", 1) + clean_key = key.strip() + + if clean_key not in self.DEFAULT_CONFIG: + print(f"Warning: Ignoring unknown config key '{clean_key}'") + continue + + try: + cfg[clean_key] = int(val.strip()) + except ValueError: + cfg[clean_key] = val.strip() + return cfg + + def _build_base_symbols(self): + m = self.m + c = self.config + + self.p = gp.Set(m, "p", records=[f"p{i}" for i in range(c["N_PRODUCTS"])]) + self.t = gp.Set(m, "t", records=[f"t{i}" for i in range(c["N_TIME_STEPS"])]) + self.o = gp.Set(m, "o", records=[f"o{i}" for i in range(c["N_ORDERS"])]) + + self.v = gp.Set( + m, + "v", + records=( + ["v_source", "v_magic"] + + [f"v_fac{i}" for i in range(c["N_FACTORIES"])] + + [f"v_sort{i}" for i in range(c["N_SORT"])] + + [f"v_db{i}" for i in range(c["N_DIEBANK"])] + + [f"v_asm{i}" for i in range(c["N_ASSEMBLY"])] + + [f"v_test{i}" for i in range(c["N_TEST"])] + + [f"v_dc{i}" for i in range(c["N_DISTRIBUTION"])] + ), + description="nodes", + ) + + v, t, o, p = self.v, self.t, self.o, self.p + + # Alias + self.v1 = gp.Alias(m, "v1", alias_with=v) + self.v2 = gp.Alias(m, "v2", alias_with=v) + self.t1 = gp.Alias(m, "t1", alias_with=t) + self.t2 = gp.Alias(m, "t2", alias_with=t) + + # simple network nodes + self.v_source = gp.Set(m, "v_source", domain=v, records=["v_source"]) + self.v_magic = gp.Set(m, "v_magic", domain=v, records=["v_magic"]) + + self.v_factory = gp.Set( + m, + "v_factory", + domain=v, + records=[f"v_fac{i}" for i in range(c["N_FACTORIES"])], + ) + self.v_sort = gp.Set( + m, + "v_sort", + domain=v, + records=[f"v_sort{i}" for i in range(c["N_SORT"])], + ) + self.v_diebank = gp.Set( + m, + "v_diebank", + domain=v, + records=[f"v_db{i}" for i in range(c["N_DIEBANK"])], + ) + self.v_assembly = gp.Set( + m, + "v_assembly", + domain=v, + records=[f"v_asm{i}" for i in range(c["N_ASSEMBLY"])], + ) + self.v_test = gp.Set( + m, + "v_test", + domain=v, + records=[f"v_test{i}" for i in range(c["N_TEST"])], + ) + self.v_distribution = gp.Set( + m, + "v_distribution", + domain=v, + records=[f"v_dc{i}" for i in range(c["N_DISTRIBUTION"])], + ) + + # edges + self.e = gp.Set(m, "e", domain=[v, t, v, t], description="directed TEN arcs") + + # edges with corresponding orders + self.eo = gp.Set( + m, + "eo", + domain=[v, t, v, t, o], + description="directed TEN arcs that can be used for order o", + ) + self.eo_delay = gp.Set( + m, + "eo_delay", + domain=[v, t, v, t, o], + description="directed TEN arcs for delayed delivery for order o", + ) + self.eo_slack = gp.Set( + m, + "eo_slack", + domain=[v, t, v, t, o], + description="directed TEN arcs for unsatisfied demand for order o", + ) + + # mappings + self.o_map = gp.Set( + m, + "o_map", + domain=[o, p, t, self.v_distribution], + description="order-product-time-distribution_center mapping", + ) + self.o_map2 = gp.Set( + m, + "o_map2", + domain=[o, t, self.v_distribution], + description="order-time-distribution_center mapping", + ) + self.o_v_distribution = gp.Set( + m, + "o_v_distribution", + domain=[o, self.v_distribution], + description="order to distribution center mapping", + ) + self.o_t = gp.Set( + m, + "o_t", + domain=[o, t], + description="order to time step mapping", + ) + self.o_p = gp.Set( + m, + "o_p", + domain=[o, p], + description="order to product mapping", + ) + + # Parameters + self.demand = gp.Parameter(m, "demand", domain=[o, p, t, v]) + self.demand_o = gp.Parameter( + m, "demand_o", domain=[o, t, v], description="demand of order o" + ) + self.total_demand = gp.Parameter( + m, "total_demand", domain=o, description="total demand of order o" + ) + + self.cycle_time = gp.Parameter( + m, "cycle_time", domain=v, description="cycle time at node v" + ) + self.capacity = gp.Parameter( + m, "capacity", domain=v, description="capacity of node v" + ) + self.load_factor = gp.Parameter( + m, + "load_factor", + domain=[v, p], + description="capacity load factor of product p at node v", + ) + + self.cost_delay = gp.Parameter( + m, + "cost_delay", + domain=[v, t, v, t, o], + description="cost for a delayed order", + ) + self.cost_slack = gp.Parameter( + m, + "cost_slack", + domain=[v, t, v, t, o], + description="cost for unsatisfied orders", + ) + + def _generate_random_data(self): + """Generates ONLY the core base data that will be saved in the JSON.""" + np.random.seed(42) + c = self.config + v, t, o, p, e = self.v, self.t, self.o, self.p, self.e + + ( + v_source, + v_factory, + v_sort, + v_diebank, + v_assembly, + v_test, + v_distribution, + ) = ( + self.v_source, + self.v_factory, + self.v_sort, + self.v_diebank, + self.v_assembly, + self.v_test, + self.v_distribution, + ) + v1, v2, t1, t2 = self.v1, self.v2, self.t1, self.t2 + + p_list = p.records["uni"].tolist() + t_list = t.records["uni"].tolist() + v_dist_list = v_distribution.records["v"].tolist() + + demand_records = [] + for rec_o in o.records["uni"]: + p_choice = np.random.choice(p_list) + t_choice = np.random.choice(t_list) + v_choice = np.random.choice(v_dist_list) + val = np.random.randint(1, 5) * c["N_DEMAND"] + demand_records.append((rec_o, p_choice, t_choice, v_choice, val)) + + self.demand.setRecords(demand_records) + + self.cycle_time[v_factory] = np.random.randint( + c["MIN_CYCLE_TIME"], c["MAX_CYCLE_TIME"] + 1 + ) + self.cycle_time[v_sort] = np.random.randint( + c["MIN_CYCLE_TIME"], c["MAX_CYCLE_TIME"] + 1 + ) + self.cycle_time[v_diebank] = 0 + self.cycle_time[v_assembly] = np.random.randint( + c["MIN_CYCLE_TIME"], c["MAX_CYCLE_TIME"] + 1 + ) + self.cycle_time[v_test] = np.random.randint( + c["MIN_CYCLE_TIME"], c["MAX_CYCLE_TIME"] + 1 + ) + self.cycle_time[v_distribution] = 0 + + self.capacity[v_factory] = np.random.randint( + c["MIN_CAPACITY"], c["MAX_CAPACITY"] + 1 + ) + self.capacity[v_sort] = np.random.randint( + c["MIN_CAPACITY"], c["MAX_CAPACITY"] + 1 + ) + self.capacity[v_diebank] = gp.SpecialValues.POSINF + self.capacity[v_assembly] = np.random.randint( + c["MIN_CAPACITY"], c["MAX_CAPACITY"] + 1 + ) + self.capacity[v_test] = np.random.randint( + c["MIN_CAPACITY"], c["MAX_CAPACITY"] + 1 + ) + self.capacity[v_distribution] = gp.SpecialValues.POSINF + + self.load_factor[v_factory, p] = np.random.randint( + c["MIN_LOAD_FACTOR"], c["MAX_LOAD_FACTOR"] + 1 + ) + self.load_factor[v_sort, p] = np.random.randint( + c["MIN_LOAD_FACTOR"], c["MAX_LOAD_FACTOR"] + 1 + ) + self.load_factor[v_diebank, p] = 0 + self.load_factor[v_assembly, p] = np.random.randint( + c["MIN_LOAD_FACTOR"], c["MAX_LOAD_FACTOR"] + 1 + ) + self.load_factor[v_test, p] = np.random.randint( + c["MIN_LOAD_FACTOR"], c["MAX_LOAD_FACTOR"] + 1 + ) + self.load_factor[v_distribution, p] = 0 + + # graph structure is not really random, but will bes saved in json + e[v_source, t1, v_factory, t2].where[t1.first] = True + e[v_factory, t1, v_sort, t2].where[ + gp.Ord(t1) + self.cycle_time[v_factory] == gp.Ord(t2) + ] = True + e[v_sort, t1, v_diebank, t2].where[ + (gp.Ord(t1) + self.cycle_time[v_sort] == gp.Ord(t2)) + & gp.Sum(e[v, t, v_sort, t1], 1) + ] = True + e[v_diebank, t1, v_diebank, t2].where[ + (gp.Ord(t1) + 1 == gp.Ord(t2)) & gp.Sum(e[v, t, v_diebank, t1], 1) + ] = True + e[v_diebank, t1, v_assembly, t2].where[ + (gp.Ord(t1) + self.cycle_time[v_diebank] == gp.Ord(t2)) + & gp.Sum(e[v, t, v_diebank, t1], 1) + ] = True + e[v_assembly, t1, v_test, t2].where[ + (gp.Ord(t1) + self.cycle_time[v_assembly] == gp.Ord(t2)) + & gp.Sum(e[v, t, v_assembly, t1], 1) + ] = True + e[v_test, t1, v_distribution, t2].where[ + (gp.Ord(t1) + self.cycle_time[v_test] == gp.Ord(t2)) + & gp.Sum(e[v, t, v_test, t1], 1) + ] = True + e[v_distribution, t1, v_distribution, t2].where[ + (gp.Ord(t1) + 1 == gp.Ord(t2)) + ] = True + + self.eo[v1, t1, v2, t2, o].where[e[v1, t1, v2, t2]] = True + + print("Random base data successfully generated.") + + def _populate_from_data(self): + symbols_to_load = [ + "cycle_time", + "capacity", + "load_factor", + "demand", + "eo", + ] + + for sym in symbols_to_load: + if sym in self.instance_data: + records_df = pd.DataFrame(self.instance_data[sym]) + records_df = records_df.replace(1e100, gp.SpecialValues.POSINF) + gams_obj = getattr(self, sym) + gams_obj.setRecords(records_df) + + print("Base data successfully populated from JSON.") + + def _derive_network_parameters(self): + c = self.config + t, o, p = self.t, self.o, self.p + v1, v2, t1, t2 = self.v1, self.v2, self.t1, self.t2 + v_distribution, v_magic = self.v_distribution, self.v_magic + + self.o_map[o, p, t, v_distribution].where[ + self.demand[o, p, t, v_distribution] + ] = True + + self.o_map2[o, t, v_distribution].where[ + gp.Sum(p, self.o_map[o, p, t, v_distribution]) + ] = True + self.o_v_distribution[o, v_distribution].where[ + gp.Sum((p, t), self.o_map[o, p, t, v_distribution]) + ] = True + self.o_t[o, t].where[ + gp.Sum((p, v_distribution), self.o_map[o, p, t, v_distribution]) + ] = True + self.o_p[o, p].where[ + gp.Sum((t, v_distribution), self.o_map[o, p, t, v_distribution]) + ] = True + + self.e[v1, t1, v2, t2].where[gp.Sum(o, self.eo[v1, t1, v2, t2, o])] = True + + self.demand_o[o, t, v_distribution].where[self.o_map2[o, t, v_distribution]] = ( + gp.Sum(p, self.demand[o, p, t, v_distribution]) + ) + self.total_demand[o] = gp.Sum( + (p, t, v_distribution), self.demand[o, p, t, v_distribution] + ) + + self.eo_delay[v_distribution, t1, v_distribution, t2, o].where[ + self.o_t[o, t2] + & self.o_v_distribution[o, v_distribution] + & (gp.Ord(t1) > gp.Ord(t2)) + ] = True + + self.eo_slack[v_magic, t, v_distribution, t, o].where[ + self.o_t[o, t] & self.o_v_distribution[o, v_distribution] + ] = True + + self.cost_delay[v1, t1, v2, t2, o].where[self.eo_delay[v1, t1, v2, t2, o]] = ( + gp.Ord(t1) - gp.Ord(t2) + ) * c["PENALTY_DELAY"] + self.cost_slack[v1, t1, v2, t2, o].where[self.eo_slack[v1, t1, v2, t2, o]] = c[ + "PENALTY_SLACK" + ] + + def build_equations_and_model(self): + m = self.m + v, t, o, p, e = self.v, self.t, self.o, self.p, self.e + o_p = self.o_p + demand_o, total_demand = self.demand_o, self.total_demand + capacity, load_factor = ( + self.capacity, + self.load_factor, + ) + + v_source = self.v_source + eo_delay, eo_slack = ( + self.eo_delay, + self.eo_slack, + ) + cost_delay, cost_slack = self.cost_delay, self.cost_slack + + v1, v2, t1, t2 = self.v1, self.v2, self.t1, self.t2 + + # time expanded network nodes for all node and time steps that are in the graph + self.vt = gp.Set(m, "vt", domain=[v, t]) + self.vt[v, t].where[ + gp.Sum(self.e[v, t, v2, t2], 1) + gp.Sum(self.e[v1, t1, v, t], 1) > 0 + ] = True + + vt = self.vt + + # Variable + self.Y = gp.Variable( + m, + "Y", + type="binary", + domain=[v1, t1, v2, t2, o], + description="binary variable that decides if a arc is used for order o", + ) + Y = self.Y + + # Equations + eq_flow_balance = gp.Equation( + m, + "eq_flow_balance", + domain=[v, t, o], + description="flow conservation (includes demand satisfaction)", + ) + eq_capacity = gp.Equation( + m, + "eq_capacity", + domain=[v, t], + description="capacity constraint to restrict inflow", + ) + + obj = gp.Sum( + eo_delay[v1, t1, v2, t2, o], + Y[eo_delay] * total_demand[o] * cost_delay[eo_delay], + ) + gp.Sum( + eo_slack[v1, t1, v2, t2, o], + Y[eo_slack] * total_demand[o] * cost_slack[eo_slack], + ) + + # all in coming and out going flows need to be equal to the demand at that node + # demand is only not zero for the distribution centers + eq_flow_balance[v1, t1, o].where[vt[v1, t1] & ~v_source[v1]] = demand_o[ + o, t1, v1 + ] == gp.Sum(e[v2, t2, v1, t1], Y[e, o] * total_demand[o]) - gp.Sum( + e[v1, t1, v2, t2], Y[e, o] * total_demand[o] + ) + gp.Sum(eo_delay[v2, t2, v1, t1, o], Y[eo_delay] * total_demand[o]) - gp.Sum( + eo_delay[v1, t1, v2, t2, o], Y[eo_delay] * total_demand[o] + ) + gp.Sum(eo_slack[v2, t2, v1, t1, o], Y[eo_slack] * total_demand[o]) - gp.Sum( + eo_slack[v1, t1, v2, t2, o], Y[eo_slack] * total_demand[o] + ) + + eq_capacity[v1, t1].where[vt[v1, t1] & ~v_source[v1]] = ( + gp.Sum( + gp.Domain(e[v2, t2, v1, t1], o_p[o, p]), + Y[e, o] * total_demand[o] * load_factor[v1, p], + ) + <= capacity[v1] + ) + + self.mcf = gp.Model( + m, + "mcf", + problem="MIP", + equations=m.getEquations(), + sense=gp.Sense.MIN, + objective=obj, + ) + + print("GAMSPy Model successfully built.") + + def save_to_json(self, filename="instance_export.json"): + export_data = {"config": self.config} + + symbols_to_save = [ + "cycle_time", + "capacity", + "load_factor", + "demand", + "eo", + ] + + for sym in symbols_to_save: + gams_obj = getattr(self, sym) + if gams_obj.records is not None: + df = gams_obj.records.copy() + + if isinstance(gams_obj, gp.Set) and "element_text" in df.columns: + df = df.drop(columns=["element_text"]) + + # Map GAMSPy's infinity to 1e100 for the JSON export + df = df.replace([float("inf"), np.inf], 1e100) + + export_data[sym] = df.values.tolist() + + with open(filename, "w") as f: + json.dump(export_data, f, indent=4) + print(f"Instance saved to {filename}") + + def export_lp(self, filename="unsplittable_qubo.lp"): + import os + + from gamspy_qubo import Qubo + + if self.mcf is None: + raise ValueError("Model not built. Call build_model() before exporting.") + + print("\n--- Reformulating MIP to QUBO ---") + penalty = self.config["PENALTY_CONSTANT"] + + mcf_qubo = Qubo(self.mcf, name="unsplittable_qubo", penalty=penalty) + + print(f"--- Exporting QUBO to LP format: {filename} ---") + + abs_lp_path = os.path.abspath(filename) + + # Export the reformulated model using CPLEX. + # We expect a ResourceInterrupt Warning from the logger here, since the time limit is set to 0 + logger = logging.getLogger("MODEL") + logger.disabled = True + mcf_qubo.solve( + solver="CPLEX", + solver_options={ + "writelp": abs_lp_path, + "ResLim ": 0, + }, + options=gp.Options(generate_name_dict=False), + ) + logger.disabled = False + + + if os.path.exists(abs_lp_path): + print( + f"QUBO successfully exported to {abs_lp_path}. Ready for the external solver!" + ) + else: + print(f"Error: LP file was not found at {abs_lp_path}.") + + def solve_classical(self, solver=None, output_html="classical_routing.html"): + """Solves the standard MIP without QUBO reformulation and plots the solution.""" + from plot_unsplittable import plot_mcf + import pandas as pd + + if not hasattr(self, "mcf") or self.mcf is None: + raise ValueError("Model not built. Call build_model() before solving.") + + print("\n--- Solving Classical MIP ---") + self.mcf.solve(solver=solver) + + print("\n--- Classical Solve Results ---") + print(f"Model Status: {self.mcf.status}") + + if self.mcf.objective_value is not None: + print(f"Objective Value: {self.mcf.objective_value}") + + # Extract native solution + classical_flows = ( + self.Y.records.copy() if self.Y.records is not None else pd.DataFrame() + ) + + print("\n--- Generating Classical Plot ---") + plot_mcf( + filename=output_html, + flow_input=classical_flows, + demand_records=self.demand.records, + ) + print(f"Done! Open '{output_html}' in your browser.") + else: + print("No solution found.") + + def compare_and_plot( + self, json_path="solution.json", output_html="unsplittable_comparison.html" + ): + """Solves classically, injects the QUBO JSON solution, and plots the comparison.""" + import pandas as pd + from gamspy_qubo import Qubo + + from plot_unsplittable import plot_comparison + + if not hasattr(self, "mcf") or self.mcf is None: + raise ValueError("Model not built. Call build_model() before comparing.") + + print("\n--- 1. Solving Classical MIP ---") + self.mcf.solve(solver="CPLEX") + print(f"Classical Objective Value: {self.mcf.objective_value}") + + classical_flows = ( + self.Y.records.copy() if self.Y.records is not None else pd.DataFrame() + ) + + print("\n--- 2. Reformulating into QUBO ---") + penalty = self.config["PENALTY_CONSTANT"] + mcf_qubo = Qubo(self.mcf, name="unsplittable_qubo", penalty=penalty) + + print("\n--- 3. Injecting JSON Solution ---") + mcf_qubo.solve(solver="json", json_path=json_path) + + qubo_flows = ( + self.Y.records.copy() if self.Y.records is not None else pd.DataFrame() + ) + + print("\n--- 4. Generating Comparison Plot ---") + plot_comparison( + filename=output_html, + classical_input=classical_flows, + qubo_input=qubo_flows, + demand_records=self.demand.records, + ) + print(f"Done! Open '{output_html}' in your browser.") + + def plot_base_network(self, output_html="base_network.html"): + """Plots all possible permitted arcs in the generated Time-Expanded Network before solving.""" + from plot_unsplittable import plot_mcf + + if not hasattr(self, "eo") or self.eo.records is None: + raise ValueError( + "Network not generated. Initialize the class with data first." + ) + + print("\n--- Generating Base Network Plot ---") + # self.eo.records contains all allowed arcs [v, t, v, t, o] + plot_mcf( + filename=output_html, + flow_input=self.eo.records, + demand_records=self.demand.records, + ) + print(f"Done! Open '{output_html}' in your browser.") + + def plot_json_solution( + self, json_path="solution.json", output_html="json_only_solution.html" + ): + """Loads the QUBO JSON solution and plots only the quantum/external routing.""" + import pandas as pd + from gamspy_qubo import Qubo + + from plot_unsplittable import plot_mcf + + if not hasattr(self, "mcf") or self.mcf is None: + raise ValueError("Model not built. Call build_model() before plotting.") + + print("\n--- Reformulating into QUBO ---") + penalty = self.config["PENALTY_CONSTANT"] + mcf_qubo = Qubo(self.mcf, name="unsplittable_qubo", penalty=penalty) + + print(f"\n--- Injecting JSON Solution from {json_path} ---") + mcf_qubo.solve(solver="json", json_path=json_path) + + qubo_flows = ( + self.Y.records.copy() if self.Y.records is not None else pd.DataFrame() + ) + + print("\n--- Generating JSON-Only Plot ---") + plot_mcf( + filename=output_html, + flow_input=qubo_flows, + demand_records=self.demand.records, + ) + print(f"Done! Open '{output_html}' in your browser.") diff --git a/solvers/qiskit/knapsack/knapsack_qiskit.py b/solvers/qiskit/knapsack/knapsack_qiskit.py index ae6416cd..7c27712f 100644 --- a/solvers/qiskit/knapsack/knapsack_qiskit.py +++ b/solvers/qiskit/knapsack/knapsack_qiskit.py @@ -2,12 +2,13 @@ import sys # Qiskit -from qiskit.primitives import Sampler -from qiskit_algorithms.optimizers import COBYLA +from qiskit.primitives import StatevectorSampler +from qiskit_optimization.minimum_eigensolvers import QAOA +from qiskit_optimization.optimizers import COBYLA from qiskit_optimization.algorithms import MinimumEigenOptimizer from qiskit_optimization.applications import Knapsack from qiskit_optimization.converters import QuadraticProgramToQubo -from qiskit_algorithms import QAOA + arg_count = len(sys.argv) - 1 if arg_count != 2: @@ -47,7 +48,7 @@ # TODO: Sampler() has to be replaces with StatevectorSampler() in newer versions. # (currently not yet supported by qiskit-optimization) # TODO: add a dedicated mixer -qaoa_mes = QAOA(sampler=Sampler(), optimizer=COBYLA()) +qaoa_mes = QAOA(sampler=StatevectorSampler(), optimizer=COBYLA()) qaoa = MinimumEigenOptimizer(qaoa_mes) qaoa_result = qaoa.solve(qubo) diff --git a/solvers/qiskit/knapsack/requirements.txt b/solvers/qiskit/knapsack/requirements.txt index 3f34f649..e42d620d 100644 --- a/solvers/qiskit/knapsack/requirements.txt +++ b/solvers/qiskit/knapsack/requirements.txt @@ -1,4 +1,4 @@ # This file describes the python package requirements for the Qiskit Knapsack solver -qiskit == 1.4.2 -qiskit-optimization == 0.6.1 +qiskit == 2.2.3 +qiskit-optimization == 0.7.0 diff --git a/solvers/qiskit/materialsimulation/material_simulation_qiskit.py b/solvers/qiskit/materialsimulation/material_simulation_qiskit.py new file mode 100644 index 00000000..b06a5704 --- /dev/null +++ b/solvers/qiskit/materialsimulation/material_simulation_qiskit.py @@ -0,0 +1,68 @@ +import argparse +from qiskit_nature.units import DistanceUnit +from qiskit_nature.second_q.drivers import PySCFDriver +from qiskit_nature.second_q.mappers import JordanWignerMapper +from qiskit_algorithms import VQE +from qiskit_algorithms.optimizers import SLSQP +from qiskit.primitives import Estimator +from qiskit_nature.second_q.algorithms import GroundStateEigensolver, QEOM, EvaluationRule +from qiskit_nature.second_q.circuit.library import HartreeFock, UCCSD + +parser = argparse.ArgumentParser( + prog="Qiskit Material Simulation", + description="A CLI Program to run Qiskit a Material Simulation" +) + + +def get_problem(molecule, charge, spin): + driver = PySCFDriver( + atom=molecule, + basis="sto3g", + charge=charge, + spin=spin, + unit=DistanceUnit.ANGSTROM, + ) + return driver.run() + + +def get_ansatz(problem, mapper): + ansatz = UCCSD( + problem.num_spatial_orbitals, + problem.num_particles, + mapper, + initial_state=HartreeFock( + problem.num_spatial_orbitals, + problem.num_particles, + mapper + ) + ) + return ansatz + + +parser.add_argument("input_file") +parser.add_argument("output_file") +parser.add_argument("--charge", type=int, default=0, help="Charge of the molecule") +parser.add_argument("--spin", type=int, default=0, help="Spin of the molecule") +args = parser.parse_args() + +with open(args.input_file, 'r') as input_file: + input_molecule = input_file.read().strip() + +es_problem = get_problem(input_molecule, args.charge, args.spin) +mapper = JordanWignerMapper() +ansatz = get_ansatz(es_problem, mapper) + +estimator = Estimator() +# This first part sets the ground state solver +solver = VQE(estimator, ansatz, SLSQP()) +solver.initial_point = [0.0] * ansatz.num_parameters +gse = GroundStateEigensolver(mapper, solver) + +# The qEOM algorithm is simply instantiated with the chosen ground state solver and Estimator primitive +qeom_excited_states_solver = QEOM(gse, estimator, "sd", EvaluationRule.ALL) + +# The qEOM algorithm is then run on the problem +qeom_result = qeom_excited_states_solver.solve(es_problem) + +with open(args.output_file, 'w') as f: + f.write(str(qeom_result)) \ No newline at end of file diff --git a/solvers/qiskit/materialsimulation/requirements.txt b/solvers/qiskit/materialsimulation/requirements.txt new file mode 100644 index 00000000..3b430447 --- /dev/null +++ b/solvers/qiskit/materialsimulation/requirements.txt @@ -0,0 +1,4 @@ +qiskit == 1.0.2 +qiskit-algorithms == 0.3.1 +qiskit-nature == 0.7.2 +pyscf == 2.9.0 \ No newline at end of file diff --git a/solvers/qiskit/max-cut/maxCut_qiskit.py b/solvers/qiskit/max-cut/maxCut_qiskit.py index 2cf67fc4..2fda6cb3 100644 --- a/solvers/qiskit/max-cut/maxCut_qiskit.py +++ b/solvers/qiskit/max-cut/maxCut_qiskit.py @@ -16,14 +16,14 @@ # Qiskit from qiskit.circuit.library import TwoLocal -from qiskit.primitives import Sampler +from qiskit.primitives import StatevectorSampler from qiskit_optimization.applications import Maxcut -from qiskit_algorithms import SamplingVQE -from qiskit_algorithms.optimizers import SPSA +from qiskit_optimization.minimum_eigensolvers import SamplingVQE +from qiskit_optimization.optimizers import SPSA #if len(sys.argv) != 3: -# raise TypeError('This script expects exactly 2 arguments. Input file (argument 1) and output file (argument 2).') +# raise ValueError('This script expects exactly 2 arguments. Input file (argument 1) and output file (argument 2).') input_path = sys.argv[1] output_path = sys.argv[2] @@ -86,7 +86,7 @@ # construct VQE optimizer=SPSA(maxiter=300) ry = TwoLocal(qubitOp.num_qubits, "ry", "cz", reps=5, entanglement="linear") -vqe = SamplingVQE(sampler=Sampler(), ansatz=ry, optimizer=optimizer) +vqe = SamplingVQE(sampler=StatevectorSampler(), ansatz=ry, optimizer=optimizer) # run VQE result = vqe.compute_minimum_eigenvalue(qubitOp) diff --git a/solvers/qiskit/max-cut/requirements.txt b/solvers/qiskit/max-cut/requirements.txt index 7823972c..d32d0f32 100644 --- a/solvers/qiskit/max-cut/requirements.txt +++ b/solvers/qiskit/max-cut/requirements.txt @@ -1,6 +1,6 @@ # This file describes the python package requirements for the Qiskit Max-Cut solver pygmlparser == 1.0.1 -qiskit == 1.4.2 -qiskit-optimization == 0.6.1 +qiskit == 2.2.3 +qiskit-optimization == 0.7.0 numpy networkx diff --git a/solvers/qiskit/qubo/qp_converter/QpConverter.py b/solvers/qiskit/qubo/qp_converter/QpConverter.py deleted file mode 100644 index fa1c2f19..00000000 --- a/solvers/qiskit/qubo/qp_converter/QpConverter.py +++ /dev/null @@ -1,125 +0,0 @@ -import re - -from qiskit_optimization import QuadraticProgram - -def parse_lp_file(file_path): - # Read the LP file - with open(file_path, 'r') as file: - lines = file.readlines() - - # Initialize components for parsing - objective_sense = 'minimize' # default - objective = '' - constraints = [] - variable_types = {} - in_objective_section = False - in_constraints_section = False - in_binary_section = False - - # Parsing logic - for line in lines: - line = line.strip() - - # Objective function - if line.lower().startswith('minimize') or line.lower().startswith('maximize'): - in_objective_section = True - objective_sense = line.split()[0].lower() - continue - - if in_objective_section and (line.lower().startswith('subject to') or line.lower().startswith('binary')): - in_objective_section = False - continue - - if in_objective_section: - objective += line - continue - - # Constraints (no constraints in this example, but you may extend it later) - if line.lower().startswith('subject to'): - in_constraints_section = True - continue - elif in_constraints_section and line.lower().startswith('binary'): - in_constraints_section = False - - # Binary variables - if line.lower().startswith('binary'): - in_binary_section = True - continue - - if in_binary_section: - variables = line.split() - for var in variables: - variable_types[var] = 'binary' - - return objective_sense, objective, constraints, variable_types - - -def parse_objective(objective_str): - """ Parse the objective function to extract linear and quadratic coefficients """ - # Remove the 'obj:' prefix - objective_str = re.sub(r'^obj:\s*', '', objective_str) - - # Extract and remove the division factor (e.g., "/ 2", "/ 3", "/ 10") - division_factor_match = re.search(r'/\s*([\d\.]+)', objective_str) - division_factor = float(division_factor_match.group(1)) if division_factor_match else 1.0 - objective_str = re.sub(r'/\s*[\d\.]+', '', objective_str) - - # Remove square brackets - objective_str = objective_str.replace('[', '').replace(']', '') - - # Extract terms using regular expression - term_pattern = re.compile(r'([+-]?\s*\d*\.?\d+(?:e[+-]?\d+)?)\s*\*?\s*([\w\d]+)\s*(?:\*\s*([\w\d]+))?') - terms = term_pattern.findall(objective_str) - linear = {} - quadratic = {} - - # Parse each term - for coeff_str, var1, var2 in terms: - coeff = float(coeff_str.replace(' ', '')) # Remove any spaces in coefficient - - if var2: # Quadratic term - if (var1, var2) in quadratic: - quadratic[(var1, var2)] += coeff - else: - quadratic[(var1, var2)] = coeff - else: # Linear term - if var1 in linear: - linear[var1] += coeff - else: - linear[var1] = coeff - - # Apply division factor - if division_factor != 1.0 and division_factor != 0: - for key in linear: - linear[key] /= division_factor - for key in quadratic: - quadratic[key] /= division_factor - - return linear, quadratic - - -def convert_to_quadratic_program(lp_file_path): - # Parse the LP file - objective_sense, objective, constraints, variable_types = parse_lp_file(lp_file_path) - - # Parse objective components - linear, quadratic = parse_objective(objective) - - # Create an instance of QuadraticProgram - qp = QuadraticProgram() - - # Add variables - for var in variable_types: - if variable_types[var] == 'binary': - qp.binary_var(var) - - # Set the objective sense - if objective_sense == 'minimize': - qp.minimize(linear=linear, quadratic=quadratic) - else: - qp.maximize(linear=linear, quadratic=quadratic) - - # Add constraints (empty in this example) - # You can extend this logic to handle any constraints - - return qp \ No newline at end of file diff --git a/solvers/qiskit/qubo/qubo_qiskit.py b/solvers/qiskit/qubo/qubo_qiskit.py index d7b3469d..44faf0d6 100644 --- a/solvers/qiskit/qubo/qubo_qiskit.py +++ b/solvers/qiskit/qubo/qubo_qiskit.py @@ -1,25 +1,52 @@ +import os import sys -from qp_converter import QpConverter +import gurobipy as gp +from qiskit_optimization import QuadraticProgram -from qiskit.primitives import Sampler -from qiskit_algorithms import QAOA -from qiskit_algorithms.optimizers import COBYLA +# Import parse_qubo_from_lp from sibling _utility directory +# Add the parent (solvers) directory to enable the import +script_dir = os.path.dirname(os.path.abspath(__file__)) +solvers_dir = os.path.join(script_dir, '..', '..') +sys.path.insert(0, solvers_dir) + +from _utility.parse_qubo_from_lp import parse_qubo_from_lp + +from qiskit.primitives import StatevectorSampler +from qiskit_optimization.minimum_eigensolvers import QAOA +from qiskit_optimization.optimizers import COBYLA from qiskit_optimization.algorithms import MinimumEigenOptimizer if len(sys.argv) != 3: - raise TypeError('This script expects exactly 2 arguments. Input file (argument 1) and output file (argument 2).') + raise ValueError('This script expects exactly 2 arguments. Input file (argument 1) and output file (argument 2).') input_path = sys.argv[1] output_path = sys.argv[2] -qubo = QpConverter.convert_to_quadratic_program(input_path) +model = gp.read(input_path) +(vars, quadratic_terms, linear_terms, constant_offset) = parse_qubo_from_lp(model) + +qubo = QuadraticProgram() + +# Add variables +for var in vars: + qubo.binary_var(var) + +# Set the objective sense +if model.ModelSense == 1: + qubo.minimize(constant=constant_offset, linear=linear_terms, quadratic=quadratic_terms) +else: + qubo.maximize(constant=constant_offset, linear=linear_terms, quadratic=quadratic_terms) + +# Add constraints (empty in this example) +# You can extend this logic to handle any constraints + # parse LP file: print(qubo.prettyprint()) # TODO: Sampler() has to be replaces with StatevectorSampler() in newer versions. # (currently not yet supported by qiskit-optimization) # TODO: add a dedicated mixer -qaoa_mes = QAOA(sampler=Sampler(), optimizer=COBYLA()) +qaoa_mes = QAOA(sampler=StatevectorSampler(), optimizer=COBYLA()) qaoa = MinimumEigenOptimizer(qaoa_mes) qaoa_result = qaoa.solve(qubo) diff --git a/solvers/qiskit/qubo/requirements.txt b/solvers/qiskit/qubo/requirements.txt index 04bd3e61..c96f4cbd 100644 --- a/solvers/qiskit/qubo/requirements.txt +++ b/solvers/qiskit/qubo/requirements.txt @@ -1,3 +1,4 @@ # This file describes the python package requirements for the Qiskit QUBO solver -qiskit == 1.4.2 -qiskit-optimization == 0.6.1 +gurobipy == 13.0.2 +qiskit == 2.2.3 +qiskit-optimization == 0.7.0 diff --git a/solvers/qrisp/qubo/requirements.txt b/solvers/qrisp/qubo/requirements.txt index 10cb2e71..c3de9c57 100644 --- a/solvers/qrisp/qubo/requirements.txt +++ b/solvers/qrisp/qubo/requirements.txt @@ -1,4 +1,5 @@ # This file describes the python package requirements for the Qrisp QUBO solver qrisp == 0.4.4 numpy == 1.26.4 -gurobipy == 11.0.1 # for lp file reading \ No newline at end of file +gurobipy == 13 # for lp file reading +sympy == 1.12.1 # needed but not specified in qrisp requirements \ No newline at end of file diff --git a/solvers/qrisp/sat/requirements.txt b/solvers/qrisp/sat/requirements.txt index 30f9c9c3..b6c8cce0 100644 --- a/solvers/qrisp/sat/requirements.txt +++ b/solvers/qrisp/sat/requirements.txt @@ -1,3 +1,4 @@ # This file describes the python package requirements for the Qrisp SAT solver qrisp == 0.4.4 -numpy == 1.26.4 \ No newline at end of file +numpy == 1.26.4 +sympy == 1.12.1 # needed but not specified in qrisp requirements \ No newline at end of file diff --git a/solvers/qrisp/vrp/requirements.txt b/solvers/qrisp/vrp/requirements.txt index c7737696..c6f0918b 100644 --- a/solvers/qrisp/vrp/requirements.txt +++ b/solvers/qrisp/vrp/requirements.txt @@ -1,4 +1,5 @@ # This file describes the python package requirements for the Qrisp VRP solver qrisp == 0.4.4 numpy == 1.26.4 -tsplib95 == 0.7.1 # for tsplib file parsing \ No newline at end of file +tsplib95 == 0.7.1 # for tsplib file parsing +sympy == 1.12.1 # needed but not specified in qrisp requirements \ No newline at end of file diff --git a/src/main/java/edu/kit/provideq/toolbox/Solution.java b/src/main/java/edu/kit/provideq/toolbox/Solution.java index 6b29f8c0..cb55ac3a 100644 --- a/src/main/java/edu/kit/provideq/toolbox/Solution.java +++ b/src/main/java/edu/kit/provideq/toolbox/Solution.java @@ -33,13 +33,13 @@ private Solution(UUID id) { this.id = id; } - public Solution(ProblemSolver problemSolver) { + public Solution(ProblemSolver problemSolver) { this.id = UUID.randomUUID(); this.solverName = problemSolver.getName(); } - public static Solution failed( - ProblemSolver problemSolver, + public static Solution failed( + ProblemSolver problemSolver, String text) { var solution = new Solution(problemSolver); solution.fail(); @@ -47,6 +47,20 @@ public static Solution failed( return solution; } + public static Solution from( + ProblemSolver solver, + Solution source, + Function mapper) { + var result = new Solution(); + result.solverName = source.solverName; + result.status = source.status; + result.metaData = source.metaData; + result.debugData = source.debugData; + result.executionMilliseconds = source.executionMilliseconds; + result.solutionData = source.solutionData == null ? null : mapper.apply(source.solutionData); + return result; + } + public UUID getId() { return this.id; } diff --git a/src/main/java/edu/kit/provideq/toolbox/SolverClassNameValidator.java b/src/main/java/edu/kit/provideq/toolbox/SolverClassNameValidator.java new file mode 100644 index 00000000..9de2f134 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/SolverClassNameValidator.java @@ -0,0 +1,42 @@ +package edu.kit.provideq.toolbox; + +import edu.kit.provideq.toolbox.meta.ProblemManager; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +/** + * Validates that all solver class names are unique upon application startup. + */ +@Component +public class SolverClassNameValidator implements ApplicationListener { + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + List solverClassNames = event.getApplicationContext() + .getBeansOfType(ProblemManager.class) + .values() + .stream() + .map(manager -> (ProblemManager) manager) + .flatMap(manager -> manager.getSolvers().stream()) + .map(solver -> solver.getClass().getSimpleName()) + .toList(); + + var duplicates = solverClassNames + .stream() + .collect(Collectors.groupingBy(name -> name, Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .toList(); + + if (!duplicates.isEmpty()) { + throw new IllegalStateException( + "Duplicate solver class names found: " + String.join(", ", duplicates) + ); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/BoundComparisonDto.java b/src/main/java/edu/kit/provideq/toolbox/api/BoundComparisonDto.java new file mode 100644 index 00000000..b6bbc33f --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/BoundComparisonDto.java @@ -0,0 +1,73 @@ +package edu.kit.provideq.toolbox.api; + +import edu.kit.provideq.toolbox.BoundWithInfo; + +/** + * A DTO for a comparison between a bound and a solution. + */ +public class BoundComparisonDto { + private BoundDto bound; + private float comparison; + + /** + * Creates a new comparison DTO out of a comparison value and a bound. + * + * @param comparison the comparison value, e.g., the ratio of the solution to the bound + * @param bound a bound + */ + public BoundComparisonDto(float comparison, BoundWithInfo bound) { + this.comparison = comparison; + this.bound = new BoundDto(bound); + } + + /** + * Creates a new comparison DTO out of a bound and with an uninitialized comparison value. + * + * @param bound a bound + */ + public BoundComparisonDto(BoundWithInfo bound) { + this.comparison = -1; + this.bound = new BoundDto(bound); + } + + public BoundComparisonDto() { + this.comparison = -1; + this.bound = null; + } + + @Override public String toString() { + return "Comparison{bound=%s, comparison=%f}" + .formatted(bound, comparison); + } + + /** + * Gets the bound of the comparison as a BoundDto. + * + * @return the bound of the comparison, e.g., the best known solution or the optimal solution + */ + public BoundDto getBound() { + return bound; + } + + public void setBound(BoundWithInfo bound) { + this.bound = new BoundDto(bound); + } + + public boolean hasBound() { + return bound != null; + } + + /** + * Gets the comparison value of the comparison. + * + * @return the comparison value, e.g., the ratio of the solution to the bound. + */ + public float getComparison() { + return comparison; + } + + public void setComparison(float comparison) { + this.comparison = comparison; + } + +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java b/src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java index e23356fa..cc90a5d2 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java @@ -10,4 +10,8 @@ public BoundDto(BoundWithInfo boundWithInfo) { boundWithInfo.bound().boundType(), boundWithInfo.executionTime()); } + + public BoundDto() { + this(-1, BoundType.LOWER, -1); + } } diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ComparisonDto.java b/src/main/java/edu/kit/provideq/toolbox/api/ComparisonDto.java deleted file mode 100644 index ad2d4ed2..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/api/ComparisonDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package edu.kit.provideq.toolbox.api; - -import edu.kit.provideq.toolbox.BoundWithInfo; -import edu.kit.provideq.toolbox.Solution; - -public record ComparisonDto(float comparison, BoundWithInfo bound, Solution solution) { - @Override public String toString() { - return "Comparison{comparison=%f, bound=%s, solution=%s}" - .formatted(comparison, bound, solution); - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java index 71fd772f..f0ab059c 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java @@ -18,7 +18,6 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.regex.Pattern; import org.springdoc.core.fn.builders.operation.Builder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -90,7 +89,7 @@ private Mono handleGet( Mono bound; try { problem.estimateBound(); - bound = Mono.just(new BoundDto(problem.getBound().orElseThrow())); + bound = Mono.just(problem.getBound().orElseThrow()); } catch (IllegalStateException | NoSuchElementException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } @@ -114,22 +113,14 @@ private Mono handleCompare( throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Bound not estimated yet!"); } - float bound = problem.getBound().get().bound().value(); - var pattern = Pattern.compile(manager.getType().getSolutionPattern()); - var solutionMatcher = pattern.matcher(problem.getSolution().get().getSolutionData().toString()); - float solutionValue; - if (solutionMatcher.find()) { - solutionValue = Float.parseFloat(solutionMatcher.group(1)); - } else { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Could not parse solution value!"); + try { + problem.compareBound(); + } catch (IllegalStateException | NoSuchElementException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } - float comparison = problem.getBound().get().bound().boundType().compare(bound, solutionValue); - ComparisonDto comparisonDto = new ComparisonDto( - comparison, - problem.getBound().get(), - problem.getSolution().get() - ); + var comparisonDto = problem.getBoundWithComparison(); + return ok().body(Mono.just(comparisonDto), new ParameterizedTypeReference<>() { }); } @@ -176,7 +167,7 @@ private static org.springdoc.core.fn.builders.content.Builder getOkResponseConte private static org.springdoc.core.fn.builders.content.Builder comparisonOkResponseContent() { return contentBuilder() .mediaType(APPLICATION_JSON_VALUE) - .schema(schemaBuilder().implementation(ComparisonDto.class)); + .schema(schemaBuilder().implementation(BoundComparisonDto.class)); } private Problem findProblemOrThrow( diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ProblemAttributesRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/ProblemAttributesRouter.java new file mode 100644 index 00000000..789976c7 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/ProblemAttributesRouter.java @@ -0,0 +1,177 @@ +package edu.kit.provideq.toolbox.api; + +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; +import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +import com.google.common.collect.Streams; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import edu.kit.provideq.toolbox.meta.ProblemType; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import org.springdoc.core.fn.builders.operation.Builder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Mono; + +/** + * This router handles requests regarding {@link Problem} instance attributes. + */ +@Configuration +@EnableWebFlux +public class ProblemAttributesRouter { + public static final String PROBLEM_ID_PARAM_NAME = "problemId"; + public static final String ATTRIBUTE_ID_PARAM_NAME = "attributeId"; + private ProblemManagerProvider managerProvider; + + @Bean + RouterFunction getAttributeRoutes() { + var managers = managerProvider.getProblemManagers(); + return Streams.concat( + managers.stream().map(this::defineGetRoute), + managers.stream().map(this::defineListRoute) + ).reduce(RouterFunction::and) + .orElse(null); + } + + /** + * Estimate Operation: GET /problems/TYPE/{problemId}/attributes/{attributeId}. + */ + private RouterFunction defineGetRoute(ProblemManager manager) { + return route().GET( + getAttributeRouteForProblemType(manager.getType()), + accept(APPLICATION_JSON), + req -> handleGet(manager, req), + ops -> handleGetDocumentation(manager, ops) + ).build(); + } + + private Mono handleGet( + ProblemManager manager, + ServerRequest req + ) { + var problemId = req.pathVariable(PROBLEM_ID_PARAM_NAME); + var attributeId = req.pathVariable(ATTRIBUTE_ID_PARAM_NAME); + var problem = RouterUtility.findProblemOrThrow(manager, problemId); + + try { + var attributeGetter = manager.getType().getAttributes().get(attributeId); + String attribute = attributeGetter.apply(problem.getInput().orElseThrow()); + return ok().body(Mono.just(attribute), new ParameterizedTypeReference<>() { + }); + } catch (IllegalStateException | NoSuchElementException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } + + private void handleGetDocumentation( + ProblemManager manager, + Builder ops + ) { + ProblemType problemType = manager.getType(); + ops + .operationId(getAttributeRouteForProblemType(problemType)) + .tag(problemType.getId()) + .description("Calculates the attribute for the given problem id and attribute id.") + .parameter(parameterBuilder().in(ParameterIn.PATH).name(PROBLEM_ID_PARAM_NAME)) + .parameter(parameterBuilder().in(ParameterIn.PATH).name(ATTRIBUTE_ID_PARAM_NAME)) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(getOkResponseContent()) + ); + } + + /** + * Estimate Operation: GET /problems/TYPE/{problemId}/attributes. + */ + private RouterFunction defineListRoute(ProblemManager manager) { + return route().GET( + getAttributesRouteForProblemType(manager.getType()), + accept(APPLICATION_JSON), + req -> handleList(manager, req), + ops -> handleListDocumentation(manager, ops) + ).build(); + } + + private Mono handleList( + ProblemManager manager, + ServerRequest req + ) { + var problemId = req.pathVariable(PROBLEM_ID_PARAM_NAME); + var problem = RouterUtility.findProblemOrThrow(manager, problemId); + + try { + var attributeValues = manager.getType().getAttributes() + .entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().apply(problem.getInput().orElseThrow()) + ) + ); + return ok().body(Mono.just(attributeValues), new ParameterizedTypeReference<>() { + }); + } catch (IllegalStateException | NoSuchElementException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } + + private void handleListDocumentation( + ProblemManager manager, + Builder ops + ) { + ProblemType problemType = manager.getType(); + ops + .operationId(getAttributesRouteForProblemType(problemType)) + .tag(problemType.getId()) + .description("Calculates all attributes for the given problem id.") + .parameter(parameterBuilder().in(ParameterIn.PATH).name(PROBLEM_ID_PARAM_NAME)) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(getOkResponseContent()) + ); + } + + private static org.springdoc.core.fn.builders.content.Builder getOkResponseContent() { + return contentBuilder() + .mediaType(APPLICATION_JSON_VALUE) + .schema(schemaBuilder().implementation(String.class)); + } + + private String getAttributeRouteForProblemType(ProblemType type) { + return "/problems/%s/{%s}/attributes/{%s}".formatted( + type.getId(), + PROBLEM_ID_PARAM_NAME, + ATTRIBUTE_ID_PARAM_NAME); + } + + private String getAttributesRouteForProblemType(ProblemType type) { + return "/problems/%s/{%s}/attributes".formatted( + type.getId(), + PROBLEM_ID_PARAM_NAME); + } + + @Autowired + void setManagerProvider(ProblemManagerProvider managerProvider) { + this.managerProvider = managerProvider; + } + +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java b/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java index 954e2f0c..239787c3 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java @@ -1,6 +1,5 @@ package edu.kit.provideq.toolbox.api; -import edu.kit.provideq.toolbox.BoundWithInfo; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.meta.Problem; import edu.kit.provideq.toolbox.meta.ProblemSolver; @@ -17,8 +16,8 @@ public class ProblemDto { private String typeId; private InputT input; private Solution solution; - private BoundWithInfo bound; private ProblemState state; + private BoundComparisonDto boundWithComparison; private String solverId; private List solverSettings; private List subProblems; @@ -40,7 +39,7 @@ public static ProblemDto fromProblem( dto.typeId = problem.getType().getId(); dto.input = problem.getInput().orElse(null); dto.solution = problem.getSolution().orElse(null); - dto.bound = problem.getBound().orElse(null); + dto.boundWithComparison = problem.getBoundWithComparison().orElse(null); dto.state = problem.getState(); dto.solverId = problem.getSolver() .map(ProblemSolver::getId) @@ -70,8 +69,8 @@ public Solution getSolution() { return solution; } - public BoundWithInfo getBound() { - return bound; + public BoundComparisonDto getBoundWithComparison() { + return boundWithComparison; } public ProblemState getState() { @@ -107,7 +106,7 @@ public String toString() { + ", solverId=" + solverId + ", input=" + input + ", solution=" + solution - + ", value=" + bound + + ", boundWithComparison=" + boundWithComparison + ", solverSettings=" + solverSettings + ", subProblems=" + subProblems + '}'; diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouteDocumentation.java b/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouteDocumentation.java index 84668549..799d4de9 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouteDocumentation.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouteDocumentation.java @@ -141,7 +141,6 @@ private static org.springdoc.core.fn.builders.apiresponse.Builder buildProblemLi ); } - private static org.springdoc.core.fn.builders.apiresponse.Builder buildProblemResponse( ProblemManager problemManager) { var contentBuilder = contentBuilder() diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouter.java index 1c2551ac..97cf3e8b 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouter.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/ProblemRouter.java @@ -32,6 +32,7 @@ @Configuration @EnableWebFlux public class ProblemRouter { + private static final String PROBLEMS_BASE_PATH = "/problems"; public static final String PROBLEM_ID_PARAM_NAME = "problemId"; private ProblemManagerProvider managerProvider; private Validator validator; @@ -40,11 +41,13 @@ public class ProblemRouter { RouterFunction getProblemRoutes() { var managers = this.managerProvider.getProblemManagers(); return Streams.concat( - managers.stream().map(this::defineCreateRoute), - managers.stream().map(this::defineReadRoute), - managers.stream().map(this::defineListRoute), - managers.stream().map(this::defineUpdateRoute) - ).reduce(RouterFunction::and).orElseThrow(); + managers.stream().map(this::defineCreateRoute), + managers.stream().map(this::defineReadRoute), + managers.stream().map(this::defineListRoute), + managers.stream().map(this::defineUpdateRoute) + ) + .reduce(RouterFunction::and) + .orElseThrow(); } /** @@ -197,7 +200,7 @@ private void validate(ProblemDto problem) { } private String getPathWithoutId(ProblemType type) { - return "/problems/%s".formatted(type.getId()); + return "%s/%s".formatted(PROBLEMS_BASE_PATH, type.getId()); } private String getPathWithId(ProblemType type) { diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ProblemTypeDto.java b/src/main/java/edu/kit/provideq/toolbox/api/ProblemTypeDto.java new file mode 100644 index 00000000..5123d764 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/ProblemTypeDto.java @@ -0,0 +1,13 @@ +package edu.kit.provideq.toolbox.api; + +import edu.kit.provideq.toolbox.meta.ProblemType; +import java.util.List; + +public record ProblemTypeDto(String id, String description, List attributes) { + public static ProblemTypeDto fromProblemType(final ProblemType problemType) { + return new ProblemTypeDto( + problemType.getId(), + problemType.getDescription(), + problemType.getAttributes().keySet().stream().toList()); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ProblemTypeRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/ProblemTypeRouter.java new file mode 100644 index 00000000..3315e312 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/ProblemTypeRouter.java @@ -0,0 +1,162 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.demonstrators.DemonstratorConfiguration.DEMONSTRATOR; +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder; +import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; +import static org.springdoc.core.fn.builders.exampleobject.Builder.exampleOjectBuilder; +import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; +import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Streams; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import edu.kit.provideq.toolbox.meta.ProblemType; +import org.springdoc.core.fn.builders.operation.Builder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.json.JsonParseException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +/** + * This router handles requests regarding {@link ProblemType} instances. + */ +@Configuration +@EnableWebFlux +public class ProblemTypeRouter { + private static final String PROBLEM_TYPES_BASE_PATH = "/problem-types"; + private ProblemManagerProvider managerProvider; + + @Bean + RouterFunction getProblemTypeRoutes() { + var managers = this.managerProvider.getProblemManagers(); + return Streams.concat( + managers.stream().map(this::defineReadRoute) + ) + .reduce(RouterFunction::and) + .orElseThrow() + .and(defineListTypesRoute()); + } + + /** + * Create operation: GET /problem-types. + */ + private RouterFunction defineListTypesRoute() { + return route().GET( + PROBLEM_TYPES_BASE_PATH, + accept(APPLICATION_JSON), + req -> handleListTypes(), + ops -> ops + .operationId("%s/%s".formatted(PROBLEM_TYPES_BASE_PATH, "listType")) + .description( + "Responds with a list of all problem types that are supported by the server.") + .tag("_") + .response(buildProblemTypeListResponse()) + ).build(); + } + + private Mono handleListTypes() { + var problemTypes = managerProvider.getProblemManagers().stream() + .filter(p -> !p.getType().equals(DEMONSTRATOR)) + .map(ProblemManager::getType) + .map(ProblemTypeDto::fromProblemType) + .toList(); + + return ok().body(Mono.just(problemTypes), new ParameterizedTypeReference<>() { + }); + } + + private org.springdoc.core.fn.builders.apiresponse.Builder buildProblemTypeListResponse() { + var problemTypes = managerProvider.getProblemManagers().stream() + .filter(p -> !p.getType().equals(DEMONSTRATOR)) + .map(ProblemManager::getType) + .map(ProblemTypeDto::fromProblemType) + .toList(); + + String problemTypeDtosJson; + try { + problemTypeDtosJson = new ObjectMapper().writeValueAsString(problemTypes); + } catch (JsonProcessingException e) { + throw new JsonParseException(e); + } + + return responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(contentBuilder() + .array(arraySchemaBuilder() + .schema(schemaBuilder().implementation(ProblemTypeDto.class)) + ) + .example(exampleOjectBuilder() + .value(problemTypeDtosJson) + ) + ); + } + + /** + * List operation: GET /problem-types/TYPE. + */ + private RouterFunction defineReadRoute(ProblemManager manager) { + return route().GET( + "%s/%s".formatted(PROBLEM_TYPES_BASE_PATH, manager.getType().getId()), + accept(APPLICATION_JSON), + req -> handleList(manager), + ops -> configureReadDocs(manager, ops) + ).build(); + } + + private Mono handleList( + ProblemManager manager + ) { + var problemTypeDto = ProblemTypeDto.fromProblemType(manager.getType()); + return ok().body(Mono.just(problemTypeDto), new ParameterizedTypeReference<>() { + }); + } + + public static void configureReadDocs(ProblemManager manager, Builder ops) { + var type = manager.getType(); + ops + .operationId("%s/%s/%s".formatted(PROBLEM_TYPES_BASE_PATH, type.getId(), "read")) + .description("This endpoint can be used to get / read the problem type '" + + type.getId() + "'.") + .tag(type.getId()) + .response(buildProblemResponse(manager)); + } + + private static org.springdoc.core.fn.builders.apiresponse.Builder buildProblemResponse( + ProblemManager problemManager) { + var problemTypeDto = ProblemTypeDto.fromProblemType(problemManager.getType()); + + String problemTypeDtoJson; + try { + problemTypeDtoJson = new ObjectMapper().writeValueAsString(problemTypeDto); + } catch (JsonProcessingException e) { + throw new JsonParseException(e); + } + + return responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(contentBuilder() + .schema(schemaBuilder() + .implementation(ProblemTypeDto.class)) + .example(exampleOjectBuilder() + .value(problemTypeDtoJson) + ) + ); + } + + @Autowired + void setManagerProvider(ProblemManagerProvider managerProvider) { + this.managerProvider = managerProvider; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineDefinitionDto.java b/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineDefinitionDto.java index 56cfdf5f..d8546ff1 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineDefinitionDto.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineDefinitionDto.java @@ -8,6 +8,7 @@ public final class SubRoutineDefinitionDto { private final String typeId; private final String description; + private final boolean isCalledOnlyOnce; /** * Internal constructor, used for de-serialization. @@ -16,6 +17,7 @@ public final class SubRoutineDefinitionDto { private SubRoutineDefinitionDto() { this.typeId = null; this.description = null; + this.isCalledOnlyOnce = true; } /** @@ -23,10 +25,12 @@ private SubRoutineDefinitionDto() { */ private SubRoutineDefinitionDto( String typeId, - String description + String description, + boolean isCalledOnlyOnce ) { this.typeId = typeId; this.description = description; + this.isCalledOnlyOnce = isCalledOnlyOnce; } /** @@ -37,8 +41,9 @@ public static SubRoutineDefinitionDto fromSubRoutineDefinition( ) { var typeId = definition.type().getId(); var description = definition.description(); + var isCalledOnlyOnce = definition.isCalledOnlyOnce(); - return new SubRoutineDefinitionDto(typeId, description); + return new SubRoutineDefinitionDto(typeId, description, isCalledOnlyOnce); } public String getTypeId() { @@ -49,10 +54,15 @@ public String getDescription() { return description; } + public boolean getIsCalledOnlyOnce() { + return isCalledOnlyOnce; + } + @Override public String toString() { return "SubRoutineDefinitionDto[" + "typeId=" + typeId + ", " - + "description=" + description + ']'; + + "description=" + description + ", " + + "isCalledOnlyOnce=" + isCalledOnlyOnce + ']'; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/CircuitProcessingConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/CircuitProcessingConfiguration.java new file mode 100644 index 00000000..4c05f037 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/CircuitProcessingConfiguration.java @@ -0,0 +1,62 @@ +package edu.kit.provideq.toolbox.circuit.processing; + +import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.circuit.processing.solvers.MoveToExecutionSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.MoveToMitigationSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.MoveToOptimizationSolver; +import edu.kit.provideq.toolbox.exception.MissingExampleException; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemType; +import java.io.IOException; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CircuitProcessingConfiguration { + public static final ProblemType CIRCUIT_PROCESSING = new ProblemType<>( + "circuit-processing", + "A quantum circuit processing problem that routes a QASM circuit through optimization, " + + "error mitigation, or execution.", + String.class, + String.class + ); + + @Bean + ProblemManager getCircuitProcessingManager( + ResourceProvider provider, + MoveToExecutionSolver moveToExecutionSolver, + MoveToOptimizationSolver moveToOptimizationSolver, + MoveToMitigationSolver moveToMitigationSolver + ) { + return new ProblemManager<>( + CIRCUIT_PROCESSING, + Set.of( + moveToExecutionSolver, + moveToOptimizationSolver, + moveToMitigationSolver + ), + loadExampleProblems(provider) + ); + } + + private Set> loadExampleProblems(ResourceProvider provider) { + try { + String[] problemNames = new String[] {"bell-state.qasm", "cswap.qasm"}; + var problemSet = new HashSet>(); + for (var problemName : problemNames) { + var problemStream = Objects.requireNonNull( + getClass().getResourceAsStream(problemName), "Problem " + problemName + " not found"); + var problem = new Problem<>(CIRCUIT_PROCESSING); + problem.setInput(provider.readStream(problemStream)); + problemSet.add(problem); + } + return problemSet; + } catch (IOException e) { + throw new MissingExampleException(CIRCUIT_PROCESSING, e); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/CircuitProcessingSolver.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/CircuitProcessingSolver.java new file mode 100644 index 00000000..b83fc3d4 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/CircuitProcessingSolver.java @@ -0,0 +1,20 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers; + +import edu.kit.provideq.toolbox.circuit.processing.CircuitProcessingConfiguration; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; + +public abstract class CircuitProcessingSolver implements ProblemSolver { + public static final SubRoutineDefinition CIRCUIT_PROCESSING_SUBROUTINE = + new SubRoutineDefinition<>( + CircuitProcessingConfiguration.CIRCUIT_PROCESSING, + "Creates a circuit processing solver", + true + ); + + @Override + public ProblemType getProblemType() { + return CircuitProcessingConfiguration.CIRCUIT_PROCESSING; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToExecutionSolver.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToExecutionSolver.java new file mode 100644 index 00000000..20906aa5 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToExecutionSolver.java @@ -0,0 +1,46 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.circuit.processing.solvers.executor.ExecutionResult; +import edu.kit.provideq.toolbox.circuit.processing.solvers.executor.ExecutorConfiguration; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import java.util.List; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class MoveToExecutionSolver extends CircuitProcessingSolver { + private static final SubRoutineDefinition EXECUTOR_SUBROUTINE = + new SubRoutineDefinition<>( + ExecutorConfiguration.EXECUTOR_CONFIG, + "Creates an execution solver", + true + ); + + @Override + public String getName() { + return "Execute QASM Code"; + } + + @Override + public String getDescription() { + return "Move QASM input to the executors"; + } + + @Override + public List> getSubRoutines() { + return List.of(EXECUTOR_SUBROUTINE); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + return subRoutineResolver.runSubRoutine(EXECUTOR_SUBROUTINE, input) + .map(s -> Solution.from(this, s, ExecutionResult::toString)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToMitigationSolver.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToMitigationSolver.java new file mode 100644 index 00000000..6e0133c9 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToMitigationSolver.java @@ -0,0 +1,44 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation.ErrorMitigationConfiguration; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import java.util.List; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class MoveToMitigationSolver extends CircuitProcessingSolver { + private static final SubRoutineDefinition MITIGATOR_SUBROUTINE = + new SubRoutineDefinition<>( + ErrorMitigationConfiguration.MITIGATION_CONFIG, + "Creates a mitigation solver", + true + ); + + @Override + public String getName() { + return "Mitigate QASM Code Errors"; + } + + @Override + public String getDescription() { + return "Move QASM input to the error mitigators"; + } + + @Override + public List> getSubRoutines() { + return List.of(MITIGATOR_SUBROUTINE); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + return subRoutineResolver.runSubRoutine(MITIGATOR_SUBROUTINE, input); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToOptimizationSolver.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToOptimizationSolver.java new file mode 100644 index 00000000..235c943d --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/MoveToOptimizationSolver.java @@ -0,0 +1,44 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.circuit.processing.solvers.optimization.OptimizationConfiguration; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import java.util.List; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class MoveToOptimizationSolver extends CircuitProcessingSolver { + private static final SubRoutineDefinition OPTIMIZER_SUBROUTINE = + new SubRoutineDefinition<>( + OptimizationConfiguration.OPTIMIZATION_CONFIG, + "Creates an optimization solver", + true + ); + + @Override + public String getName() { + return "Optimize QASM Code"; + } + + @Override + public String getDescription() { + return "Move QASM input to the optimizers"; + } + + @Override + public List> getSubRoutines() { + return List.of(OPTIMIZER_SUBROUTINE); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + return subRoutineResolver.runSubRoutine(OPTIMIZER_SUBROUTINE, input); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutionResult.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutionResult.java new file mode 100644 index 00000000..5936ac07 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutionResult.java @@ -0,0 +1,26 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers.executor; + +import java.util.Optional; + +public record ExecutionResult(Optional resultString, Optional circuit) { + public boolean hasResult() { + return resultString.isPresent(); + } + + public boolean hasCircuit() { + return circuit.isPresent(); + } + + @Override + public String toString() { + var stringBuilder = new StringBuilder(); + resultString.ifPresent(r -> stringBuilder.append("Result:\n").append(r)); + circuit.ifPresent(c -> { + if (!stringBuilder.isEmpty()) { + stringBuilder.append("\n\n"); + } + stringBuilder.append("Circuit:\n").append(c); + }); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutionSolver.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutionSolver.java new file mode 100644 index 00000000..894c776d --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutionSolver.java @@ -0,0 +1,146 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers.executor; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.meta.setting.SolverSetting; +import edu.kit.provideq.toolbox.meta.setting.basic.IntegerSetting; +import edu.kit.provideq.toolbox.meta.setting.basic.SelectSetting; +import edu.kit.provideq.toolbox.process.ProcessRunner; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class ExecutionSolver implements ProblemSolver { + private static final String SETTING_NUMBER_OF_SHOTS = "Number of shots"; + private static final String SETTING_SELECT_SIMULATOR = "Selected Simulator"; + private static final int DEFAULT_NUMBER_OF_SHOTS = 1024; + private static final QuantumSimulator DEFAULT_SIMULATOR = QuantumSimulator.AER; + + private final String scriptPath; + private final String venv; + private final ApplicationContext context; + + @Autowired + public ExecutionSolver( + @Value("${path.circuitprocessing.circuitexecution}") String scriptPath, + @Value("${venv.circuitprocessing.circuitexecution}") String venv, + ApplicationContext context + ) { + this.context = context; + this.venv = venv; + this.scriptPath = scriptPath; + } + + @Override + public String getName() { + return "Execute OpenQASM circuit"; + } + + @Override + public String getDescription() { + return "Execute an OpenQASM circuit"; + } + + @Override + public List getSolverSettings() { + return List.of( + new IntegerSetting( + SETTING_NUMBER_OF_SHOTS, + "The number of shots to run", + 1, + 1000000, + DEFAULT_NUMBER_OF_SHOTS), + new SelectSetting<>( + SETTING_SELECT_SIMULATOR, + "The simulator to run the code with", + List.of(QuantumSimulator.values()), + QuantumSimulator.AER, + QuantumSimulator::getValue + ) + ); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + var solution = new Solution<>(this); + + int shotNumber = properties.getSetting(SETTING_NUMBER_OF_SHOTS) + .map(IntegerSetting::getValue) + .orElse(DEFAULT_NUMBER_OF_SHOTS); + + QuantumSimulator selectedSimulator = properties + .>getSetting(SETTING_SELECT_SIMULATOR) + .map(s -> s.getSelectedOptionT(QuantumSimulator::fromValue)) + .orElse(DEFAULT_SIMULATOR); + + var processResult = context + .getBean(PythonProcessRunner.class, scriptPath, venv) + .withArguments( + ProcessRunner.INPUT_FILE_PATH, + String.valueOf(shotNumber), + selectedSimulator.getBackendKey() + ) + .writeInputFile(input) + .readOutputString() + .run(getProblemType(), solution.getId()); + + if (processResult.success()) { + solution.complete(); + solution.setSolutionData(new ExecutionResult(processResult.output(), Optional.of(input))); + return Mono.just(solution); + } + solution.fail(); + processResult.errorOutput().ifPresent(solution::setDebugData); + return Mono.just(solution); + } + + @Override + public ProblemType getProblemType() { + return ExecutorConfiguration.EXECUTOR_CONFIG; + } + + enum QuantumSimulator { + AER("AerBackend", "aer"), + // PROJECTQ("ProjectQBackend", "projectq"), + QULACS("QulacsBackend", "qulacs"), + AER_NOISY("Aer Noisy Backend (max. 2 qubits)", "aer_noisy"); + + private final String value; + private final String backendKey; + + QuantumSimulator(String value, String backendKey) { + this.value = value; + this.backendKey = backendKey; + } + + public String getValue() { + return value; + } + + public String getBackendKey() { + return backendKey; + } + + public static QuantumSimulator fromValue(String value) { + for (QuantumSimulator simulator : values()) { + if (simulator.value.equals(value)) { + return simulator; + } + } + throw new IllegalArgumentException("Unknown value: " + value); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutorConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutorConfiguration.java new file mode 100644 index 00000000..cbd2f88e --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/executor/ExecutorConfiguration.java @@ -0,0 +1,48 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers.executor; + +import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.MissingExampleException; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemType; +import java.io.IOException; +import java.util.Objects; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ExecutorConfiguration { + public static final ProblemType EXECUTOR_CONFIG = new ProblemType<>( + "circuit-processing-executor", + "A quantum circuit execution problem that runs a given QASM circuit on a quantum backend " + + "and returns the measurement results.", + String.class, + ExecutionResult.class + ); + + @Bean + ProblemManager getExecutorProblemManager( + ResourceProvider provider, + ExecutionSolver executionSolver + ) { + return new ProblemManager<>( + EXECUTOR_CONFIG, + Set.of(executionSolver), + loadExampleProblems(provider) + ); + } + + private Set> loadExampleProblems(ResourceProvider provider) { + try { + var problemStream = Objects.requireNonNull( + getClass().getResourceAsStream("../../bell-state.qasm"), + "Problem bell-state.qasm not found"); + var problem = new Problem<>(EXECUTOR_CONFIG); + problem.setInput(provider.readStream(problemStream)); + return Set.of(problem); + } catch (IOException e) { + throw new MissingExampleException(EXECUTOR_CONFIG, e); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/mitigation/ErrorMitigationConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/mitigation/ErrorMitigationConfiguration.java new file mode 100644 index 00000000..69156d7d --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/mitigation/ErrorMitigationConfiguration.java @@ -0,0 +1,48 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation; + +import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.MissingExampleException; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemType; +import java.io.IOException; +import java.util.Objects; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ErrorMitigationConfiguration { + public static final ProblemType MITIGATION_CONFIG = new ProblemType<>( + "circuit-processing-mitigation", + "A quantum circuit error mitigation problem that applies error mitigation techniques to " + + "a given QASM circuit.", + String.class, + String.class + ); + + @Bean + ProblemManager getMitigationProblemManager( + ResourceProvider provider, + ErrorMitigationSolver errorMitigationSolver + ) { + return new ProblemManager<>( + MITIGATION_CONFIG, + Set.of(errorMitigationSolver), + loadExampleProblems(provider) + ); + } + + private Set> loadExampleProblems(ResourceProvider provider) { + try { + var problemStream = Objects.requireNonNull( + getClass().getResourceAsStream("../../bell-state.qasm"), + "Problem bell-state.qasm not found"); + var problem = new Problem<>(MITIGATION_CONFIG); + problem.setInput(provider.readStream(problemStream)); + return Set.of(problem); + } catch (IOException e) { + throw new MissingExampleException(MITIGATION_CONFIG, e); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/mitigation/ErrorMitigationSolver.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/mitigation/ErrorMitigationSolver.java new file mode 100644 index 00000000..29fd5c7f --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/mitigation/ErrorMitigationSolver.java @@ -0,0 +1,40 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class ErrorMitigationSolver implements ProblemSolver { + + @Override + public String getName() { + return "Mitigate Errors for OpenQASM"; + } + + @Override + public String getDescription() { + return "Run error mitigation strategies on an OpenQASM circuit"; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + var solution = new Solution<>(this); + solution.setSolutionData(input); + solution.complete(); + return Mono.just(solution); + } + + @Override + public ProblemType getProblemType() { + return ErrorMitigationConfiguration.MITIGATION_CONFIG; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/optimization/OptimizationConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/optimization/OptimizationConfiguration.java new file mode 100644 index 00000000..bc9aba98 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/optimization/OptimizationConfiguration.java @@ -0,0 +1,48 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers.optimization; + +import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.MissingExampleException; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemType; +import java.io.IOException; +import java.util.Objects; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OptimizationConfiguration { + public static final ProblemType OPTIMIZATION_CONFIG = new ProblemType<>( + "circuit-processing-optimization", + "A quantum circuit optimization problem that reduces gate count or circuit depth of a " + + "given QASM circuit.", + String.class, + String.class + ); + + @Bean + ProblemManager getOptimizationProblemManager( + ResourceProvider provider, + OptimizationSolver optimizationSolver + ) { + return new ProblemManager<>( + OPTIMIZATION_CONFIG, + Set.of(optimizationSolver), + loadExampleProblems(provider) + ); + } + + private Set> loadExampleProblems(ResourceProvider provider) { + try { + var problemStream = Objects.requireNonNull( + getClass().getResourceAsStream("../../bell-state.qasm"), + "Problem bell-state.qasm not found"); + var problem = new Problem<>(OPTIMIZATION_CONFIG); + problem.setInput(provider.readStream(problemStream)); + return Set.of(problem); + } catch (IOException e) { + throw new MissingExampleException(OPTIMIZATION_CONFIG, e); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/optimization/OptimizationSolver.java b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/optimization/OptimizationSolver.java new file mode 100644 index 00000000..5106a296 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/circuit/processing/solvers/optimization/OptimizationSolver.java @@ -0,0 +1,139 @@ +package edu.kit.provideq.toolbox.circuit.processing.solvers.optimization; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.circuit.processing.solvers.CircuitProcessingSolver; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.meta.setting.SolverSetting; +import edu.kit.provideq.toolbox.meta.setting.basic.SelectSetting; +import edu.kit.provideq.toolbox.process.ProcessRunner; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class OptimizationSolver implements ProblemSolver { + private static final String SETTING_SELECT_OPTIMIZER = "Selected Optimization Pass"; + private static final OptimizationSolver.QuantumOptimizer DEFAULT_OPTIMIZER = + QuantumOptimizer.DECOMPOSE_MULTI_CX; + + private final Map optimizerScriptPaths; + private final String venv; + private final ApplicationContext context; + + @Autowired + public OptimizationSolver( + @Value("${path.circuitprocessing.circuitoptimization.decomposemulticx}") + String decomposeMultiCxPath, + @Value("${path.circuitprocessing.circuitoptimization.removeredundancies}") + String removeRedundanciesPath, + @Value("${venv.circuitprocessing.circuitoptimization}") String venv, + ApplicationContext context + ) { + this.optimizerScriptPaths = Map.of( + QuantumOptimizer.DECOMPOSE_MULTI_CX, decomposeMultiCxPath, + QuantumOptimizer.REMOVE_REDUNDANCIES, removeRedundanciesPath + ); + this.venv = venv; + this.context = context; + } + + @Override + public String getName() { + return "Apply Tket Optimization Pass"; + } + + @Override + public String getDescription() { + return "Transform the given circuit into an optimized but equivalent circuit using" + + "Tket compilation passes (e.g. removing redundancies)."; + } + + @Override + public List> getSubRoutines() { + return List.of(CircuitProcessingSolver.CIRCUIT_PROCESSING_SUBROUTINE); + } + + @Override + public List getSolverSettings() { + return List.of( + new SelectSetting<>( + SETTING_SELECT_OPTIMIZER, + "The optimization pass to refactor the code with", + List.of(OptimizationSolver.QuantumOptimizer.values()), + QuantumOptimizer.DECOMPOSE_MULTI_CX, + OptimizationSolver.QuantumOptimizer::getValue + ) + ); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + var solution = new Solution<>(this); + + OptimizationSolver.QuantumOptimizer selectedOptimizer = properties + .>getSetting(SETTING_SELECT_OPTIMIZER) + .map(s -> s.getSelectedOptionT(OptimizationSolver.QuantumOptimizer::fromValue)) + .orElse(DEFAULT_OPTIMIZER); + + var processResult = context + .getBean(PythonProcessRunner.class, optimizerScriptPaths.get(selectedOptimizer), venv) + .withArguments( + ProcessRunner.INPUT_FILE_PATH + ) + .writeInputFile(input) + .readOutputString() + .run(getProblemType(), solution.getId()); + + if (processResult.success() && processResult.output().isPresent()) { + return subRoutineResolver + .runSubRoutine(CircuitProcessingSolver.CIRCUIT_PROCESSING_SUBROUTINE, + processResult.output().get()) + .map(subRoutineSolution -> Solution.from(this, subRoutineSolution, s -> s)); + } + solution.fail(); + processResult.errorOutput().ifPresent(solution::setDebugData); + return Mono.just(solution); + } + + @Override + public ProblemType getProblemType() { + return OptimizationConfiguration.OPTIMIZATION_CONFIG; + } + + enum QuantumOptimizer { + DECOMPOSE_MULTI_CX("DecomposeMultiQubitsCX"), + REMOVE_REDUNDANCIES("RemoveRedundancies"); + + private final String value; + + QuantumOptimizer(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static OptimizationSolver.QuantumOptimizer fromValue(String value) { + for (OptimizationSolver.QuantumOptimizer simulator : values()) { + if (simulator.value.equals(value)) { + return simulator; + } + } + throw new IllegalArgumentException("Unknown value: " + value); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java index 5767fe52..f29878d5 100644 --- a/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java @@ -15,16 +15,19 @@ public class DemonstratorConfiguration { */ public static final ProblemType DEMONSTRATOR = new ProblemType<>( "demonstrator", + "A demonstrator is a broad group of processes that demonstrate " + + "certain aspects of quantum computing.", String.class, String.class ); @Bean ProblemManager getDemonstratorManager( - CplexMipDemonstrator cplexMipDemonstrator + CplexMipDemonstrator cplexMipDemonstrator, + MoleculeEnergySimulator moleculeEnergySimulator ) { return new ProblemManager<>(DEMONSTRATOR, - Set.of(cplexMipDemonstrator), + Set.of(cplexMipDemonstrator, moleculeEnergySimulator), Set.of(new Problem<>(DEMONSTRATOR))); } } diff --git a/src/main/java/edu/kit/provideq/toolbox/demonstrators/MoleculeEnergySimulator.java b/src/main/java/edu/kit/provideq/toolbox/demonstrators/MoleculeEnergySimulator.java new file mode 100644 index 00000000..2210bcb2 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/demonstrators/MoleculeEnergySimulator.java @@ -0,0 +1,79 @@ +package edu.kit.provideq.toolbox.demonstrators; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.meta.setting.SolverSetting; +import edu.kit.provideq.toolbox.meta.setting.basic.TextSetting; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * Demonstrator for the Molecule Energy simulation. + * Note that its python dependencies can only be installed on Linux and macOS. + * Based on this Jupyter Notebook + */ +@Component +public class MoleculeEnergySimulator implements Demonstrator { + private final String scriptPath; + private final String venv; + private final ApplicationContext context; + + private static final String SETTING_MOLECULE = "Molecule"; + private static final String DEFAULT_MOLECULE = "H .0 .0 .0; H .0 .0 0.74279"; + + @Autowired + public MoleculeEnergySimulator( + @Value("${path.demonstrators.qiskit.molecule-energy}") String scriptPath, + @Value("${venv.demonstrators.qiskit.molecule-energy}") String venv, + ApplicationContext context) { + this.scriptPath = scriptPath; + this.venv = venv; + this.context = context; + } + + @Override + public String getName() { + return "Molecule Energy Simulator"; + } + + @Override + public String getDescription() { + return "Computes the ground state energy for a given molecule using VQE algorithm."; + } + + @Override + public List getSolverSettings() { + return List.of( + new TextSetting( + false, + SETTING_MOLECULE, + "The molecule to be simulated in XYZ format - a di-hydrogen molecule by default", + DEFAULT_MOLECULE + ) + ); + } + + @Override + public Mono> solve(String input, SubRoutineResolver subRoutineResolver, + SolvingProperties properties) { + var solution = new Solution<>(this); + + var molecule = properties.getSetting(SETTING_MOLECULE) + .map(TextSetting::getText) + .orElse(DEFAULT_MOLECULE); + + var processResult = context + .getBean(PythonProcessRunner.class, scriptPath, venv) + .withArguments('"' + molecule + '"') + .readOutputString() + .run(getProblemType(), solution.getId()); + + return Mono.just(processResult.applyTo(solution)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java index 80d21ab7..3541f013 100644 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java @@ -24,7 +24,8 @@ public class DeadFeatureConfiguration { * "Explaining Anomalies in Feature Models", Kowal et al., 2016 */ public static final ProblemType FEATURE_MODEL_ANOMALY_DEAD = new ProblemType<>( - "feature-model-anomaly-dead", + "FeatureModelAnomalyDead", + "A searching problem: For a given feature model, check if the model contains dead features.", String.class, String.class ); diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/SatBasedDeadFeatureSolver.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/SatBasedDeadFeatureSolver.java index 7178598e..03408b50 100644 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/SatBasedDeadFeatureSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/SatBasedDeadFeatureSolver.java @@ -30,7 +30,8 @@ public class SatBasedDeadFeatureSolver implements ProblemSolver private static final SubRoutineDefinition SAT_SUBROUTINE = new SubRoutineDefinition<>( SatConfiguration.SAT, - "Called per feature to determine if it is dead" + "Called per feature to determine if it is dead", + false ); @Override diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/SatBasedVoidFeatureSolver.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/SatBasedVoidFeatureSolver.java index 7e644983..2e4fbc8c 100644 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/SatBasedVoidFeatureSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/SatBasedVoidFeatureSolver.java @@ -23,7 +23,8 @@ public class SatBasedVoidFeatureSolver implements ProblemSolver private static final SubRoutineDefinition SAT_SUBROUTINE = new SubRoutineDefinition<>( SatConfiguration.SAT, - "Used to determine if there is any valid configurations of the Feature Model" + "Used to determine if there is any valid configurations of the Feature Model", + true ); @Override diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java index 7061de70..be383dcf 100644 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java @@ -24,7 +24,8 @@ public class VoidModelConfiguration { * "Explaining Anomalies in Feature Models", Kowal et al., 2016 */ public static final ProblemType FEATURE_MODEL_ANOMALY_VOID = new ProblemType<>( - "feature-model-anomaly-void", + "FeatureModelAnomalyVoid", + "A searching problem: For a given feature model, check if the model is void.", String.class, String.class ); diff --git a/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java index c88d2418..e0320cf3 100644 --- a/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java @@ -68,12 +68,23 @@ public class KnapsackConfiguration { * and the sum of values is as large as possible. */ public static final ProblemType KNAPSACK = new ProblemType<>( - "knapsack", - String.class, - String.class, - estimator, - "^(\\d+)" - ); + "Knapsack", + "An optimization problem: For given items each with a weight and value, " + + "determine which items are part of a collection where the total weight is " + + "less than or equal to a given limit and the sum of values is as large as possible.", + String.class, + String.class, + estimator, + "^(\\d+)", + Map.ofEntries( + Map.entry("item_count", problem -> problem.lines() + .findFirst() + .orElse("")), + Map.entry("capacity", p2 -> p2.lines() + .reduce((a, b) -> b) + .orElse("")) + ) + ); @Bean ProblemManager getKnapsackManager( diff --git a/src/main/java/edu/kit/provideq/toolbox/materialsimulation/MaterialSimulationConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/materialsimulation/MaterialSimulationConfiguration.java new file mode 100644 index 00000000..9dd32af3 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/materialsimulation/MaterialSimulationConfiguration.java @@ -0,0 +1,60 @@ +package edu.kit.provideq.toolbox.materialsimulation; + +import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.MissingExampleException; +import edu.kit.provideq.toolbox.materialsimulation.solvers.QiskitMaterialSimulationSolver; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemType; +import java.io.IOException; +import java.util.Objects; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +/** + * Definition and registration of the Quantum Material Simulation problem. + */ +@Configuration +public class MaterialSimulationConfiguration { + + /** + * A simulation problem, computing electronic properties of a molecular system. + */ + public static final ProblemType MATERIAL_SIMULATION = new ProblemType<>( + "MaterialSimulation", + "A simulation problem, computing electronic properties of a molecular system.", + String.class, + String.class + ); + + @Bean + ProblemManager getMaterialSimulationManager( + QiskitMaterialSimulationSolver qiskitSolver, + ResourceProvider resourceProvider + ) { + return new ProblemManager<>( + MATERIAL_SIMULATION, + Set.of(qiskitSolver), + loadExampleProblems(resourceProvider) + ); + } + + private Set> loadExampleProblems( + ResourceProvider resourceProvider) { + try { + var problemInputStream = Objects.requireNonNull( + getClass().getResourceAsStream("h2.txt"), + "H2 example for Material Simulation is unavailable!" + ); + var problem = new Problem<>(MATERIAL_SIMULATION); + problem.setInput(resourceProvider.readStream(problemInputStream)); + return Set.of(problem); + } catch (IOException e) { + throw new MissingExampleException(MATERIAL_SIMULATION, e); + } + } + + +} diff --git a/src/main/java/edu/kit/provideq/toolbox/materialsimulation/solvers/MaterialSimulationSolver.java b/src/main/java/edu/kit/provideq/toolbox/materialsimulation/solvers/MaterialSimulationSolver.java new file mode 100644 index 00000000..ddb79c4b --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/materialsimulation/solvers/MaterialSimulationSolver.java @@ -0,0 +1,12 @@ +package edu.kit.provideq.toolbox.materialsimulation.solvers; + +import edu.kit.provideq.toolbox.materialsimulation.MaterialSimulationConfiguration; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; + +public abstract class MaterialSimulationSolver implements ProblemSolver { + @Override + public ProblemType getProblemType() { + return MaterialSimulationConfiguration.MATERIAL_SIMULATION; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/materialsimulation/solvers/QiskitMaterialSimulationSolver.java b/src/main/java/edu/kit/provideq/toolbox/materialsimulation/solvers/QiskitMaterialSimulationSolver.java new file mode 100644 index 00000000..986a516a --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/materialsimulation/solvers/QiskitMaterialSimulationSolver.java @@ -0,0 +1,112 @@ +package edu.kit.provideq.toolbox.materialsimulation.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.materialsimulation.MaterialSimulationConfiguration; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.meta.setting.SolverSetting; +import edu.kit.provideq.toolbox.meta.setting.basic.IntegerSetting; +import edu.kit.provideq.toolbox.process.ProcessRunner; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + + +/** + * {@link MaterialSimulationConfiguration#MATERIAL_SIMULATION} solver using a Qiskit + * implementation. + */ +@Component +public class QiskitMaterialSimulationSolver extends MaterialSimulationSolver { + private final String scriptPath; + private final String venv; + private final ApplicationContext context; + + private static final String SETTING_CHARGE = "Charge"; + private static final int DEFAULT_CHARGE = 0; + + private static final String SETTING_SPIN = "Spin"; + private static final int DEFAULT_SPIN = 0; + + @Autowired + public QiskitMaterialSimulationSolver( + @Value("${path.qiskit.materialsimulation}") String scriptPath, + @Value("${venv.qiskit.materialsimulation}") String venv, + ApplicationContext context) { + this.scriptPath = scriptPath; + this.venv = venv; + this.context = context; + } + + @Override + public String getName() { + return "Qiskit Material Simulation"; + } + + @Override + public String getDescription() { + return "Solves the material simulation problem using Qiskit with VQE."; + } + + @Override + public List getSolverSettings() { + return List.of( + new IntegerSetting( + SETTING_CHARGE, + "The charge of the molecule. The parameter to define " + + "the total number of electrons in the system.", + DEFAULT_CHARGE, + 0, Integer.MAX_VALUE + ), + new IntegerSetting( + SETTING_SPIN, + "The spin of the molecule. Equals the number of unpaired electrons 2S," + + " i.e. the difference between the number of alpha and beta electrons.", + DEFAULT_SPIN, + 0, Integer.MAX_VALUE + ) + ); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + var solution = new Solution<>(this); + + int charge = properties.getSetting(SETTING_CHARGE) + .map(IntegerSetting::getValue) + .orElse(DEFAULT_CHARGE); + + int spin = properties.getSetting(SETTING_SPIN) + .map(IntegerSetting::getValue) + .orElse(DEFAULT_SPIN); + + if (charge < 0) { + throw new IllegalArgumentException("Charge must be non-negative"); + } + if (spin < 0) { + throw new IllegalArgumentException("Spin must be non-negative"); + } + + var processResult = context + .getBean(PythonProcessRunner.class, scriptPath, venv) + .withArguments( + ProcessRunner.INPUT_FILE_PATH, + ProcessRunner.OUTPUT_FILE_PATH, + "--charge", String.valueOf(charge), + "--spin", String.valueOf(spin) + ) + .writeInputFile(input) + .readOutputFile() + .run(getProblemType(), solution.getId()); + + return Mono.just(processResult.applyTo(solution)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java index 17e14867..2476c3aa 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java @@ -25,7 +25,9 @@ public class MaxCutConfiguration { * weight sum. */ public static final ProblemType MAX_CUT = new ProblemType<>( - "max-cut", + "MaxCut", + "An optimization problem: For a given graph, find the optimal separation of vertices " + + "that maximises the cut crossing edge weight sum.", String.class, String.class ); diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java b/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java index 2b8791ae..c135e6b4 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java @@ -2,6 +2,8 @@ import edu.kit.provideq.toolbox.BoundWithInfo; import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.api.BoundComparisonDto; +import edu.kit.provideq.toolbox.api.BoundDto; import edu.kit.provideq.toolbox.meta.setting.SolverSetting; import java.util.Collections; import java.util.HashSet; @@ -10,6 +12,7 @@ import java.util.Set; import java.util.UUID; import java.util.function.Consumer; +import java.util.regex.Pattern; import java.util.stream.Collectors; import reactor.core.publisher.Mono; @@ -30,7 +33,7 @@ public class Problem { private InputT input; private Solution solution; - private BoundWithInfo bound; + private BoundComparisonDto boundWithComparison; private ProblemState state; private ProblemSolver solver; private List solverSettings; @@ -84,6 +87,10 @@ public Mono> solve() { }); } + /** + * Estimates a bound for the problem's solution. Uses the estimator provided by the problem type. + * Also sets the execution time of the estimation in the boundWithComparison object. + */ public void estimateBound() { if (this.input == null) { throw new IllegalStateException("Cannot estimate value without input!"); @@ -93,6 +100,7 @@ public void estimateBound() { if (optionalEstimator.isEmpty()) { throw new IllegalStateException("Cannot estimate value without an estimator!"); } + var estimator = optionalEstimator.get(); long start = System.currentTimeMillis(); @@ -101,7 +109,29 @@ public void estimateBound() { long finish = System.currentTimeMillis(); var executionTime = finish - start; - this.bound = new BoundWithInfo(estimatedBound, executionTime); + this.boundWithComparison = + new BoundComparisonDto(new BoundWithInfo(estimatedBound, executionTime)); + } + + /** + * Compares the current solution with the bound according to the bound type. + */ + public void compareBound() { + if (this.solution == null) { + throw new IllegalStateException("Cannot compare bound without solution!"); + } + if (this.boundWithComparison == null || !this.boundWithComparison.hasBound()) { + throw new IllegalStateException("Cannot compare bound without bound!"); + } + + var bound = this.boundWithComparison.getBound(); + var solutionData = this.solution.getSolutionData(); + + float solutionValue = getSolutionValue(solutionData); + + var comparison = bound.boundType().compare(bound.bound(), solutionValue); + + this.boundWithComparison.setComparison(comparison); } public UUID getId() { @@ -231,7 +261,27 @@ public String toString() { + '}'; } - public Optional getBound() { - return Optional.ofNullable(bound); + public Optional getBound() { + if (boundWithComparison == null) { + return Optional.empty(); + } + + return Optional.ofNullable(boundWithComparison.getBound()); + } + + public Optional getBoundWithComparison() { + return Optional.ofNullable(boundWithComparison); + } + + private float getSolutionValue(ResultT solutionData) { + var pattern = Pattern.compile(this.type.getSolutionPattern()); + var solutionMatcher = pattern.matcher(solutionData.toString()); + float solutionValue; + if (solutionMatcher.find()) { + solutionValue = Float.parseFloat(solutionMatcher.group(1)); + } else { + throw new IllegalStateException("Solution does not match the expected pattern!"); + } + return solutionValue; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemSolver.java b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemSolver.java index d4b0798d..5fe7e803 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemSolver.java @@ -19,7 +19,7 @@ public interface ProblemSolver { * @return id of the solver */ default String getId() { - return getClass().getName(); + return getClass().getSimpleName(); } /** diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java index 874aa32d..33872044 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java @@ -1,6 +1,7 @@ package edu.kit.provideq.toolbox.meta; import edu.kit.provideq.toolbox.Bound; +import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -9,47 +10,79 @@ */ public class ProblemType { private final String id; + private final String description; private final Class inputClass; private final Class resultClass; private final Function estimator; private final String solutionPattern; + private final Map> attributes; /** * Defines a new problem type. * - * @param id a unique string identifier for this type of problem. - * @param inputClass the Java class object corresponding to the {@link InputT} type parameter. - * @param resultClass the Java class object corresponding to the {@link ResultT} type parameter. - * @param estimator the bound estimator for this problem type. - * null if estimation is not supported. + * @param id a unique string identifier for this type of problem. + * @param inputClass the Java class object matching the {@link InputT} type parameter. + * @param resultClass the Java class object matching the {@link ResultT} type parameter. + * @param estimator the bound estimator for this problem type, + * null if estimation is not supported. + * @param solutionPattern a regex pattern describing the format of valid solutions + * needs to contain one group to match a float number describing + * the problem quality if estimation is supported, null otherwise. */ public ProblemType( String id, + String description, Class inputClass, Class resultClass, Function estimator, - String solutionPattern + String solutionPattern, + Map> attributes ) { this.id = id; + this.description = description; this.inputClass = inputClass; this.resultClass = resultClass; this.estimator = estimator; this.solutionPattern = solutionPattern; + this.attributes = attributes; } /** * Defines a new problem type without estimation support. * * @param id a unique string identifier for this type of problem. + * @param description a description of the problem type. * @param inputClass the Java class object corresponding to the {@link InputT} type parameter. * @param resultClass the Java class object corresponding to the {@link ResultT} type parameter. + * @param attributes a map of attributes for this problem type, + * where the key is the attribute name and the value is + * a function to get the attribute value for a given problem instance */ public ProblemType( String id, + String description, + Class inputClass, + Class resultClass, + Map> attributes + ) { + this(id, description, inputClass, resultClass, null, null, attributes); + } + + /** + * Defines a new problem type without estimation support. + * + * @param id a unique string identifier for this type of problem. + * @param description a description of the problem type. + * @param inputClass the Java class object corresponding to the {@link InputT} type parameter. + * @param resultClass the Java class object corresponding to the {@link ResultT} type parameter. + */ + public ProblemType( + String id, + String description, Class inputClass, Class resultClass ) { - this(id, inputClass, resultClass, null, null); + this(id, description, inputClass, resultClass, null, null, Map.of()); } /** @@ -59,6 +92,13 @@ public String getId() { return id; } + /** + * Returns a description of this problem type. + */ + public String getDescription() { + return description; + } + /** * Returns the Java class corresponding to the {@link InputT} type parameter. */ @@ -84,6 +124,13 @@ public String getSolutionPattern() { return solutionPattern; } + /** + * Returns a map of attributes for this problem type. + */ + public Map> getAttributes() { + return Map.copyOf(attributes); + } + @Override public String toString() { return "ProblemType{" @@ -91,6 +138,8 @@ public String toString() { + ", inputClass=%s".formatted(inputClass) + ", resultClass=%s".formatted(resultClass) + ", estimator?=%s".formatted(estimator != null) + + ", description='%s'".formatted(description) + + ", attributes=%s".formatted(attributes) + '}'; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java b/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java index 49e91713..15a5474d 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java @@ -4,13 +4,16 @@ * A sub-routine definition describes which problem type needs to be solved by a sub-routine and why * it needs to be solved. * - * @param type {@link ProblemType} that needs to be solved - * by this sub-routine. - * @param description description of the sub-routine call to provide information where and why it is - * needed. + * @param type {@link ProblemType} that needs to be solved + * by this sub-routine. + * @param description description of the sub-routine call to provide information where and + * why it is needed. + * @param isCalledOnlyOnce whether this sub-routine is called only once during the solution + * or if it may be called multiple times.a */ public record SubRoutineDefinition( ProblemType type, - String description + String description, + boolean isCalledOnlyOnce ) { } diff --git a/src/main/java/edu/kit/provideq/toolbox/process/ProcessRunner.java b/src/main/java/edu/kit/provideq/toolbox/process/ProcessRunner.java index 21314223..1c792378 100644 --- a/src/main/java/edu/kit/provideq/toolbox/process/ProcessRunner.java +++ b/src/main/java/edu/kit/provideq/toolbox/process/ProcessRunner.java @@ -26,8 +26,21 @@ @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class ProcessRunner { + /** + * Placeholder for the problem directory path in the process arguments. + */ public static final String PROBLEM_DIRECTORY_PATH = "PROBLEM_DIRECTORY_PATH"; + /** + * Placeholder for the input file path in the process arguments. + */ public static final String INPUT_FILE_PATH = "INPUT_FILE_PATH"; + /** + * Placeholder for another input file path in the process arguments. + */ + public static final String INPUT_FILE_PATH2 = "INPUT_FILE_PATH2"; + /** + * Placeholder for the output file path in the process arguments. + */ public static final String OUTPUT_FILE_PATH = "OUTPUT_FILE_PATH"; /** @@ -98,6 +111,21 @@ public ProcessRunner writeInputFile(String inputData) { * @return ProcessRunner instance for chaining. */ public ProcessRunner writeInputFile(String inputData, String inputFileName) { + return writeInputFile(inputData, inputFileName, INPUT_FILE_PATH); + } + + /** + * Writes the input data to a specific input file. + * + * @param inputData The input data to be written to the input file. + * @param inputFileName The name of the file to write the input data to. + * @param inputFilePathPlaceholder The placeholder to be replaced with the input + * file path in the arguments. + * @return ProcessRunner instance for chaining. + */ + public ProcessRunner writeInputFile(String inputData, + String inputFileName, + String inputFilePathPlaceholder) { // Add at the beginning of the pre-processors list // This ensures that the argument transformers are applied for every argument preProcessors.add(0, (problemType, solutionId) -> { @@ -116,7 +144,7 @@ public ProcessRunner writeInputFile(String inputData, String inputFileName) { // Add support for replacing the input file path in the arguments argumentTransformers.add( - argument -> argument.replace(INPUT_FILE_PATH, normalizedInputFilePath)); + argument -> argument.replace(inputFilePathPlaceholder, normalizedInputFilePath)); return Optional.empty(); }); @@ -294,8 +322,6 @@ protected ProcessRunnerExecutor getExecutor( int processExitCode; try { processBuilder.directory(new File(System.getProperty("user.dir"))); - String command = - processBuilder.command().stream().reduce("", (a, b) -> a + " |break| " + b); Process process = processBuilder.start(); processOutput = resourceProvider.readStream(process.inputReader()); diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java index 30f5ba6f..ee8bb95f 100644 --- a/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java @@ -6,10 +6,9 @@ import edu.kit.provideq.toolbox.meta.ProblemManager; import edu.kit.provideq.toolbox.meta.ProblemType; import edu.kit.provideq.toolbox.qubo.solvers.DwaveQuboSolver; -import edu.kit.provideq.toolbox.qubo.solvers.KipuQuboSolver; +import edu.kit.provideq.toolbox.qubo.solvers.GamsQuboSolver; import edu.kit.provideq.toolbox.qubo.solvers.QiskitQuboSolver; import edu.kit.provideq.toolbox.qubo.solvers.QrispQuboSolver; -import edu.kit.provideq.toolbox.qubo.solvers.QuantagoniaQuboSolver; import java.io.IOException; import java.util.Objects; import java.util.Set; @@ -28,7 +27,10 @@ public class QuboConfiguration { * find the minimal variable assignment of the term. */ public static final ProblemType QUBO = new ProblemType<>( - "qubo", + "QUBO", + "QUBO (Quadratic Unconstrained Binary Optimization) A combinatorial optimization problem. " + + "For a given quadratic term with binary decision variables, find the " + + "minimal variable assignment of the term.", String.class, String.class ); @@ -38,13 +40,12 @@ ProblemManager getQuboManager( QiskitQuboSolver qiskitSolver, DwaveQuboSolver dwaveSolver, QrispQuboSolver qrispSolver, - QuantagoniaQuboSolver quantagoniaQuboSolver, - KipuQuboSolver kipuQuboSolver, + GamsQuboSolver gamsQuboSolver, ResourceProvider resourceProvider ) { return new ProblemManager<>( QUBO, - Set.of(qiskitSolver, dwaveSolver, qrispSolver, quantagoniaQuboSolver, kipuQuboSolver), + Set.of(qiskitSolver, dwaveSolver, qrispSolver, gamsQuboSolver), loadExampleProblems(resourceProvider) ); } 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 new file mode 100644 index 00000000..960195df --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/GamsQuboSolver.java @@ -0,0 +1,65 @@ +package edu.kit.provideq.toolbox.qubo.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.ProcessRunner; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import edu.kit.provideq.toolbox.qubo.QuboConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * {@link QuboConfiguration#QUBO} solver using a GAMS CPLEX implementation. + */ +@Component +public class GamsQuboSolver extends QuboSolver { + private final String scriptPath; + private final String venv; + private final ApplicationContext context; + + @Autowired + public GamsQuboSolver( + @Value("${path.gams.qubo}") String scriptPath, + @Value("${venv.gams.qubo}") String venv, + ApplicationContext context) { + this.scriptPath = scriptPath; + this.venv = venv; + this.context = context; + } + + @Override + public String getName() { + return "(GAMS) CPLEX 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"; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + var solution = new Solution<>(this); + + var processResult = context + .getBean(PythonProcessRunner.class, scriptPath, venv) + .withArguments( + ProcessRunner.INPUT_FILE_PATH, + ProcessRunner.OUTPUT_FILE_PATH + ) + .writeInputFile(input, "problem.lp") + .readOutputFile("solution.json") + .run(getProblemType(), solution.getId()); + + return Mono.just(processResult.applyTo(solution)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/KipuQuboSolver.java b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/KipuQuboSolver.java index 060c01f1..42a8591e 100644 --- a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/KipuQuboSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/KipuQuboSolver.java @@ -26,8 +26,11 @@ /** * {@link QuboConfiguration#QUBO} solver using the * Kipu Digitized Counterdiabatic Quantum Optimization (DCQO) solver hosted on the PlanQK platform. + * + * @deprecated This solver is not provided on the Kipu platform anymore, use different Qubo solvers. */ @Component +@Deprecated(forRemoval = true) public class KipuQuboSolver extends QuboSolver { private static final String SETTING_PLANQK_TOKEN = "PlanQK Access Token"; diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QuantagoniaQuboSolver.java b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QuantagoniaQuboSolver.java index 5862a833..6a8d44cf 100644 --- a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QuantagoniaQuboSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QuantagoniaQuboSolver.java @@ -25,8 +25,11 @@ /** * {@link QuboConfiguration#QUBO} solver using the * Quantagonia QUBO solver hosted on the PlanQK platform. + * + * @deprecated This solver is not provided on the Kipu platform anymore, use different Qubo solvers. */ @Component +@Deprecated(forRemoval = true) public class QuantagoniaQuboSolver extends QuboSolver { private static final String SETTING_PLANQK_TOKEN = "PlanQK Access Token"; diff --git a/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java index b6f12a4c..b0794ae2 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java @@ -1,7 +1,9 @@ package edu.kit.provideq.toolbox.sat; import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.ConversionException; import edu.kit.provideq.toolbox.exception.MissingExampleException; +import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnf; import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnfSolution; import edu.kit.provideq.toolbox.meta.Problem; import edu.kit.provideq.toolbox.meta.ProblemManager; @@ -10,8 +12,10 @@ import edu.kit.provideq.toolbox.sat.solvers.QrispExactGroverSolver; import edu.kit.provideq.toolbox.sat.solvers.QrispGroverSolver; import java.io.IOException; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Function; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -25,11 +29,33 @@ public class SatConfiguration { * For a given boolean formula, check if there is an interpretation that satisfies the formula. */ public static final ProblemType SAT = new ProblemType<>( - "sat", + "SAT", + "A satisfiability problem: For a given boolean formula, check " + + "if there is an interpretation that satisfies the formula.", String.class, - DimacsCnfSolution.class + DimacsCnfSolution.class, + getAttributes() ); + public static Map> getAttributes() { + return Map.ofEntries( + Map.entry("variable_count", problem -> { + try { + return String.valueOf(DimacsCnf.fromString(problem).getVariables().size()); + } catch (ConversionException e) { + return ""; + } + }), + Map.entry("clause_count", problem -> { + try { + return String.valueOf(DimacsCnf.fromString(problem).getOrClauses().size()); + } catch (ConversionException e) { + return ""; + } + }) + ); + } + @Bean ProblemManager getSatManager( GamsSatSolver gamsSolver, diff --git a/src/main/java/edu/kit/provideq/toolbox/sat/solvers/QrispExactGroverSolver.java b/src/main/java/edu/kit/provideq/toolbox/sat/solvers/QrispExactGroverSolver.java index c894d0ff..03202439 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sat/solvers/QrispExactGroverSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/sat/solvers/QrispExactGroverSolver.java @@ -28,7 +28,8 @@ public class QrispExactGroverSolver extends SatSolver { private static final SubRoutineDefinition SHARPSAT_SUBROUTINE = new SubRoutineDefinition<>( SharpSatConfiguration.SHARPSAT, - "Count the number of solutions using a SharpSAT solver" + "Count the number of solutions using a SharpSAT solver", + true ); private final String scriptPath; diff --git a/src/main/java/edu/kit/provideq/toolbox/sharpsat/SharpSatConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/sharpsat/SharpSatConfiguration.java index 0138c421..67c66b27 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sharpsat/SharpSatConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/sharpsat/SharpSatConfiguration.java @@ -5,6 +5,7 @@ import edu.kit.provideq.toolbox.meta.Problem; import edu.kit.provideq.toolbox.meta.ProblemManager; import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.sat.SatConfiguration; import edu.kit.provideq.toolbox.sharpsat.solvers.GanakSolver; import edu.kit.provideq.toolbox.sharpsat.solvers.PythonBruteForceSolver; import java.io.IOException; @@ -24,9 +25,12 @@ public class SharpSatConfiguration { * NP-Hard in nature. */ public static final ProblemType SHARPSAT = new ProblemType<>( - "sharpsat", + "SharpSAT", + "A satisfiability counting problem: For a given boolean formula, count number of " + + "interpretations that satisfies the formula. NP-Hard in nature.", String.class, - Integer.class + Integer.class, + SatConfiguration.getAttributes() ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java index ab772850..6db1aa59 100644 --- a/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java @@ -9,6 +9,7 @@ import edu.kit.provideq.toolbox.tsp.solvers.QuboTspSolver; import java.io.IOException; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; import org.springframework.context.annotation.Bean; @@ -23,9 +24,18 @@ public class TspConfiguration { * between a given set of connected cities. */ public static final ProblemType TSP = new ProblemType<>( - "tsp", + "TSP", + "A Traveling Sales Person Problem. Optimization Problem with the goal of find " + + "an optimal route between a given set of connected cities.", String.class, - String.class + String.class, + Map.ofEntries( + Map.entry("dimension", problem -> problem.lines() + .filter(line -> line.startsWith("DIMENSION")) + .findFirst() + .map(line -> line.split(":")[1].trim()) + .orElse("")) + ) ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/QuboTspSolver.java b/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/QuboTspSolver.java index 6f993a41..548f851c 100644 --- a/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/QuboTspSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/QuboTspSolver.java @@ -26,7 +26,12 @@ @Component public class QuboTspSolver extends TspSolver { private static final SubRoutineDefinition QUBO_SUBROUTINE = - new SubRoutineDefinition<>(QuboConfiguration.QUBO, "How should the QUBO be solved?"); + new SubRoutineDefinition<>( + QuboConfiguration.QUBO, + "How should the QUBO be solved?", + true + ); + private final ApplicationContext context; private final String binaryPath; private ResourceProvider resourceProvider; diff --git a/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/UnsplittableMcfConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/UnsplittableMcfConfiguration.java new file mode 100644 index 00000000..0ec3b615 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/UnsplittableMcfConfiguration.java @@ -0,0 +1,105 @@ +package edu.kit.provideq.toolbox.unsplittablemcf; + +import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.MissingExampleException; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.unsplittablemcf.solvers.GamsUnsplittableMcfClassicalSolver; +import edu.kit.provideq.toolbox.unsplittablemcf.solvers.GamsUnsplittableMcfQuboSolver; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Definition and registration of the Unsplittable Multi Commodity Flow problem. + */ +@Configuration +public class UnsplittableMcfConfiguration { + + /** + * An optimization problem: + * For a given time-expanded network with products, orders, and capacity constraints, + * find optimal routing paths to fulfill customer orders through a Time-Expanded Network + * without splitting an order's demand. + */ + public static final ProblemType UNSPLITTABLE_MCF = new ProblemType<>( + "UnsplittableMCF", + "An optimization problem: For a given time-expanded network with products, orders, " + + "and capacity constraints, find optimal routing paths to fulfill customer orders " + + "through a Time-Expanded Network without splitting an order's demand", + String.class, + String.class, + Map.ofEntries( + Map.entry("products", problem -> getValue(problem, "N_PRODUCTS")), + Map.entry("time_steps", problem -> getValue(problem, "N_TIME_STEPS")), + Map.entry("orders", problem -> getValue(problem, "N_ORDERS")), + Map.entry("factories", problem -> getValue(problem, "N_FACTORIES")), + Map.entry("distribution_centers", problem -> getValue(problem, "N_DISTRIBUTION")) + ) + ); + + /** + * Helper method to extract configuration values from JSON input. + */ + private static String getValue(String json, String key) { + try { + // Simple JSON parsing without external library dependency + int configStart = json.indexOf("\"config\""); + if (configStart == -1) return ""; + + int keyStart = json.indexOf("\"" + key + "\"", configStart); + if (keyStart == -1) return ""; + + int colonPos = json.indexOf(":", keyStart); + if (colonPos == -1) return ""; + + int valueStart = colonPos + 1; + while (valueStart < json.length() + && (json.charAt(valueStart) == ' ' || json.charAt(valueStart) == '\n')) { + valueStart++; + } + + int valueEnd = valueStart; + while (valueEnd < json.length() + && Character.isDigit(json.charAt(valueEnd))) { + valueEnd++; + } + + return json.substring(valueStart, valueEnd).trim(); + } catch (Exception e) { + return ""; + } + } + + @Bean + ProblemManager getUnsplittableMcfManager( + GamsUnsplittableMcfClassicalSolver classicalSolver, + GamsUnsplittableMcfQuboSolver lpExportSolver, + ResourceProvider resourceProvider + ) { + return new ProblemManager<>( + UNSPLITTABLE_MCF, + Set.of(classicalSolver, lpExportSolver), + loadExampleProblems(resourceProvider) + ); + } + + private Set> loadExampleProblems( + ResourceProvider resourceProvider) { + try { + var problemInputStream = Objects.requireNonNull( + getClass().getResourceAsStream("unsplittable_instance.json"), + "Example instance for UnsplittableMCF is unavailable!" + ); + var problem = new Problem<>(UNSPLITTABLE_MCF); + problem.setInput(resourceProvider.readStream(problemInputStream)); + return Set.of(problem); + } catch (IOException e) { + throw new MissingExampleException(UNSPLITTABLE_MCF, e); + } + } +} 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 new file mode 100644 index 00000000..6e298aca --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfClassicalSolver.java @@ -0,0 +1,68 @@ +package edu.kit.provideq.toolbox.unsplittablemcf.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.ProcessRunner; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import edu.kit.provideq.toolbox.unsplittablemcf.UnsplittableMcfConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * Classical CPLEX solver for the {@link UnsplittableMcfConfiguration#UNSPLITTABLE_MCF} problem. + * This solver uses GAMSPy to build the MIP model and solves it using CPLEX. + */ +@Component +public class GamsUnsplittableMcfClassicalSolver extends GamsUnsplittableMcfSolver { + private final String scriptPath; + private final String venv; + private final ApplicationContext context; + + @Autowired + public GamsUnsplittableMcfClassicalSolver( + @Value("${path.gams.unsplittable-mcf-classical}") String scriptPath, + @Value("${venv.gams.unsplittable-mcf}") String venv, + ApplicationContext context) { + this.scriptPath = scriptPath; + this.venv = venv; + this.context = context; + } + + @Override + public String getName() { + return "(GAMS) CPLEX Classical MCF Solver"; + } + + @Override + public String getDescription() { + return "Solves the Unsplittable Multi Commodity Flow problem using GAMSPy with CPLEX. " + + "Builds a time-expanded network model and finds optimal flow routes minimizing " + + "delay and slack penalties."; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + var solution = new Solution<>(this); + + // Run the classical MCF solver via Python process + var processResult = context + .getBean(PythonProcessRunner.class, scriptPath, venv) + .withArguments( + ProcessRunner.INPUT_FILE_PATH, + ProcessRunner.OUTPUT_FILE_PATH + ) + .writeInputFile(input, "unsplittable-mcf.json") + .readOutputFile("output.html") + .run(getProblemType(), solution.getId()); + + return Mono.just(processResult.applyTo(solution)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfQuboSolver.java b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfQuboSolver.java new file mode 100644 index 00000000..f1abc5af --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfQuboSolver.java @@ -0,0 +1,122 @@ +package edu.kit.provideq.toolbox.unsplittablemcf.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SolvingProperties; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.ProcessRunner; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import edu.kit.provideq.toolbox.qubo.QuboConfiguration; +import edu.kit.provideq.toolbox.unsplittablemcf.UnsplittableMcfConfiguration; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * LP Export and QUBO solver for the {@link UnsplittableMcfConfiguration#UNSPLITTABLE_MCF} problem. + * This solver exports the MCF model to LP format (QUBO reformulation) and then solves it using + * the QUBO subroutine from the GAMS solvers. + */ +@Component +public class GamsUnsplittableMcfQuboSolver extends GamsUnsplittableMcfSolver { + private static final SubRoutineDefinition QUBO_SUBROUTINE = + new SubRoutineDefinition<>( + QuboConfiguration.QUBO, + "How should the QUBO be solved?", + true + ); + + private final String exportScriptPath; + private final String plotQuboSolutionScriptPath; + private final String venv; + private final ApplicationContext context; + + @Autowired + public GamsUnsplittableMcfQuboSolver( + @Value("${path.gams.unsplittable-mcf-lp-export}") String exportScriptPath, + @Value("${path.gams.unsplittable-mcf-plot_qubo_solution}") String plotQuboSolutionScriptPath, + @Value("${venv.gams.unsplittable-mcf}") String venv, + ApplicationContext context) { + this.exportScriptPath = exportScriptPath; + this.plotQuboSolutionScriptPath = plotQuboSolutionScriptPath; + this.venv = venv; + this.context = context; + } + + @Override + public String getName() { + return "U-MCF to QUBO Transformation"; + } + + @Override + public String getDescription() { + return "Solves the Unsplittable Multi Commodity Flow problem by transforming it to a " + + "QUBO problem and solving with a QUBO solver."; + } + + @Override + public List> getSubRoutines() { + return List.of(QUBO_SUBROUTINE); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver, + SolvingProperties properties + ) { + var solution = new Solution<>(this); + + // Run the LP exporter + var quboExportResult = context + .getBean(PythonProcessRunner.class, exportScriptPath, venv) + .withArguments( + ProcessRunner.INPUT_FILE_PATH, + ProcessRunner.OUTPUT_FILE_PATH + ) + .writeInputFile(input, "unsplittable-mcf.json") + .readOutputFile() + .run(getProblemType(), solution.getId()); + + if (!quboExportResult.success()) { + solution.setDebugData("LP export failed: " + + quboExportResult.errorOutput().orElse("Unknown error")); + solution.abort(); + return Mono.just(solution); + } + + var qubo = quboExportResult.output(); + if (qubo.isEmpty()) { + solution.setDebugData("LP export did not produce any output."); + solution.abort(); + return Mono.just(solution); + } + + // Call the QUBO subroutine + return subRoutineResolver.runSubRoutine(QUBO_SUBROUTINE, qubo.get()) + .map(quboSolution -> { + var plotQuboResult = context + .getBean(PythonProcessRunner.class, plotQuboSolutionScriptPath, venv) + .withArguments( + ProcessRunner.INPUT_FILE_PATH, + ProcessRunner.INPUT_FILE_PATH2, + ProcessRunner.OUTPUT_FILE_PATH + ) + .writeInputFile( + input, + "unsplittable-mcf.json", + ProcessRunner.INPUT_FILE_PATH) + .writeInputFile( + quboSolution.getSolutionData(), + "qubo-solution.json", + ProcessRunner.INPUT_FILE_PATH2) + .readOutputFile("output.html") + .run(getProblemType(), solution.getId()); + + return plotQuboResult.applyTo(solution); + }); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfSolver.java b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfSolver.java new file mode 100644 index 00000000..cfefaffd --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/unsplittablemcf/solvers/GamsUnsplittableMcfSolver.java @@ -0,0 +1,15 @@ +package edu.kit.provideq.toolbox.unsplittablemcf.solvers; + +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.unsplittablemcf.UnsplittableMcfConfiguration; + +/** + * Base class for Unsplittable Multi Commodity Flow solvers. + */ +public abstract class GamsUnsplittableMcfSolver implements ProblemSolver { + @Override + public ProblemType getProblemType() { + return UnsplittableMcfConfiguration.UNSPLITTABLE_MCF; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java index dc5a000a..6ff30144 100644 --- a/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java @@ -10,6 +10,7 @@ import edu.kit.provideq.toolbox.vrp.solvers.QrispVrpSolver; import java.io.IOException; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; import org.springframework.context.annotation.Bean; @@ -21,12 +22,26 @@ public class VrpConfiguration { /** * A Capacitated Vehicle Routing Problem * Optimization Problem with the goal to find a minimal route - * for a given set of trucks and cities with demand. + * for a given set of trucks and cities with demand. */ public static final ProblemType VRP = new ProblemType<>( - "vrp", + "VRP", + "A Capacitated Vehicle Routing Problem Optimization Problem with the goal " + + "to find a minimal route for a given set of trucks and cities with demand.", String.class, - String.class + String.class, + Map.ofEntries( + Map.entry("dimension", problem -> problem.lines() + .filter(line -> line.startsWith("DIMENSION")) + .findFirst() + .map(line -> line.split(":")[1].trim()) + .orElse("")), + Map.entry("capacity", problem -> problem.lines() + .filter(line -> line.startsWith("CAPACITY")) + .findFirst() + .map(line -> line.split(":")[1].trim()) + .orElse("")) + ) ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/KmeansClusterer.java b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/KmeansClusterer.java index 0d64661d..feb944f8 100644 --- a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/KmeansClusterer.java +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/KmeansClusterer.java @@ -36,7 +36,8 @@ public KmeansClusterer( protected static final SubRoutineDefinition VRP_SUBROUTINE = new SubRoutineDefinition<>( VrpConfiguration.VRP, - "Solve a VRP problem" + "Solve a VRP problem", + false ); @Override diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/TwoPhaseClusterer.java b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/TwoPhaseClusterer.java index 7f5aac5b..86581db2 100644 --- a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/TwoPhaseClusterer.java +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/TwoPhaseClusterer.java @@ -21,7 +21,8 @@ public class TwoPhaseClusterer extends VrpClusterer { protected static final SubRoutineDefinition TSP_SUBROUTINE = new SubRoutineDefinition<>( TspConfiguration.TSP, - "Solve a TSP problem" + "Solve a TSP problem", + false ); @Autowired diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java index 1d2e244c..58f999aa 100644 --- a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java @@ -17,11 +17,10 @@ public class VrpClustererConfiguration { * A Configuration for Vehicle Routing Problem Clusterer. */ public static final ProblemType CLUSTER_VRP = new ProblemType<>( - "cluster-vrp", + "ClusterVRP", + "A Configuration for Vehicle Routing Problem Clusterer.", String.class, - String.class, - null, - null + String.class ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/ClusterAndSolveVrpSolver.java b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/ClusterAndSolveVrpSolver.java index 237279e0..9b35fdb2 100644 --- a/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/ClusterAndSolveVrpSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/ClusterAndSolveVrpSolver.java @@ -6,8 +6,6 @@ import edu.kit.provideq.toolbox.meta.SubRoutineResolver; import edu.kit.provideq.toolbox.vrp.clusterer.VrpClustererConfiguration; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -16,15 +14,9 @@ public class ClusterAndSolveVrpSolver extends VrpSolver { private static final SubRoutineDefinition CLUSTER_SUBROUTINE = new SubRoutineDefinition<>( VrpClustererConfiguration.CLUSTER_VRP, - "Creates a cluster of multiple vehicle routing problems" + "Creates a cluster of multiple vehicle routing problems", + true ); - private final ApplicationContext context; - - @Autowired - public ClusterAndSolveVrpSolver( - ApplicationContext context) { - this.context = context; - } @Override public String getName() { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 86d54fb5..0a020977 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,134 @@ -# default spring profile, correct one will be set during runtime (see ToolboxServerApplication.java) # options: mac, windows, linux spring.profiles.active=linux springdoc.swagger-ui.operationsSorter=alpha springdoc.swagger-ui.tagsSorter=alpha working.directory=jobs examples.directory=examples springdoc.swagger-ui.path=/ # Solvers name.solvers=solvers # Non OS-specific solvers: (typically GAMS and Python) name.gams=gams path.gams=${name.solvers}/${name.gams} name.gams.max-cut=max-cut path.gams.max-cut=${path.gams}/${name.gams.max-cut}/maxcut.gms name.gams.sat=sat path.gams.sat=${path.gams}/${name.gams.sat}/sat.gms name.qiskit=qiskit path.qiskit=${name.solvers}/${name.qiskit} name.qiskit.knapsack=knapsack path.qiskit.knapsack=${path.qiskit}/${name.qiskit.knapsack}/knapsack_qiskit.py venv.qiskit.knapsack=${name.solvers}_${name.qiskit}_${name.qiskit.knapsack} name.qiskit.max-cut=max-cut path.qiskit.max-cut=${path.qiskit}/${name.qiskit.max-cut}/maxCut_qiskit.py venv.qiskit.max-cut=${name.solvers}_${name.qiskit}_${name.qiskit.max-cut} name.qiskit.qubo=qubo path.qiskit.qubo=${path.qiskit}/${name.qiskit.qubo}/qubo_qiskit.py venv.qiskit.qubo=${name.solvers}_${name.qiskit}_${name.qiskit.qubo} name.cirq=cirq path.cirq=${name.solvers}/${name.cirq} name.cirq.max-cut=max-cut path.cirq.max-cut=${path.cirq}/${name.cirq.max-cut}/max_cut_cirq.py venv.cirq.max-cut=${name.solvers}_${name.cirq}_${name.cirq.max-cut} name.qrisp=qrisp path.qrisp=${name.solvers}/${name.qrisp} name.qrisp.vrp=vrp path.qrisp.vrp=${path.qrisp}/${name.qrisp.vrp}/grover.py venv.qrisp.vrp=${name.solvers}_${name.qrisp}_${name.qrisp.vrp} name.qrisp.qubo=qubo path.qrisp.qubo=${path.qrisp}/${name.qrisp.qubo}/qaoa.py venv.qrisp.qubo=${name.solvers}_${name.qrisp}_${name.qrisp.qubo} name.qrisp.sat=sat path.qrisp.sat.grover=${path.qrisp}/${name.qrisp.sat}/grover.py path.qrisp.sat.exact=${path.qrisp}/${name.qrisp.sat}/exact_grover.py venv.qrisp.sat=${name.solvers}_${name.qrisp}_${name.qrisp.sat} name.dwave=dwave path.dwave=${name.solvers}/${name.dwave} name.dwave.qubo=qubo path.dwave.qubo=${path.dwave}/${name.dwave.qubo}/main.py venv.dwave.qubo=${name.solvers}_${name.dwave}_${name.dwave.qubo} # Non OS-specific custom solvers: (solvers that are not part of a framework) name.custom=custom path.custom=${name.solvers}/${name.custom} name.custom.hs-knapsack=hs-knapsack path.custom.hs-knapsack=${path.custom}/${name.custom.hs-knapsack}/knapsack.py venv.custom.hs-knapsack=${name.solvers}_${name.custom}_${name.custom.hs-knapsack} name.custom.lkh=lkh path.custom.lkh=${path.custom}/${name.custom.lkh}/vrp_lkh.py venv.custom.lkh=${name.solvers}_${name.custom}_${name.custom.lkh} name.custom.berger-vrp=berger-vrp name.custom.sharp-sat-bruteforce=sharp-sat-bruteforce path.custom.sharp-sat-bruteforce=${path.custom}/${name.custom.sharp-sat-bruteforce}/exact-solution-counter.py venv.custom.sharp-sat-bruteforce=${name.solvers}_${name.custom}_${name.custom.sharp-sat-bruteforce} name.custom.sharp-sat-ganak=sharp-sat-ganak venv.custom.sharp-sat-ganak=${name.solvers}_${name.custom}_${name.custom.sharp-sat-ganak} # Demonstrators name.demonstrators=demonstrators name.demonstrators.cplex=cplex path.demonstrators.cplex=${name.demonstrators}/${name.demonstrators.cplex} name.demonstrators.cplex.mip=mip-solver path.demonstrators.cplex.mip=${path.demonstrators.cplex}/${name.demonstrators.cplex.mip}/mip-solver.py venv.demonstrators.cplex.mip=${name.demonstrators}_${name.demonstrators.cplex}_${name.demonstrators.cplex.mip} \ No newline at end of file +# default spring profile, correct one will be set during runtime (see ToolboxServerApplication.java) +# options: mac, windows, linux +spring.profiles.active=linux +springdoc.swagger-ui.operationsSorter=alpha +springdoc.swagger-ui.tagsSorter=alpha + +working.directory=jobs +examples.directory=examples +springdoc.swagger-ui.path=/ + +# Solvers +name.solvers=solvers + +# Non OS-specific solvers: (typically GAMS and Python) +name.gams=gams +path.gams=${name.solvers}/${name.gams} + +name.gams.max-cut=max-cut +path.gams.max-cut=${path.gams}/${name.gams.max-cut}/maxcut.gms + +name.gams.sat=sat +path.gams.sat=${path.gams}/${name.gams.sat}/sat.gms + +name.gams.qubo=qubo +path.gams.qubo=${path.gams}/${name.gams.qubo}/solver_lp_qubo.py +venv.gams.qubo=${name.solvers}_${name.gams}_${name.gams.qubo} + +name.gams.unsplittable-mcf=unsplittable-mcf +path.gams.unsplittable-mcf-classical=${path.gams}/${name.gams.unsplittable-mcf}/solver_classical.py +path.gams.unsplittable-mcf-lp-export=${path.gams}/${name.gams.unsplittable-mcf}/solver_lp_export.py +path.gams.unsplittable-mcf-plot_qubo_solution=${path.gams}/${name.gams.unsplittable-mcf}/plot_qubo_solution.py +venv.gams.unsplittable-mcf=${name.solvers}_${name.gams}_${name.gams.unsplittable-mcf} + +name.qiskit=qiskit +path.qiskit=${name.solvers}/${name.qiskit} + +name.qiskit.knapsack=knapsack +path.qiskit.knapsack=${path.qiskit}/${name.qiskit.knapsack}/knapsack_qiskit.py +venv.qiskit.knapsack=${name.solvers}_${name.qiskit}_${name.qiskit.knapsack} + +name.qiskit.materialsimulation=materialsimulation +path.qiskit.materialsimulation=${path.qiskit}/${name.qiskit.materialsimulation}/material_simulation_qiskit.py +venv.qiskit.materialsimulation=${name.solvers}_${name.qiskit}_${name.qiskit.materialsimulation} + +name.qiskit.max-cut=max-cut +path.qiskit.max-cut=${path.qiskit}/${name.qiskit.max-cut}/maxCut_qiskit.py +venv.qiskit.max-cut=${name.solvers}_${name.qiskit}_${name.qiskit.max-cut} + +name.qiskit.qubo=qubo +path.qiskit.qubo=${path.qiskit}/${name.qiskit.qubo}/qubo_qiskit.py +venv.qiskit.qubo=${name.solvers}_${name.qiskit}_${name.qiskit.qubo} + + +name.cirq=cirq +path.cirq=${name.solvers}/${name.cirq} + +name.cirq.max-cut=max-cut +path.cirq.max-cut=${path.cirq}/${name.cirq.max-cut}/max_cut_cirq.py +venv.cirq.max-cut=${name.solvers}_${name.cirq}_${name.cirq.max-cut} + + +name.qrisp=qrisp +path.qrisp=${name.solvers}/${name.qrisp} + +name.qrisp.vrp=vrp +path.qrisp.vrp=${path.qrisp}/${name.qrisp.vrp}/grover.py +venv.qrisp.vrp=${name.solvers}_${name.qrisp}_${name.qrisp.vrp} + +name.qrisp.qubo=qubo +path.qrisp.qubo=${path.qrisp}/${name.qrisp.qubo}/qaoa.py +venv.qrisp.qubo=${name.solvers}_${name.qrisp}_${name.qrisp.qubo} + +name.qrisp.sat=sat +path.qrisp.sat.grover=${path.qrisp}/${name.qrisp.sat}/grover.py +path.qrisp.sat.exact=${path.qrisp}/${name.qrisp.sat}/exact_grover.py +venv.qrisp.sat=${name.solvers}_${name.qrisp}_${name.qrisp.sat} + +name.dwave=dwave +path.dwave=${name.solvers}/${name.dwave} + +name.dwave.qubo=qubo +path.dwave.qubo=${path.dwave}/${name.dwave.qubo}/main.py +venv.dwave.qubo=${name.solvers}_${name.dwave}_${name.dwave.qubo} + + +# Non OS-specific custom solvers: (solvers that are not part of a framework) +name.custom=custom +path.custom=${name.solvers}/${name.custom} + +name.custom.hs-knapsack=hs-knapsack +path.custom.hs-knapsack=${path.custom}/${name.custom.hs-knapsack}/knapsack.py +venv.custom.hs-knapsack=${name.solvers}_${name.custom}_${name.custom.hs-knapsack} + +name.custom.lkh=lkh +path.custom.lkh=${path.custom}/${name.custom.lkh}/vrp_lkh.py +venv.custom.lkh=${name.solvers}_${name.custom}_${name.custom.lkh} + +name.custom.berger-vrp=berger-vrp + +name.custom.sharp-sat-bruteforce=sharp-sat-bruteforce +path.custom.sharp-sat-bruteforce=${path.custom}/${name.custom.sharp-sat-bruteforce}/exact-solution-counter.py +venv.custom.sharp-sat-bruteforce=${name.solvers}_${name.custom}_${name.custom.sharp-sat-bruteforce} + +name.custom.sharp-sat-ganak=sharp-sat-ganak +venv.custom.sharp-sat-ganak=${name.solvers}_${name.custom}_${name.custom.sharp-sat-ganak} + + +# Demonstrators +name.demonstrators=demonstrators + +name.demonstrators.cplex=cplex +path.demonstrators.cplex=${name.demonstrators}/${name.demonstrators.cplex} + +name.demonstrators.cplex.mip=mip-solver +path.demonstrators.cplex.mip=${path.demonstrators.cplex}/${name.demonstrators.cplex.mip}/mip-solver.py +venv.demonstrators.cplex.mip=${name.demonstrators}_${name.demonstrators.cplex}_${name.demonstrators.cplex.mip} + +name.demonstrators.qiskit=qiskit +path.demonstrators.qiskit=${name.demonstrators}/${name.demonstrators.qiskit} + +name.demonstrators.qiskit.molecule-energy=molecule-energy +path.demonstrators.qiskit.molecule-energy=${path.demonstrators.qiskit}/${name.demonstrators.qiskit.molecule-energy}/molecule-energy.py +venv.demonstrators.qiskit.molecule-energy=${name.demonstrators}_${name.demonstrators.qiskit}_${name.demonstrators.qiskit.molecule-energy} + +name.circuitprocessing=circuitprocessing +path.circuitprocessing=${name.solvers}/${name.circuitprocessing} +name.circuitprocessing.circuitexecution=circuitexecution +path.circuitprocessing.circuitexecution=${path.circuitprocessing}/${name.circuitprocessing.circuitexecution}/executor.py +venv.circuitprocessing.circuitexecution=${name.solvers}_${name.circuitprocessing}_${name.circuitprocessing.circuitexecution} +name.circuitprocessing.circuitoptimization=circuitoptimizing +path.circuitprocessing.circuitoptimization=${path.circuitprocessing}/${name.circuitprocessing.circuitoptimization} +path.circuitprocessing.circuitoptimization.decomposemulticx=${path.circuitprocessing.circuitoptimization}/decompose-multi-cx/decompose_multi_cx_optimizer.py +path.circuitprocessing.circuitoptimization.removeredundancies=${path.circuitprocessing.circuitoptimization}/remove-redundancies/remove_redundancies_optimizer.py +venv.circuitprocessing.circuitoptimization=${name.solvers}_${name.circuitprocessing}_${name.circuitprocessing.circuitoptimization} diff --git a/src/main/resources/edu/kit/provideq/toolbox/circuit/processing/bell-state.qasm b/src/main/resources/edu/kit/provideq/toolbox/circuit/processing/bell-state.qasm new file mode 100644 index 00000000..c3bf3c2b --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/circuit/processing/bell-state.qasm @@ -0,0 +1,8 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +creg c[2]; +h q[0]; +cx q[0],q[1]; +measure q[0] -> c[0]; +measure q[1] -> c[1]; diff --git a/src/main/resources/edu/kit/provideq/toolbox/circuit/processing/cswap.qasm b/src/main/resources/edu/kit/provideq/toolbox/circuit/processing/cswap.qasm new file mode 100644 index 00000000..b2bce4dc --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/circuit/processing/cswap.qasm @@ -0,0 +1,6 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[3]; +crz(0.5) q[0], q[1]; +t q[2]; +cswap q[2], q[0], q[1]; diff --git a/src/main/resources/edu/kit/provideq/toolbox/materialsimulation/h2.txt b/src/main/resources/edu/kit/provideq/toolbox/materialsimulation/h2.txt new file mode 100644 index 00000000..62bd654d --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/materialsimulation/h2.txt @@ -0,0 +1 @@ +H 0 0 0; H 0 0 0.735 \ No newline at end of file diff --git a/src/main/resources/edu/kit/provideq/toolbox/unsplittablemcf/unsplittable_instance.json b/src/main/resources/edu/kit/provideq/toolbox/unsplittablemcf/unsplittable_instance.json new file mode 100644 index 00000000..06e7995b --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/unsplittablemcf/unsplittable_instance.json @@ -0,0 +1,221 @@ +{ + "config": { + "N_PRODUCTS": 1, + "N_TIME_STEPS": 2, + "N_ORDERS": 2, + "N_FACTORIES": 1, + "N_SORT": 1, + "N_DIEBANK": 1, + "N_ASSEMBLY": 1, + "N_TEST": 1, + "N_DISTRIBUTION": 1, + "N_DEMAND": 1, + "MIN_CYCLE_TIME": 0, + "MAX_CYCLE_TIME": 1, + "MIN_CAPACITY": 10, + "MAX_CAPACITY": 15, + "MIN_LOAD_FACTOR": 1, + "MAX_LOAD_FACTOR": 2, + "PENALTY_DELAY": 5, + "PENALTY_SLACK": 40, + "PENALTY_CONSTANT": 50 + }, + "cycle_time": [ + [ + "v_sort0", + 1.0 + ] + ], + "capacity": [ + [ + "v_fac0", + 11.0 + ], + [ + "v_sort0", + 12.0 + ], + [ + "v_db0", + 1e+100 + ], + [ + "v_asm0", + 12.0 + ], + [ + "v_test0", + 12.0 + ], + [ + "v_dc0", + 1e+100 + ] + ], + "load_factor": [ + [ + "v_fac0", + "p0", + 2.0 + ], + [ + "v_sort0", + "p0", + 1.0 + ], + [ + "v_asm0", + "p0", + 2.0 + ], + [ + "v_test0", + "p0", + 2.0 + ] + ], + "demand": [ + [ + "o0", + "p0", + "t0", + "v_dc0", + 4.0 + ], + [ + "o1", + "p0", + "t0", + "v_dc0", + 3.0 + ] + ], + "eo": [ + [ + "v_source", + "t0", + "v_fac0", + "t0", + "o0" + ], + [ + "v_source", + "t0", + "v_fac0", + "t0", + "o1" + ], + [ + "v_source", + "t0", + "v_fac0", + "t1", + "o0" + ], + [ + "v_source", + "t0", + "v_fac0", + "t1", + "o1" + ], + [ + "v_fac0", + "t0", + "v_sort0", + "t0", + "o0" + ], + [ + "v_fac0", + "t0", + "v_sort0", + "t0", + "o1" + ], + [ + "v_fac0", + "t1", + "v_sort0", + "t1", + "o0" + ], + [ + "v_fac0", + "t1", + "v_sort0", + "t1", + "o1" + ], + [ + "v_sort0", + "t0", + "v_db0", + "t1", + "o0" + ], + [ + "v_sort0", + "t0", + "v_db0", + "t1", + "o1" + ], + [ + "v_db0", + "t1", + "v_asm0", + "t1", + "o0" + ], + [ + "v_db0", + "t1", + "v_asm0", + "t1", + "o1" + ], + [ + "v_asm0", + "t1", + "v_test0", + "t1", + "o0" + ], + [ + "v_asm0", + "t1", + "v_test0", + "t1", + "o1" + ], + [ + "v_test0", + "t1", + "v_dc0", + "t1", + "o0" + ], + [ + "v_test0", + "t1", + "v_dc0", + "t1", + "o1" + ], + [ + "v_dc0", + "t0", + "v_dc0", + "t1", + "o0" + ], + [ + "v_dc0", + "t0", + "v_dc0", + "t1", + "o1" + ] + ] +} \ No newline at end of file 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 53511624..2d2abb1f 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java @@ -153,11 +153,18 @@ public static ProblemDto trySolveFor( }) .returnResult() .getResponseBody(); - builder.append("Fetched problem: " + problemDto + "\n"); + if (problemDto == null) { + System.out.println("Testcase with id " + problemId + + " will terminate because problemDto is null"); + System.out.println("problemType: " + problemType); + } assertNotNull(problemDto); + builder.append("Fetched problem: " + problemDto + "\n"); + if (problemDto.getState() == ProblemState.SOLVED) { + System.out.println(problemId + " solved successful."); break; } @@ -174,6 +181,12 @@ public static ProblemDto trySolveFor( .getResponseBody(); builder.append("Fetched sub problem: " + subProblemDto + "\n"); + if (subProblemDto == null) { + System.out.println("Subproblem with id " + subProblemId + + " will terminate because subProblemDto is null"); + System.out.println("subProblem: " + subProblem); + System.out.println("Log: " + builder); + } assertNotNull(subProblemDto); } } @@ -187,6 +200,8 @@ public static ProblemDto trySolveFor( } if (hasTimeout.get()) { + System.out.println("Stopping testcase due to timeout."); + System.out.println("log: " + builder); throw new IllegalStateException("Problem did not solve in time: " + problemDto); } } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/CircuitProcessingSolversTest.java b/src/test/java/edu/kit/provideq/toolbox/api/CircuitProcessingSolversTest.java new file mode 100644 index 00000000..98ba83d2 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/CircuitProcessingSolversTest.java @@ -0,0 +1,119 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.circuit.processing.CircuitProcessingConfiguration.CIRCUIT_PROCESSING; +import static edu.kit.provideq.toolbox.circuit.processing.solvers.executor.ExecutorConfiguration.EXECUTOR_CONFIG; +import static edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation.ErrorMitigationConfiguration.MITIGATION_CONFIG; +import static edu.kit.provideq.toolbox.circuit.processing.solvers.optimization.OptimizationConfiguration.OPTIMIZATION_CONFIG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.provideq.toolbox.circuit.processing.solvers.MoveToExecutionSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.MoveToMitigationSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.MoveToOptimizationSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.executor.ExecutionSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation.ErrorMitigationSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.optimization.OptimizationSolver; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest +@AutoConfigureMockMvc +class CircuitProcessingSolversTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @Autowired + private MoveToMitigationSolver moveToMitigationSolver; + + @Autowired + private MoveToExecutionSolver moveToExecutionSolver; + + @Autowired + private MoveToOptimizationSolver moveToOptimizationSolver; + + @Autowired + private ErrorMitigationSolver errorMitigationSolver; + + @Autowired + private ExecutionSolver executionSolver; + + @Autowired + private OptimizationSolver optimizationSolver; + + private ProblemManager problemManager; + private List problems; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) + .build(); + problemManager = problemManagerProvider.findProblemManagerForType(CIRCUIT_PROCESSING).get(); + problems = problemManager.getExampleInstances() + .stream() + .map(Problem::getInput) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + @Test + void testMoveToMitigationSolver() { + var circuit = problems.get(0); + var problemDto = ApiTestHelper.createProblem(client, moveToMitigationSolver, + circuit, CIRCUIT_PROCESSING); + var subProblemId = problemDto.getSubProblems().get(0) + .getSubProblemIds().get(0); + ApiTestHelper.setProblemSolver(client, errorMitigationSolver, subProblemId, + MITIGATION_CONFIG.getId()); + var solvedDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), CIRCUIT_PROCESSING); + ApiTestHelper.testSolution(solvedDto); + assertEquals(circuit, solvedDto.getSolution().getSolutionData()); + } + + @Test + void testMoveToExecutionSolver() { + var circuit = problems.get(0); + var problemDto = ApiTestHelper.createProblem(client, moveToExecutionSolver, + circuit, CIRCUIT_PROCESSING); + var subProblemId = problemDto.getSubProblems().get(0).getSubProblemIds().get(0); + ApiTestHelper.setProblemSolver(client, executionSolver, subProblemId, EXECUTOR_CONFIG.getId()); + var solvedDto = ApiTestHelper.trySolveFor(120, client, + problemDto.getId(), CIRCUIT_PROCESSING); + ApiTestHelper.testSolution(solvedDto); + assertFalse(solvedDto.getSolution().getSolutionData().isBlank()); + } + + @Test + void testMoveToOptimizationSolver() { + var circuit = problems.get(0); + var problemDto = ApiTestHelper.createProblem(client, moveToOptimizationSolver, + circuit, CIRCUIT_PROCESSING); + var optSubProblemId = problemDto.getSubProblems().get(0).getSubProblemIds().get(0); + var optDto = ApiTestHelper.setProblemSolver( + client, optimizationSolver, optSubProblemId, OPTIMIZATION_CONFIG.getId()); + var circuitSubProblemId = optDto.getSubProblems().get(0).getSubProblemIds().get(0); + var mitigationEntryDto = ApiTestHelper.setProblemSolver( + client, moveToMitigationSolver, circuitSubProblemId, CIRCUIT_PROCESSING.getId()); + var mitigationId = mitigationEntryDto.getSubProblems().get(0).getSubProblemIds().get(0); + ApiTestHelper.setProblemSolver(client, errorMitigationSolver, mitigationId, + MITIGATION_CONFIG.getId()); + var solvedDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), CIRCUIT_PROCESSING); + ApiTestHelper.testSolution(solvedDto); + assertTrue(solvedDto.getSolution().getSolutionData().contains("OPENQASM")); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/CplexMipDemonstratorTest.java b/src/test/java/edu/kit/provideq/toolbox/api/CplexMipDemonstratorTest.java deleted file mode 100644 index 93039c61..00000000 --- a/src/test/java/edu/kit/provideq/toolbox/api/CplexMipDemonstratorTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package edu.kit.provideq.toolbox.api; - -import static edu.kit.provideq.toolbox.demonstrators.DemonstratorConfiguration.DEMONSTRATOR; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import edu.kit.provideq.toolbox.SolutionStatus; -import edu.kit.provideq.toolbox.demonstrators.CplexMipDemonstrator; -import edu.kit.provideq.toolbox.meta.ProblemState; -import java.time.Duration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.reactive.server.WebTestClient; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@SpringBootTest -@AutoConfigureMockMvc -class CplexMipDemonstratorTest { - @Autowired - private WebTestClient client; - - @Autowired - private CplexMipDemonstrator cplexMipDemonstrator; - - @BeforeEach - void beforeEach() { - this.client = this.client.mutate() - .responseTimeout(Duration.ofSeconds(20)) - .build(); - } - - @Test - void testCplexMipDemonstrator() { - var problem = ApiTestHelper.createProblem(client, cplexMipDemonstrator, "", DEMONSTRATOR); - ApiTestHelper.testSolution(problem); - } -} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/DemonstratorTest.java b/src/test/java/edu/kit/provideq/toolbox/api/DemonstratorTest.java new file mode 100644 index 00000000..535ea044 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/DemonstratorTest.java @@ -0,0 +1,52 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.demonstrators.DemonstratorConfiguration.DEMONSTRATOR; + +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import java.time.Duration; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest +@AutoConfigureMockMvc +class DemonstratorTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) + .build(); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + Stream provideArguments() { + var problemManager = problemManagerProvider.findProblemManagerForType(DEMONSTRATOR).get(); + + return problemManager + .getSolvers() + .stream() + .map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("provideArguments") + void testDemonstratorsWithEmptyInput(ProblemSolver solver) { + var problem = ApiTestHelper.createProblem(client, solver, "", DEMONSTRATOR); + ApiTestHelper.testSolution(problem); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/ErrorMitigationSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/ErrorMitigationSolverTest.java new file mode 100644 index 00000000..28fde2a0 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/ErrorMitigationSolverTest.java @@ -0,0 +1,53 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation.ErrorMitigationConfiguration.MITIGATION_CONFIG; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation.ErrorMitigationSolver; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest +@AutoConfigureMockMvc +class ErrorMitigationSolverTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @Autowired + private ErrorMitigationSolver errorMitigationSolver; + + private ProblemManager problemManager; + private List problems; + + @BeforeEach + void beforeEach() { + problemManager = problemManagerProvider.findProblemManagerForType(MITIGATION_CONFIG).get(); + problems = problemManager.getExampleInstances() + .stream() + .map(Problem::getInput) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + @Test + void testErrorMitigationSolver() { + var circuit = problems.get(0); + var problem = ApiTestHelper.createProblem(client, errorMitigationSolver, + circuit, MITIGATION_CONFIG); + ApiTestHelper.testSolution(problem); + assertEquals(circuit, problem.getSolution().getSolutionData()); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/ExecutionSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/ExecutionSolverTest.java new file mode 100644 index 00000000..5e386689 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/ExecutionSolverTest.java @@ -0,0 +1,56 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.circuit.processing.solvers.executor.ExecutorConfiguration.EXECUTOR_CONFIG; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.circuit.processing.solvers.executor.ExecutionSolver; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest +@AutoConfigureMockMvc +class ExecutionSolverTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @Autowired + private ExecutionSolver executionSolver; + + private List problems; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) + .build(); + problems = problemManagerProvider.findProblemManagerForType(EXECUTOR_CONFIG).get() + .getExampleInstances() + .stream() + .map(Problem::getInput) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + @Test + void testExecutionSolver() { + var circuit = problems.get(0); + var problem = ApiTestHelper.createProblem(client, executionSolver, circuit, EXECUTOR_CONFIG); + ApiTestHelper.testSolution(problem); + Solution solution = problem.getSolution(); + assertTrue(solution.getSolutionData().toString().contains("OPENQASM")); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolversTest.java b/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolversTest.java index 3a493ed1..ef45afc6 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolversTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolversTest.java @@ -37,7 +37,7 @@ class FeatureModelAnomalySolversTest { @BeforeEach void beforeEach() { this.client = this.client.mutate() - .responseTimeout(Duration.ofSeconds(20)) + .responseTimeout(Duration.ofSeconds(60)) .build(); } @@ -87,7 +87,7 @@ void testFeatureModelAnomalySolver( } } - problem = ApiTestHelper.trySolveFor(15, client, problem.getId(), problemType); + problem = ApiTestHelper.trySolveFor(120, client, problem.getId(), problemType); ApiTestHelper.testSolution(problem); } } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/MaterialSimulationSolversTest.java b/src/test/java/edu/kit/provideq/toolbox/api/MaterialSimulationSolversTest.java new file mode 100644 index 00000000..0a121df2 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/MaterialSimulationSolversTest.java @@ -0,0 +1,52 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.materialsimulation.MaterialSimulationConfiguration.MATERIAL_SIMULATION; + +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import java.time.Duration; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest +@AutoConfigureMockMvc +class MaterialSimulationSolversTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(20)) + .build(); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + Stream provideArguments() { + var problemManager = problemManagerProvider + .findProblemManagerForType(MATERIAL_SIMULATION) + .get(); + + return ApiTestHelper.getAllArgumentCombinations(problemManager) + .map(list -> Arguments.of(list.get(0), list.get(1))); + } + + @ParameterizedTest + @MethodSource("provideArguments") + void testMaterialSimulationSolver(ProblemSolver solver, String input) { + var problem = ApiTestHelper.createProblem(client, solver, input, MATERIAL_SIMULATION); + ApiTestHelper.testSolution(problem); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/OptimizationSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/OptimizationSolverTest.java new file mode 100644 index 00000000..1d491bd3 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/OptimizationSolverTest.java @@ -0,0 +1,74 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.circuit.processing.CircuitProcessingConfiguration.CIRCUIT_PROCESSING; +import static edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation.ErrorMitigationConfiguration.MITIGATION_CONFIG; +import static edu.kit.provideq.toolbox.circuit.processing.solvers.optimization.OptimizationConfiguration.OPTIMIZATION_CONFIG; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.provideq.toolbox.circuit.processing.solvers.MoveToMitigationSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.mitigation.ErrorMitigationSolver; +import edu.kit.provideq.toolbox.circuit.processing.solvers.optimization.OptimizationSolver; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemManager; +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest +@AutoConfigureMockMvc +class OptimizationSolverTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @Autowired + private OptimizationSolver optimizationSolver; + + @Autowired + private MoveToMitigationSolver moveToMitigationSolver; + + @Autowired + private ErrorMitigationSolver errorMitigationSolver; + + private ProblemManager problemManager; + private List problems; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) + .build(); + problemManager = problemManagerProvider.findProblemManagerForType(OPTIMIZATION_CONFIG).get(); + problems = problemManager.getExampleInstances() + .stream() + .map(Problem::getInput) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + @Test + void testOptimizationSolver() { + var circuit = problems.get(0); + var problemDto = ApiTestHelper.createProblem(client, optimizationSolver, + circuit, OPTIMIZATION_CONFIG); + var circuitSubProblemId = problemDto.getSubProblems().get(0).getSubProblemIds().get(0); + var mitigationEntryDto = ApiTestHelper.setProblemSolver( + client, moveToMitigationSolver, circuitSubProblemId, CIRCUIT_PROCESSING.getId()); + var mitigationId = mitigationEntryDto.getSubProblems().get(0).getSubProblemIds().get(0); + ApiTestHelper.setProblemSolver(client, errorMitigationSolver, mitigationId, + MITIGATION_CONFIG.getId()); + var solvedDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), OPTIMIZATION_CONFIG); + ApiTestHelper.testSolution(solvedDto); + assertTrue(solvedDto.getSolution().getSolutionData().contains("OPENQASM")); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/QuboSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/QuboSolverTest.java index 5cdcad08..4a507f6d 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/QuboSolverTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/QuboSolverTest.java @@ -2,11 +2,8 @@ import static edu.kit.provideq.toolbox.qubo.QuboConfiguration.QUBO; -import edu.kit.provideq.toolbox.SolutionStatus; import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; import edu.kit.provideq.toolbox.meta.ProblemSolver; -import edu.kit.provideq.toolbox.qubo.solvers.KipuQuboSolver; -import edu.kit.provideq.toolbox.qubo.solvers.QuantagoniaQuboSolver; import java.time.Duration; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -41,11 +38,6 @@ Stream provideArguments() { var problemManager = problemManagerProvider.findProblemManagerForType(QUBO).get(); return ApiTestHelper.getAllArgumentCombinations(problemManager) - .filter(list -> { - Object solver = list.get(0); - return !(solver instanceof KipuQuboSolver) - && !(solver instanceof QuantagoniaQuboSolver); - }) .map(list -> Arguments.of(list.get(0), list.get(1))); } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/SatSolversTest.java b/src/test/java/edu/kit/provideq/toolbox/api/SatSolversTest.java index 50845c93..4c15925d 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/SatSolversTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/SatSolversTest.java @@ -68,7 +68,7 @@ Stream provideQrispExactGroverArguments() { var satManager = problemManagerProvider.findProblemManagerForType(SAT).get(); var qrispSolvers = satManager.getSolvers().stream() - .filter(s -> s instanceof QrispExactGroverSolver) + .filter(QrispExactGroverSolver.class::isInstance) .toList(); var exampleInputs = satManager.getExampleInstances().stream() @@ -119,7 +119,7 @@ void testQrispExactGroverSolver( ApiTestHelper.testSolution(assignedSubProblem); } } - var solvedProblemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), problemType); + var solvedProblemDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), problemType); ApiTestHelper.testSolution(solvedProblemDto); } } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/TspSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/TspSolverTest.java index f608d628..8d0dae97 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/TspSolverTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/TspSolverTest.java @@ -77,7 +77,7 @@ void testLkhTspSolver() { private Stream quboSolvers() { return Stream.of( - Arguments.of(dwaveQuboSolver, "NAME : small sample"), + // Arguments.of(dwaveQuboSolver, "NAME : small sample"), Arguments.of(qrispQuboSolver, "NAME : VerySmallSample") ); } @@ -105,7 +105,7 @@ void testQuboTspSolver(QuboSolver solver, String problemName) { ); //solve problem: - var solvedProblemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), TSP); + var solvedProblemDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), TSP); ApiTestHelper.testSolution(solvedProblemDto); } } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/UnsplittableMcfSolversTest.java b/src/test/java/edu/kit/provideq/toolbox/api/UnsplittableMcfSolversTest.java new file mode 100644 index 00000000..5ff9ecf8 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/UnsplittableMcfSolversTest.java @@ -0,0 +1,73 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.qubo.QuboConfiguration.QUBO; +import static edu.kit.provideq.toolbox.unsplittablemcf.UnsplittableMcfConfiguration.UNSPLITTABLE_MCF; + +import edu.kit.provideq.toolbox.meta.ProblemManagerProvider; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.qubo.solvers.GamsQuboSolver; +import java.time.Duration; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests for Unsplittable Multi Commodity Flow solvers. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest +@AutoConfigureMockMvc +class UnsplittableMcfSolversTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) // Longer timeout for MCF solving + .build(); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + Stream provideArguments() { + var problemManager = problemManagerProvider.findProblemManagerForType(UNSPLITTABLE_MCF).get(); + var quboProblemManager = problemManagerProvider.findProblemManagerForType(QUBO).get(); + + // TODO: For now just allow the Gams Qubo solver until the Qubo solution format is standardized + var quboSolvers = quboProblemManager.getSolvers().stream() + .filter(GamsQuboSolver.class::isInstance) + .toList(); + + return ApiTestHelper.getAllArgumentCombinations(problemManager, quboSolvers) + .map(list -> Arguments.of(list.get(0), list.get(1), list.get(2))); + } + + @ParameterizedTest + @MethodSource("provideArguments") + void testUnsplittableMcfSolver(ProblemSolver solver, + String input, + ProblemSolver quboSolver) { + var problem = ApiTestHelper.createProblem(client, solver, input, UNSPLITTABLE_MCF); + + // Configure qubo solver for possible subroutines + for (SubProblemReferenceDto subProblem : problem.getSubProblems()) { + var subProblemTypeId = subProblem.getSubRoutine().getTypeId(); + for (String subProblemId : subProblem.getSubProblemIds()) { + ApiTestHelper.setProblemSolver(client, quboSolver, subProblemId, subProblemTypeId); + } + } + + problem = ApiTestHelper.trySolveFor(120, client, problem.getId(), UNSPLITTABLE_MCF); + ApiTestHelper.testSolution(problem); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/VrpSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/VrpSolverTest.java index d6bea5a0..b3bbb72a 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/VrpSolverTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/VrpSolverTest.java @@ -135,7 +135,7 @@ void testKmeansWithLkhForVrp() { } //solve the problem: - problemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), VRP); + problemDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), VRP); ApiTestHelper.testSolution(problemDto); } } @@ -176,7 +176,7 @@ void testTwoPhaseWithLkhForTsp() { } //solve the problem: - var solvedProblemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), VRP); + var solvedProblemDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), VRP); ApiTestHelper.testSolution(solvedProblemDto); } } @@ -226,7 +226,7 @@ void testTwoPhaseWithAnnealer() { } //solve the problem: - var solvedProblemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), VRP); + var solvedProblemDto = ApiTestHelper.trySolveFor(120, client, problemDto.getId(), VRP); ApiTestHelper.testSolution(solvedProblemDto); } }