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 @@
-[](https://github.com/ProvideQ/toolbox-server/actions/workflows/deploy-main.yml)
+[](https://github.com/ProvideQ/toolbox-server/actions/workflows/ci-cd.yml)
[](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);
}
}