diff --git a/.gitignore b/.gitignore index dbd7adc1..5b23f68f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ HELP.md +*.DS_Store .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ -*.DS_Store +### Solution Files from Vehicle Routing Solver: +*.sol ### STS ### .apt_generated @@ -14,7 +16,8 @@ build/ .settings .springBeans .sts4-cache -bin/ +bin/main +bin/test !**/src/main/**/bin/ !**/src/test/**/bin/ @@ -38,6 +41,9 @@ out/ .vscode/ +### virtual envs and python ### +venv/ +__pycache__/ ### ProvideQ toolbox-server ### # input, output and temporary files for solving problems with our solvers @@ -49,5 +55,5 @@ gamslice.txt # Listing files compiled from our GAMS scripts *.lst *.op2 -gams/**/225a +solvers/gams/**/225a *.out diff --git a/Dockerfile b/Dockerfile index 633a8305..cf0c4c30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,10 +48,7 @@ COPY --from=builder /app/build/jre /opt/java ENV PATH="${PATH}:/opt/java/bin" # Install the toolbox server and its solver scripts -COPY gams gams -COPY qiskit qiskit -COPY cirq cirq -COPY python python +COPY solvers solvers RUN scripts/install-solver-dependencies.sh COPY --from=builder /app/build/libs/toolbox-server-*.jar toolbox-server.jar diff --git a/README.md b/README.md index 85868037..ef96126c 100644 --- a/README.md +++ b/README.md @@ -7,28 +7,44 @@ A web-based user interface can be found in our [ProvideQ/ProvideQ repository](https://github.com/ProvideQ/ProvideQ). ## Development setup -1. Install Java 17 (check with `java -version`) +1. Install Java 17 or newer (check with `java -version`) 2. Clone this repository -3. Install a python env that works with GAMS (skip this step if you don't need GAMS) - 1. Install GAMS. - 2. Install miniconda (or anaconda, if you prefer that): - https://conda.io/projects/conda/en/stable/user-guide/install/index.html - 3. Create a GAMS conda environment: `conda create --name gams python=3.10 --yes` - 4. Activate your conda environment: `conda activate gams`. - 5. Make GAMS use that python environment by setting the `GMSPYTHONLIB=/envs/gams/lib/libpython3.10.so` - environment variable. - 6. Install GAMS packages to the GAMS conda env: - `pip install gams[core,connect] --find-links /api/python/bdist` - * If you get an error building `psycopg2`, try to install these postgres packages: - `sudo apt-get install postgresql libpq-dev` and run the `pip install ...` command again - 7. Install the python dependencies we use in our python packages: `pip install -r gams/requirements.txt` -4. Install solver dependencies (skip this step if you don't want to use the solvers): - * Note that these dependencies must be installed to the gams conda env if you want to use GAMS and other solvers from - the same toolbox installation! - 1. Install GAMS solver dependencies: `pip install -r gams/requirements.txt` - 2. Install Qiskit solver dependencies: `pip install -r qiskit/requirements.txt` - 3. Install Cirq solver dependencies: `pip install -r cirq/requirements.txt` -5. Run the server using `./gradlew bootRun` +3. [Optional, Solver Installation - install the Solvers that you want/need] + * We hope to provide an automated script for most of this in the future. + 1. Python-based Solvers (Qiskit, Cirq, Dwave, Qrisp) + 1. Install Python and the required libraries by using `pip install -r [path to requirements.txt]` + 2. Example for Qiskit: `pip install -r solvers/qiskit/requirements.txt` + * Note: These dependencies must be installed to the gams conda env if you want to use GAMS and other solvers from + the same toolbox installation! + 2. Compiled Solvers (e.g. used for VRP and TSP) + 1. Solvers implemented in compiled languages must be executed via binaries that are compiled for your operating system. For those types of solvers we usually include pre-compiled binaries for windows, mac (only arm), and unix. + 2. In case the pre-compiled versions do not work on your machine: re-compile them: + * LKH-3: + 1. Build LKH-3 using the offical guide: http://webhotel4.ruc.dk/~keld/research/LKH-3/ + 2. Put the build binary in `solvers/lkh/bin`, replace the binary that matches your OS. + * VRP-Pipeline (used for K-means, Two Phase Clustering, VRP to QUBO convertion): + 1. Install Rust: https://www.rust-lang.org/tools/install + 2. Install a specific Rust nightly build (needed cause the solver uses experimental features): `rustup install nightly-2023-07-01` + 3. Check how the nightly build is called on your machine (this is shown when running the install command, on Mac it is called *nightly-2023-07-01-aarch64-apple-darwin*) + 4. Set the nightly build as default: `rustup default nightly-2023-07-01(... specific version name on machine)` + 5. Download source code of the VRP-Pipeline: https://github.com/ProvideQ/hybrid-vrp-solver + 6. build the source code using `cargo build` + 7. Put the build binary in `solvers/berger-vrp/bin`, replace the binary that matches your OS. + 3. GAMS (multiple solvers are build on this): + 1. Install a python env that works with GAMS (skip this step if you don't need GAMS) + 2. Install GAMS. (https://www.gams.com/download/) + 3. Install miniconda (or anaconda, if you prefer that): + https://conda.io/projects/conda/en/stable/user-guide/install/index.html + 4. Create a GAMS conda environment: `conda create --name gams python=3.10 --yes` + 5. Activate your conda environment: `conda activate gams`. + 6. Make GAMS use that python environment by setting the `GMSPYTHONLIB=/envs/gams/lib/libpython3.10.so` + environment variable. + 7. Install GAMS packages to the GAMS conda env: + `pip install gams[core,connect] --find-links /api/python/bdist` + * If you get an error building `psycopg2`, try to install these postgres packages: + `sudo apt-get install postgresql libpq-dev` and run the `pip install ...` command again + 8. Install the python dependencies we use in our python packages: `pip install -r gams/requirements.txt` +4. Run the server using `./gradlew bootRun` ## Deployment This repository is designed to be deployed with [Dokku](https://dokku.com/), but you can also run @@ -83,4 +99,4 @@ To use this, enable GitHub Actions and configure the following secrets in the Gi ## License Copyright (c) 2022 - 2023 ProvideQ -This project is available under the [MIT License](./LICENSE). \ No newline at end of file +This project is available under the [MIT License](./LICENSE). diff --git a/scripts/install-solver-dependencies.sh b/scripts/install-solver-dependencies.sh index 1be60548..b23c9b4f 100755 --- a/scripts/install-solver-dependencies.sh +++ b/scripts/install-solver-dependencies.sh @@ -1,4 +1,5 @@ #!/bin/bash +# this script is made for the CI-pipeline, do not use this to install dependencies on your private machine! # exit on error set -e @@ -10,7 +11,12 @@ REPO_DIR=$(dirname "$(dirname "$(readlink -f "$0")")") source /opt/conda/bin/activate gams # install solver dependencies -pip install -r "$REPO_DIR/gams/requirements.txt" -pip install -r "$REPO_DIR/qiskit/requirements.txt" -pip install -r "$REPO_DIR/cirq/requirements.txt" -pip install -r "$REPO_DIR/python/requirements.txt" \ No newline at end of file +pip install -r "$REPO_DIR/solvers/gams/requirements.txt" +# quantum frameworks: +pip install -r "$REPO_DIR/solvers/qiskit/requirements.txt" +pip install -r "$REPO_DIR/solvers/cirq/requirements.txt" +pip install -r "$REPO_DIR/solvers/dwave/requirements.txt" +pip install -r "$REPO_DIR/solvers/qrisp/requirements.txt" +# custom solvers with python wrapper: +pip install -r "$REPO_DIR/solvers/custom/hs-knapsack/requirements.txt" +pip install -r "$REPO_DIR/solvers/custom/lkh/requirements.txt" diff --git a/cirq/max-cut/max_cut_cirq.py b/solvers/cirq/max-cut/max_cut_cirq.py similarity index 100% rename from cirq/max-cut/max_cut_cirq.py rename to solvers/cirq/max-cut/max_cut_cirq.py diff --git a/cirq/requirements.txt b/solvers/cirq/requirements.txt similarity index 100% rename from cirq/requirements.txt rename to solvers/cirq/requirements.txt diff --git a/solvers/custom/berger-vrp/bin/README.md b/solvers/custom/berger-vrp/bin/README.md new file mode 100644 index 00000000..14f4ef06 --- /dev/null +++ b/solvers/custom/berger-vrp/bin/README.md @@ -0,0 +1,35 @@ +### How to Cross-compile the Rust Pipeline: + +Guide is written for Mac-ARM System + +1. Download Repo: https://github.com/ProvideQ/hybrid-vrp-solver +2. Install Rust: https://www.rust-lang.org/tools/install +3. Install a specific Rust nightly build (needed cause the solver uses experimental features): `rustup install nightly-2023-07-01` +4. Check how the nightly build is called on your machine (this is shown when running the install command, on Mac it is called *nightly-2023-07-01-aarch64-apple-darwin*) +5. Set the nightly build as default: `rustup default nightly-2023-07-01(... specific version name on machine)` + +#### Mac Version: (native) +1. build the source code using `cargo build --release` + +#### Windows Version (cross compilation) +1. Add Target for Windows: `rustup target add x86_64-pc-windows-gnu` +2. Install Cross Toolchain: `brew install mingw-w64` +3. Create .cargo/config.toml and add the following lines:
+``` +[target.x86_64-pc-windows-gnu] +linker = "x86_64-w64-mingw32-gcc" +``` +4. run `cargo build --target x86_64-pc-windows-gnu --release` + +#### Linux Version (cross compilation) +* only gnu is supported, Lucas Bergers code does not support static linking +1. Target for Linux: `rustup target add 86_64-unknown-linux-gnu` +2. Toolchain: `arch -arm64 brew install SergioBenitez/osxct/x86_64-unknown-linux-gnu` +3. Create .cargo/config.toml and add the following lines: (Path for linker: `which x86_64-unknown-linux-gnu`)
+``` +[target.x86_64-pc-windows-gnu] +linker = "/opt/homebrew/bin/x86_64-unknown-linux-gnu-gcc" +``` +4. run `cargo build --target x86_64-unknown-linux-gnu --release` +5. When you have issues, check here: https://github.com/briansmith/ring/issues/1605 + diff --git a/solvers/custom/berger-vrp/bin/pipeline-linux-gnu b/solvers/custom/berger-vrp/bin/pipeline-linux-gnu new file mode 100755 index 00000000..2bc807a9 Binary files /dev/null and b/solvers/custom/berger-vrp/bin/pipeline-linux-gnu differ diff --git a/solvers/custom/berger-vrp/bin/pipeline-mac b/solvers/custom/berger-vrp/bin/pipeline-mac new file mode 100755 index 00000000..81b113e1 Binary files /dev/null and b/solvers/custom/berger-vrp/bin/pipeline-mac differ diff --git a/solvers/custom/berger-vrp/bin/pipeline-windows.exe b/solvers/custom/berger-vrp/bin/pipeline-windows.exe new file mode 100755 index 00000000..4874196b Binary files /dev/null and b/solvers/custom/berger-vrp/bin/pipeline-windows.exe differ diff --git a/python/knapsack/knapsack.py b/solvers/custom/hs-knapsack/knapsack.py similarity index 100% rename from python/knapsack/knapsack.py rename to solvers/custom/hs-knapsack/knapsack.py diff --git a/python/requirements.txt b/solvers/custom/hs-knapsack/requirements.txt similarity index 81% rename from python/requirements.txt rename to solvers/custom/hs-knapsack/requirements.txt index 3a04554f..5f6abbe6 100644 --- a/python/requirements.txt +++ b/solvers/custom/hs-knapsack/requirements.txt @@ -1,3 +1,3 @@ # This file describes the python package requirements for the knapsack problem solver -knapsack-pip +knapsack-pip == 0.2 diff --git a/solvers/custom/lkh/bin/LKH-unix b/solvers/custom/lkh/bin/LKH-unix new file mode 100755 index 00000000..1697b4a7 Binary files /dev/null and b/solvers/custom/lkh/bin/LKH-unix differ diff --git a/solvers/custom/lkh/bin/LKH-windows.exe b/solvers/custom/lkh/bin/LKH-windows.exe new file mode 100644 index 00000000..7d25f0c7 Binary files /dev/null and b/solvers/custom/lkh/bin/LKH-windows.exe differ diff --git a/solvers/custom/lkh/requirements.txt b/solvers/custom/lkh/requirements.txt new file mode 100644 index 00000000..27a256ad --- /dev/null +++ b/solvers/custom/lkh/requirements.txt @@ -0,0 +1,6 @@ +# This file describes the python package requirements for all LKH scripts +# supported by ProvideQ + +# required for LKH VRP solver +lkh == 1.1.1 +tsplib95 == 0.7.1 diff --git a/solvers/custom/lkh/vrp_lkh.py b/solvers/custom/lkh/vrp_lkh.py new file mode 100644 index 00000000..ebf0ca85 --- /dev/null +++ b/solvers/custom/lkh/vrp_lkh.py @@ -0,0 +1,47 @@ +import argparse + +from lkh import LKHProblem, solve +from tsplib95.models import StandardProblem + +parser = argparse.ArgumentParser( + prog="LKH-3 Interface", + description="A CLI Program to initiate solving CVRP files with LKH-3", + epilog="Made by Lucas Berger for scientific purposes", +) + + +parser.add_argument("tsplib_file") +parser.add_argument("--lkh-instance", default="./bin/LKH-unix") +parser.add_argument("--output-file") +parser.add_argument("-t", "--max-trials", default=1000) +parser.add_argument("-r", "--runs", default=10) + +args = parser.parse_args() + +problem = LKHProblem.load(args.tsplib_file) + +print(f"solving {args.tsplib_file}") + +if sum(problem.demands.values()) <= problem.capacity: + problem.type = "TSP" + +if len(problem.node_coords.values()) > 2: + extra = {} + tours = solve( + args.lkh_instance, problem=problem, max_trials=args.max_trials, **extra + ) + + tour = StandardProblem() + + tour.tours = [[*problem.depots, *path] for path in tours] + tour.type = "TOUR" + tour.name = problem.name + " solution" +else: + tour = StandardProblem() + tour.tours = [problem.node_coords.keys()] + tour.type = "TOUR" + tour.name = problem.name + " solution" + + +if args.output_file is not None: + tour.save(args.output_file) diff --git a/solvers/dwave/qubo/embeddings.py b/solvers/dwave/qubo/embeddings.py new file mode 100644 index 00000000..ec325426 --- /dev/null +++ b/solvers/dwave/qubo/embeddings.py @@ -0,0 +1 @@ +cached_embeddings = {} diff --git a/solvers/dwave/qubo/main.py b/solvers/dwave/qubo/main.py new file mode 100644 index 00000000..050fb3ec --- /dev/null +++ b/solvers/dwave/qubo/main.py @@ -0,0 +1,81 @@ +import argparse +import os +from datetime import datetime +from typing import Literal + +from dimod import BINARY, BinaryQuadraticModel, binary, constrained, lp +from dimod.serialization import coo +from dwave.cloud import Client +from solver import solve_with + + +def main(): + parser = argparse.ArgumentParser( + prog="DWave QUBO solver", + description="A CLI Program to initiate solving COOrdinate files with DWave Systems", + epilog="Made by Lucas Berger for scientific purposes", + ) + + parser.add_argument("file") + parser.add_argument( + "type", default="sim", choices=["sim", "hybrid", "qbsolv", "direct"] + ) + parser.add_argument("--output-file") + + args = parser.parse_args() + type: Literal["sim", "hybrid", "qbsolv", "direct"] = args.type + + bqm: BinaryQuadraticModel | None = None + with open(args.file) as problem: + bqm = coo.load(problem, vartype=BINARY) + if len(bqm.quadratic) == 0: + 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 + + filename = os.path.basename(args.file) + + if bqm is None: + raise Exception("Could not load file") + + last = datetime.now().timestamp() + print("started") + + now = datetime.now().timestamp() + print(f"connected after {now - last}. starting solver") + sampleset = solve_with(bqm, type, filename) + + # accessing the sampleset's properties await for the future + print(sampleset.info) + + now = datetime.now().timestamp() + print(f"ended {now - last}") + + if args.output_file: + with open(args.output_file, "w") as out: + out.writelines([f"{bin}\n" for bin in sampleset.first.sample.values()]) + else: + print(sampleset.first.energy) + print(sampleset.first.sample) + + now = datetime.now().timestamp() + print(f"connection closed after {now - last}") + + +if __name__ == "__main__": + main() diff --git a/solvers/dwave/qubo/solver.py b/solvers/dwave/qubo/solver.py new file mode 100644 index 00000000..42517f40 --- /dev/null +++ b/solvers/dwave/qubo/solver.py @@ -0,0 +1,76 @@ +import json +from datetime import datetime +from math import sqrt +from typing import Literal + +from dimod import BinaryQuadraticModel, Sampler, SampleSet, SimulatedAnnealingSampler +from dwave.embedding import minorminer +from dwave.system import DWaveSampler, FixedEmbeddingComposite, LeapHybridSampler +from embeddings import cached_embeddings +from hybrid import SimplifiedQbsolv, State +from hybrid.profiling import make_timeit + +solvertype = Literal["sim", "hybrid", "qbsolv", "direct"] + + +def solve_with(bqm: BinaryQuadraticModel, type: solvertype, label: str) -> SampleSet: + if type == "sim": + last = datetime.now().timestamp() + sampler: Sampler = SimulatedAnnealingSampler() + print(f"sampler created took {datetime.now().timestamp() - last}") + return sampler.sample(bqm) + elif type == "direct": + last = datetime.now().timestamp() + sampler: Sampler = DWaveSampler(solver={"topology__type": "zephyr"}) + print(f"sampler created took {datetime.now().timestamp() - last}") + last = datetime.now().timestamp() + tsp_size = int(sqrt(bqm.num_variables)) + if tsp_size in cached_embeddings.keys(): + embedding = cached_embeddings[tsp_size] + else: + print("start embedding") + embedding = minorminer.find_embedding( + list(bqm.quadratic) + [(v, v) for v in bqm.linear], + sampler.edgelist, + ) + print(f"found new embedding for {tsp_size}") + print(embedding) + print(f"got embedding {datetime.now().timestamp() - last}") + max_tries = 3 + + while max_tries > 0: + sampleset = FixedEmbeddingComposite(sampler, embedding).sample( + bqm, + num_reads=250, + label=f"DWaveSampler with embedding num_reads=1000 {label}", + ) + values = list(sampleset.first.sample.values()) + print(f"checking out sample: {values}") + valid = True + for i in range(0, tsp_size): + x = sum(values[i * tsp_size : (i + 1) * tsp_size]) + print(x) + if x != 1: + valid = False + + if valid: + return sampleset + max_tries -= 1 + print(f"{max_tries} left") + elif type == "hybrid": + last = datetime.now().timestamp() + sampler: Sampler = LeapHybridSampler() + print(f"sampler created took {datetime.now().timestamp() - last}") + return sampler.sample( + bqm, time_limit=10, label=f"LeapHybridSampler num_reads=250: {label}" + ) + elif type == "qbsolv": + last = datetime.now().timestamp() + init_state = State.from_problem(bqm) + workflow = SimplifiedQbsolv(max_iter=3, max_time=10) + print(f"workflow created took {datetime.now().timestamp() - last}") + final_state = workflow.run(init_state).result() + + print(json.dumps(workflow.timers)) + + return final_state.samples diff --git a/solvers/dwave/requirements.txt b/solvers/dwave/requirements.txt new file mode 100644 index 00000000..6e6460d8 --- /dev/null +++ b/solvers/dwave/requirements.txt @@ -0,0 +1,5 @@ +# This file describes the python package requirements for all Dwave scripts +# supported by ProvideQ + +# required for Dwave Qubo solver +dwave-ocean-sdk == 6.4.1 diff --git a/gams/max-cut/README.md b/solvers/gams/max-cut/README.md similarity index 100% rename from gams/max-cut/README.md rename to solvers/gams/max-cut/README.md diff --git a/gams/max-cut/maxcut.gms b/solvers/gams/max-cut/maxcut.gms similarity index 100% rename from gams/max-cut/maxcut.gms rename to solvers/gams/max-cut/maxcut.gms diff --git a/gams/requirements.txt b/solvers/gams/requirements.txt similarity index 100% rename from gams/requirements.txt rename to solvers/gams/requirements.txt diff --git a/gams/sat/sat.gms b/solvers/gams/sat/sat.gms similarity index 100% rename from gams/sat/sat.gms rename to solvers/gams/sat/sat.gms diff --git a/qiskit/max-cut/maxCut_qiskit.py b/solvers/qiskit/max-cut/maxCut_qiskit.py similarity index 100% rename from qiskit/max-cut/maxCut_qiskit.py rename to solvers/qiskit/max-cut/maxCut_qiskit.py diff --git a/qiskit/qubo/qubo_qiskit.py b/solvers/qiskit/qubo/qubo_qiskit.py similarity index 100% rename from qiskit/qubo/qubo_qiskit.py rename to solvers/qiskit/qubo/qubo_qiskit.py diff --git a/qiskit/requirements.txt b/solvers/qiskit/requirements.txt similarity index 100% rename from qiskit/requirements.txt rename to solvers/qiskit/requirements.txt diff --git a/solvers/qrisp/requirements.txt b/solvers/qrisp/requirements.txt new file mode 100644 index 00000000..f42a2bda --- /dev/null +++ b/solvers/qrisp/requirements.txt @@ -0,0 +1,4 @@ +qrisp == 0.4.4 +numpy == 1.26.4 +gurobipy == 11.0.1 # for lp file reading +tsplib95 == 0.7.1 # for tsplib file parsing \ No newline at end of file diff --git a/solvers/qrisp/vrp/grover.py b/solvers/qrisp/vrp/grover.py new file mode 100644 index 00000000..75fd1296 --- /dev/null +++ b/solvers/qrisp/vrp/grover.py @@ -0,0 +1,137 @@ +import argparse +import sys +from math import ceil, factorial + +import numpy as np +from qrisp.grover import grovers_alg +from qrisp_solver.oracle import eval_distance_threshold +from qrisp_solver.permutation import create_perm_specifiers, eval_perm +from qrisp_solver.vrp import calc_paths, normalize_vrp +from tsplib95 import load +from tsplib95.models import StandardProblem + +from qrisp import QuantumCircuit, QuantumFloat, multi_measurement + +parser = argparse.ArgumentParser( + prog="Qrisp Grover VRP Solver", + description="A CLI Program to initiate solving CVRP files with Qrisp", + epilog="Made by Lucas Berger for scientific purposes", +) + + +parser.add_argument("tsplib_file") +parser.add_argument("--size-gate") +parser.add_argument("--output-file") + +args = parser.parse_args() + +problem = load(args.tsplib_file) + +max_cap = problem.capacity + +city_amount = problem.dimension + +city_coords = np.array(list(problem.node_coords.values())) + + +distance_matrix = np.array( + [ + [np.linalg.norm(city_coords[i] - city_coords[j]) for i in range(city_amount)] + for j in range(city_amount) + ] +) + + +city_demand = np.array(list(problem.demands.values())) + +distance_matrix, scaling = normalize_vrp(distance_matrix, city_demand, max_cap) + +print(distance_matrix) + +dump_length, _ = calc_paths( + distance_matrix, city_demand, max_cap, np.arange(1, city_amount) +) + +precision = 5 + int(ceil(np.log2(scaling)) / 2) + +perm_specifiers = create_perm_specifiers(city_amount) + +winner_state_amount = 2 ** sum([qv.size for qv in perm_specifiers]) / factorial( + city_amount - 1 +) + +print("estimated winners: ", winner_state_amount) + + +grovers_alg( + perm_specifiers, # Permutation specifiers + eval_distance_threshold, # Oracle function + kwargs={ + "threshold": dump_length * 0.8, + "precision": precision, + "city_amount": city_amount, + "distance_matrix": distance_matrix, + "city_demand": city_demand, + "max_cap": max_cap, + }, # Specify the keyword arguments for the Oracle + winner_state_amount=1, +) # Specify the estimated amount of winners + +pre_compile_qubits = perm_specifiers[0].qs.num_qubits() +print(f"Before compilation number of qubits: {pre_compile_qubits}") + +if args.size_gate and pre_compile_qubits > int(args.size_gate) * 5: + print( + f"Stop execution to prevent denial of service due to a large VRP. Size of the VRP circuit before compiling in qubit ({pre_compile_qubits})" + ) + sys.exit(1) + +compiled_qs: QuantumCircuit = perm_specifiers[0].qs.compile() + +num_qubits = len(compiled_qs.qubits) +print("Number of qubits: ", num_qubits) +if args.size_gate and num_qubits > int(args.size_gate): + print( + f"Stop execution to prevent denial of service due to a large VRP. Size of the VRP circuit in qubit ({num_qubits})" + ) + sys.exit(1) + +res = multi_measurement(perm_specifiers) + +best_perm_spec = sorted(list(res.items()), key=lambda x: x[1], reverse=True)[0][0] +best_perm_spec_qc: list[QuantumFloat] = [] +for spec in best_perm_spec: + new_qv = QuantumFloat(perm_specifiers[0].size) + new_qv[:] = spec + best_perm_spec_qc.append(new_qv) +best_perm = eval_perm(best_perm_spec_qc, city_amount=city_amount).most_likely() +print(best_perm) + +paths = [] +current_path = [0] +current_demand = 0 + +for i in best_perm: + demand = city_demand[i] + + if max_cap < current_demand + demand: + paths.append(current_path) + current_path = [0] + current_demand = 0 + + current_path.append(i) + current_demand += demand + +paths.append(current_path) + +print(paths) + + +tour = StandardProblem() + +tour.tours = paths +tour.type = "TOUR" +tour.name = problem.name + " solution" + +if args.output_file: + tour.save(args.output_file) diff --git a/solvers/qrisp/vrp/qaoa.py b/solvers/qrisp/vrp/qaoa.py new file mode 100644 index 00000000..178f3181 --- /dev/null +++ b/solvers/qrisp/vrp/qaoa.py @@ -0,0 +1,96 @@ +import argparse +import sys +from math import sqrt + +import numpy as np +from gurobipy import read +from qrisp.core import demux +from qrisp.qaoa import ( + QAOAProblem, + RX_mixer, + create_QUBO_cl_cost_function, + create_QUBO_cost_operator, + def_backend, +) +from qrisp_solver.permutation import create_perm_specifiers, eval_perm_old + +from qrisp import QuantumArray, QuantumFloat, QuantumVariable, cyclic_shift, h, x + +parser = argparse.ArgumentParser( + prog="DWave QUBO solver", + description="A CLI Program to initiate solving LP files with Qrisps QAOA Solver", + epilog="Made by Lucas Berger for scientific purposes", +) + +parser.add_argument("file") +parser.add_argument("--output-file") +parser.add_argument("--size-gate") + +args = parser.parse_args() + +m = read(args.file) + +vars = [x.VarName for x in m.getVars() if x.VType == "B"] + +qubo_size = len(vars) +size = int(sqrt(qubo_size)) +qubo = np.zeros((qubo_size, qubo_size)) + +if args.size_gate and int(args.size_gate) < size: + print( + f"Stop execution to prevent denial of service due to a large QUBO. Size of the QUBO as VRP ({size})" + ) + sys.exit(1) + +obj = m.getObjective() + +for term in range(obj.size()): + num = obj.getCoeff(term) + i = vars.index(obj.getVar1(term).VarName) + j = vars.index(obj.getVar2(term).VarName) + + qubo[i, j] = num + +problem = QAOAProblem( + create_QUBO_cost_operator(qubo), + RX_mixer, + create_QUBO_cl_cost_function(qubo), +) + + +def init_func(qarg: QuantumArray): + perm_specifiers = create_perm_specifiers(size) + for qv in perm_specifiers: + h(qv) + perm = QuantumArray(QuantumFloat(int(np.ceil(np.log2(size)))), size) + eval_perm_old(perm_specifiers, city_amount=size, qa=perm) + + for i in range(size * size): + x_pos = int(i % size) + if x_pos == 0: + x(qarg[i]) + + for i in range(size): + cyclic_shift(qarg[i * size : (i + 1) * size], shift_amount=perm[i]) + + for i in reversed(range(len(perm_specifiers))): + demux(perm[i], perm_specifiers[i], perm[i:], permit_mismatching_size=True) + + for i in range(len(perm)): + perm[i] -= i + + perm.delete() + + +problem.set_init_function(init_func) + + +qarg = QuantumArray(qtype=QuantumVariable(1), shape=(qubo_size)) +res = problem.run(qarg, mes_kwargs={"backend": def_backend}, depth=1) +res = dict(list(res.items())[:1]) + +if args.output_file: + with open(args.output_file, "w") as out: + out.writelines([f"{bin}\n" for bin in list(list(res.keys())[0])]) +else: + print(list(list(res.keys())[0])) diff --git a/solvers/qrisp/vrp/qrisp_solver/__init__.py b/solvers/qrisp/vrp/qrisp_solver/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/solvers/qrisp/vrp/qrisp_solver/distance.py b/solvers/qrisp/vrp/qrisp_solver/distance.py new file mode 100644 index 00000000..db9385fc --- /dev/null +++ b/solvers/qrisp/vrp/qrisp_solver/distance.py @@ -0,0 +1,201 @@ +from math import ceil, log +from typing import Any, Tuple + +import numpy as np +from qrisp import QuantumArray, QuantumBool, QuantumDictionary, QuantumFloat, cx + + +def qdict_calc_perm_travel_distance_forward( + itinerary: QuantumArray, + precision: int, + city_amount: int, + distance_matrix: np.ndarray[Any, np.dtype[np.float64]], + demand: np.ndarray[Any, np.dtype[np.int64]], + capacity: int, +) -> Tuple[QuantumFloat, QuantumArray, QuantumFloat]: + # A QuantumFloat with n qubits and exponent -n + # can represent values between 0 and 1 + res = QuantumFloat(precision, -precision) + + # Fill QuantumDictionary with values + qd = QuantumDictionary(return_type=res) + for i in range(city_amount): + for j in range(city_amount): + qd[i, j] = res.truncate(distance_matrix[i, j]) + + qd_to_zero = QuantumDictionary(return_type=res) + + for i in range(city_amount): + qd_to_zero[i] = res.truncate(distance_matrix[0, i]) + + first_trip_distance = qd_to_zero[itinerary[0]] + res += first_trip_distance + first_trip_distance.uncompute(recompute=True) + + # Add the distance of the final trip + final_trip_distance = qd_to_zero[itinerary[-1]] + res += final_trip_distance + final_trip_distance.uncompute(recompute=True) + + max_demand = max(demand) + bit_number_cap = ceil(np.log2(capacity + max_demand)) + + demand_count_type = QuantumFloat(bit_number_cap) + + qa = QuantumDictionary(return_type=demand_count_type) + for i in range(city_amount): + qa[i] = demand[i] + + est_worst_clusters = (sum(demand) / capacity) * 2 + index_bit_number = ceil(np.log2(est_worst_clusters)) + + demand_indexer = QuantumFloat(index_bit_number) + demand_counter = QuantumArray(qtype=demand_count_type) + demand_counter[:] = np.zeros(2**index_bit_number) + # print(demand_counter) + + demand_indexer[:] = 0 + with demand_counter[demand_indexer] as demand: + first_demand = qa[itinerary[0]] + demand += first_demand + first_demand.uncompute() + + # print(demand_counter) + + # Evaluate result + for i in range(city_amount - 2): + demand_index = itinerary[(i + 1) % city_amount] + # print(demand_index) + city_demand = qa[demand_index] + # print(city_demand) + + capped: QuantumBool + + with demand_counter[demand_indexer] as demand: + demand += city_demand + capped = demand <= capacity + + # print(capped) + # print(demand_counter) + + with capped: + trip_distance = qd[itinerary[i], demand_index] + res += trip_distance + trip_distance.uncompute(recompute=True) + capped.flip() + # print(capped) + with capped: + with demand_counter[demand_indexer] as demand: + demand -= city_demand + demand_indexer += 1 + with demand_counter[demand_indexer] as demand: + demand += city_demand + + long_first_trip_distance = qd_to_zero[itinerary[i]] + res += long_first_trip_distance + long_first_trip_distance.uncompute(recompute=True) + long_second_trip_distance = qd_to_zero[itinerary[(i + 1) % city_amount]] + res += long_second_trip_distance + long_second_trip_distance.uncompute(recompute=True) + # print(demand_counter) + with demand_counter[demand_indexer] as demand: + should_reverse_capped = demand == city_demand + cx(should_reverse_capped, capped) + should_reverse_capped.uncompute() + + capped.delete() # already verfied once + city_demand.uncompute() + # print(demand_counter) + + return res, demand_counter, demand_indexer + + +def qdict_calc_perm_travel_distance_backward( + itinerary: QuantumArray, + precision: int, + city_amount: int, + distance_matrix: np.ndarray[Any, np.dtype[np.float64]], + demand: np.ndarray[Any, np.dtype[np.int64]], + capacity: int, + forward_result: Tuple[QuantumFloat, QuantumArray, QuantumFloat], +) -> None: + res, demand_counter, demand_indexer = forward_result + + # Fill QuantumDictionary with values + qd = QuantumDictionary(return_type=res) + for i in range(city_amount): + for j in range(city_amount): + qd[i, j] = res.truncate(distance_matrix[i, j]) + + qd_to_zero = QuantumDictionary(return_type=res) + + for i in range(city_amount): + qd_to_zero[i] = res.truncate(distance_matrix[0, i]) + + bit_number_cap = ceil(log(capacity)) + 1 + + demand_count_type = QuantumFloat(bit_number_cap) + + qa = QuantumDictionary(return_type=demand_count_type) + for i in range(city_amount): + qa[i] = demand[i] + + # uncompute demand_counter + for i in reversed(range(city_amount - 2)): + demand_index = itinerary[(i + 1) % city_amount] + city_demand = qa[demand_index] + + was_capped: QuantumBool + + with demand_counter[demand_indexer] as demand: + demand -= city_demand + was_capped = demand == 0 + + with was_capped: + demand_indexer -= 1 + + long_first_trip_distance = qd_to_zero[itinerary[i]] + res -= long_first_trip_distance + long_first_trip_distance.uncompute(recompute=True) + long_second_trip_distance = qd_to_zero[itinerary[(i + 1) % city_amount]] + res -= long_second_trip_distance + long_second_trip_distance.uncompute(recompute=True) + was_capped.flip() + with was_capped: + trip_distance = qd[itinerary[i], demand_index] + res -= trip_distance + trip_distance.uncompute(recompute=True) + + with demand_counter[demand_indexer] as demand: + added = demand + city_demand + should_reverse_capped = added <= capacity + + cx(should_reverse_capped, was_capped) + + should_reverse_capped.uncompute() + + added.uncompute(recompute=True) + + was_capped.delete() # verified + + city_demand.uncompute() + + last_demand = qa[itinerary[0]] + with demand_counter[demand_indexer] as demand: + demand -= last_demand + + last_demand.uncompute() + + demand_indexer.delete() # verified + demand_counter.delete() # verified + + # remove the distance of the first and final trip + first_trip_distance = qd_to_zero[itinerary[0]] + res -= first_trip_distance + first_trip_distance.uncompute(recompute=True) + + final_trip_distance = qd_to_zero[itinerary[-1]] + res -= final_trip_distance + final_trip_distance.uncompute(recompute=True) + + res.delete() # verified diff --git a/solvers/qrisp/vrp/qrisp_solver/oracle.py b/solvers/qrisp/vrp/qrisp_solver/oracle.py new file mode 100644 index 00000000..d6650910 --- /dev/null +++ b/solvers/qrisp/vrp/qrisp_solver/oracle.py @@ -0,0 +1,47 @@ +import numpy as np +from qrisp import * + +from .distance import ( + qdict_calc_perm_travel_distance_backward, + qdict_calc_perm_travel_distance_forward, +) +from .permutation import eval_perm, eval_perm_backward + + +def eval_distance_threshold( + perm_specifiers, + precision, + threshold, + city_amount, + distance_matrix, + city_demand, + max_cap, +): + itinerary = QuantumArray( + QuantumFloat(int(np.ceil(np.log2(city_amount)))), city_amount - 1 + ) + + eval_perm(perm_specifiers, city_amount=city_amount, qa=itinerary) + + distance, demand_array, demand_indexer = qdict_calc_perm_travel_distance_forward( + itinerary, precision, city_amount, distance_matrix, city_demand, max_cap + ) + + is_below_treshold = distance <= threshold + + z(is_below_treshold) + + is_below_treshold.uncompute() + + qdict_calc_perm_travel_distance_backward( + itinerary, + precision, + city_amount, + distance_matrix, + city_demand, + max_cap, + forward_result=(distance, demand_array, demand_indexer), + ) + + eval_perm_backward(perm_specifiers, city_amount, itinerary) + itinerary.delete() diff --git a/solvers/qrisp/vrp/qrisp_solver/permutation.py b/solvers/qrisp/vrp/qrisp_solver/permutation.py new file mode 100644 index 00000000..b03395c4 --- /dev/null +++ b/solvers/qrisp/vrp/qrisp_solver/permutation.py @@ -0,0 +1,93 @@ +import numpy as np +from qrisp import QuantumArray, QuantumFloat +from qrisp.core import demux +from qrisp.environments import invert + + +# Create a function that generates a state of superposition of all permutations +def swap_to_front(qa, index): + with invert(): + # The keyword ctrl_method = "gray_pt" allows the controlled swaps to be synthesized + # using Margolus gates. These gates perform the same operation as a regular Toffoli + # but add a different phase for each input. This phase will not matter though, + # since it will be reverted once the ancilla values of the oracle are uncomputed. + demux(qa[0], index, qa, permit_mismatching_size=True) + + +def eval_perm(perm_specifiers, city_amount, qa=None): + N = len(perm_specifiers) + + # To filter out the cyclic permutations, we impose that the first city is always city 0 + # We will have to consider this assumption later when calculating the route distance + # by manually adding the trip distance of the first trip (from city 0) and the + # last trip (to city 0) + if qa is None: + qa = QuantumArray( + QuantumFloat(int(np.ceil(np.log2(city_amount)))), city_amount - 1 + ) + + for i in range(city_amount - 1): + qa[i] += i + 1 + + for i in range(N): + swap_to_front(qa[i:], perm_specifiers[i]) + + return qa + + +def eval_perm_backward(perm_specifiers, city_amount, qa=None): + N = len(perm_specifiers) + + # To filter out the cyclic permutations, we impose that the first city is always city 0 + # We will have to consider this assumption later when calculating the route distance + # by manually adding the trip distance of the first trip (from city 0) and the + # last trip (to city 0) + + for i in reversed(range(N)): + demux(qa[i], perm_specifiers[i], qa[i:], permit_mismatching_size=True) + + for i in range(city_amount - 1): + qa[i] -= i + 1 + + return qa + + +def eval_perm_old(perm_specifiers, city_amount, qa=None): + N = len(perm_specifiers) + + # To filter out the cyclic permutations, we impose that the first city is always city 0 + # We will have to consider this assumption later when calculating the route distance + # by manually adding the trip distance of the first trip (from city 0) and the + # last trip (to city 0) + if qa is None: + qa = QuantumArray(QuantumFloat(int(np.ceil(np.log2(city_amount)))), city_amount) + + add = np.arange(0, city_amount) + + for i in range(city_amount): + qa[i] += int(add[i]) + + for i in range(N): + swap_to_front(qa[i:], perm_specifiers[i]) + + return qa + + +# Create function that returns QuantumFloats specifying the permutations (these will be in uniform superposition) +def create_perm_specifiers(city_amount, init_seq=None) -> list[QuantumFloat]: + perm_specifiers = [] + + for i in range(city_amount - 1): + qf_size = int(np.ceil(np.log2(city_amount - i))) + + if i == 0: + continue + + temp_qf = QuantumFloat(qf_size) + + if not init_seq is None: + temp_qf[:] = init_seq[i - 1] + + perm_specifiers.append(temp_qf) + + return perm_specifiers diff --git a/solvers/qrisp/vrp/qrisp_solver/vrp.py b/solvers/qrisp/vrp/qrisp_solver/vrp.py new file mode 100644 index 00000000..142ddb22 --- /dev/null +++ b/solvers/qrisp/vrp/qrisp_solver/vrp.py @@ -0,0 +1,71 @@ +from typing import Any + +import numpy as np + + +def normalize_vrp( + distance_matrix: np.ndarray[Any, np.dtype[np.float64]], + demand_array: np.ndarray[Any, np.dtype[np.float64]], + capacity: int, +): + city_amount = len(distance_matrix) + + est_cluster = estimate_worst_cluster_nr(capacity, demand_array) + + sorted_demand = np.sort(distance_matrix[0])[::-1] + sorted_distance_matrix = np.sort(distance_matrix)[::-1] + path_distance_for_going_back = np.sum(sorted_demand[:est_cluster]) * 2 + path_distance_for_normal_path = np.sum( + sorted_distance_matrix[: int(city_amount - est_cluster / 2)] + ) + + total_path = path_distance_for_going_back + path_distance_for_normal_path + + print(total_path) + + return distance_matrix / total_path, total_path + + +def estimate_worst_cluster_nr( + capacity: int, demand_array: np.ndarray[Any, np.dtype[np.float64]] +): + return int(np.ceil(np.sum(demand_array) / capacity) * 2) + + +def calc_paths( + distance_matrix: np.ndarray[Any, np.dtype[np.float64]], + demand_array: np.ndarray[Any, np.dtype[np.float64]], + capacity: int, + perm: list[int], +): + paths = [] + current_path = [0] + current_demand = 0 + + for i in perm: + demand = demand_array[i] + + if capacity < current_demand + demand: + paths.append(current_path) + current_path = [0] + current_demand = 0 + + current_path.append(i) + current_demand += demand + + paths.append(current_path) + + total_length = 0.0 + for path in paths: + path_length = 0.0 + + first = path[0] + + last = first + for next in path[1:]: + path_length += distance_matrix[(last, next)] + last = next + path_length += distance_matrix[(last, first)] + total_length += path_length + + return total_length, paths diff --git a/src/main/java/edu/kit/provideq/toolbox/ProcessResult.java b/src/main/java/edu/kit/provideq/toolbox/ProcessResult.java deleted file mode 100644 index b908190f..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/ProcessResult.java +++ /dev/null @@ -1,26 +0,0 @@ -package edu.kit.provideq.toolbox; - -/** - * Result of running a process. - * - * @param success did the process complete successfully - * @param output process console output - */ -public record ProcessResult(boolean success, String output) { - /** - * Utility method for storing the contents of a process result in a string solution object. - * - * @param solution the solution to apply this data to. - * @return the given, modified solution. - */ - public Solution applyTo(Solution solution) { - if (this.success) { - solution.setSolutionData(this.output); - solution.complete(); - } else { - solution.setDebugData(this.output); - solution.fail(); - } - return solution; - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/Solution.java b/src/main/java/edu/kit/provideq/toolbox/Solution.java index e5584d06..11824d8b 100644 --- a/src/main/java/edu/kit/provideq/toolbox/Solution.java +++ b/src/main/java/edu/kit/provideq/toolbox/Solution.java @@ -80,7 +80,7 @@ public void abort() { } /** - * sets the status to 'invalid'. irreversible + * sets the status to 'error'. irreversible */ public void fail() { if (!this.status.isCompleted()) { @@ -161,6 +161,7 @@ public String toString() { + "id=" + id + ", " + "status=" + status + ", " + "metaData=" + metaData + ", " + + "debugData=" + debugData + ", " + "solutionData" + solutionData + ']'; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/ToolboxServerApplication.java b/src/main/java/edu/kit/provideq/toolbox/ToolboxServerApplication.java index 103e6014..1022cbe9 100644 --- a/src/main/java/edu/kit/provideq/toolbox/ToolboxServerApplication.java +++ b/src/main/java/edu/kit/provideq/toolbox/ToolboxServerApplication.java @@ -1,13 +1,29 @@ package edu.kit.provideq.toolbox; +import edu.kit.provideq.toolbox.exception.MissingSpringProfileException; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ToolboxServerApplication { - public static void main(String[] args) { - SpringApplication.run(ToolboxServerApplication.class, args); + public static void main(String[] args) throws MissingSpringProfileException { + String springProfileArg = "--spring.profiles.active=" + getSpringProfileForOs(); + SpringApplication.run(ToolboxServerApplication.class, springProfileArg); + } + + private static String getSpringProfileForOs() throws MissingSpringProfileException { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.contains("win")) { + return "win"; + } else if (osName.contains("mac")) { + return "mac"; + } else if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) { + return "linux"; + } else { + throw new MissingSpringProfileException("Could not start Toolbox, " + + "there is no Spring Profile that matches your OS specification."); + } } } diff --git a/src/main/java/edu/kit/provideq/toolbox/exception/MissingExampleException.java b/src/main/java/edu/kit/provideq/toolbox/exception/MissingExampleException.java new file mode 100644 index 00000000..4c9629b8 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/exception/MissingExampleException.java @@ -0,0 +1,11 @@ +package edu.kit.provideq.toolbox.exception; + +public class MissingExampleException extends RuntimeException { + public MissingExampleException(String message) { + super(message); + } + + public MissingExampleException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/exception/MissingSpringProfileException.java b/src/main/java/edu/kit/provideq/toolbox/exception/MissingSpringProfileException.java new file mode 100644 index 00000000..71fe67f7 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/exception/MissingSpringProfileException.java @@ -0,0 +1,7 @@ +package edu.kit.provideq.toolbox.exception; + +public class MissingSpringProfileException extends Exception { + public MissingSpringProfileException(String message) { + super(message); + } +} 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 196af683..54f15dad 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 @@ -1,6 +1,7 @@ package edu.kit.provideq.toolbox.featuremodel.anomaly.dead; 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; @@ -20,7 +21,7 @@ public class DeadFeatureConfiguration { * For a given feature model, check if the model contains dead features. * * @see - * "Explaining Anomalies in Feature Models", Kowal et al., 2016 + * "Explaining Anomalies in Feature Models", Kowal et al., 2016 */ public static final ProblemType FEATURE_MODEL_ANOMALY_DEAD = new ProblemType<>( "feature-model-anomaly-dead", @@ -50,7 +51,7 @@ private Set> loadExampleProblems(ResourceProvider resour problem.setInput(resourceProvider.readStream(problemInputStream)); return Set.of(problem); } catch (IOException e) { - throw new RuntimeException("Could not load example problems", e); + throw new MissingExampleException("Could not load example problems", e); } } } diff --git a/src/main/java/edu/kit/provideq/toolbox/knapsack/solvers/PythonKnapsackSolver.java b/src/main/java/edu/kit/provideq/toolbox/knapsack/solvers/PythonKnapsackSolver.java index face8d1d..88faeb73 100644 --- a/src/main/java/edu/kit/provideq/toolbox/knapsack/solvers/PythonKnapsackSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/knapsack/solvers/PythonKnapsackSolver.java @@ -1,8 +1,8 @@ package edu.kit.provideq.toolbox.knapsack.solvers; -import edu.kit.provideq.toolbox.PythonProcessRunner; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; @@ -16,7 +16,7 @@ public class PythonKnapsackSolver extends KnapsackSolver { @Autowired public PythonKnapsackSolver( - @Value("${python.directory}/knapsack") String knapsackPath, + @Value("${custom.hs_knapsack.directory}") String knapsackPath, ApplicationContext context) { this.knapsackPath = knapsackPath; this.context = context; @@ -24,7 +24,7 @@ public PythonKnapsackSolver( @Override public String getName() { - return "Python Knapsack"; + return "Horowitz-Sahni Knapsack"; } @Override 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 42f55e9b..bb3fc354 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java @@ -1,6 +1,7 @@ package edu.kit.provideq.toolbox.maxcut; import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.MissingExampleException; import edu.kit.provideq.toolbox.maxcut.solvers.CirqMaxCutSolver; import edu.kit.provideq.toolbox.maxcut.solvers.GamsMaxCutSolver; import edu.kit.provideq.toolbox.maxcut.solvers.QiskitMaxCutSolver; @@ -53,7 +54,7 @@ private Set> loadExampleProblems(ResourceProvider resour problem.setInput(resourceProvider.readStream(problemInputStream)); return Set.of(problem); } catch (IOException e) { - throw new RuntimeException("Could not load example problems", e); + throw new MissingExampleException("Could not load example problems", e); } } } diff --git a/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/CirqMaxCutSolver.java b/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/CirqMaxCutSolver.java index bb4aa9f6..22477749 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/CirqMaxCutSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/CirqMaxCutSolver.java @@ -1,9 +1,9 @@ package edu.kit.provideq.toolbox.maxcut.solvers; -import edu.kit.provideq.toolbox.PythonProcessRunner; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.maxcut.MaxCutConfiguration; import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; @@ -39,9 +39,9 @@ public Mono> solve( var solution = new Solution(); var processResult = context.getBean( - PythonProcessRunner.class, - scriptDir, - "max_cut_cirq.py") + PythonProcessRunner.class, + scriptDir, + "max_cut_cirq.py") .addProblemFilePathToProcessCommand() .addSolutionFilePathToProcessCommand() .run(getProblemType(), solution.getId(), input); diff --git a/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/GamsMaxCutSolver.java b/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/GamsMaxCutSolver.java index 861d9fd1..ac902ce8 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/GamsMaxCutSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/GamsMaxCutSolver.java @@ -1,9 +1,9 @@ package edu.kit.provideq.toolbox.maxcut.solvers; -import edu.kit.provideq.toolbox.GamsProcessRunner; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.maxcut.MaxCutConfiguration; import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.GamsProcessRunner; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; diff --git a/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/QiskitMaxCutSolver.java b/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/QiskitMaxCutSolver.java index a231e679..46ff923c 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/QiskitMaxCutSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/solvers/QiskitMaxCutSolver.java @@ -1,11 +1,11 @@ package edu.kit.provideq.toolbox.maxcut.solvers; -import edu.kit.provideq.toolbox.PythonProcessRunner; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.exception.ConversionException; import edu.kit.provideq.toolbox.format.gml.Gml; import edu.kit.provideq.toolbox.maxcut.MaxCutConfiguration; import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -68,19 +68,19 @@ public Mono> solve( } // Parse solution data and add partition data to GML - Optional solutionLine = processResult.output() - .lines() - .filter(s -> s.startsWith(SOLUTION_LINE_PREFIX)) - .findFirst(); + Optional solutionLine = processResult.output().get() + .lines() + .filter(s -> s.startsWith(SOLUTION_LINE_PREFIX)) + .findFirst(); if (solutionLine.isPresent()) { // Prepare solution data from python output String s = solutionLine.get(); var solutionData = s - // Remove brackets around the solution data - .substring(SOLUTION_LINE_PREFIX.length() + 1, s.length() - 1) - .trim() - .split("\\."); + // Remove brackets around the solution data + .substring(SOLUTION_LINE_PREFIX.length() + 1, s.length() - 1) + .trim() + .split("\\."); // Add partition data to each node in GML // We're expecting that the nodes are in the same order as in the solution data diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/BooleanState.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/BooleanState.java index 3f1befcd..5cd820b0 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/setting/BooleanState.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/BooleanState.java @@ -1,15 +1,22 @@ package edu.kit.provideq.toolbox.meta.setting; public class BooleanState extends MetaSolverSetting { - public boolean state; + private boolean state; - public BooleanState(String name) { - this(name, false); + public BooleanState(String name, String title) { + this(name, title, false); } - public BooleanState(String name, boolean state) { - super(name, MetaSolverSettingType.CHECKBOX); + public BooleanState(String name, String title, boolean state) { + super(name, title, MetaSolverSettingType.CHECKBOX); + this.state = state; + } + + public boolean getState() { + return state; + } + public void setState(boolean state) { this.state = state; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/BoundedInteger.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/BoundedInteger.java index f142fe56..737b4a21 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/setting/BoundedInteger.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/BoundedInteger.java @@ -1,19 +1,30 @@ package edu.kit.provideq.toolbox.meta.setting; public class BoundedInteger extends MetaSolverSetting { - public double min; - public double max; - public double value; + private double min; + private double max; + private double value; - public BoundedInteger(String name, double min, double max) { - this(name, min, max, (max - min) / 2); + public BoundedInteger(String name, String title, double min, double max) { + this(name, title, min, max, (max - min) / 2); } - public BoundedInteger(String name, double min, double max, double value) { - super(name, MetaSolverSettingType.RANGE); - + public BoundedInteger(String name, String title, double min, double max, double value) { + super(name, title, MetaSolverSettingType.RANGE); this.min = min; this.max = max; this.value = value; } + + public double getMin() { + return min; + } + + public double getMax() { + return max; + } + + public double getValue() { + return value; + } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/IntegerSetting.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/IntegerSetting.java new file mode 100644 index 00000000..2b64539a --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/IntegerSetting.java @@ -0,0 +1,23 @@ +package edu.kit.provideq.toolbox.meta.setting; + +public class IntegerSetting extends MetaSolverSetting { + private int number; + + public IntegerSetting(String name, String title) { + this(name, title, 0); + } + + public IntegerSetting(String name, String title, int defaultValue) { + super(name, title, MetaSolverSettingType.INTEGER); + + this.number = defaultValue; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSetting.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSetting.java index f588b242..45ae5231 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSetting.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSetting.java @@ -4,14 +4,38 @@ @JsonDeserialize(using = MetaSolverSettingDeserializer.class) public abstract class MetaSolverSetting { - public String name; - public MetaSolverSettingType type; + private String name; + private MetaSolverSettingType type; + private String title; - public MetaSolverSetting() { + protected MetaSolverSetting(String name, String title, MetaSolverSettingType type) { + this.setName(name); + this.setType(type); + this.setTitle(title); } - protected MetaSolverSetting(String name, MetaSolverSettingType type) { + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getName() { + return name; + } + + public void setName(String name) { this.name = name; + } + + public MetaSolverSettingType getType() { + return type; + } + + public void setType(MetaSolverSettingType type) { this.type = type; } + } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingDeserializer.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingDeserializer.java index 4ec32dc1..da30ddf7 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingDeserializer.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingDeserializer.java @@ -20,17 +20,22 @@ public MetaSolverSetting deserialize(JsonParser jsonParser, // Get type MetaSolverSettingType type = MetaSolverSettingType.valueOf(node.get("type").asText()); + + var name = node.get("name").asText(); + var title = node.get("title").asText(); // Create subclass based on the type switch (type) { case CHECKBOX -> { return new BooleanState( - node.get("name").asText(), + name, + title, node.get("state").asBoolean()); } case RANGE -> { return new BoundedInteger( - node.get("name").asText(), + name, + title, node.get("min").asDouble(), node.get("max").asDouble(), node.get("value").asDouble()); @@ -42,15 +47,23 @@ public MetaSolverSetting deserialize(JsonParser jsonParser, options.add(codec.treeToValue(optionNode, Object.class).toString()); } return new Select<>( - node.get("name").asText(), + name, + title, options, codec.treeToValue(node.get("selectedOption"), Object.class).toString()); } case TEXT -> { return new Text( - node.get("name").asText(), + name, + title, node.get("text").asText()); } + case INTEGER -> { + return new IntegerSetting( + name, + title, + node.get("number").asInt()); + } default -> throw new IllegalArgumentException("Invalid MetaSolverSettingType: " + type); } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingType.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingType.java index 227a4789..7700740c 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingType.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/MetaSolverSettingType.java @@ -4,5 +4,6 @@ public enum MetaSolverSettingType { RANGE, CHECKBOX, TEXT, - SELECT + SELECT, + INTEGER, } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/Select.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/Select.java index 6099bb45..0038a3b1 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/setting/Select.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/Select.java @@ -4,18 +4,35 @@ import javax.annotation.Nullable; public class Select extends MetaSolverSetting { - public List options; + private List options; @Nullable - public T selectedOption; + private T selectedOption; - public Select(String name, List options) { - this(name, options, null); + public Select(String name, String title, List options) { + this(name, title, options, null); } - public Select(String name, List options, T selectedOption) { - super(name, MetaSolverSettingType.SELECT); + public Select(String name, String title, List options, T selectedOption) { + super(name, title, MetaSolverSettingType.SELECT); + this.setOptions(options); + this.setSelectedOption(selectedOption); + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { this.options = options; + } + + @Nullable + public T getSelectedOption() { + return selectedOption; + } + + public void setSelectedOption(@Nullable T selectedOption) { this.selectedOption = selectedOption; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/setting/Text.java b/src/main/java/edu/kit/provideq/toolbox/meta/setting/Text.java index db4b5f19..f6aecbd7 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/setting/Text.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/setting/Text.java @@ -3,12 +3,12 @@ public class Text extends MetaSolverSetting { public String text; - public Text(String name) { - this(name, ""); + public Text(String name, String title) { + this(name, title, ""); } - public Text(String name, String text) { - super(name, MetaSolverSettingType.TEXT); + public Text(String name, String title, String text) { + super(name, title, MetaSolverSettingType.TEXT); this.text = text; } diff --git a/src/main/java/edu/kit/provideq/toolbox/process/BinaryProcessRunner.java b/src/main/java/edu/kit/provideq/toolbox/process/BinaryProcessRunner.java new file mode 100644 index 00000000..a9ce3945 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/process/BinaryProcessRunner.java @@ -0,0 +1,41 @@ +package edu.kit.provideq.toolbox.process; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +/** + * General process runner for any type of binaries. + * Starts the binary with the given args. No specialized processing. + */ +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class BinaryProcessRunner extends ProcessRunner { + + /** + * Creates a process runner for a binary. + * + * @param directory the working directory to run the binary in. + * @param executable the filename of the binary to run. + * @param command first argument for the binary / command to run. + */ + public BinaryProcessRunner(String directory, String executable, String command) { + this(directory, executable, command, new String[0]); + } + + /** + * Creates a process runner for a binary. + * + * @param directory the working directory to run the binary in. + * @param executable the filename of the binary to run. + * @param command first argument for the binary / command to run. + * @param arguments extra arguments to pass to the binary. Use this to pass problem input to the + * solver. + */ + @Autowired + public BinaryProcessRunner(String directory, String executable, String command, + String... arguments) { + super(createGenericProcessBuilder(directory, executable, command), arguments); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/GamsProcessRunner.java b/src/main/java/edu/kit/provideq/toolbox/process/GamsProcessRunner.java similarity index 86% rename from src/main/java/edu/kit/provideq/toolbox/GamsProcessRunner.java rename to src/main/java/edu/kit/provideq/toolbox/process/GamsProcessRunner.java index 2b0e36d6..c33ed065 100644 --- a/src/main/java/edu/kit/provideq/toolbox/GamsProcessRunner.java +++ b/src/main/java/edu/kit/provideq/toolbox/process/GamsProcessRunner.java @@ -1,6 +1,7 @@ -package edu.kit.provideq.toolbox; +package edu.kit.provideq.toolbox.process; import edu.kit.provideq.toolbox.meta.ProblemType; +import java.util.Optional; import java.util.UUID; import org.apache.logging.log4j.util.Strings; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -48,19 +49,11 @@ public GamsProcessRunner(String directory, String scriptFileName) { * solver. */ public GamsProcessRunner(String directory, String scriptFileName, String... arguments) { - super(createGenericProcessBuilder(directory, GAMS_EXECUTABLE_NAME, scriptFileName, arguments)); + super(createGenericProcessBuilder(directory, GAMS_EXECUTABLE_NAME, scriptFileName), arguments); addProblemFilePathToProcessCommand("--INPUT=\"%s\""); } - @Override - public ProcessResult run(ProblemType problemType, UUID solutionId, String problemData) { - var result = super.run(problemType, solutionId, problemData); - - var obfuscatedOutput = obfuscateGamsLicense(result.output()); - return new ProcessResult(result.success(), obfuscatedOutput); - } - /** * Removes GAMS' license output from an output log. */ @@ -88,4 +81,13 @@ private static String obfuscateGamsLicense(String output) { private static String obfuscateLine(String line) { return Strings.repeat("*", line.length()); } + + @Override + public ProcessResult run(ProblemType problemType, UUID solutionId, + String problemData) { + var result = super.run(problemType, solutionId, problemData); + + var obfuscatedOutput = obfuscateGamsLicense(result.output().orElse("no license found")); + return new ProcessResult<>(result.success(), Optional.of(obfuscatedOutput), Optional.empty()); + } } diff --git a/src/main/java/edu/kit/provideq/toolbox/process/MultiFileProcessResultReader.java b/src/main/java/edu/kit/provideq/toolbox/process/MultiFileProcessResultReader.java new file mode 100644 index 00000000..8fbff9fa --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/process/MultiFileProcessResultReader.java @@ -0,0 +1,52 @@ +package edu.kit.provideq.toolbox.process; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Optional; + +/** + * A reader that reads the output of a ProcessRunner that created multiple files. + * The content of the files is combined to a HashMap (File path : File content (String)). + */ +public class MultiFileProcessResultReader implements ProcessResultReader> { + + private final String globPattern; + + public MultiFileProcessResultReader(String globPattern) { + this.globPattern = globPattern; + } + + public ProcessResult> read(Path solutionPath, Path problemPath, + Path problemDirectory) { + + HashMap solutions = new HashMap<>(); + + // Split globPattern at the last slash + String directoryPath = globPattern.substring(0, globPattern.lastIndexOf('/')); + String filePattern = globPattern.substring(globPattern.lastIndexOf('/') + 1); + + try (DirectoryStream stream = Files.newDirectoryStream( + Path.of(problemDirectory.toString(), directoryPath), filePattern)) { + for (Path file : stream) { + solutions.put(file, Files.readString(file)); + } + } catch (IOException e) { + return new ProcessResult<>( + false, + Optional.empty(), + Optional.of("Error: The problem data couldn't be read from %s:%n%s%nCommand".formatted( + solutionPath, e.getMessage())) + ); + } + + // Return the solution + return new ProcessResult<>( + true, + Optional.of(solutions), + Optional.empty() + ); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/process/ProcessResult.java b/src/main/java/edu/kit/provideq/toolbox/process/ProcessResult.java new file mode 100644 index 00000000..c285b069 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/process/ProcessResult.java @@ -0,0 +1,33 @@ +package edu.kit.provideq.toolbox.process; + +import edu.kit.provideq.toolbox.Solution; +import java.util.Optional; + +/** + * Result of running a process. + * + * @param success did the process complete successfully + * @param output process console output + */ +public record ProcessResult(boolean success, Optional output, Optional errorOutput) { + /** + * Utility method for storing the contents of a process result in a string solution object. + * + * @param solution the solution to apply this data to. + * @return the given, modified solution. + */ + public Solution applyTo(Solution solution) { + if (this.success) { + if (output().isPresent()) { + solution.setSolutionData(output().get()); + } else { + solution.setDebugData("Solution was found, but could not retrieve Solution Data"); + } + solution.complete(); + } else { + solution.setDebugData(errorOutput().orElse("Unknown error occurred.")); + solution.fail(); + } + return solution; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/process/ProcessResultReader.java b/src/main/java/edu/kit/provideq/toolbox/process/ProcessResultReader.java new file mode 100644 index 00000000..925ee050 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/process/ProcessResultReader.java @@ -0,0 +1,8 @@ +package edu.kit.provideq.toolbox.process; + +import java.nio.file.Path; + + +public interface ProcessResultReader { + public ProcessResult read(Path solutionPath, Path problemPath, Path problemDirectory); +} diff --git a/src/main/java/edu/kit/provideq/toolbox/ProcessRunner.java b/src/main/java/edu/kit/provideq/toolbox/process/ProcessRunner.java similarity index 61% rename from src/main/java/edu/kit/provideq/toolbox/ProcessRunner.java rename to src/main/java/edu/kit/provideq/toolbox/process/ProcessRunner.java index 813b824a..2f705ae7 100644 --- a/src/main/java/edu/kit/provideq/toolbox/ProcessRunner.java +++ b/src/main/java/edu/kit/provideq/toolbox/process/ProcessRunner.java @@ -1,11 +1,16 @@ -package edu.kit.provideq.toolbox; +package edu.kit.provideq.toolbox.process; +import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.meta.ProblemType; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -31,19 +36,40 @@ public class ProcessRunner { * Note that the executed process NEEDs to use this exact name. */ private static final String SOLUTION_FILE_NAME = "solution"; - protected final ProcessBuilder processBuilder; protected ResourceProvider resourceProvider; + /** + * Arguments that are passed to the command line call. + */ + private String[] arguments; + private HashMap env; - private String problemFilePathCommandFormat; - private String solutionFilePathCommandFormat; + private String[] problemFilePathCommandFormat; + private String[] solutionFilePathCommandFormat; private String problemFileName = PROBLEM_FILE_NAME; private String solutionFileName = SOLUTION_FILE_NAME; public ProcessRunner( - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") - ProcessBuilder processBuilder) { + ProcessBuilder processBuilder, + String[] arguments) { this.processBuilder = processBuilder; + this.arguments = arguments; + this.env = new HashMap<>(); + } + + protected static ProcessBuilder createGenericProcessBuilder( + String directory, + String executableName, + String scriptName, + String... arguments) { + String[] commands = new String[arguments.length + 2]; + commands[0] = executableName; + commands[1] = scriptName; + System.arraycopy(arguments, 0, commands, 2, arguments.length); + + return new ProcessBuilder() + .directory(new File(directory)) + .command(commands); } @Autowired @@ -69,7 +95,7 @@ public ProcessRunner addProblemFilePathToProcessCommand() { * the path to a file that contains the problem data. * @return Returns this instance for chaining. */ - public ProcessRunner addProblemFilePathToProcessCommand(String inputPathCommandFormat) { + public ProcessRunner addProblemFilePathToProcessCommand(String... inputPathCommandFormat) { this.problemFilePathCommandFormat = inputPathCommandFormat; return this; @@ -93,7 +119,7 @@ public ProcessRunner addSolutionFilePathToProcessCommand() { * the path to a file that contains the solution data. * @return Returns this instance for chaining. */ - public ProcessRunner addSolutionFilePathToProcessCommand(String outputPathCommandFormat) { + public ProcessRunner addSolutionFilePathToProcessCommand(String... outputPathCommandFormat) { this.solutionFilePathCommandFormat = outputPathCommandFormat; return this; @@ -132,7 +158,24 @@ public ProcessRunner solutionFileName(String fileName) { * @return Returns the process result, which contains the solution data * or an error as output depending on the success of the process. */ - public ProcessResult run(ProblemType problemType, UUID solutionId, String problemData) { + public ProcessResult run(ProblemType problemType, UUID solutionId, + String problemData) { + return run(problemType, solutionId, problemData, new SimpleProcessResultReader()); + } + + /** + * Runs the process provided in the constructor. + * TODO: split this method into prepareRun, executeRun, and readResults. + * + * @param problemType The type of the problem that is run + * @param solutionId The id of the resulting solution + * @param problemData The problem data that should be solved + * @param reader The reader that retrieves the output of the process + * @return Returns the process result, which contains the solution data, + * or an error as output depending on the success of the process. + */ + public ProcessResult run(ProblemType problemType, UUID solutionId, + String problemData, ProcessResultReader reader) { // Retrieve the problem directory String problemDirectoryPath; try { @@ -140,9 +183,11 @@ public ProcessResult run(ProblemType problemType, UUID solutionId, String .getProblemDirectory(problemType, solutionId) .getAbsolutePath(); } catch (IOException e) { - return new ProcessResult( + return new ProcessResult<>( false, - "Error: The problem directory couldn't be retrieved:%n%s".formatted(e.getMessage()) + Optional.empty(), + Optional.of( + "Error: The problem directory couldn't be retrieved:%n%s".formatted(e.getMessage())) ); } @@ -157,21 +202,35 @@ public ProcessResult run(ProblemType problemType, UUID solutionId, String try { Files.writeString(problemFilePath, problemData); } catch (IOException e) { - return new ProcessResult( + return new ProcessResult<>( false, - "Error: The problem data couldn't be written to %s:%n%s".formatted( - normalizedProblemFilePath, e.getMessage()) + Optional.empty(), + Optional.of("Error: The problem data couldn't be written to %s:%n%s".formatted( + normalizedProblemFilePath, e.getMessage())) ); } + for (String argument : arguments) { + addCommand(argument.formatted(normalizedProblemFilePath, normalizedSolutionFilePath, + problemDirectoryPath)); + } + + for (Entry entry : env.entrySet()) { + addEnvironmentVariableToBuilder(entry.getKey(), entry.getValue()); + } + // Optionally add the problem file path to the command if (problemFilePathCommandFormat != null) { - addCommand(problemFilePathCommandFormat.formatted(normalizedProblemFilePath)); + for (String format : problemFilePathCommandFormat) { + addCommand(format.formatted(normalizedProblemFilePath)); + } } // Optionally add the solution path to the command if (solutionFilePathCommandFormat != null) { - addCommand(solutionFilePathCommandFormat.formatted(normalizedSolutionFilePath)); + for (String format : solutionFilePathCommandFormat) { + addCommand(format.formatted(normalizedSolutionFilePath)); + } } // Run the process @@ -181,42 +240,53 @@ public ProcessResult run(ProblemType problemType, UUID solutionId, String Process process = processBuilder.start(); processOutput = resourceProvider.readStream(process.inputReader()) - + resourceProvider.readStream(process.errorReader()); + + resourceProvider.readStream(process.errorReader()); processExitCode = process.waitFor(); - } catch (IOException | InterruptedException e) { - return new ProcessResult( + } catch (IOException e) { + return new ProcessResult<>( false, - "Solving %s problem resulted in exception:%n%s" - .formatted(problemType.getId(), e.getMessage()) + Optional.empty(), + Optional.of( + "Solving %s problem resulted in IO Exception:%n%s".formatted(problemType.getId(), + e.getMessage()) + ) + ); + } catch (InterruptedException e) { + // interrupt current thread: + Thread.currentThread().interrupt(); + return new ProcessResult<>( + false, Optional.empty(), + Optional.of("Thread InterruptedException while Solving Problem, no solution found\n" + + e.getMessage()) ); } // Return prematurely if the process failed if (processExitCode != 0) { - return new ProcessResult( + return new ProcessResult<>( false, - "%s problem couldn't be solved:%n%s" - .formatted(problemType.getId(), processOutput)); + Optional.empty(), + Optional.of( + "%s problem couldn't be solved:%n%s".formatted(problemType.getId(), processOutput))); } // Read the solution file - String solutionText; - try { - solutionText = Files.readString(solutionFile); - } catch (IOException e) { - return new ProcessResult( - false, - "Error: The problem data couldn't be read from %s:%n%s".formatted( - normalizedProblemFilePath, e.getMessage()) + ProcessResult result = + reader.read(solutionFile, problemFilePath, Path.of(problemDirectoryPath)); + + if (!result.success()) { + return new ProcessResult<>( + result.success(), + result.output(), + result.errorOutput().isPresent() ? Optional.of( + result.errorOutput().get() + "%nCommand Output: %s".formatted(processOutput)) : + Optional.empty() ); } // Return the solution - return new ProcessResult( - true, - solutionText - ); + return result; } @@ -226,18 +296,11 @@ private void addCommand(String command) { processBuilder.command(existingCommands); } - protected static ProcessBuilder createGenericProcessBuilder( - String directory, - String executableName, - String scriptName, - String... arguments) { - String[] commands = new String[arguments.length + 2]; - commands[0] = executableName; - commands[1] = scriptName; - System.arraycopy(arguments, 0, commands, 2, arguments.length); + public void addEnvironmentVariable(String key, String value) { + env.put(key, value); + } - return new ProcessBuilder() - .directory(new File(directory)) - .command(commands); + private void addEnvironmentVariableToBuilder(String key, String value) { + processBuilder.environment().put(key, value); } } diff --git a/src/main/java/edu/kit/provideq/toolbox/PythonProcessRunner.java b/src/main/java/edu/kit/provideq/toolbox/process/PythonProcessRunner.java similarity index 94% rename from src/main/java/edu/kit/provideq/toolbox/PythonProcessRunner.java rename to src/main/java/edu/kit/provideq/toolbox/process/PythonProcessRunner.java index 7a68f086..0e34a3eb 100644 --- a/src/main/java/edu/kit/provideq/toolbox/PythonProcessRunner.java +++ b/src/main/java/edu/kit/provideq/toolbox/process/PythonProcessRunner.java @@ -1,4 +1,4 @@ -package edu.kit.provideq.toolbox; +package edu.kit.provideq.toolbox.process; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; @@ -35,6 +35,6 @@ public PythonProcessRunner(String directory, String scriptFileName) { */ public PythonProcessRunner(String directory, String scriptFileName, String... arguments) { super( - createGenericProcessBuilder(directory, PYTHON_EXECUTABLE_NAME, scriptFileName, arguments)); + createGenericProcessBuilder(directory, PYTHON_EXECUTABLE_NAME, scriptFileName), arguments); } } diff --git a/src/main/java/edu/kit/provideq/toolbox/process/SimpleProcessResultReader.java b/src/main/java/edu/kit/provideq/toolbox/process/SimpleProcessResultReader.java new file mode 100644 index 00000000..a6ff3958 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/process/SimpleProcessResultReader.java @@ -0,0 +1,35 @@ +package edu.kit.provideq.toolbox.process; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +/** + * Basic reader for results of ProcessRunners. + * Reads the content of the file associated with the solutionPath. + * No specialized processing is done. + */ +public class SimpleProcessResultReader implements ProcessResultReader { + public ProcessResult read(Path solutionPath, Path problemPath, Path problemDirectory) { + // Read the solution file + String solutionText; + try { + solutionText = Files.readString(solutionPath); + } catch (IOException e) { + return new ProcessResult<>( + false, + Optional.empty(), + Optional.of("Error: The problem data couldn't be read from %s:%n%s%n".formatted( + solutionPath, e.getMessage())) + ); + } + + // Return the solution + return new ProcessResult<>( + true, + Optional.of(solutionText), + Optional.empty() + ); + } +} 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 09ad8b68..57856980 100644 --- a/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java @@ -1,10 +1,13 @@ package edu.kit.provideq.toolbox.qubo; 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.qubo.solvers.DwaveQuboSolver; import edu.kit.provideq.toolbox.qubo.solvers.QiskitQuboSolver; +import edu.kit.provideq.toolbox.qubo.solvers.QrispQuboSolver; import java.io.IOException; import java.util.Objects; import java.util.Set; @@ -31,11 +34,13 @@ public class QuboConfiguration { @Bean ProblemManager getQuboManager( QiskitQuboSolver qiskitSolver, + DwaveQuboSolver dwaveSolver, + QrispQuboSolver qrispSolver, ResourceProvider resourceProvider ) { return new ProblemManager<>( QUBO, - Set.of(qiskitSolver), + Set.of(qiskitSolver, dwaveSolver, qrispSolver), loadExampleProblems(resourceProvider) ); } @@ -50,7 +55,7 @@ private Set> loadExampleProblems(ResourceProvider resour problem.setInput(resourceProvider.readStream(problemInputStream)); return Set.of(problem); } catch (IOException e) { - throw new RuntimeException("Could not load example problems", e); + throw new MissingExampleException("Could not load example problems", e); } } } diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/DwaveQuboSolver.java b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/DwaveQuboSolver.java new file mode 100644 index 00000000..33e7335c --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/DwaveQuboSolver.java @@ -0,0 +1,71 @@ +package edu.kit.provideq.toolbox.qubo.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import edu.kit.provideq.toolbox.qubo.QuboConfiguration; +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; + +/** + * {@link QuboConfiguration#QUBO} solver using a Dwaves Quantum Annealer implementation. + */ +@Component +public class DwaveQuboSolver extends QuboSolver { + private final String quboScriptPath; + private final ApplicationContext context; + + @Autowired + public DwaveQuboSolver( + @Value("${dwave.directory.qubo}") String quboScriptPath, + ApplicationContext context) { + this.quboScriptPath = quboScriptPath; + this.context = context; + } + + @Override + public String getName() { + return "(D-Wave) Annealing QUBO Solver"; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver + ) { + + // there is currently no field where a token can be added by the user + // this field is kept because it was used in Lucas implementation and will be added back later + // TODO: add user setting that allows passing Tokens + Optional dwaveToken = Optional.empty(); + + // this field is only relevant when a dwaveToken is added + // (a token is needed to access the d-wave hardware) + // options are: sim, hybrid, absolv, direct + String dwaveAnnealingMethod = "sim"; + + var solution = new Solution(); + + var processRunner = context.getBean( + PythonProcessRunner.class, + quboScriptPath, + "main.py", + new String[] {"%1$s", dwaveAnnealingMethod, "--output-file", "%2$s"} + ) + .problemFileName("problem.lp") + .solutionFileName("problem.bin"); + + if (dwaveToken.isPresent()) { + processRunner.addEnvironmentVariable("DWAVE_API_TOKEN", dwaveToken.get()); + } + + var processResult = processRunner + .run(getProblemType(), solution.getId(), input); + + return Mono.just(processResult.applyTo(solution)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QiskitQuboSolver.java b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QiskitQuboSolver.java index 6e830643..70890c51 100644 --- a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QiskitQuboSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QiskitQuboSolver.java @@ -1,8 +1,8 @@ package edu.kit.provideq.toolbox.qubo.solvers; -import edu.kit.provideq.toolbox.PythonProcessRunner; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +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; @@ -28,7 +28,7 @@ public QiskitQuboSolver( @Override public String getName() { - return "Qiskit QUBO"; + return "(Qiskit) QAOA Solver for QUBOs"; } @Override diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QrispQuboSolver.java b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QrispQuboSolver.java new file mode 100644 index 00000000..12159c2f --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/solvers/QrispQuboSolver.java @@ -0,0 +1,60 @@ +package edu.kit.provideq.toolbox.qubo.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +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 Qrisps QAOA implementation. + */ +@Component +public class QrispQuboSolver extends QuboSolver { + private final String vrpPath; + private final ApplicationContext context; + + @Autowired + public QrispQuboSolver( + @Value("${qrisp.directory.vrp}") String vrpPath, + ApplicationContext context) { + this.vrpPath = vrpPath; + this.context = context; + } + + @Override + public String getName() { + return "(Qrisp) QAOA Solver for QUBOs"; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver subRoutineResolver + ) { + var solution = new Solution(); + + // This value will be passed to the python script, + // it is used to prevent denial of service issues for large simulations. + // Default value is 4, higher values are possible but might take much longer to simulate. + // TODO: allow user to pass a custom gate size as a solver setting + int maxNumberOfCities = 4; + + var processResult = context.getBean( + PythonProcessRunner.class, + vrpPath, + "qaoa.py", + new String[] {"%1$s", "--output-file", "%2$s", + "--size-gate", String.valueOf(maxNumberOfCities)} + ) + .problemFileName("problem.lp") + .solutionFileName("problem.bin") + .run(getProblemType(), solution.getId(), input); + + return Mono.just(processResult.applyTo(solution)); + } +} 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 517d612e..dc17ea49 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java @@ -1,6 +1,7 @@ package edu.kit.provideq.toolbox.sat; import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.exception.MissingExampleException; import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnfSolution; import edu.kit.provideq.toolbox.meta.Problem; import edu.kit.provideq.toolbox.meta.ProblemManager; @@ -51,7 +52,7 @@ private Set> loadExampleProblems( problem.setInput(resourceProvider.readStream(problemInputStream)); return Set.of(problem); } catch (IOException e) { - throw new RuntimeException("Could not load example problems", e); + throw new MissingExampleException("Could not load example problems", e); } } } diff --git a/src/main/java/edu/kit/provideq/toolbox/sat/solvers/GamsSatSolver.java b/src/main/java/edu/kit/provideq/toolbox/sat/solvers/GamsSatSolver.java index 6e1f8615..23d72b18 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sat/solvers/GamsSatSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/sat/solvers/GamsSatSolver.java @@ -1,11 +1,12 @@ package edu.kit.provideq.toolbox.sat.solvers; -import edu.kit.provideq.toolbox.GamsProcessRunner; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.exception.ConversionException; import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnf; import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnfSolution; import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.GamsProcessRunner; +import edu.kit.provideq.toolbox.process.ProcessResult; import edu.kit.provideq.toolbox.sat.SatConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -52,7 +53,7 @@ public Mono> solve( } // Run SAT with GAMS via console - var processResult = context + ProcessResult processResult = context .getBean( GamsProcessRunner.class, satPath, @@ -60,12 +61,13 @@ public Mono> solve( .run(getProblemType(), solution.getId(), dimacsCnf.toString()); if (processResult.success()) { - var dimacsCnfSolution = DimacsCnfSolution.fromString(dimacsCnf, processResult.output()); + var dimacsCnfSolution = + DimacsCnfSolution.fromString(dimacsCnf, processResult.output().orElse("")); solution.setSolutionData(dimacsCnfSolution); solution.complete(); } else { - solution.setDebugData(processResult.output()); + solution.setDebugData(processResult.errorOutput().orElse("Unknown error occurred.")); solution.fail(); } return Mono.just(solution); diff --git a/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java new file mode 100644 index 00000000..da9d146b --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java @@ -0,0 +1,61 @@ +package edu.kit.provideq.toolbox.tsp; + +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.tsp.solvers.LkhTspSolver; +import edu.kit.provideq.toolbox.tsp.solvers.QuboTspSolver; +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 TspConfiguration { + + /** + * A Traveling Sales Person Problem. + * Optimization Problem with the goal of find an optimal route + * between a given set of connected cities. + */ + public static final ProblemType TSP = new ProblemType<>( + "tsp", + String.class, + String.class + ); + + @Bean + ProblemManager getTspManager( + ResourceProvider provider, + QuboTspSolver quboTspSolver, + LkhTspSolver lkhTspSolver + ) { + return new ProblemManager<>(TSP, + Set.of(quboTspSolver, lkhTspSolver), + loadExampleProblems(provider)); + } + + private Set> loadExampleProblems(ResourceProvider provider) { + try { + String[] problemNames = new String[] { + "att48.tsp", "SmallSampleTSP.tsp", "VerySmallSampleTSP.tsp" + }; + + var problemSet = new HashSet>(); + for (var problemName : problemNames) { + var problemStream = Objects.requireNonNull(getClass().getResourceAsStream(problemName), + "Problem " + problemName + " not found"); + var problem = new Problem<>(TSP); + problem.setInput(provider.readStream(problemStream)); + problemSet.add(problem); + } + return problemSet; + } catch (IOException e) { + throw new MissingExampleException("Could not load example problems", e); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/LkhTspSolver.java b/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/LkhTspSolver.java new file mode 100644 index 00000000..1276c8fe --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/LkhTspSolver.java @@ -0,0 +1,75 @@ +package edu.kit.provideq.toolbox.tsp.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +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 Solver for the TSP Problem that uses the LKH-3 heuristics. + */ +@Component +public class LkhTspSolver extends TspSolver { + + private final String scriptDir; + private final ApplicationContext context; + private final String solverBinary; + + @Autowired + public LkhTspSolver( + /* + * uses the LKH script dir because it is the same as LKH-3 for VRP + * (LKH can solve VRP and TSP) + */ + @Value("${custom.lkh.directory}") String scriptDir, + @Value("${custom.lkh.solver}") String solverBinary, + ApplicationContext context) { + this.scriptDir = scriptDir; + this.solverBinary = solverBinary; + this.context = context; + } + + @Override + public String getName() { + return "LKH-3 TSP Solver"; + } + + @Override + public Mono> solve(String input, SubRoutineResolver subRoutineResolver) { + var solution = new Solution(); + var processResult = context.getBean( + PythonProcessRunner.class, + scriptDir, + "vrp_lkh.py", + new String[] {"--lkh-instance", solverBinary} + ) + .addProblemFilePathToProcessCommand() + .addSolutionFilePathToProcessCommand("--output-file", "%s") + .problemFileName("problem.vrp") + .solutionFileName("problem.sol") + .run(getProblemType(), solution.getId(), adaptInput(input)); + + return Mono.just(processResult.applyTo(solution)); + } + + /** + * LKH-3 solver has an issue when the "EOF" tag is used in a TSP file. + * This method removes this substring. + * + * @param originalInput original input of the TSP problem + * @return adapted input with "EOF" + */ + private String adaptInput(String originalInput) { + String inputAsVrp = originalInput; + if (inputAsVrp.endsWith("EOF")) { + inputAsVrp = inputAsVrp.replaceAll("EOF$", ""); + } + return inputAsVrp; + } +} 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 new file mode 100644 index 00000000..d027f215 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/QuboTspSolver.java @@ -0,0 +1,158 @@ +package edu.kit.provideq.toolbox.tsp.solvers; + +import edu.kit.provideq.toolbox.ResourceProvider; +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.BinaryProcessRunner; +import edu.kit.provideq.toolbox.qubo.QuboConfiguration; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +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; +import reactor.core.scheduler.Schedulers; + +/** + * Transforms TSP Problems into QUBOs. + */ +@Component +public class QuboTspSolver extends TspSolver { + private static final SubRoutineDefinition QUBO_SUBROUTINE = + new SubRoutineDefinition<>(QuboConfiguration.QUBO, "How should the QUBO be solved?"); + private final ApplicationContext context; + private final String binaryDir; + private final String binaryName; + private ResourceProvider resourceProvider; + + + @Autowired + public QuboTspSolver( + //"vrp" value is correct because this uses the VRP framework from Lucas Bergers thesis + @Value("${custom.berger-vrp.directory}") String binaryDir, + @Value("${custom.berger-vrp.solver}") String binaryName, + ApplicationContext context) { + this.binaryName = binaryName; + this.binaryDir = binaryDir; + this.context = context; + } + + @Autowired + public void setResourceProvider(ResourceProvider resourceProvider) { + this.resourceProvider = resourceProvider; + } + + @Override + public String getName() { + return "TSP to QUBO Transformation"; + } + + @Override + public List> getSubRoutines() { + return List.of(QUBO_SUBROUTINE); + } + + private String getProblemDirectory(Solution solutionObject) { + // write solution to current problem directory + String problemDirectoryPath = null; + try { + problemDirectoryPath = resourceProvider + .getProblemDirectory(getProblemType(), solutionObject.getId()) + .getAbsolutePath(); + } catch (IOException e) { + solutionObject.setDebugData("Failed to retrieve problem directory."); + solutionObject.abort(); + } + return problemDirectoryPath; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver resolver + ) { + var solution = new Solution(); + + // change "TYPE" keyword from "TSP" to "CVRP" + // add capacity declaration of "0" (is ignored later) + // this is theoretically wrong, but needed for Lucas' QUBO converter to work + String typeRegex = "(?i)\\btype\\s*:\\s*tsp\\b"; + input = input.replaceAll(typeRegex, "TYPE : CVRP\nCAPACITY : 0"); + + // translate into qubo in lp-file format with rust vrp meta solver + var processResult = context.getBean( + BinaryProcessRunner.class, + binaryDir, + binaryName, + "partial", + new String[] {"solve", "%1$s", "simulated", "--transform-only"} + ) + .problemFileName("problem.vrp") + .solutionFileName("problem.lp") + .run(getProblemType(), solution.getId(), input); + + if (!processResult.success() || processResult.output().isEmpty()) { + solution.setDebugData(processResult.errorOutput().orElse("Unknown error occurred.")); + solution.abort(); + return Mono.just(solution); + } + + String problemDirectoryPath = getProblemDirectory(solution); + if (problemDirectoryPath == null) { + solution.setDebugData("Unable to solve Problem, the problemDirectoryPath is null."); + solution.abort(); + return Mono.just(solution); + } + Path quboSolutionFilePath = Path.of(problemDirectoryPath, "problem.bin"); + + String finalInput = input; + return resolver.runSubRoutine(QUBO_SUBROUTINE, processResult.output().get()) + .publishOn(Schedulers.boundedElastic()) //avoids block from Files.writeString() in try/catch + .map(subRoutineSolution -> { + if (subRoutineSolution.getSolutionData() == null + || subRoutineSolution.getSolutionData().isEmpty()) { + solution.setDebugData("Unable to process at least one Subroutine, " + + "because its SolutionData does not exist"); + solution.abort(); + return solution; + } + + try { + Files.writeString(quboSolutionFilePath, subRoutineSolution.getSolutionData()); + } catch (IOException e) { + solution.setDebugData( + "Failed to write qubo solution file with path: " + quboSolutionFilePath); + solution.abort(); + return solution; + } + + var processRetransformResult = context.getBean( + BinaryProcessRunner.class, + binaryDir, + binaryName, + "partial", + new String[] {"solve", "%1$s", "simulated", "--qubo-solution", + quboSolutionFilePath.toString()} + ) + .problemFileName("problem.vrp") + .solutionFileName("problem.sol") + .run(getProblemType(), solution.getId(), finalInput); + + if (!processRetransformResult.success()) { + solution.setDebugData( + processRetransformResult.errorOutput().orElse("Unable to retransform result.")); + solution.abort(); + return solution; + } + + solution.setSolutionData(processRetransformResult.output().orElse("Empty Solution")); + solution.complete(); + + return solution; + }); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/TspSolver.java b/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/TspSolver.java new file mode 100644 index 00000000..860bec64 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/tsp/solvers/TspSolver.java @@ -0,0 +1,12 @@ +package edu.kit.provideq.toolbox.tsp.solvers; + +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.tsp.TspConfiguration; + +public abstract class TspSolver implements ProblemSolver { + @Override + public ProblemType getProblemType() { + return TspConfiguration.TSP; + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java new file mode 100644 index 00000000..1fac13ad --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java @@ -0,0 +1,71 @@ +package edu.kit.provideq.toolbox.vrp; + +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.tsp.solvers.QuboTspSolver; +import edu.kit.provideq.toolbox.vrp.solvers.ClusterAndSolveVrpSolver; +import edu.kit.provideq.toolbox.vrp.solvers.LkhVrpSolver; +import edu.kit.provideq.toolbox.vrp.solvers.QrispVrpSolver; +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 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. + */ + public static final ProblemType VRP = new ProblemType<>( + "vrp", + String.class, + String.class + ); + + @Bean + ProblemManager getVrpManager( + ResourceProvider resourceProvider, + ClusterAndSolveVrpSolver clusterAndSolveVrpSolver, + LkhVrpSolver lkhVrpSolver, + QuboTspSolver quboTspSolver, + QrispVrpSolver qrispVrpSolver) { + return new ProblemManager<>( + VRP, + Set.of(clusterAndSolveVrpSolver, + lkhVrpSolver, + quboTspSolver, + qrispVrpSolver), + loadExampleProblems(resourceProvider) + ); + } + + private Set> loadExampleProblems( + ResourceProvider resourceProvider + ) { + try { + String[] problemNames = new String[] { + "CMT1.vrp", "SmallSample.vrp", "VerySmallSampleForGrover.vrp" + }; + + var problemSet = new HashSet>(); + for (var problemName : problemNames) { + var problemStream = Objects.requireNonNull(getClass().getResourceAsStream(problemName), + "Problem " + problemName + " not found"); + var problem = new Problem<>(VRP); + problem.setInput(resourceProvider.readStream(problemStream)); + problemSet.add(problem); + } + return problemSet; + } catch (IOException e) { + throw new MissingExampleException("Could not load example problems", e); + } + } +} 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 new file mode 100644 index 00000000..ebdde074 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/KmeansClusterer.java @@ -0,0 +1,67 @@ +package edu.kit.provideq.toolbox.vrp.clusterer; + + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.BinaryProcessRunner; +import edu.kit.provideq.toolbox.process.MultiFileProcessResultReader; +import edu.kit.provideq.toolbox.process.ProcessResult; +import edu.kit.provideq.toolbox.vrp.VrpConfiguration; +import java.nio.file.Path; +import java.util.HashMap; +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; + +@Component +public class KmeansClusterer extends VrpClusterer { + + @Autowired + public KmeansClusterer(@Value("${custom.berger-vrp.directory}") String binaryDir, + @Value("${custom.berger-vrp.solver}") String binaryName, + ApplicationContext context) { + super(binaryDir, binaryName, context); + } + + protected static final SubRoutineDefinition VRP_SUBROUTINE = + new SubRoutineDefinition<>( + VrpConfiguration.VRP, + "Solve a VRP problem" + ); + + @Override + public List> getSubRoutines() { + return List.of(VRP_SUBROUTINE); + } + + @Override + public String getName() { + return "K-means Clustering (VRP -> Set of VRP)"; + } + + @Override + public Mono> solve(String input, SubRoutineResolver resolver) { + + // for now, set the cluster number to three. Our architecture currently does not allow settings. + // TODO: change this in the future, cluster numbers can be any positive integer (>0) + // called in python script via "kmeans-cluster-number" + int clusterNumber = 3; + + var solution = new Solution(); + + // cluster with kmeans + ProcessResult> processResult = + context.getBean(BinaryProcessRunner.class, binaryDir, binaryName, "partial", + new String[] {"cluster", "%1$s", "kmeans", "--build-dir", "%3$s/.vrp", + "--cluster-number", String.valueOf(clusterNumber)}) + .problemFileName("problem.vrp") + .run(getProblemType(), solution.getId(), input, + new MultiFileProcessResultReader("./.vrp/problem_*.vrp")); + + return getSolutionForCluster(input, solution, processResult, resolver, VRP_SUBROUTINE); + } +} 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 new file mode 100644 index 00000000..a93ab2c2 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/TwoPhaseClusterer.java @@ -0,0 +1,65 @@ +package edu.kit.provideq.toolbox.vrp.clusterer; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.BinaryProcessRunner; +import edu.kit.provideq.toolbox.process.MultiFileProcessResultReader; +import edu.kit.provideq.toolbox.tsp.TspConfiguration; +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; + +@Component +public class TwoPhaseClusterer extends VrpClusterer { + + protected static final SubRoutineDefinition TSP_SUBROUTINE = + new SubRoutineDefinition<>( + TspConfiguration.TSP, + "Solve a TSP problem" + ); + + @Autowired + public TwoPhaseClusterer( + @Value("${custom.berger-vrp.directory}") String binaryDir, + @Value("${custom.berger-vrp.solver}") String binaryName, + ApplicationContext context) { + super(binaryDir, binaryName, context); + } + + @Override + public List> getSubRoutines() { + return List.of(TSP_SUBROUTINE); + } + + @Override + public String getName() { + return "Two Phase Clustering (VRP -> Set of TSP)"; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver resolver + ) { + + var solution = new Solution(); + + // cluster with tsp/two-phase clustering + var processResult = context.getBean( + BinaryProcessRunner.class, + binaryDir, + binaryName, + "partial", + new String[] {"cluster", "%1$s", "tsp", "--build-dir", "%3$s/.vrp"} + ) + .problemFileName("problem.vrp") + .run(getProblemType(), solution.getId(), input, + new MultiFileProcessResultReader("./.vrp/problem_*.vrp")); + + return getSolutionForCluster(input, solution, processResult, resolver, TSP_SUBROUTINE); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClusterer.java b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClusterer.java new file mode 100644 index 00000000..7967e378 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClusterer.java @@ -0,0 +1,136 @@ +package edu.kit.provideq.toolbox.vrp.clusterer; + +import edu.kit.provideq.toolbox.ResourceProvider; +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.SubRoutineDefinition; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.BinaryProcessRunner; +import edu.kit.provideq.toolbox.process.ProcessResult; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +/** + * A solver for SAT problems. + */ +public abstract class VrpClusterer implements ProblemSolver { + + protected final ApplicationContext context; + protected final String binaryDir; + protected final String binaryName; + protected ResourceProvider resourceProvider; + + protected VrpClusterer( + String binaryDir, + String binaryName, + ApplicationContext context + ) { + this.binaryDir = binaryDir; + this.binaryName = binaryName; + this.context = context; + } + + @Override + public ProblemType getProblemType() { + return VrpClustererConfiguration.CLUSTER_VRP; + } + + @Autowired + public void setResourceProvider(ResourceProvider resourceProvider) { + this.resourceProvider = resourceProvider; + } + + protected Mono> getSolutionForCluster( + String input, + Solution solution, + ProcessResult> processResult, + SubRoutineResolver resolver, + SubRoutineDefinition definition) { + if (processResult.output().isEmpty() || !processResult.success()) { + solution.setDebugData(processResult.errorOutput() + .orElse("Unknown Error Occured: Map of Cluster could not be retrieved.")); + solution.abort(); + return Mono.just(solution); + } + + var mapOfClusters = processResult.output().get(); + + //solve TSP clusters: + return Flux.fromIterable(mapOfClusters.entrySet()) + .flatMap(cluster -> resolver.runSubRoutine(definition, cluster.getValue()) + .map(clusterSolution -> Tuples.of(cluster.getKey(), clusterSolution))) + .collectMap(Tuple2::getT1, Tuple2::getT2) + .publishOn(Schedulers.boundedElastic()) + .map(clusterSolutionMap -> solveCluster(input, solution, clusterSolutionMap)); + } + + protected Solution solveCluster( + String input, + Solution solution, + Map> clusterSolutionMap) { + // Retrieve the problem directory + String problemDirectoryPath; + try { + problemDirectoryPath = + resourceProvider.getProblemDirectory(getProblemType(), solution.getId()) + .getAbsolutePath(); + } catch (IOException e) { + solution.setDebugData("Failed to retrieve problem directory."); + solution.fail(); + return solution; + } + + // write solutions of the clusters into files: + for (var entry : clusterSolutionMap.entrySet()) { + // get file name of the entry and replace .vrp with .sol + // (making clear that this file is a solution) + String fileName = entry.getKey().getFileName().toString().replace(".vrp", ".sol"); + + // get path for the solution file: [problemDirectoryPath]/.vrp/[fileName] + Path solutionFilePath = Path.of(problemDirectoryPath, ".vrp", fileName); + + // create the solution file at the associated path: + var clusterSolution = entry.getValue(); + try { + Files.writeString(solutionFilePath, clusterSolution.getSolutionData()); + } catch (IOException e) { + solution.setDebugData("Failed to write solution file. Path: " + solutionFilePath); + solution.fail(); + return solution; + } + } + + // use the combineProcessRunner to combine the solution from the written files + // into one solution of the original problem + var combineProcessRunner = + context.getBean(BinaryProcessRunner.class, binaryDir, binaryName, "solve", + new String[] {"%1$s", "cluster-from-file", "solution-from-file", + "--build-dir", + "%3$s/.vrp", "--solution-dir", "%3$s/.vrp", "--cluster-file", + "%3$s/.vrp/problem.map"}) + .problemFileName("problem.vrp") + .solutionFileName("problem.sol") + .run(getProblemType(), solution.getId(), input); + + if (combineProcessRunner.output().isEmpty() || !combineProcessRunner.success()) { + solution.setDebugData( + combineProcessRunner.errorOutput().orElse("Unknown Error: Could not combine clusters.")); + solution.fail(); + return solution; + } + + solution.complete(); + return solution; + } +} 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 new file mode 100644 index 00000000..79ce124f --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java @@ -0,0 +1,52 @@ +package edu.kit.provideq.toolbox.vrp.clusterer; + +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 VrpClustererConfiguration { + /** + * A Configuration for Vehicle Routing Problem Clusterer. + */ + public static final ProblemType CLUSTER_VRP = new ProblemType<>( + "cluster-vrp", + String.class, + String.class + ); + + @Bean + ProblemManager getClusterVrpManager( + ResourceProvider resourceProvider, + KmeansClusterer kmeans, + TwoPhaseClusterer twoPhase) { + return new ProblemManager<>( + CLUSTER_VRP, + Set.of(kmeans, twoPhase), + loadExampleProblems(resourceProvider) + ); + } + + private Set> loadExampleProblems( + ResourceProvider resourceProvider + ) { + try { + var problemStream = Objects.requireNonNull( + getClass().getResourceAsStream("../CMT1.vrp"), + "Simple VRP CMT1 Problem unavailable!" + ); + var problem = new Problem<>(CLUSTER_VRP); + problem.setInput(resourceProvider.readStream(problemStream)); + return Set.of(problem); + } catch (IOException e) { + throw new MissingExampleException("Could not load example problems", e); + } + } +} 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 new file mode 100644 index 00000000..2a3685b6 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/ClusterAndSolveVrpSolver.java @@ -0,0 +1,45 @@ +package edu.kit.provideq.toolbox.vrp.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +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; + +@Component +public class ClusterAndSolveVrpSolver extends VrpSolver { + private static final SubRoutineDefinition CLUSTER_SUBROUTINE = + new SubRoutineDefinition<>( + VrpClustererConfiguration.CLUSTER_VRP, + "Creates a cluster of multiple vehicle routing problems" + ); + private final ApplicationContext context; + + @Autowired + public ClusterAndSolveVrpSolver( + ApplicationContext context) { + this.context = context; + } + + @Override + public String getName() { + return "Cluster before Solving"; + } + + @Override + public List> getSubRoutines() { + return List.of(CLUSTER_SUBROUTINE); + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver resolver + ) { + return resolver.runSubRoutine(CLUSTER_SUBROUTINE, input); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/LkhVrpSolver.java b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/LkhVrpSolver.java new file mode 100644 index 00000000..97f11776 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/LkhVrpSolver.java @@ -0,0 +1,59 @@ +package edu.kit.provideq.toolbox.vrp.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import edu.kit.provideq.toolbox.vrp.VrpConfiguration; +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 VrpConfiguration#VRP} classical solver using the LKH-3 heuristic. + */ +@Component +public class LkhVrpSolver extends VrpSolver { + private final String scriptDir; + private final ApplicationContext context; + private final String solverBinary; + + @Autowired + public LkhVrpSolver( + @Value("${custom.lkh.directory}") String scriptDir, + @Value("${custom.lkh.solver}") String solverBinary, + ApplicationContext context) { + this.scriptDir = scriptDir; + this.solverBinary = solverBinary; + this.context = context; + } + + @Override + public String getName() { + return "LKH-3 VRP Solver"; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver resolver + ) { + + var solution = new Solution(); + + var processResult = context.getBean( + PythonProcessRunner.class, + scriptDir, + "vrp_lkh.py", + new String[] {"--lkh-instance", solverBinary} + ) + .addProblemFilePathToProcessCommand() + .addSolutionFilePathToProcessCommand("--output-file", "%s") + .problemFileName("problem.vrp") + .solutionFileName("problem.sol") + .run(getProblemType(), solution.getId(), input); + + return Mono.just(processResult.applyTo(solution)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/QrispVrpSolver.java b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/QrispVrpSolver.java new file mode 100644 index 00000000..8369318e --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/QrispVrpSolver.java @@ -0,0 +1,62 @@ +package edu.kit.provideq.toolbox.vrp.solvers; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.meta.SubRoutineResolver; +import edu.kit.provideq.toolbox.process.PythonProcessRunner; +import edu.kit.provideq.toolbox.vrp.VrpConfiguration; +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 VrpConfiguration#VRP} solver using Qrisp QAOA implementation. + */ +@Component +public class QrispVrpSolver extends VrpSolver { + private final String scriptDir; + private final ApplicationContext context; + + @Autowired + public QrispVrpSolver( + @Value("${qrisp.directory.vrp}") String scriptDir, + ApplicationContext context) { + this.scriptDir = scriptDir; + this.context = context; + } + + @Override + public String getName() { + return "Grover-based VRP Solver (Qrisp)"; + } + + @Override + public Mono> solve( + String input, + SubRoutineResolver resolver + ) { + + // This value will be passed to the python script, + // it is used to prevent denial of service issues for large simulations. + // Default value is 35, higher values are possible but might take much longer to simulate. + // TODO: allow user to pass a custom gate size as a solver setting + int maxNumberOfUsedQubits = 35; + + var solution = new Solution(); + + var processResult = context.getBean( + PythonProcessRunner.class, + scriptDir, + "grover.py", + new String[] {"--size-gate", String.valueOf(maxNumberOfUsedQubits)} + ) + .addProblemFilePathToProcessCommand() + .addSolutionFilePathToProcessCommand("--output-file", "%s") + .problemFileName("problem.vrp") + .solutionFileName("problem.sol") + .run(getProblemType(), solution.getId(), input); + + return Mono.just(processResult.applyTo(solution)); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/VrpSolver.java b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/VrpSolver.java new file mode 100644 index 00000000..ba11ad4c --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/solvers/VrpSolver.java @@ -0,0 +1,15 @@ +package edu.kit.provideq.toolbox.vrp.solvers; + +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.vrp.VrpConfiguration; + +/** + * A solver for VRP problems. + */ +public abstract class VrpSolver implements ProblemSolver { + @Override + public ProblemType getProblemType() { + return VrpConfiguration.VRP; + } +} diff --git a/src/main/resources/application-linux.properties b/src/main/resources/application-linux.properties new file mode 100644 index 00000000..9a43b73c --- /dev/null +++ b/src/main/resources/application-linux.properties @@ -0,0 +1,5 @@ +# linux-specific Binary for the Berger-Vrp pipeline: +custom.berger-vrp.solver=bin/pipeline-linux-gnu + +# linux-specific Binary for the LKH solver: +custom.lkh.solver=./bin/LKH-unix diff --git a/src/main/resources/application-mac.properties b/src/main/resources/application-mac.properties new file mode 100644 index 00000000..86c4cb73 --- /dev/null +++ b/src/main/resources/application-mac.properties @@ -0,0 +1,5 @@ +# Mac-specific Binary for the Berger-Vrp pipeline: +custom.berger-vrp.solver=bin/pipeline-mac + +# Mac-specific Binary for the LKH solver: +custom.lkh.solver=./bin/LKH-unix diff --git a/src/main/resources/application-windows.properties b/src/main/resources/application-windows.properties new file mode 100644 index 00000000..cdaa915c --- /dev/null +++ b/src/main/resources/application-windows.properties @@ -0,0 +1,5 @@ +# windows-specific Binary for the Berger-Vrp pipeline: +custom.berger-vrp.solver=bin/pipeline-windows.exe + +# windows-specific Binary for the LKH solver: +custom.lkh.solver=./bin/LKH-windows diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d1c6ccd2..fc2a440a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,18 +1,33 @@ +# default spring profile, correct one will be set during runtime (see ToolboxServerApplication.java) +# options: mac, windows, linux +spring.profiles.active=mac + working.directory=jobs +examples.directory=examples +springdoc.swagger-ui.path=/ + +solvers.directory=solvers -gams.directory=gams +# Non OS-specific solvers: (typically GAMS and Python) +gams.directory=${solvers.directory}/gams gams.directory.max-cut=${gams.directory}/max-cut gams.directory.sat=${gams.directory}/sat -qiskit.directory=qiskit +qiskit.directory=${solvers.directory}/qiskit qiskit.directory.max-cut=${qiskit.directory}/max-cut qiskit.directory.qubo=${qiskit.directory}/qubo -cirq.directory=cirq +cirq.directory=${solvers.directory}/cirq cirq.directory.max-cut=${cirq.directory}/max-cut -python.directory=python -python.directory.knapsack=${python.directory}/knapsack +qrisp.directory=${solvers.directory}/qrisp +qrisp.directory.vrp=${qrisp.directory}/vrp -examples.directory=examples -springdoc.swagger-ui.path=/ +dwave.directory=${solvers.directory}/dwave +dwave.directory.qubo=${dwave.directory}/qubo + +# Non OS-specific custom solvers: (solvers that are not part of a framework) +solvers.custom.directory=${solvers.directory}/custom +custom.hs_knapsack.directory=${solvers.custom.directory}/hs-knapsack +custom.lkh.directory=${solvers.custom.directory}/lkh +custom.berger-vrp.directory=${solvers.custom.directory}/berger-vrp diff --git a/src/main/resources/edu/kit/provideq/toolbox/tsp/SmallSampleTSP.tsp b/src/main/resources/edu/kit/provideq/toolbox/tsp/SmallSampleTSP.tsp new file mode 100644 index 00000000..30f871e4 --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/tsp/SmallSampleTSP.tsp @@ -0,0 +1,11 @@ +NAME : small sample +TYPE : TSP +DIMENSION : 5 +EDGE_WEIGHT_TYPE : EUC_2D +NODE_COORD_SECTION +1 0.0 0.0 +2 2.0 1.0 +3 1.0 -2.0 +4 -4.0 1.0 +5 -2.0 -3.0 +EOF diff --git a/src/main/resources/edu/kit/provideq/toolbox/tsp/VerySmallSampleTSP.tsp b/src/main/resources/edu/kit/provideq/toolbox/tsp/VerySmallSampleTSP.tsp new file mode 100644 index 00000000..d8a01d18 --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/tsp/VerySmallSampleTSP.tsp @@ -0,0 +1,9 @@ +NAME : VerySmallSample +TYPE : TSP +DIMENSION : 3 +EDGE_WEIGHT_TYPE : EUC_2D +NODE_COORD_SECTION +1 0.00000 0.00000 +2 1.00000 0.50000 +3 0.50000 -1.00000 +EOF diff --git a/src/main/resources/edu/kit/provideq/toolbox/tsp/att48.tsp b/src/main/resources/edu/kit/provideq/toolbox/tsp/att48.tsp new file mode 100644 index 00000000..a1cf11e2 --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/tsp/att48.tsp @@ -0,0 +1,55 @@ +NAME : att48 +COMMENT : 48 capitals of the US (Padberg/Rinaldi) +TYPE : TSP +DIMENSION : 48 +EDGE_WEIGHT_TYPE : ATT +NODE_COORD_SECTION +1 6734 1453 +2 2233 10 +3 5530 1424 +4 401 841 +5 3082 1644 +6 7608 4458 +7 7573 3716 +8 7265 1268 +9 6898 1885 +10 1112 2049 +11 5468 2606 +12 5989 2873 +13 4706 2674 +14 4612 2035 +15 6347 2683 +16 6107 669 +17 7611 5184 +18 7462 3590 +19 7732 4723 +20 5900 3561 +21 4483 3369 +22 6101 1110 +23 5199 2182 +24 1633 2809 +25 4307 2322 +26 675 1006 +27 7555 4819 +28 7541 3981 +29 3177 756 +30 7352 4506 +31 7545 2801 +32 3245 3305 +33 6426 3173 +34 4608 1198 +35 23 2216 +36 7248 3779 +37 7762 4595 +38 7392 2244 +39 3484 2829 +40 6271 2135 +41 4985 140 +42 1916 1569 +43 7280 4899 +44 7509 3239 +45 10 2676 +46 6807 2993 +47 5185 3258 +48 3023 1942 +EOF diff --git a/src/main/resources/edu/kit/provideq/toolbox/vrp/CMT1.vrp b/src/main/resources/edu/kit/provideq/toolbox/vrp/CMT1.vrp new file mode 100644 index 00000000..94537a8f --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/vrp/CMT1.vrp @@ -0,0 +1,113 @@ +NAME : CMT1 +COMMENT : 524.61 +TYPE : CVRP +DIMENSION : 51 +EDGE_WEIGHT_TYPE : EUC_2D +CAPACITY : 160 +NODE_COORD_SECTION +1 30.00000 40.00000 +2 37.00000 52.00000 +3 49.00000 49.00000 +4 52.00000 64.00000 +5 20.00000 26.00000 +6 40.00000 30.00000 +7 21.00000 47.00000 +8 17.00000 63.00000 +9 31.00000 62.00000 +10 52.00000 33.00000 +11 51.00000 21.00000 +12 42.00000 41.00000 +13 31.00000 32.00000 +14 5.00000 25.00000 +15 12.00000 42.00000 +16 36.00000 16.00000 +17 52.00000 41.00000 +18 27.00000 23.00000 +19 17.00000 33.00000 +20 13.00000 13.00000 +21 57.00000 58.00000 +22 62.00000 42.00000 +23 42.00000 57.00000 +24 16.00000 57.00000 +25 8.00000 52.00000 +26 7.00000 38.00000 +27 27.00000 68.00000 +28 30.00000 48.00000 +29 43.00000 67.00000 +30 58.00000 48.00000 +31 58.00000 27.00000 +32 37.00000 69.00000 +33 38.00000 46.00000 +34 46.00000 10.00000 +35 61.00000 33.00000 +36 62.00000 63.00000 +37 63.00000 69.00000 +38 32.00000 22.00000 +39 45.00000 35.00000 +40 59.00000 15.00000 +41 5.00000 6.00000 +42 10.00000 17.00000 +43 21.00000 10.00000 +44 5.00000 64.00000 +45 30.00000 15.00000 +46 39.00000 10.00000 +47 32.00000 39.00000 +48 25.00000 32.00000 +49 25.00000 55.00000 +50 48.00000 28.00000 +51 56.00000 37.00000 +DEMAND_SECTION +1 0 +2 7 +3 30 +4 16 +5 9 +6 21 +7 15 +8 19 +9 23 +10 11 +11 5 +12 19 +13 29 +14 23 +15 21 +16 10 +17 15 +18 3 +19 41 +20 9 +21 28 +22 8 +23 8 +24 16 +25 10 +26 28 +27 7 +28 15 +29 14 +30 6 +31 19 +32 11 +33 12 +34 23 +35 26 +36 17 +37 6 +38 9 +39 15 +40 14 +41 7 +42 27 +43 13 +44 11 +45 16 +46 10 +47 5 +48 25 +49 17 +50 18 +51 10 +DEPOT_SECTION +1 +-1 \ No newline at end of file diff --git a/src/main/resources/edu/kit/provideq/toolbox/vrp/SmallSample.vrp b/src/main/resources/edu/kit/provideq/toolbox/vrp/SmallSample.vrp new file mode 100644 index 00000000..e6909276 --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/vrp/SmallSample.vrp @@ -0,0 +1,20 @@ +NAME : small sample +TYPE : CVRP +CAPACITY : 3 +DIMENSION : 5 +EDGE_WEIGHT_TYPE : EUC_2D +NODE_COORD_SECTION +1 0.0 0.0 +2 2.0 1.0 +3 1.0 -2.0 +4 -4.0 1.0 +5 -2.0 -3.0 +DEMAND_SECTION +1 0 +2 1 +3 1 +4 1 +5 1 +DEPOT_SECTION + 1 + -1 diff --git a/src/main/resources/edu/kit/provideq/toolbox/vrp/VerySmallSampleForGrover.vrp b/src/main/resources/edu/kit/provideq/toolbox/vrp/VerySmallSampleForGrover.vrp new file mode 100644 index 00000000..bad18b8b --- /dev/null +++ b/src/main/resources/edu/kit/provideq/toolbox/vrp/VerySmallSampleForGrover.vrp @@ -0,0 +1,16 @@ +NAME : test +TYPE : CVRP +DIMENSION : 3 +EDGE_WEIGHT_TYPE : EUC_2D +CAPACITY : 2 +NODE_COORD_SECTION +1 0.00000 0.00000 +2 1.00000 0.50000 +3 0.50000 -1.00000 +DEMAND_SECTION +1 0 +2 1 +3 2 +DEPOT_SECTION +1 +-1 \ No newline at end of file diff --git a/src/test/java/edu/kit/provideq/toolbox/ToolboxServerApplicationTests.java b/src/test/java/edu/kit/provideq/toolbox/ToolboxServerApplicationTests.java deleted file mode 100644 index e786209c..00000000 --- a/src/test/java/edu/kit/provideq/toolbox/ToolboxServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package edu.kit.provideq.toolbox; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ToolboxServerApplicationTests { - - @Test - void contextLoads() { - } - -} 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 895786f2..8855b048 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/ApiTestHelper.java @@ -179,7 +179,6 @@ public static ProblemDto trySolveFor( } } - System.out.println(problemDto.getSolution()); assertEquals(ProblemState.SOLVED, problemDto.getState()); Assertions.assertEquals(SolutionStatus.SOLVED, problemDto.getSolution().getStatus()); diff --git a/src/test/java/edu/kit/provideq/toolbox/api/QuboSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/QuboSolverTest.java new file mode 100644 index 00000000..bf91b318 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/QuboSolverTest.java @@ -0,0 +1,59 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.qubo.QuboConfiguration.QUBO; +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.meta.ProblemManagerProvider; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemState; +import java.time.Duration; +import java.util.stream.Stream; +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 QuboSolverTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) + .build(); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + Stream provideArguments() { + var problemManager = problemManagerProvider.findProblemManagerForType(QUBO).get(); + + return ApiTestHelper.getAllArgumentCombinations(problemManager) + .map(list -> Arguments.of(list.get(0), list.get(1))); + } + + @ParameterizedTest + @MethodSource("provideArguments") + void testQuboSolvers(ProblemSolver solver, String input) { + var problemDto = ApiTestHelper.createProblem(client, solver, input, QUBO); + + System.out.println("Solver: " + solver); + System.out.println("Solution: " + problemDto.getSolution()); + System.out.println("Debug Data: " + problemDto.getSolution().getDebugData()); + + assertEquals(ProblemState.SOLVED, problemDto.getState()); + assertNotNull(problemDto.getSolution()); + assertEquals(SolutionStatus.SOLVED, problemDto.getSolution().getStatus()); + } +} 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 81d10b54..a56fa06f 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/SatSolversTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/SatSolversTest.java @@ -48,7 +48,7 @@ Stream provideArguments() { @ParameterizedTest @MethodSource("provideArguments") - void testMaxCutSolver(ProblemSolver solver, String input) { + void testSatSolver(ProblemSolver solver, String input) { var problem = ApiTestHelper.createProblem(client, solver, input, SAT); assertEquals(ProblemState.SOLVED, problem.getState()); assertNotNull(problem.getSolution()); diff --git a/src/test/java/edu/kit/provideq/toolbox/api/TspSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/TspSolverTest.java new file mode 100644 index 00000000..4a00c2ef --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/TspSolverTest.java @@ -0,0 +1,119 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.qubo.QuboConfiguration.QUBO; +import static edu.kit.provideq.toolbox.tsp.TspConfiguration.TSP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.provideq.toolbox.SolutionStatus; +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.ProblemState; +import edu.kit.provideq.toolbox.qubo.solvers.DwaveQuboSolver; +import edu.kit.provideq.toolbox.qubo.solvers.QrispQuboSolver; +import edu.kit.provideq.toolbox.qubo.solvers.QuboSolver; +import edu.kit.provideq.toolbox.tsp.solvers.LkhTspSolver; +import edu.kit.provideq.toolbox.tsp.solvers.QuboTspSolver; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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 TspSolverTest { + + @Autowired + private WebTestClient client; + @Autowired + private ProblemManagerProvider problemManagerProvider; + @Autowired + private LkhTspSolver lkhTspSolver; + @Autowired + private QuboTspSolver quboTspSolver; + @Autowired + private DwaveQuboSolver dwaveQuboSolver; + @Autowired + private QrispQuboSolver qrispQuboSolver; + + private ProblemManager problemManager; + private List problems; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) + .build(); + problemManager = problemManagerProvider.findProblemManagerForType(TSP).get(); + problems = problemManager.getExampleInstances() + .stream() + .map(Problem::getInput) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + /** + * Solves tsp problem with LKH-3. + */ + @Test + void testLkhTspSolver() { + for (String problem : problems) { + var problemDto = ApiTestHelper.createProblem(client, lkhTspSolver, problem, TSP); + assertEquals(ProblemState.SOLVED, problemDto.getState()); + assertNotNull(problemDto.getSolution()); + assertEquals(SolutionStatus.SOLVED, problemDto.getSolution().getStatus()); + } + } + + private Stream quboSolvers() { + return Stream.of( + Arguments.of(dwaveQuboSolver, "NAME : small sample"), + Arguments.of(qrispQuboSolver, "NAME : VerySmallSample") + ); + } + + /** + * Transforms Tsp Problem to QUBO and then solves it with a Quantum Annealer solver. + */ + @ParameterizedTest + @MethodSource("quboSolvers") + void testQuboTspSolver(QuboSolver solver, String problemName) { + //get the small problem, cause quantum simulation is used: + var problem = problems.stream().filter(element -> element.contains(problemName)).findFirst(); + assertTrue(problem.isPresent()); + + var problemDto = ApiTestHelper.createProblem(client, quboTspSolver, problem.get(), TSP); + assertEquals(ProblemState.SOLVING, problemDto.getState()); + + //Set a QUBO solver: + var quboSubProblem = problemDto.getSubProblems().get(0).getSubProblemIds(); + ApiTestHelper.setProblemSolver( + client, + solver, + quboSubProblem.get(0), + QUBO.getId() + ); + + //solve problem: + var solvedProblemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), TSP); + + //validate result: + assertEquals(ProblemState.SOLVED, solvedProblemDto.getState()); + assertNotNull(solvedProblemDto.getSolution()); + assertEquals(SolutionStatus.SOLVED, solvedProblemDto.getSolution().getStatus()); + } +} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/VrpSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/VrpSolverTest.java new file mode 100644 index 00000000..fd2cb042 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/api/VrpSolverTest.java @@ -0,0 +1,243 @@ +package edu.kit.provideq.toolbox.api; + +import static edu.kit.provideq.toolbox.qubo.QuboConfiguration.QUBO; +import static edu.kit.provideq.toolbox.tsp.TspConfiguration.TSP; +import static edu.kit.provideq.toolbox.vrp.VrpConfiguration.VRP; +import static edu.kit.provideq.toolbox.vrp.clusterer.VrpClustererConfiguration.CLUSTER_VRP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.provideq.toolbox.SolutionStatus; +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.ProblemState; +import edu.kit.provideq.toolbox.qubo.solvers.DwaveQuboSolver; +import edu.kit.provideq.toolbox.tsp.solvers.LkhTspSolver; +import edu.kit.provideq.toolbox.tsp.solvers.QuboTspSolver; +import edu.kit.provideq.toolbox.vrp.clusterer.KmeansClusterer; +import edu.kit.provideq.toolbox.vrp.clusterer.TwoPhaseClusterer; +import edu.kit.provideq.toolbox.vrp.solvers.ClusterAndSolveVrpSolver; +import edu.kit.provideq.toolbox.vrp.solvers.LkhVrpSolver; +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.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 VrpSolverTest { + @Autowired + private WebTestClient client; + + @Autowired + private ProblemManagerProvider problemManagerProvider; + + @Autowired + private LkhVrpSolver lkh3vrpSolver; + + @Autowired + private LkhTspSolver lkh3tspSolver; + + @Autowired + private ClusterAndSolveVrpSolver abstractClusterer; + + @Autowired + private KmeansClusterer kmeansClusterer; + + @Autowired + private TwoPhaseClusterer twoPhaseClusterer; + + @Autowired + private QuboTspSolver quboTspSolver; + + @Autowired + private DwaveQuboSolver dwaveQuboSolver; + + @Autowired + private LkhVrpSolver lkhVrpSolver; + + private ProblemManager problemManager; + private List problems; + + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(60)) + .build(); + problemManager = problemManagerProvider.findProblemManagerForType(VRP).get(); + problems = problemManager.getExampleInstances() + .stream() + .map(Problem::getInput) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + @Test + void testLkh3SolverIsolated() { + for (String problem : problems) { + var problemDto = ApiTestHelper.createProblem(client, lkh3vrpSolver, problem, VRP); + assertEquals(ProblemState.SOLVED, problemDto.getState()); + assertNotNull(problemDto.getSolution()); + assertEquals(SolutionStatus.SOLVED, problemDto.getSolution().getStatus()); + } + } + + /** + * test LKH-3 solver in combination with k-means = 3 + */ + @Test + void testKmeansWithLkhForVrp() { + for (String problem : problems) { + //skip the small "test" problem because clustering small problems + //can lead to errors + if (problem.contains("DIMENSION : 3")) { + continue; + } + + //create VRP problem, has Clusterable VRP as subproblem + var problemDto = ApiTestHelper.createProblem(client, abstractClusterer, problem, VRP); + assertEquals(ProblemState.SOLVING, problemDto.getState()); + + //check if subproblem is set correctly + List vrpSubProblems = problemDto.getSubProblems(); + //there should be exactly one subproblem, the vrp that needs to be clustered: + assertEquals(1, vrpSubProblems.size()); + SubProblemReferenceDto vrpProblem = vrpSubProblems.get(0); + List clusterSubProblems = vrpProblem.getSubProblemIds(); + assertEquals(1, clusterSubProblems.size()); //there should also only one subproblem Id + assertEquals(vrpProblem.getSubRoutine().getTypeId(), CLUSTER_VRP.getId()); + + //set k-means as CLUSTER_VRP solver: + var clustererDto = + ApiTestHelper.setProblemSolver(client, kmeansClusterer, clusterSubProblems.get(0), + CLUSTER_VRP.getId()); + + //solve sub-problems (clusters): + var vrpClusters = clustererDto.getSubProblems(); + for (var cluster : vrpClusters) { + assertEquals(cluster.getSubRoutine().getTypeId(), + VRP.getId()); //check if subproblem is VRP again + for (String problemId : cluster.getSubProblemIds()) { + //set lkh-3 as solver: + var vrpClusterProblem = + ApiTestHelper.setProblemSolver(client, lkhVrpSolver, problemId, VRP.getId()); + assertNotNull(vrpClusterProblem.getInput()); + assertNotNull(vrpClusterProblem.getSolution()); + assertEquals(ProblemState.SOLVED, vrpClusterProblem.getState()); + } + } + + //solve the problem: + problemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), VRP); + assertNotNull(problemDto.getSolution()); + assertEquals(SolutionStatus.SOLVED, problemDto.getSolution().getStatus()); + assertEquals(ProblemState.SOLVED, problemDto.getState()); + } + } + + + /** + * Tests the two phase clusterer in combination with Lkh-3 tsp solver. + */ + @Test + void testTwoPhaseWithLkhForTsp() { + for (String problem : problems) { + var problemDto = ApiTestHelper.createProblem(client, abstractClusterer, problem, VRP); + assertEquals(ProblemState.SOLVING, problemDto.getState()); + + //set two-phase as CLUSTER_VRP solver: + var clusterSubProblems = problemDto.getSubProblems().get(0).getSubProblemIds(); + var clustererDto = ApiTestHelper.setProblemSolver( + client, + twoPhaseClusterer, + clusterSubProblems.get(0), + CLUSTER_VRP.getId()); + + //solve sub-problems (clusters): + var tspClusters = clustererDto.getSubProblems(); + for (var cluster : tspClusters) { + //check if subproblem is TSP now + assertEquals(cluster.getSubRoutine().getTypeId(), TSP.getId()); + for (String problemId : cluster.getSubProblemIds()) { + //set lkh-3 as solver: + var tspClusterProblem = ApiTestHelper.setProblemSolver( + client, + lkh3tspSolver, + problemId, + TSP.getId()); + + assertNotNull(tspClusterProblem.getInput()); + assertNotNull(tspClusterProblem.getSolution()); + assertEquals(ProblemState.SOLVED, tspClusterProblem.getState()); + } + } + + //solve the problem: + var solvedProblemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), VRP); + assertNotNull(solvedProblemDto.getSolution()); + assertEquals(SolutionStatus.SOLVED, solvedProblemDto.getSolution().getStatus()); + assertEquals(ProblemState.SOLVED, solvedProblemDto.getState()); + } + } + + /** + * Tests the two phase clusterer in combination with a qubo transformation and quantum annealer + */ + @Test + void testTwoPhaseWithAnnealer() { + //get the small problem, cause quantum simulation is used: + var problem = + problems.stream().filter(element -> element.contains("NAME : small sample")).findFirst(); + assertTrue(problem.isPresent()); + + var problemDto = ApiTestHelper.createProblem(client, abstractClusterer, problem.get(), VRP); + assertEquals(ProblemState.SOLVING, problemDto.getState()); + + //set two-phase as CLUSTER_VRP solver: + var clusterSubProblems = problemDto.getSubProblems().get(0).getSubProblemIds(); + var clustererDto = ApiTestHelper.setProblemSolver( + client, + twoPhaseClusterer, + clusterSubProblems.get(0), + CLUSTER_VRP.getId()); + + //solve sub-problems (clusters): + var tspClusters = clustererDto.getSubProblems(); + for (var cluster : tspClusters) { + //check if subproblem is TSP now + assertEquals(cluster.getSubRoutine().getTypeId(), TSP.getId()); + for (String problemId : cluster.getSubProblemIds()) { + //set QUBO tsp as solver: + var tspClusterProblem = ApiTestHelper.setProblemSolver( + client, + quboTspSolver, + problemId, + TSP.getId()); + + //set d-wave annealer as qubo solver: + assertEquals(1, tspClusterProblem.getSubProblems().size()); + var quboProblem = tspClusterProblem.getSubProblems().get(0); + assertEquals(1, quboProblem.getSubProblemIds().size()); + assertEquals(quboProblem.getSubRoutine().getTypeId(), QUBO.getId()); + ApiTestHelper.setProblemSolver(client, dwaveQuboSolver, + quboProblem.getSubProblemIds().get(0), QUBO.getId()); + } + } + + //solve the problem: + var solvedProblemDto = ApiTestHelper.trySolveFor(60, client, problemDto.getId(), VRP); + assertNotNull(solvedProblemDto.getSolution()); + assertEquals(SolutionStatus.SOLVED, solvedProblemDto.getSolution().getStatus()); + assertEquals(ProblemState.SOLVED, solvedProblemDto.getState()); + } +}