30 | The Munich Quantum Toolkit has been supported by the European Research Council
31 | (ERC) under the European Union's Horizon 2020 research and innovation program
32 | (grant agreement No. 101001318), the Bavarian State Ministry for Science and
33 | Arts through the Distinguished Professorship Program, as well as the Munich
34 | Quantum Valley, which is supported by the Bavarian state government with funds
35 | from the Hightech Agenda Bayern Plus.
36 |
37 |
38 |
42 |
43 |
44 |
48 |
49 |
50 |
55 |
56 |
57 |
61 |
62 |
63 |
67 |
68 |
69 |
73 |
74 |
75 |
76 | {% endblock footer %}
77 |
--------------------------------------------------------------------------------
/src/mqt/qubomaker/utils.py:
--------------------------------------------------------------------------------
1 | """Provides utility functions that can be used with QuboMaker."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import no_type_check
6 |
7 | import numpy as np
8 | from IPython.display import Math, clear_output, display
9 | from ipywidgets import widgets
10 |
11 |
12 | @no_type_check
13 | def print_matrix(array: Iterable[Iterable[float]]) -> None:
14 | """Print a matrix data structure in LaTeX format.
15 |
16 | Args:
17 | array (Iterable[Iterable[float]]): The matrix to be printed.
18 | """
19 | matrix = ""
20 | for row in array:
21 | try:
22 | for number in row:
23 | matrix += f"{number}&"
24 | except TypeError:
25 | matrix += f"{row}&"
26 | matrix = matrix[:-1] + r"\\"
27 | display(Math(r"Q = \begin{bmatrix}" + matrix + r"\end{bmatrix}"))
28 |
29 |
30 | @no_type_check
31 | def optimize_classically(
32 | qubo: npt.NDArray[np.int_ | np.float64], show_progress_bar: bool = False
33 | ) -> tuple[list[int], float]:
34 | """Classically optimizes a given QUBO problem of the form x^TQx.
35 |
36 | Args:
37 | qubo (npt.NDArray[np.int_ | np.float64]): A matrix representing the QUBO problem.
38 | show_progress_bar (bool, optional): If True, shows a progress bar in jupyter notebooks during calculation. Defaults to False.
39 |
40 | Returns:
41 | tuple[list[int], float]: The optimal solution and its corresponding score.
42 | """
43 | progress_bar: widgets.FloatProgress | None = None
44 | if show_progress_bar:
45 | progress_bar = widgets.FloatProgress(
46 | value=0,
47 | min=0,
48 | max=1,
49 | description="Calculating:",
50 | bar_style="info",
51 | style={"bar_color": "#0055bb"},
52 | orientation="horizontal",
53 | )
54 |
55 | def int_to_fixed_length_binary(number: int, length: int) -> list[int]:
56 | binary_string = f"{number:b}"
57 | padding_zeros = max(0, length - len(binary_string))
58 | binary_string = "0" * padding_zeros + binary_string
59 | return [int(bit) for bit in binary_string]
60 |
61 | all_tests = [int_to_fixed_length_binary(i, qubo.shape[0]) for i in range(2 ** qubo.shape[0])]
62 |
63 | best_test: list[int] = []
64 | best_score = 999999999999
65 |
66 | for i, test in enumerate(all_tests):
67 | x = np.array(test)
68 | score = np.matmul(x.T, np.matmul(qubo, x))
69 | if best_score > score:
70 | best_score = score
71 | best_test = test
72 | if i % 2000 == 0 and show_progress_bar and progress_bar is not None:
73 | progress_bar.value = i / len(all_tests)
74 | clear_output(True)
75 | display(progress_bar)
76 |
77 | if show_progress_bar and progress_bar is not None:
78 | progress_bar.value = 1
79 | clear_output(True)
80 | display(progress_bar)
81 | return (best_test, best_score)
82 |
--------------------------------------------------------------------------------
/tests/test_graph.py:
--------------------------------------------------------------------------------
1 | """Tests the features of the `graph` module."""
2 |
3 | from __future__ import annotations
4 |
5 | from pathlib import Path
6 | from tempfile import NamedTemporaryFile
7 |
8 | import numpy as np
9 |
10 | from mqt.qubomaker import Graph
11 |
12 |
13 | def test_init_with_edge_list() -> None:
14 | """Tests the initialization of a `Graph` object with an edge list or an adjacency matrix."""
15 | g = Graph(5, [(1, 2, 4), (3, 5, 2), (1, 3, 2), (4, 5, 5), (2, 4, 3), (5, 1)])
16 |
17 | adjacency_matrix = [[0, 4, 2, 0, 0], [0, 0, 0, 3, 0], [0, 0, 0, 0, 2], [0, 0, 0, 0, 5], [1, 0, 0, 0, 0]]
18 |
19 | g2 = Graph.from_adjacency_matrix(adjacency_matrix)
20 |
21 | assert np.array_equal(g.adjacency_matrix, g2.adjacency_matrix)
22 | assert g.all_edges == g2.all_edges
23 | assert g.all_vertices == g2.all_vertices
24 | assert g.non_edges == g2.non_edges
25 | assert np.array_equal(g.adjacency_matrix, adjacency_matrix)
26 | assert g == g2
27 |
28 |
29 | def test_read_write() -> None:
30 | """Tests the read and write operations of the `Graph` class."""
31 | g = Graph(5, [(1, 2, 4), (3, 5, 2), (1, 3, 2), (4, 5, 5), (2, 4, 3), (5, 1)])
32 |
33 | with NamedTemporaryFile("w+", delete=False, encoding="utf-8") as temp_file:
34 | temp_file_path = temp_file.name
35 |
36 | with Path(str(temp_file_path)).open("w", encoding="utf-8") as file:
37 | g.store(file)
38 |
39 | with Path(str(temp_file_path)).open("r", encoding="utf-8") as file:
40 | g2 = Graph.read(file)
41 |
42 | assert g == g2
43 |
44 | with Path(str(temp_file_path)).open("w", encoding="utf-8") as file:
45 | file.write(g.serialize())
46 |
47 | with Path(str(temp_file_path)).open("r", encoding="utf-8") as file:
48 | g2 = Graph.deserialize(file.read())
49 |
50 | assert g == g2
51 |
52 |
53 | def test_eq() -> None:
54 | """Tests the equality operator on `Graph` objects."""
55 | g1 = Graph(3, [(1, 2, 4), (1, 3, 1), (2, 3, 1), (2, 1, 5), (3, 1, 4), (3, 2, 5)])
56 |
57 | g2 = Graph(
58 | 3,
59 | [
60 | (3, 2, 5),
61 | (1, 2, 4),
62 | (2, 1, 5),
63 | (2, 3, 1),
64 | (3, 1, 4),
65 | (1, 3, 1),
66 | ],
67 | )
68 |
69 | g3 = Graph(3, [(1, 2, 4), (1, 3, 1), (2, 3, 1), (3, 1, 4), (3, 2, 5)])
70 |
71 | g4 = Graph(3, [(1, 2, 4), (1, 3, 1), (2, 3, 1), (2, 1, 3), (3, 1, 4), (3, 2, 5)])
72 |
73 | g5 = Graph(3, [(1, 2, 4), (1, 3), (2, 3), (2, 1, 5), (3, 1, 4), (3, 2, 5)])
74 |
75 | assert g1 == g2
76 | assert g1 == g5
77 | assert g2 == g5
78 | assert g1 != g3
79 | assert g1 != g4
80 | assert g2 != g3
81 | assert g2 != g4
82 | assert g5 != g3
83 | assert g5 != g4
84 | assert g3 != g4
85 |
86 | assert g2 == g1
87 | assert g5 == g1
88 | assert g5 == g2
89 | assert g3 != g1
90 | assert g4 != g1
91 | assert g3 != g2
92 | assert g4 != g2
93 | assert g3 != g5
94 | assert g4 != g5
95 | assert g4 != g3
96 |
--------------------------------------------------------------------------------
/docs/Quickstart.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Quickstart Guide\n",
8 | "\n",
9 | "This document describes how to get started with MQT QUBOMaker, based on the `pathfinder` submodule."
10 | ]
11 | },
12 | {
13 | "cell_type": "markdown",
14 | "metadata": {},
15 | "source": [
16 | "Import the necessary modules:"
17 | ]
18 | },
19 | {
20 | "cell_type": "code",
21 | "execution_count": null,
22 | "metadata": {},
23 | "outputs": [],
24 | "source": [
25 | "import mqt.qubomaker as qm\n",
26 | "import mqt.qubomaker.pathfinder as pf"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {},
32 | "source": [
33 | "Define an example graph for the problem:"
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": null,
39 | "metadata": {},
40 | "outputs": [],
41 | "source": [
42 | "graph = qm.Graph.from_adjacency_matrix([\n",
43 | " [0, 1, 3, 4],\n",
44 | " [2, 0, 4, 2],\n",
45 | " [1, 5, 0, 3],\n",
46 | " [3, 8, 1, 0],\n",
47 | "])"
48 | ]
49 | },
50 | {
51 | "cell_type": "markdown",
52 | "metadata": {},
53 | "source": [
54 | "Select settings for the problem instance and the solution process:"
55 | ]
56 | },
57 | {
58 | "cell_type": "code",
59 | "execution_count": null,
60 | "metadata": {},
61 | "outputs": [],
62 | "source": [
63 | "settings = pf.PathFindingQuboGeneratorSettings(\n",
64 | " encoding_type=pf.EncodingType.ONE_HOT, n_paths=1, max_path_length=4, loops=True\n",
65 | ")"
66 | ]
67 | },
68 | {
69 | "cell_type": "markdown",
70 | "metadata": {},
71 | "source": [
72 | "Define the `QuboGenerator` to be used for this example:"
73 | ]
74 | },
75 | {
76 | "cell_type": "code",
77 | "execution_count": null,
78 | "metadata": {},
79 | "outputs": [],
80 | "source": [
81 | "generator = pf.PathFindingQuboGenerator(\n",
82 | " objective_function=pf.MinimizePathLength(path_ids=[1]),\n",
83 | " graph=graph,\n",
84 | " settings=settings,\n",
85 | ")"
86 | ]
87 | },
88 | {
89 | "cell_type": "markdown",
90 | "metadata": {},
91 | "source": [
92 | "Add constraints to the `QuboGenerator`"
93 | ]
94 | },
95 | {
96 | "cell_type": "code",
97 | "execution_count": null,
98 | "metadata": {},
99 | "outputs": [],
100 | "source": [
101 | "generator.add_constraint(pf.PathIsValid(path_ids=[1]))\n",
102 | "generator.add_constraint(pf.PathContainsVerticesExactlyOnce(vertex_ids=graph.all_vertices, path_ids=[1]))"
103 | ]
104 | },
105 | {
106 | "cell_type": "markdown",
107 | "metadata": {},
108 | "source": [
109 | "Generate and view the problem's QUBO formulation as a QUBO matrix:"
110 | ]
111 | },
112 | {
113 | "cell_type": "code",
114 | "execution_count": null,
115 | "metadata": {},
116 | "outputs": [],
117 | "source": [
118 | "matrix = generator.construct_qubo_matrix()\n",
119 | "qm.print_matrix(matrix)"
120 | ]
121 | }
122 | ],
123 | "metadata": {
124 | "language_info": {
125 | "name": "python"
126 | }
127 | },
128 | "nbformat": 4,
129 | "nbformat_minor": 2
130 | }
131 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # To run all pre-commit checks, use:
2 | #
3 | # pre-commit run -a
4 | #
5 | # To install pre-commit hooks that run every time you commit:
6 | #
7 | # pre-commit install
8 | #
9 |
10 | ci:
11 | autoupdate_commit_msg: "⬆️🪝 update pre-commit hooks"
12 | autofix_commit_msg: "🎨 pre-commit fixes"
13 | skip: [mypy]
14 |
15 | repos:
16 | # Standard hooks
17 | - repo: https://github.com/pre-commit/pre-commit-hooks
18 | rev: v5.0.0
19 | hooks:
20 | - id: check-added-large-files
21 | - id: check-case-conflict
22 | - id: check-docstring-first
23 | - id: check-merge-conflict
24 | - id: check-toml
25 | - id: check-yaml
26 | - id: debug-statements
27 | - id: end-of-file-fixer
28 | - id: mixed-line-ending
29 | - id: trailing-whitespace
30 |
31 | # Handling unwanted unicode characters
32 | - repo: https://github.com/sirosen/texthooks
33 | rev: 0.6.8
34 | hooks:
35 | - id: fix-ligatures
36 | - id: fix-smartquotes
37 |
38 | # Check for common mistakes
39 | - repo: https://github.com/pre-commit/pygrep-hooks
40 | rev: v1.10.0
41 | hooks:
42 | - id: rst-backticks
43 | - id: rst-directive-colons
44 | - id: rst-inline-touching-normal
45 |
46 | # Check for spelling
47 | - repo: https://github.com/crate-ci/typos
48 | rev: v1.32.0
49 | hooks:
50 | - id: typos
51 |
52 | # Format configuration files with prettier
53 | - repo: https://github.com/rbubley/mirrors-prettier
54 | rev: "v3.5.3"
55 | hooks:
56 | - id: prettier
57 | types_or: [yaml, markdown, html, css, javascript, json]
58 |
59 | - repo: https://github.com/astral-sh/ruff-pre-commit
60 | rev: v0.11.13
61 | hooks:
62 | - id: ruff
63 | args: ["--fix", "--show-fixes"]
64 | - id: ruff-format
65 |
66 | # Also run Black on examples in the documentation
67 | - repo: https://github.com/adamchainz/blacken-docs
68 | rev: 1.19.1
69 | hooks:
70 | - id: blacken-docs
71 | additional_dependencies: [black==24.*]
72 |
73 | # Clean jupyter notebooks
74 | - repo: https://github.com/srstevenson/nb-clean
75 | rev: "4.0.1"
76 | hooks:
77 | - id: nb-clean
78 |
79 | - repo: https://github.com/pre-commit/mirrors-mypy
80 | rev: v1.16.1
81 | hooks:
82 | - id: mypy
83 | files: ^(src|tests|noxfile.py)
84 | args: []
85 | additional_dependencies:
86 | - importlib_resources
87 | - types-setuptools
88 | - networkx
89 | - pytest
90 | - sympy
91 | - ipywidgets
92 | - IPython
93 | - tsplib95
94 | - types-Pillow
95 | - referencing
96 | - nox
97 |
98 | # Catch common capitalization mistakes
99 | - repo: local
100 | hooks:
101 | - id: disallow-caps
102 | name: Disallow improper capitalization
103 | language: pygrep
104 | entry: PyBind|Numpy|Cmake|CCache|Github|PyTest|Mqt|Tum
105 | exclude: .pre-commit-config.yaml
106 |
107 | # Check best practices for scientific Python code
108 | - repo: https://github.com/scientific-python/cookie
109 | rev: 2025.05.02
110 | hooks:
111 | - id: sp-repo-review
112 | additional_dependencies: ["repo-review[cli]"]
113 |
--------------------------------------------------------------------------------
/docs/DevelopmentGuide.rst:
--------------------------------------------------------------------------------
1 | Development Guide
2 | =================
3 |
4 | Ready to contribute to the project? Here is how to set up a local development environment.
5 |
6 | Initial Setup
7 | #############
8 |
9 | 1. Fork the `cda-tum/mqt-qubomaker `_ repository on GitHub (see https://docs.github.com/en/get-started/quickstart/fork-a-repo).
10 |
11 | 2. Clone your fork locally
12 |
13 | .. code-block:: console
14 |
15 | $ git clone git@github.com:your_name_here/mqt-qubomaker
16 |
17 |
18 | 3. Change into the project directory
19 |
20 | .. code-block:: console
21 |
22 | $ cd mqt-qubomaker
23 |
24 | 4. Create a branch for local development
25 |
26 | .. code-block:: console
27 |
28 | $ git checkout -b name-of-your-bugfix-or-feature
29 |
30 | Now you can make your changes locally.
31 |
32 | 5. (Optional, **highly recommended**) Set up a virtual environment
33 |
34 | .. code-block:: console
35 |
36 | $ python3 -m venv venv
37 | $ source venv/bin/activate
38 |
39 | .. note::
40 |
41 | If you are using Windows, you can use the following command instead:
42 |
43 | .. code-block:: console
44 |
45 | $ python3 -m venv venv
46 | $ venv\Scripts\activate.bat
47 |
48 | Ensure that pip, setuptools, and wheel are up to date:
49 |
50 | .. code-block:: console
51 |
52 | (venv) $ pip install --upgrade pip setuptools wheel
53 |
54 |
55 | 6. (Optional) Install `pre-commit `_ to automatically run a set of checks before each commit.
56 |
57 | .. code-block:: console
58 |
59 | (venv) $ pipx install pre-commit
60 | (venv) $ pre-commit install
61 |
62 | If you use macOS, then pre-commit is in brew, use :code:`brew install pre-commit`.
63 |
64 | Building the Python module
65 | ##########################
66 |
67 | The recommended way of building the Python module is to perform an editable install using `pip `_.
68 |
69 | .. code-block:: console
70 |
71 | (venv) $ pip install -e .
72 |
73 | The :code:`--editable` flag ensures that changes in the Python code are instantly available without re-running the command.
74 |
75 | Running Python Tests
76 | --------------------
77 |
78 | The Python part of the code base is tested by unit tests using the `pytest `_ framework.
79 | The corresponding test files can be found in the :code:`tests/` directory.
80 |
81 | .. code-block:: console
82 |
83 | (venv) $ pip install -e ".[test]"
84 | (venv) $ pytest
85 |
86 | This installs all dependencies necessary to run the tests in an isolated environment, builds the Python package, and then runs the tests.
87 |
88 | Python Code Formatting and Linting
89 | ----------------------------------
90 |
91 | The Python code is formatted and linted using a collection of `pre-commit hooks `_.
92 | This collection includes:
93 |
94 | - `ruff `_ -- an extremely fast Python linter and formatter, written in Rust.
95 | - `mypy `_ -- a static type checker for Python code
96 |
97 |
98 | You can install the hooks manually by running :code:`pre-commit install` in the project root directory.
99 | The hooks will then be executed automatically when committing changes.
100 |
101 | .. code-block:: console
102 |
103 | (venv) $ pre-commit run -a
104 |
--------------------------------------------------------------------------------
/docs/pathfinder/Encodings.rst:
--------------------------------------------------------------------------------
1 | Encoding Schemes
2 | ================
3 |
4 | The *Pathfinder* submodule provides three different encoding schemes. The encoding scheme determines, how
5 | many binary variables are required to represent the problem's cost function and how these variables are to be
6 | interpreted.
7 |
8 | An example of the same path represented in each of the three encoding schemes is shown below.
9 |
10 | .. image:: ../_static/encodings.png
11 | :align: center
12 | :alt: Encoding schemes
13 |
14 |
15 | For each encoding scheme, the function :math:`\delta(x, \pi^{(i)}, v, j)` is defined, returning 1 if vertex :math:`v` is located at position :math:`j` in path :math:`\pi^{(i)}`, and 0 otherwise.
16 | The complexity of this function depends on the encoding scheme used.
17 |
18 | Below, :math:`N` denotes the maximum length of a path, :math:`|V|` the number of vertices in the graph, and :math:`|\Pi|` the number of paths to be found.
19 |
20 |
21 | One-Hot
22 | -------
23 |
24 | In the one-hot encoding scheme, :math:`N \cdot |V| \cdot |\Pi|` binary variables :math:`x_{v,j,\pi^{(i)}}` are
25 | used to represent the problem.
26 |
27 | An individual variable with value 1 indicates that the corresponding vertex :math:`v` is located at position
28 | :math:`j` in path :math:`\pi^{(i)}`. Assignments such that :math:`x_{v,j,\pi^{(i)}} = 1` for more than one :math:`v` and the same :math:`j` and :math:`\pi^{(i)}``
29 | are invalid.
30 |
31 | This encoding scheme is very expressive, but also uses a large amount of binary variables. It is also
32 | very sparse, meaning that there exists a large number of invalid assignments. Moving from one valid assignment
33 | to another requires at least two bitflips.
34 |
35 | $$\\delta(x, \\pi^{(i)}, v, j) = x_{v, j, \\pi^{(i)}}$$
36 |
37 | Domain-Wall
38 | -----------
39 |
40 | In the domain-wall encoding scheme, :math:`N \cdot |V| \cdot |\Pi|` binary variables :math:`x_{v,j,\pi^{(i)}}` are used
41 | to represent the problem.
42 |
43 | For each position :math:`j` and path :math:`\pi^{(i)}`, the variables :math:`x_{v,j,\pi^{(i)}}` are read as a bitstring
44 | :math:`\overline{x_{j,\pi^{(i)}}}` of length :math:`|V|`. If the first :math:`n` bits of this bitstring are 1, this indicates that
45 | vertex :math:`v_n` is located at position :math:`j` in path :math:`\pi^{(i)}`.
46 |
47 | Compared to the one-hot encoding, it is easier to move from one valid assignment to another, as only one bitflip
48 | is required for that. However, there exist just as many invalid encodings.
49 |
50 | $$\\delta(x, \\pi^{(i)}, v, j) = x_{v, j, \\pi^{(i)}} - x_{v + 1, j, \\pi^{(i)}}$$
51 |
52 | Binary
53 | ------
54 |
55 | In binary encoding :math:`N \cdot \text{log}(|V|) \cdot |\Pi|` binary variables :math:`x_{v,j,\pi^{(i)}}` are used
56 | to represent the problem.
57 |
58 | For each position :math:`j` and path :math:`\pi^{(i)}`, the variables :math:`x_{v,j,\pi^{(i)}}` are read as a bitstring
59 | :math:`\overline{x_{j,\pi^{(i)}}}` of length :math:`\text{log}(|V|)`. This bitstring is interpreted as a binary number,
60 | representing the index of the vertex located at position :math:`j` in path :math:`\pi^{(i)}`.
61 |
62 | This is a dense encoding, as no invalid assignment exists. However, it is less expressive and more complex
63 | than the other encodings. In particular, cost functions using the binary encoding are rarely of quadratic order,
64 | and, therefore, often require additional auxiliary variables.
65 |
66 | $$\\delta(x, \\pi^{(i)}, v, j) = \\prod_{w=1}^{\\text{log}_2(\|V\| + 1)} (\\text{b}(v, w) x_{w, j, \\pi^{(i)}}) + ((1 - \\text{b}(v, w)) (1 - x_{w, j, \\pi^{(i)}}))$$
67 |
68 | *where* :math:`\text{b}(n, i)` *denotes the* :math:`i\text{-th}` *bit of the binary representation of* :math:`n` *.*
69 |
--------------------------------------------------------------------------------
/.github/workflows/nextjs.yml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages
2 | #
3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started
4 | #
5 | name: Deploy Next.js site to Pages
6 |
7 | on:
8 | # Runs on pushes targeting the default branch
9 | push:
10 | branches: ["ui"]
11 |
12 | # Allows you to run this workflow manually from the Actions tab
13 | workflow_dispatch:
14 |
15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
16 | permissions:
17 | contents: read
18 | pages: write
19 | id-token: write
20 |
21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
23 | concurrency:
24 | group: "pages"
25 | cancel-in-progress: false
26 |
27 | jobs:
28 | # Build job
29 | build:
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Detect package manager
35 | id: detect-package-manager
36 | run: |
37 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then
38 | echo "manager=yarn" >> $GITHUB_OUTPUT
39 | echo "command=install" >> $GITHUB_OUTPUT
40 | echo "runner=yarn" >> $GITHUB_OUTPUT
41 | exit 0
42 | elif [ -f "${{ github.workspace }}/package.json" ]; then
43 | echo "manager=npm" >> $GITHUB_OUTPUT
44 | echo "command=ci" >> $GITHUB_OUTPUT
45 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT
46 | exit 0
47 | else
48 | echo "Unable to determine package manager"
49 | exit 1
50 | fi
51 | - name: Setup Node
52 | uses: actions/setup-node@v4
53 | with:
54 | node-version: "22"
55 | cache: ${{ steps.detect-package-manager.outputs.manager }}
56 | - name: Setup Pages
57 | uses: actions/configure-pages@v5
58 | with:
59 | # Automatically inject basePath in your Next.js configuration file and disable
60 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
61 | #
62 | # You may remove this line if you want to manage the configuration yourself.
63 | static_site_generator: next
64 | - name: Restore cache
65 | uses: actions/cache@v4
66 | with:
67 | path: |
68 | .next/cache
69 | # Generate a new cache whenever packages or source files change.
70 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
71 | # If source files changed but packages didn't, rebuild from a prior cache.
72 | restore-keys: |
73 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
74 | - name: Install dependencies
75 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
76 | - name: Build with Next.js
77 | run: ${{ steps.detect-package-manager.outputs.runner }} next build
78 | - name: Static HTML export with Next.js
79 | run: ${{ steps.detect-package-manager.outputs.runner }} next export
80 | - name: Upload artifact
81 | uses: actions/upload-pages-artifact@v3
82 | with:
83 | path: ./out
84 |
85 | # Deployment job
86 | deploy:
87 | environment:
88 | name: github-pages
89 | url: ${{ steps.deployment.outputs.page_url }}
90 | runs-on: ubuntu-latest
91 | needs: build
92 | steps:
93 | - name: Deploy to GitHub Pages
94 | id: deployment
95 | uses: actions/deploy-pages@v4
96 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | """Sphinx configuration file."""
2 |
3 | from __future__ import annotations
4 |
5 | import warnings
6 | from importlib import metadata
7 | from pathlib import Path
8 | from typing import TYPE_CHECKING
9 |
10 | import pybtex.plugin
11 | from pybtex.style.formatting.unsrt import Style as UnsrtStyle
12 | from pybtex.style.template import field, href
13 |
14 | ROOT = Path(__file__).parent.parent.resolve()
15 |
16 |
17 | try:
18 | version = metadata.version("mqt.qubomaker")
19 | except ModuleNotFoundError:
20 | msg = (
21 | "Package should be installed to produce documentation! "
22 | "Assuming a modern git archive was used for version discovery."
23 | )
24 | warnings.warn(msg, stacklevel=1)
25 |
26 | from setuptools_scm import get_version
27 |
28 | version = get_version(root=str(ROOT), fallback_root=ROOT)
29 |
30 | # Filter git details from version
31 | release = version.split("+")[0]
32 | if TYPE_CHECKING:
33 | from pybtex.database import Entry
34 | from pybtex.richtext import HRef
35 |
36 | project = "MQT QUBOMaker"
37 | author = "Chair for Design Automation, Technical University of Munich"
38 | language = "en"
39 | project_copyright = "2024, Chair for Design Automation, Technical University of Munich"
40 | # -- General configuration ---------------------------------------------------
41 |
42 | master_doc = "index"
43 |
44 | templates_path = ["_templates"]
45 | html_css_files = ["custom.css"]
46 |
47 | extensions = [
48 | "sphinx.ext.napoleon",
49 | "sphinx.ext.autodoc",
50 | "sphinx.ext.autosummary",
51 | "sphinx.ext.mathjax",
52 | "sphinx.ext.intersphinx",
53 | "sphinx.ext.autosectionlabel",
54 | "sphinx.ext.viewcode",
55 | "sphinx.ext.githubpages",
56 | "sphinxcontrib.bibtex",
57 | "sphinx_copybutton",
58 | "sphinxext.opengraph",
59 | ]
60 |
61 | pygments_style = "colorful"
62 |
63 | add_module_names = False
64 |
65 | modindex_common_prefix = ["mqt.qubomaker."]
66 |
67 | intersphinx_mapping = {
68 | "python": ("https://docs.python.org/3", None),
69 | "typing_extensions": ("https://typing-extensions.readthedocs.io/en/latest/", None),
70 | "qiskit": ("https://docs.quantum.ibm.com/api/qiskit/", None),
71 | "mqt": ("https://mqt.readthedocs.io/en/latest/", None),
72 | }
73 |
74 | highlight_language = "python3"
75 |
76 | autosectionlabel_prefix_document = True
77 |
78 | exclude_patterns = ["_build", "build", "**.ipynb_checkpoints", "Thumbs.db", ".DS_Store", ".env"]
79 |
80 |
81 | class CDAStyle(UnsrtStyle):
82 | """Custom style for including PDF links."""
83 |
84 | def format_url(self, _e: Entry) -> HRef: # noqa: PLR6301
85 | """Format URL field as a link to the PDF."""
86 | url = field("url", raw=True)
87 | return href()[url, "[PDF]"]
88 |
89 |
90 | pybtex.plugin.register_plugin("pybtex.style.formatting", "cda_style", CDAStyle)
91 |
92 | bibtex_bibfiles = ["refs.bib"]
93 | bibtex_default_style = "cda_style"
94 |
95 | copybutton_prompt_text = r"(?:\(venv\) )?(?:\[.*\] )?\$ "
96 | copybutton_prompt_is_regexp = True
97 | copybutton_line_continuation_character = "\\"
98 |
99 | autosummary_generate = True
100 |
101 |
102 | typehints_use_rtype = False
103 | napoleon_use_rtype = False
104 | napoleon_google_docstring = True
105 | napoleon_numpy_docstring = False
106 |
107 | # -- Options for HTML output -------------------------------------------------
108 | html_theme = "furo"
109 | html_static_path = ["_static"]
110 | html_theme_options = {
111 | "light_logo": "mqt_dark.png",
112 | "dark_logo": "mqt_light.png",
113 | "source_repository": "https://github.com/cda-tum/mqt-qubomaker/",
114 | "source_branch": "main",
115 | "source_directory": "docs/",
116 | "navigation_with_keys": True,
117 | }
118 |
--------------------------------------------------------------------------------
/src/mqt/qubomaker/pathfinder/README.md:
--------------------------------------------------------------------------------
1 | This submodule of MQT QUBOMaker is responsible for the QUBO formulation of pathfinding problems.
2 |
3 | ### Settings
4 |
5 | Constructing QUBO formulation for pathfinding problems requires a `PathfindingQuboGenerator` instance with the corresponding settings:
6 |
7 | - `encoding_type`: An element of the `EncodingTypes` enum that represents the variable encoding scheme to be used. One of `ONE_HOT`, `DOMAIN_WALL`, or `BINARY`.
8 | - `n_paths`: The number of paths to be found. For most problem instances, this value will be `1`.
9 | - `max_path_length`: The maximum number of vertices a found path can consist of. Required to determine the number of binary variables that have to be used for the QUBO formulation.
10 | - `loops`: Determines, whether the path represents a loop, i.e., the final vertex in its sequence is connected back to the starting vertex.
11 |
12 | An example settings instance can be constructed as follows:
13 |
14 | ```python3
15 | import mqt.qubomaker.pathfinder as pf
16 |
17 | settings = pf.PathFindingQuboGeneratorSettings(
18 | encoding_type=pf.EncodingType.ONE_HOT, n_paths=1, max_path_length=4, loops=True
19 | )
20 | ```
21 |
22 | ### PathFindingQuboGenerator
23 |
24 | The `PathFindingQuboGenerator` class represents the main QUBO factory for pathfinding problems. It can be set up with predefined settings and populated with problem-specific constraints.
25 |
26 | To create a `PathFindingQuboGenerator`, a `Graph` instance further has to be provided, representing the graph to be investigated for the problem instance.
27 |
28 | ```python3
29 | ...
30 |
31 | qubo_generator = pf.PathFindingQuboGenerator(
32 | objective_function=None, graph=my_graph, settings=settings
33 | )
34 | ```
35 |
36 | When creating a `PathFindingQuboGenerator` instance, an objective function, as discussed below, can be added to add an optimization criterion.
37 |
38 | ### Cost Functions
39 |
40 | The `pathfinder` module provides cost functions representing various constraints related to pathfinding problems. The following constraints are supported:
41 |
42 | - `PathIsValid`: Checks, whether the encoding represents a valid path. Should be included in most cases.
43 | - `PathPositionIs`: Enforces that one of a set of vertices is located at a given position of a graph.
44 | - `PathStartsAt`: Enforces that one of a set of vertices is located at the start of a graph.
45 | - `PathEndsAt`: Enforces that one of a set of vertices is located at the end of a graph.
46 | - `PathContainsVerticesExactlyOnce`: Enforces that each element of a given set of vertices appears exactly once in a path.
47 | - `PathContainsVerticesAtLeastOnce`: Enforces that each element of a given set of vertices appears at least once in a path.
48 | - `PathContainsVerticesAtMostOnce`: Enforces that each element of a given set of vertices appears at most once in a path.
49 | - `PathContainsEdgesExactlyOnce`: Enforces that each element of a given set of edges appears exactly once in a path.
50 | - `PathContainsEdgesAtLeastOnce`: Enforces that each element of a given set of edges appears at least once in a path.
51 | - `PathContainsEdgesAtMostOnce`: Enforces that each element of a given set of edges appears at most once in a path.
52 | - `PrecedenceConstraint`: Enforces, that a given vertex does not appear in a path that didn't visit another given vertex first.
53 | - `PathsShareNoVertices`: Enforces, that two provided paths do not share any vertices.
54 | - `PathsShareNoEdges`: Enforces, that two provided paths do not share any edges.
55 |
56 | Furthermore, the `pathfinder` module also provides two objective functions:
57 |
58 | - `MinimizePathLength`: Adds a cost to the total cost function that penalizes paths with a higher total weight.
59 | - `MaximizePathLength`: Adds a cost to the total cost function that penalizes paths with a lower total weight.
60 |
61 | These constraints are represented by classes, and instances of the classes can be created to define the specific constraint and added to the QuboGenerator.
62 |
63 | ```python3
64 | ...
65 |
66 | contains_all_vertices = pf.PathContainsVerticesExactlyOnce(
67 | vertex_ids=graph.all_vertices, path_ids=[1]
68 | )
69 | starts_at_1 = pf.PathStartsAt(vertex_ids=[1], path=1)
70 |
71 | qubo_generator.add_constraint(contains_all_vertices)
72 | qubo_generator.add_constraint(starts_at_1)
73 | ```
74 |
--------------------------------------------------------------------------------
/src/mqt/qubomaker/graph.py:
--------------------------------------------------------------------------------
1 | """Provides a simple implementation for graphs to be used with QuboMaker."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import TYPE_CHECKING, cast
6 |
7 | import numpy as np
8 | import numpy.typing as npt
9 |
10 | if TYPE_CHECKING:
11 | from io import TextIOWrapper
12 |
13 | Edge = tuple[int, int] | tuple[int, int, int] | tuple[int, int, float]
14 |
15 |
16 | class Graph:
17 | """Represents a graph to be used with QuboMaker.
18 |
19 | Attributes:
20 | n_vertices (int): The number of vertices in the graph.
21 | adjacency_matrix (npt.NDArray[np.int_ | np.float64]): The adjacency matrix of the graph.
22 | all_vertices (list[int]): A list of all vertices in the graph.
23 | all_edges (list[tuple[int, int]]): A list of all edges in the graph.
24 | non_edges (list[tuple[int, int]]): A list of all non-edges in the graph.
25 | """
26 |
27 | n_vertices: int
28 | adjacency_matrix: npt.NDArray[np.int_ | np.float64]
29 |
30 | @property
31 | def all_vertices(self) -> list[int]:
32 | """A list of all vertices in the graph."""
33 | return list(range(1, self.n_vertices + 1))
34 |
35 | @property
36 | def all_edges(self) -> list[tuple[int, int]]:
37 | """A list of all edges in the graph."""
38 | return [(i, j) for i in self.all_vertices for j in self.all_vertices if self.adjacency_matrix[i - 1, j - 1] > 0]
39 |
40 | @property
41 | def non_edges(self) -> list[tuple[int, int]]:
42 | """A list of all pairs `(i, j)` that are not edges in the graph."""
43 | return [
44 | (i + 1, j + 1)
45 | for i in range(self.n_vertices)
46 | for j in range(self.n_vertices)
47 | if self.adjacency_matrix[i, j] <= 0
48 | ]
49 |
50 | def __init__(self, n_vertices: int, edges: list[Edge]) -> None:
51 | """Initialises a Graph object.
52 |
53 | Args:
54 | n_vertices (int): The number of vertices in the graph.
55 | edges (list[Edge]): A list of edges in the graph.
56 | """
57 | self.n_vertices = n_vertices
58 | self.adjacency_matrix = np.zeros((n_vertices, n_vertices))
59 | for edge in edges:
60 | if len(edge) == 2:
61 | (from_vertex, to_vertex, weight) = (edge[0], edge[1], 1.0)
62 | else:
63 | (from_vertex, to_vertex, weight) = edge
64 | self.adjacency_matrix[from_vertex - 1, to_vertex - 1] = weight if weight != -1 else 0
65 |
66 | @staticmethod
67 | def read(file: TextIOWrapper) -> Graph:
68 | """Reads a graph from a file.
69 |
70 | Args:
71 | file (TextIOWrapper): The file to read the graph from.
72 |
73 | Returns:
74 | Graph: The graph read from the file.
75 | """
76 | m = np.loadtxt(file)
77 | g = Graph(m.shape[0], [])
78 | g.adjacency_matrix = m
79 | return g
80 |
81 | def store(self, file: TextIOWrapper) -> None:
82 | """Stores the graph in a file.
83 |
84 | Args:
85 | file (TextIOWrapper): The file to store the graph in.
86 | """
87 | np.savetxt(file, self.adjacency_matrix)
88 |
89 | @staticmethod
90 | def from_adjacency_matrix(
91 | adjacency_matrix: npt.NDArray[np.int_ | np.float64] | list[list[int]] | list[list[float]],
92 | ) -> Graph:
93 | """Creates a graph from an adjacency matrix.
94 |
95 | Args:
96 | adjacency_matrix (npt.NDArray[np.int_ | np.float64]): The adjacency matrix to create the graph from.
97 |
98 | Returns:
99 | Graph: The graph created from the adjacency matrix.
100 | """
101 | if isinstance(adjacency_matrix, list):
102 | adjacency_matrix = np.array(adjacency_matrix)
103 | g = Graph(adjacency_matrix.shape[0], [])
104 | g.adjacency_matrix = adjacency_matrix
105 | return g
106 |
107 | @staticmethod
108 | def deserialize(encoding: str) -> Graph:
109 | """Deserializes a graph from a string.
110 |
111 | Args:
112 | encoding (str): The string to deserialize the graph from.
113 |
114 | Returns:
115 | Graph: The deserialized graph.
116 | """
117 | m = np.array([[float(cell) for cell in line.split() if cell] for line in encoding.splitlines() if line.strip()])
118 | g = Graph(m.shape[0], [])
119 | g.adjacency_matrix = m
120 | return g
121 |
122 | def serialize(self) -> str:
123 | """Serializes the graph into a string.
124 |
125 | Returns:
126 | str: The serialized graph as a string.
127 | """
128 | return str(self.adjacency_matrix).replace("]", "").replace("[", "").replace("\n ", "\n")
129 |
130 | def __eq__(self, value: object) -> bool:
131 | """Checks if two graphs are equal.
132 |
133 | Args:
134 | value (object): The other graph to compare to.
135 |
136 | Returns:
137 | bool: True if the graphs are equal, False otherwise.
138 | """
139 | if not isinstance(value, Graph):
140 | return False
141 | return cast("bool", np.array_equal(self.adjacency_matrix, value.adjacency_matrix))
142 |
143 | def __hash__(self) -> int:
144 | """Returns the hash of the graph.
145 |
146 | Returns:
147 | int: The hash of the graph.
148 | """
149 | return hash(self.adjacency_matrix)
150 |
--------------------------------------------------------------------------------
/tests/pathfinder/test_tsplib_input.py:
--------------------------------------------------------------------------------
1 | """Tests the correctness of the tsplib input format."""
2 |
3 | from __future__ import annotations
4 |
5 | import sys
6 | from pathlib import Path
7 |
8 | import pytest
9 |
10 | if sys.version_info >= (3, 13):
11 | pytest.skip("This module requires Python 3.12 or lower", allow_module_level=True)
12 |
13 | import tsplib95
14 |
15 | import mqt.qubomaker.pathfinder as pf
16 | import mqt.qubomaker.pathfinder.cost_functions as cf
17 |
18 | from .utils_test import check_equal, get_test_graph
19 |
20 | TEST_GRAPH = get_test_graph()
21 |
22 |
23 | def read_from_path(path: str, encoding: pf.EncodingType = pf.EncodingType.ONE_HOT) -> pf.PathFindingQuboGenerator:
24 | """Reads a tsplib input file and returns the corresponding `PathFindingQuboGenerator`.
25 |
26 | Args:
27 | path: The path to the tsplib input file.
28 | encoding: The encoding to use.
29 |
30 | Returns:
31 | The corresponding `PathFindingQuboGenerator`.
32 | """
33 | pth = Path("tests") / "pathfinder" / "resources" / "tsplib" / path
34 | problem = tsplib95.load(str(pth))
35 |
36 | return pf.from_tsplib_problem(problem, encoding)
37 |
38 |
39 | def test_hcp() -> None:
40 | """Tests a tsplib input file that represents a HCP problem."""
41 | json_generator = read_from_path("hcp-5.hcp")
42 | graph = json_generator.graph
43 |
44 | settings = pf.PathFindingQuboGeneratorSettings(
45 | encoding_type=pf.EncodingType.ONE_HOT,
46 | n_paths=1,
47 | max_path_length=5,
48 | loops=True,
49 | )
50 |
51 | manual_generator = pf.PathFindingQuboGenerator(objective_function=None, graph=graph, settings=settings)
52 |
53 | manual_generator.add_constraint(cf.PathIsValid([1]))
54 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce(graph.all_vertices, [1]))
55 | check_equal(json_generator, manual_generator)
56 |
57 |
58 | def test_tsp() -> None:
59 | """Tests a tsplib input file that represents a TSP problem."""
60 | json_generator = read_from_path("tsp-5.tsp")
61 | graph = json_generator.graph
62 |
63 | settings = pf.PathFindingQuboGeneratorSettings(
64 | encoding_type=pf.EncodingType.ONE_HOT,
65 | n_paths=1,
66 | max_path_length=5,
67 | loops=True,
68 | )
69 |
70 | manual_generator = pf.PathFindingQuboGenerator(
71 | objective_function=cf.MinimizePathLength([1]), graph=graph, settings=settings
72 | )
73 |
74 | manual_generator.add_constraint(cf.PathIsValid([1]))
75 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce(graph.all_vertices, [1]))
76 | check_equal(json_generator, manual_generator)
77 |
78 |
79 | def test_atsp() -> None:
80 | """Tests a tsplib input file that represents an ATSP problem."""
81 | json_generator = read_from_path("atsp-5.atsp")
82 | graph = json_generator.graph
83 |
84 | settings = pf.PathFindingQuboGeneratorSettings(
85 | encoding_type=pf.EncodingType.ONE_HOT,
86 | n_paths=1,
87 | max_path_length=5,
88 | loops=True,
89 | )
90 |
91 | manual_generator = pf.PathFindingQuboGenerator(
92 | objective_function=cf.MinimizePathLength([1]), graph=graph, settings=settings
93 | )
94 |
95 | manual_generator.add_constraint(cf.PathIsValid([1]))
96 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce(graph.all_vertices, [1]))
97 | check_equal(json_generator, manual_generator)
98 |
99 |
100 | def test_sop() -> None:
101 | """Tests a tsplib input file that represents a SOP problem."""
102 | json_generator = read_from_path("sop-5.sop")
103 | graph = json_generator.graph
104 |
105 | settings = pf.PathFindingQuboGeneratorSettings(
106 | encoding_type=pf.EncodingType.ONE_HOT,
107 | n_paths=1,
108 | max_path_length=5,
109 | loops=False,
110 | )
111 |
112 | manual_generator = pf.PathFindingQuboGenerator(
113 | objective_function=cf.MinimizePathLength([1]), graph=graph, settings=settings
114 | )
115 |
116 | manual_generator.add_constraint(cf.PathIsValid([1]))
117 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce(graph.all_vertices, [1]))
118 | manual_generator.add_constraint(cf.PrecedenceConstraint(1, 2, [1]))
119 | manual_generator.add_constraint(cf.PrecedenceConstraint(1, 5, [1]))
120 | manual_generator.add_constraint(cf.PrecedenceConstraint(2, 4, [1]))
121 | manual_generator.add_constraint(cf.PrecedenceConstraint(2, 5, [1]))
122 | check_equal(json_generator, manual_generator)
123 |
124 |
125 | def test_fail_cvrp() -> None:
126 | """Tests a tsplib input file that represents a CVRP problem. This should fail."""
127 | with pytest.raises(ValueError, match="CVRP"):
128 | read_from_path("fail/cvrp-7.vrp")
129 |
130 |
131 | def test_with_forced_edges() -> None:
132 | """Tests a tsplib input file that includes forced edges."""
133 | json_generator = read_from_path("forced-edges.tsp")
134 | graph = json_generator.graph
135 |
136 | settings = pf.PathFindingQuboGeneratorSettings(
137 | encoding_type=pf.EncodingType.ONE_HOT,
138 | n_paths=1,
139 | max_path_length=5,
140 | loops=True,
141 | )
142 |
143 | manual_generator = pf.PathFindingQuboGenerator(
144 | objective_function=cf.MinimizePathLength([1]), graph=graph, settings=settings
145 | )
146 |
147 | manual_generator.add_constraint(cf.PathIsValid([1]))
148 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce(graph.all_vertices, [1]))
149 | manual_generator.add_constraint(cf.PathContainsEdgesExactlyOnce([(1, 5), (5, 3)], [1]))
150 | check_equal(json_generator, manual_generator)
151 |
--------------------------------------------------------------------------------
/tests/pathfinder/test_pathfinder.py:
--------------------------------------------------------------------------------
1 | """Tests the end-to-end performance of the pathfinder module."""
2 |
3 | from __future__ import annotations
4 |
5 | import pytest
6 |
7 | import mqt.qubomaker as qm
8 | import mqt.qubomaker.pathfinder as pf
9 | import mqt.qubomaker.pathfinder.cost_functions as cf
10 |
11 | from .utils_test import get_test_graph_small, paths_equal_with_loops, paths_to_assignment_list
12 |
13 | TEST_GRAPH = get_test_graph_small()
14 |
15 |
16 | @pytest.mark.parametrize(
17 | "encoding_type",
18 | [
19 | cf.EncodingType.ONE_HOT,
20 | cf.EncodingType.DOMAIN_WALL,
21 | cf.EncodingType.BINARY,
22 | ],
23 | )
24 | def test_decoding(encoding_type: pf.EncodingType) -> None:
25 | """Test the decoding of a solution."""
26 | generator = setup_tsp()
27 | generator.settings.encoding_type = encoding_type
28 | generator2 = setup_tsp()
29 | generator2.settings.n_paths = 2
30 | generator2.settings.encoding_type = encoding_type
31 | s1 = [[1, 2, 4, 3]]
32 | s2 = [[1, 3, 3, 2]]
33 | s3 = [[1, 4]]
34 | s4 = [[1, 2], [4, 3]]
35 |
36 | d1 = generator.decode_bit_array(paths_to_assignment_list(s1, 4, 4, encoding_type))
37 | d2 = generator.decode_bit_array(paths_to_assignment_list(s2, 4, 4, encoding_type))
38 | d3 = generator.decode_bit_array(paths_to_assignment_list(s3, 4, 4, encoding_type))
39 | d4 = generator2.decode_bit_array(paths_to_assignment_list(s4, 4, 4, encoding_type))
40 |
41 | assert s1 == d1
42 | assert s2 == d2
43 | assert s3 == d3
44 | assert s4 == d4
45 | assert s1 != d2
46 | assert s2 != d3
47 | assert s3 != d4
48 | assert s4 != d1
49 |
50 |
51 | @pytest.mark.parametrize(
52 | "encoding_type",
53 | [
54 | cf.EncodingType.ONE_HOT,
55 | cf.EncodingType.DOMAIN_WALL,
56 | cf.EncodingType.BINARY,
57 | ],
58 | )
59 | def test_tsp(encoding_type: pf.EncodingType) -> None:
60 | """Test the module with a TSP problem."""
61 | settings = pf.PathFindingQuboGeneratorSettings(
62 | encoding_type=encoding_type,
63 | n_paths=1,
64 | max_path_length=4,
65 | loops=True,
66 | )
67 | generator = pf.PathFindingQuboGenerator(
68 | objective_function=cf.MinimizePathLength([1]),
69 | graph=TEST_GRAPH,
70 | settings=settings,
71 | )
72 | generator.add_constraint(cf.PathIsValid([1]))
73 | generator.add_constraint(cf.PathContainsVerticesExactlyOnce(TEST_GRAPH.all_vertices, [1]))
74 |
75 | if encoding_type != cf.EncodingType.BINARY:
76 | # Binary encoding is too complex for evaluation in this test.
77 | qubo_matrix = generator.construct_qubo_matrix()
78 | optimal_solution, _ = qm.optimize_classically(qubo_matrix)
79 |
80 | path_representation = generator.decode_bit_array(optimal_solution)
81 |
82 | assert paths_equal_with_loops(path_representation[0], [4, 1, 2, 3])
83 | assert generator.get_cost(optimal_solution) == 20.0
84 |
85 | # Test if constructing the Quantum Circuit works without a problem.
86 | generator.construct_qaoa_circuit()
87 |
88 | solution = paths_to_assignment_list([[4, 1, 2, 3]], 4, 4, encoding_type)
89 | assert generator.get_cost(solution) == 20.0
90 |
91 |
92 | @pytest.mark.parametrize(
93 | "encoding_type",
94 | [
95 | cf.EncodingType.ONE_HOT,
96 | cf.EncodingType.DOMAIN_WALL,
97 | cf.EncodingType.BINARY,
98 | ],
99 | )
100 | def test_2dpp(encoding_type: pf.EncodingType) -> None:
101 | """Test the module with a 2DPP problem."""
102 | settings = pf.PathFindingQuboGeneratorSettings(
103 | encoding_type=encoding_type,
104 | n_paths=2,
105 | max_path_length=2,
106 | loops=False,
107 | )
108 | generator = pf.PathFindingQuboGenerator(
109 | objective_function=cf.MinimizePathLength([1, 2]),
110 | graph=TEST_GRAPH,
111 | settings=settings,
112 | )
113 | generator.add_constraint(cf.PathIsValid([1, 2]))
114 | generator.add_constraint(cf.PathsShareNoVertices(1, 2))
115 | generator.add_constraint(cf.PathStartsAt([1], 1))
116 | generator.add_constraint(cf.PathStartsAt([2], 2))
117 | generator.add_constraint(cf.PathEndsAt([3], 1))
118 | generator.add_constraint(cf.PathEndsAt([4], 2))
119 |
120 | solution = paths_to_assignment_list([[1, 3], [2, 4]], 4, 2, encoding_type)
121 | assert generator.get_cost(solution) == 9.0
122 |
123 |
124 | def setup_tsp() -> pf.PathFindingQuboGenerator:
125 | """Set up a `PathFindingQuboGenerator` for a TSP problem."""
126 | settings = pf.PathFindingQuboGeneratorSettings(
127 | encoding_type=pf.EncodingType.ONE_HOT,
128 | n_paths=1,
129 | max_path_length=4,
130 | loops=True,
131 | )
132 | generator = pf.PathFindingQuboGenerator(
133 | objective_function=cf.MinimizePathLength([1]),
134 | graph=TEST_GRAPH,
135 | settings=settings,
136 | )
137 | generator.add_constraint(cf.PathIsValid([1]))
138 | generator.add_constraint(cf.PathContainsVerticesExactlyOnce(TEST_GRAPH.all_vertices, [1]))
139 | return generator
140 |
141 |
142 | def test_fail_get_cost_variable_count() -> None:
143 | """Tests whether passing an incorrect number of variables to `get_cost()` fails."""
144 | generator = setup_tsp()
145 | with pytest.raises(ValueError, match="Invalid assignment length"):
146 | generator.get_cost([1, 0])
147 |
148 |
149 | def test_fail_get_cost_variable_values() -> None:
150 | """Tests whether passing non-binary variables to `get_cost()` fails."""
151 | generator = setup_tsp()
152 | with pytest.raises(ValueError, match="Provided values are not binary \\(1/0\\)"):
153 | generator.get_cost([2, 0])
154 |
155 | with pytest.raises(ValueError, match="Provided values are not binary \\(1/0\\)"):
156 | generator.get_cost([-1, 0])
157 |
--------------------------------------------------------------------------------
/tests/pathfinder/test_json_input.py:
--------------------------------------------------------------------------------
1 | """Tests the correctness of the JSON input format."""
2 |
3 | from __future__ import annotations
4 |
5 | from pathlib import Path
6 |
7 | import pytest
8 |
9 | import mqt.qubomaker.pathfinder as pf
10 | import mqt.qubomaker.pathfinder.cost_functions as cf
11 |
12 | from .utils_test import check_equal, get_test_graph
13 |
14 | TEST_GRAPH = get_test_graph()
15 |
16 |
17 | def read_from_path(path: str) -> pf.PathFindingQuboGenerator:
18 | """Reads a JSON input file and returns the corresponding `PathFindingQuboGenerator`.
19 |
20 | Args:
21 | path (str): The path to the JSON input file.
22 |
23 | Returns:
24 | pf.PathFindingQuboGenerator: The corresponding `PathFindingQuboGenerator`.
25 | """
26 | with Path.open(Path("tests") / "pathfinder" / "resources" / "json" / path) as file:
27 | return pf.PathFindingQuboGenerator.from_json(file.read(), TEST_GRAPH)
28 |
29 |
30 | def test_all_constraints() -> None:
31 | """Tests a JSON input file that includes all constraints."""
32 | json_generator = read_from_path("all.json")
33 |
34 | settings = pf.PathFindingQuboGeneratorSettings(
35 | encoding_type=pf.EncodingType.ONE_HOT,
36 | n_paths=3,
37 | max_path_length=4,
38 | loops=True,
39 | )
40 | manual_generator = pf.PathFindingQuboGenerator(
41 | objective_function=cf.MinimizePathLength([1]), graph=TEST_GRAPH, settings=settings
42 | )
43 |
44 | manual_generator.add_constraint(cf.PathContainsEdgesAtLeastOnce([(1, 2)], [1]))
45 | manual_generator.add_constraint(cf.PathContainsEdgesAtMostOnce([(1, 2)], [1]))
46 | manual_generator.add_constraint(cf.PathContainsEdgesExactlyOnce([(1, 2)], [1]))
47 | manual_generator.add_constraint(cf.PathContainsVerticesAtLeastOnce([1, 2, 3], [1]))
48 | manual_generator.add_constraint(cf.PathContainsVerticesAtMostOnce([1, 2, 3], [1]))
49 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce([1, 2, 3], [1]))
50 | manual_generator.add_constraint(cf.PathEndsAt([1, 2, 3], 1))
51 | manual_generator.add_constraint(cf.PathStartsAt([1, 2, 3], 1))
52 | manual_generator.add_constraint(cf.PathPositionIs(2, [1, 2, 3], 1))
53 | manual_generator.add_constraint(cf.PrecedenceConstraint(1, 2, [1]))
54 | manual_generator.add_constraint(cf.PathsShareNoEdges(1, 2))
55 | manual_generator.add_constraint(cf.PathsShareNoEdges(2, 3))
56 | manual_generator.add_constraint(cf.PathsShareNoEdges(1, 3))
57 | manual_generator.add_constraint(cf.PathsShareNoVertices(1, 2))
58 | manual_generator.add_constraint(cf.PathsShareNoVertices(2, 3))
59 | manual_generator.add_constraint(cf.PathsShareNoVertices(1, 3))
60 | manual_generator.add_constraint(cf.PathIsValid([1, 2, 3]))
61 |
62 | check_equal(json_generator, manual_generator)
63 |
64 |
65 | def test_alternative_options() -> None:
66 | """Tests a JSON input file that includes alternative (non-default) options."""
67 | json_generator = read_from_path("alternative_options.json")
68 |
69 | settings = pf.PathFindingQuboGeneratorSettings(
70 | encoding_type=pf.EncodingType.BINARY,
71 | n_paths=2,
72 | max_path_length=5,
73 | loops=False,
74 | )
75 | manual_generator = pf.PathFindingQuboGenerator(objective_function=None, graph=TEST_GRAPH, settings=settings)
76 |
77 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce(TEST_GRAPH.all_vertices, [1]))
78 | manual_generator.add_constraint(cf.PathContainsVerticesAtLeastOnce([1, 2], [1]))
79 | manual_generator.add_constraint(cf.PathContainsVerticesAtMostOnce(TEST_GRAPH.all_vertices, [1, 2]))
80 | manual_generator.add_constraint(cf.PrecedenceConstraint(1, 2, [1]))
81 | manual_generator.add_constraint(cf.PrecedenceConstraint(2, 3, [1]))
82 |
83 | check_equal(json_generator, manual_generator)
84 |
85 |
86 | def test_suggest_encoding() -> None:
87 | """Tests the encoding suggestion feature for a JSON input file."""
88 | with Path.open(Path("tests") / "pathfinder" / "resources" / "json" / "with_weight.json") as file:
89 | j = file.read()
90 | assert pf.PathFindingQuboGenerator.suggest_encoding(j, TEST_GRAPH) == pf.EncodingType.ONE_HOT
91 |
92 |
93 | def test_with_weight() -> None:
94 | """Tests a JSON input file that includes weights for some constraints."""
95 | json_generator = read_from_path("with_weight.json")
96 |
97 | settings = pf.PathFindingQuboGeneratorSettings(
98 | encoding_type=pf.EncodingType.DOMAIN_WALL,
99 | n_paths=1,
100 | max_path_length=5,
101 | loops=False,
102 | )
103 | manual_generator = pf.PathFindingQuboGenerator(
104 | objective_function=cf.MaximizePathLength([1]), graph=TEST_GRAPH, settings=settings
105 | )
106 |
107 | manual_generator.add_constraint(cf.PathContainsVerticesExactlyOnce(TEST_GRAPH.all_vertices, [1]), weight=500)
108 |
109 | check_equal(json_generator, manual_generator)
110 |
111 |
112 | def test_fail_excess_field() -> None:
113 | """Tests a JSON input file that should fail because it includes an excess field."""
114 | with pytest.raises(ValueError, match="JSON"):
115 | read_from_path("fail/excess_field.json")
116 |
117 |
118 | def test_fail_missing_field() -> None:
119 | """Tests a JSON input file that should fail because it is missing a field."""
120 | with pytest.raises(ValueError, match="JSON"):
121 | read_from_path("fail/missing_field.json")
122 |
123 |
124 | def test_fail_too_few_elements() -> None:
125 | """Tests a JSON input file that should fail because some options have too few elements."""
126 | with pytest.raises(ValueError, match="JSON"):
127 | read_from_path("fail/too_few_elements.json")
128 |
129 |
130 | def test_fail_unknown_type() -> None:
131 | """Tests a JSON input file that should fail because it includes an unknown type."""
132 | with pytest.raises(ValueError, match="JSON"):
133 | read_from_path("fail/unknown_type.json")
134 |
--------------------------------------------------------------------------------
/.github/contributing.rst:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | Thank you for your interest in contributing to this project.
5 | We value contributions from people with all levels of experience.
6 | In particular if this is your first pull request not everything has to be perfect.
7 | We will guide you through the process.
8 |
9 | We use GitHub to `host code `_, to `track issues and feature requests `_, as well as accept `pull requests `_.
10 | See https://docs.github.com/en/get-started/quickstart for a general introduction to working with GitHub and contributing to projects.
11 |
12 | Types of Contributions
13 | ######################
14 |
15 | You can contribute in several ways:
16 |
17 | - 🐛 Report Bugs
18 | Report bugs at https://github.com/cda-tum/mqt-qubomaker/issues using the *🐛 Bug report* issue template. Please make sure to fill out all relevant information in the respective issue form.
19 |
20 | - 🐛 Fix Bugs
21 | Look through the `GitHub Issues `_ for bugs. Anything tagged with "bug" is open to whoever wants to try and fix it.
22 |
23 | - ✨ Propose New Features
24 | Propose new features at https://github.com/cda-tum/mqt-qubomaker/issues using the *✨ Feature request* issue template. Please make sure to fill out all relevant information in the respective issue form.
25 |
26 | - ✨ Implement New Features
27 | Look through the `GitHub Issues `_ for features. Anything tagged with "feature" is open to whoever wants to implement it. We highly appreciate external contributions to the project.
28 |
29 | - 📝 Write Documentation
30 | MQT QUBOMaker could always use some more `documentation `_, and we appreciate any help with that.
31 |
32 | 🎉 Get Started
33 | ##############
34 |
35 | Ready to contribute? Check out the :doc:`Development Guide ` to set up MQT QUBOMaker for local development and learn about the style guidelines and conventions used throughout the project.
36 |
37 | We value contributions from people with all levels of experience.
38 | In particular if this is your first PR not everything has to be perfect.
39 | We will guide you through the PR process.
40 | Nevertheless, please try to follow the guidelines below as well as you can to help make the PR process quick and smooth.
41 |
42 | Core Guidelines
43 | ###############
44 |
45 | - `"Commit early and push often" `_.
46 | - Write meaningful commit messages (preferably using `gitmoji `_ to give additional context to your commits).
47 | - Focus on a single feature/bug at a time and only touch relevant files. Split multiple features into multiple contributions.
48 | - If you added a new feature, you should add tests that ensure it works as intended. Furthermore, the new feature should be documented appropriately.
49 | - If you fixed a bug, you should add tests that demonstrate that the bug has been fixed.
50 | - Document your code thoroughly and write readable code.
51 | - Keep your code clean. Remove any debug statements, left-over comments, or code unrelated to your contribution.
52 | - Run :code:`pre-commit run -a` to check your code for style and linting errors before committing.
53 |
54 | Pull Request Workflow
55 | #####################
56 |
57 | - Create PRs early. It is ok to create work-in-progress PRs. You may mark these as draft PRs on GitHub.
58 | - Describe your PR. Start with a descriptive title, reference any related issues by including the issue number in the PR description, and add a comprehensive description of the changes. We provide a PR template that you can (and should) follow to create a PR.
59 | - Whenever a PR is created or updated, several workflows on all supported platforms and versions of Python are executed. Make sure your PR passes *all* these continuous integration (CI) checks. Here are some tips for finding the cause of certain failures:
60 | - If any of the :code:`Python Packaging/\*` checks fail, this indicates an error in the creation of the Python wheels and/or source distribution. Look through the respective logs on GitHub for any error or failure messages.
61 | - If any of the :code:`Python/\*` checks fail, this indicates an error in the Python part of the code base. Look through the respective logs on GitHub for any error or failure messages.
62 | - If any of the :code:`codecov/\*` checks fail, this means that your changes are not appropriately covered by tests or that the overall project coverage decreased too much. Ensure that you include tests for all your changes in the PR.
63 | - If the :code:`docs/readthedocs.org:mqt-qubomaker` check fails, the documentation could not be built properly. Inspect the corresponding log file for any errors.
64 | - If the :code:`pre-commit.ci` check fails, some of the :code:`pre-commit` checks failed and could not be fixed automatically by the *pre-commit.ci* bot. Such failures are most likely related to the Python part of the code base. The individual log messages frequently provide helpful suggestions on how to fix the warnings.
65 |
66 | - Once your PR is ready, change it from a draft PR to a regular PR and request a review from one of the project maintainers.
67 | - If your PR gets a "Changes requested" review, you will need to address the feedback and update your PR by pushing to the same branch. You don't need to close the PR and open a new one. Respond to review comments on the PR (e.g., with "done 👍"). Be sure to re-request review once you have made changes after a code review so that maintainers know that the requests have been addressed.
68 |
69 | .. raw:: html
70 |
71 |
72 |
73 | This document was inspired by and partially adapted from
74 |
75 | - https://matplotlib.org/stable/devel/coding_guide.html
76 | - https://opensource.creativecommons.org/contributing-code/pr-guidelines/
77 | - https://yeoman.io/contributing/pull-request.html
78 | - https://github.com/scikit-build/scikit-build
79 |
--------------------------------------------------------------------------------
/src/mqt/qubomaker/pathfinder/tsplib.py:
--------------------------------------------------------------------------------
1 | """Provides support for the TSPLib format as input for the pathfinding QuboMaker."""
2 |
3 | from __future__ import annotations
4 |
5 | import importlib.util
6 | from typing import TYPE_CHECKING
7 |
8 | import mqt.qubomaker
9 | import mqt.qubomaker.pathfinder
10 | from mqt.qubomaker.pathfinder import cost_functions
11 |
12 | if TYPE_CHECKING:
13 | import contextlib
14 |
15 | import networkx as nx
16 |
17 | with contextlib.suppress(ImportError):
18 | from tsplib95.models import StandardProblem
19 |
20 |
21 | def __check_forced_edges(problem: StandardProblem) -> cost_functions.CostFunction | None:
22 | if not problem.fixed_edges:
23 | return None
24 | forced_edges: list[tuple[int, int]] = []
25 | for i, j in problem.fixed_edges:
26 | forced_edges.append((i + 1, j + 1))
27 | return cost_functions.PathContainsEdgesExactlyOnce(forced_edges, [1])
28 |
29 |
30 | def to_graph(g: nx.Graph) -> mqt.qubomaker.Graph:
31 | """Transforms a networkx graph into a Graph object.
32 |
33 | Args:
34 | g (nx.Graph): The networkx graph to be transformed.
35 |
36 | Returns:
37 | Graph: The transformed graph.
38 | """
39 | return mqt.qubomaker.Graph(g.number_of_nodes(), g.edges.data("weight"))
40 |
41 |
42 | def __tsp(
43 | problem: StandardProblem, encoding_type: cost_functions.EncodingType
44 | ) -> mqt.qubomaker.pathfinder.PathFindingQuboGenerator:
45 | """Constructs a QUBO generator for a TSP problem.
46 |
47 | Args:
48 | problem (StandardProblem): The TSP problem.
49 | encoding_type (cost_functions.EncodingType): The desired encoding type.
50 |
51 | Returns:
52 | PathFindingQuboGenerator: The constructed QUBO generator.
53 | """
54 | g = to_graph(problem.get_graph())
55 | settings = mqt.qubomaker.pathfinder.PathFindingQuboGeneratorSettings(encoding_type, 1, g.n_vertices, True)
56 | generator = mqt.qubomaker.pathfinder.PathFindingQuboGenerator(cost_functions.MinimizePathLength([1]), g, settings)
57 |
58 | generator.add_constraint(cost_functions.PathIsValid([1]))
59 | generator.add_constraint(cost_functions.PathContainsVerticesExactlyOnce(g.all_vertices, [1]))
60 |
61 | generator.add_constraint_if_exists(__check_forced_edges(problem))
62 |
63 | return generator
64 |
65 |
66 | def __hcp(
67 | problem: StandardProblem, encoding_type: cost_functions.EncodingType
68 | ) -> mqt.qubomaker.pathfinder.PathFindingQuboGenerator:
69 | """Constructs a QUBO generator for a HCP problem.
70 |
71 | Args:
72 | problem (StandardProblem): The HCP problem.
73 | encoding_type (cost_functions.EncodingType): The desired encoding type.
74 |
75 | Returns:
76 | PathFindingQuboGenerator: The constructed QUBO generator.
77 | """
78 | g = to_graph(problem.get_graph())
79 | settings = mqt.qubomaker.pathfinder.PathFindingQuboGeneratorSettings(encoding_type, 1, g.n_vertices, True)
80 | generator = mqt.qubomaker.pathfinder.PathFindingQuboGenerator(None, g, settings)
81 |
82 | generator.add_constraint(cost_functions.PathIsValid([1]))
83 | generator.add_constraint(cost_functions.PathContainsVerticesExactlyOnce(g.all_vertices, [1]))
84 |
85 | generator.add_constraint_if_exists(__check_forced_edges(problem))
86 |
87 | return generator
88 |
89 |
90 | def __sop(
91 | problem: StandardProblem, encoding_type: cost_functions.EncodingType
92 | ) -> mqt.qubomaker.pathfinder.PathFindingQuboGenerator:
93 | """Constructs a QUBO generator for a SOP problem.
94 |
95 | Args:
96 | problem (StandardProblem): The SOP problem.
97 | encoding_type (cost_functions.EncodingType): The desired encoding type.
98 |
99 | Returns:
100 | PathFindingQuboGenerator: The constructed QUBO generator.
101 | """
102 | g = to_graph(problem.get_graph())
103 | settings = mqt.qubomaker.pathfinder.PathFindingQuboGeneratorSettings(encoding_type, 1, g.n_vertices, False)
104 | generator = mqt.qubomaker.pathfinder.PathFindingQuboGenerator(cost_functions.MinimizePathLength([1]), g, settings)
105 | generator.add_constraint(cost_functions.PathIsValid([1]))
106 | generator.add_constraint(cost_functions.PathContainsVerticesExactlyOnce(g.all_vertices, [1]))
107 | sop_pairs = []
108 | for u, v, weight in problem.get_graph().edges.data("weight"):
109 | if weight == -1:
110 | sop_pairs.append((v + 1, u + 1))
111 | for u, v in sop_pairs:
112 | generator.add_constraint(cost_functions.PrecedenceConstraint(u, v, [1]))
113 |
114 | generator.add_constraint_if_exists(__check_forced_edges(problem))
115 |
116 | return generator
117 |
118 |
119 | def from_tsplib_problem(
120 | problem: StandardProblem, encoding_type: cost_functions.EncodingType
121 | ) -> mqt.qubomaker.pathfinder.PathFindingQuboGenerator:
122 | """Constructs a QUBO generator for a given problem in TSPLib format.
123 |
124 | Args:
125 | problem (StandardProblem): The TSPLib problem.
126 | encoding_type (cost_functions.EncodingType): The desired encoding type.
127 |
128 | Raises:
129 | NotImplementedError: If a CVRP problem is given, as this problem type cannot be solved by the pathfinder.
130 | ValueError: If an unknown problem type is given.
131 |
132 | Returns:
133 | PathFindingQuboGenerator: The constructed QUBO generator.
134 | """
135 | try:
136 | importlib.util.find_spec("tsplib95")
137 | except ImportError:
138 | msg = "The 'tsplib95' package is required to use this function."
139 | raise RuntimeError(msg) from None
140 |
141 | if problem.type in {"TSP", "ATSP"}:
142 | return __tsp(problem, encoding_type)
143 | if problem.type == "HCP":
144 | return __hcp(problem, encoding_type)
145 | if problem.type == "SOP":
146 | return __sop(problem, encoding_type)
147 | if problem.type == "CVRP":
148 | msg = "CVRP is not supported as it is not a pure path-finding problem."
149 | raise ValueError(msg)
150 | msg = "Problem type not supported."
151 | raise ValueError(msg)
152 |
--------------------------------------------------------------------------------
/tests/test_device.py:
--------------------------------------------------------------------------------
1 | """Tests the features of the device representation module."""
2 |
3 | from __future__ import annotations
4 |
5 | import pytest
6 |
7 | from mqt.qubomaker import Calibration
8 |
9 |
10 | @pytest.fixture
11 | def sample_device_heavy_hex() -> Calibration:
12 | """Provides a test device calibration with 500 qubits and nearest-neighbor connectivity."""
13 | heavy_hex_coupling = []
14 |
15 | def get_qubit_index(row: int, cell: int, off: bool) -> int:
16 | """Converts a (row, cell, off) tuple to a qubit index.
17 |
18 | Args:
19 | row (int): The row of the qubit.
20 | cell (int): The cell of the qubit.
21 | off (bool): Whether the qubit is in a main row or an off row.
22 |
23 | Returns:
24 | int: The qubit index.
25 | """
26 | if not off:
27 | return 19 * row + cell
28 | return 19 * row + 15 + cell
29 |
30 | for row in range(7):
31 | for cell in range(15):
32 | current = get_qubit_index(row, cell, False)
33 | if cell != 14:
34 | neighbor = get_qubit_index(row, cell + 1, False)
35 | heavy_hex_coupling.append((current, neighbor))
36 | if cell % 4 == 0:
37 | if row % 2 == 0 and row != 6:
38 | below = get_qubit_index(row, cell // 4, True)
39 | heavy_hex_coupling.append((current, below))
40 | elif row % 2 == 1:
41 | above = get_qubit_index(row - 1, cell // 4, True)
42 | heavy_hex_coupling.append((current, above))
43 | if cell % 4 == 2:
44 | if row % 2 == 0 and row != 0:
45 | above = get_qubit_index(row - 1, cell // 4, True)
46 | heavy_hex_coupling.append((current, above))
47 | elif row % 2 == 1:
48 | below = get_qubit_index(row, cell // 4, True)
49 | heavy_hex_coupling.append((current, below))
50 |
51 | longest_hamiltonian_path = []
52 | for row in range(7):
53 | for cell in range(15):
54 | if row == 0 and cell == 0:
55 | continue # we skip the first qubit.
56 | current = get_qubit_index(row, cell if row % 2 == 1 else (14 - cell), False)
57 | longest_hamiltonian_path.append(current)
58 | if row != 6:
59 | longest_hamiltonian_path.append(get_qubit_index(row, 3 if row % 2 == 1 else 0, True))
60 |
61 | num_qubits = max(max(pair) for pair in heavy_hex_coupling) + 1
62 |
63 | cal = Calibration(
64 | num_qubits=num_qubits,
65 | one_qubit=dict.fromkeys(range(num_qubits), 0.99),
66 | two_qubit=dict.fromkeys(heavy_hex_coupling, 0.99),
67 | measurement_confidences=dict.fromkeys(range(num_qubits), 0.99),
68 | basis_gates=["cz", "id", "rx", "rz", "rzz", "sx", "x"],
69 | t1=dict.fromkeys(range(num_qubits), 0.0001),
70 | t2=dict.fromkeys(range(num_qubits), 0.0002),
71 | )
72 | cal.test_tags = {} # type: ignore[attr-defined]
73 | cal.test_tags["longest_hamiltonian_path"] = longest_hamiltonian_path # type: ignore[attr-defined]
74 | cal.test_tags["longest_heavy_chain"] = [ # type: ignore[attr-defined]
75 | 12,
76 | 31,
77 | 29,
78 | 27,
79 | 25,
80 | 23,
81 | 21,
82 | 40,
83 | 42,
84 | 44,
85 | 46,
86 | 48,
87 | 50,
88 | 69,
89 | 67,
90 | 65,
91 | 63,
92 | 61,
93 | 59,
94 | 78,
95 | 80,
96 | 82,
97 | 84,
98 | 86,
99 | 88,
100 | 107,
101 | 105,
102 | 103,
103 | 101,
104 | 99,
105 | 97,
106 | 116,
107 | ]
108 | return cal
109 |
110 |
111 | def test_connected_qubit_chain(sample_device_heavy_hex: Calibration) -> None:
112 | """Tests the computation of the longest hamiltonian path through the device topology.
113 |
114 | Args:
115 | sample_device_heavy_hex (Calibration): The testing device calibration.
116 | """
117 | chain = sample_device_heavy_hex.get_connected_qubit_chain()
118 | assert chain == sample_device_heavy_hex.test_tags["longest_hamiltonian_path"] # type: ignore[attr-defined]
119 |
120 |
121 | def test_heavy_chain(sample_device_heavy_hex: Calibration) -> None:
122 | """Tests the computation of the longest heavy chain through the device topology.
123 |
124 | Args:
125 | sample_device_heavy_hex (Calibration): The testing device calibration.
126 | """
127 | chain = sample_device_heavy_hex.get_heavy_chain()
128 | assert chain == sample_device_heavy_hex.test_tags["longest_heavy_chain"] # type: ignore[attr-defined]
129 |
130 |
131 | def test_shared_neighbor(sample_device_heavy_hex: Calibration) -> None:
132 | """Tests the computation of a shared neighbor between two qubits.
133 |
134 | Args:
135 | sample_device_heavy_hex (Calibration): The testing device calibration.
136 | """
137 | assert sample_device_heavy_hex.get_shared_neighbor(0, 2) == 1
138 | assert sample_device_heavy_hex.get_shared_neighbor(0, 19) == 15
139 | assert sample_device_heavy_hex.get_shared_neighbor(1, 3) == 2
140 | assert sample_device_heavy_hex.get_shared_neighbor(124, 105) == 112
141 |
142 |
143 | def test_from_dict(sample_device_heavy_hex: Calibration) -> None:
144 | """Tests the deserialization of a Calibration object through the `from_dict` method.
145 |
146 | Args:
147 | sample_device_heavy_hex (Calibration): The testing device calibration.
148 | """
149 | obj = {
150 | "num_qubits": len(sample_device_heavy_hex.one_qubit),
151 | "one_qubit": sample_device_heavy_hex.one_qubit,
152 | "two_qubit": sample_device_heavy_hex.two_qubit,
153 | "measurement_confidences": sample_device_heavy_hex.measurement_confidences,
154 | "t1": sample_device_heavy_hex.t1,
155 | "t2": sample_device_heavy_hex.t2,
156 | }
157 |
158 | c = Calibration.from_dict(obj, ["cz", "id", "rx", "rz", "rzz", "sx", "x"])
159 | assert c == sample_device_heavy_hex
160 |
--------------------------------------------------------------------------------
/noxfile.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 - 2025 Chair for Design Automation, TUM
2 | # Copyright (c) 2025 Munich Quantum Software Company GmbH
3 | # All rights reserved.
4 | #
5 | # SPDX-License-Identifier: MIT
6 | #
7 | # Licensed under the MIT License
8 |
9 | """Nox sessions."""
10 |
11 | from __future__ import annotations
12 |
13 | import argparse
14 | import contextlib
15 | import os
16 | import shutil
17 | import tempfile
18 | from typing import TYPE_CHECKING
19 |
20 | import nox
21 |
22 | if TYPE_CHECKING:
23 | from collections.abc import Generator, Sequence
24 |
25 | nox.needs_version = ">=2024.3.2"
26 | nox.options.default_venv_backend = "uv"
27 |
28 | nox.options.sessions = ["lint", "tests", "minimums"]
29 |
30 | PYTHON_ALL_VERSIONS = ["3.10", "3.11", "3.12", "3.13"]
31 |
32 | if os.environ.get("CI", None):
33 | nox.options.error_on_missing_interpreters = True
34 |
35 |
36 | @contextlib.contextmanager
37 | def preserve_lockfile() -> Generator[None]:
38 | """Preserve the lockfile by moving it to a temporary directory."""
39 | with tempfile.TemporaryDirectory() as temp_dir_name:
40 | shutil.move("uv.lock", f"{temp_dir_name}/uv.lock")
41 | try:
42 | yield
43 | finally:
44 | shutil.move(f"{temp_dir_name}/uv.lock", "uv.lock")
45 |
46 |
47 | @nox.session(reuse_venv=True)
48 | def lint(session: nox.Session) -> None:
49 | """Run the linter."""
50 | if shutil.which("pre-commit") is None:
51 | session.install("pre-commit")
52 |
53 | session.run("pre-commit", "run", "--all-files", *session.posargs, external=True)
54 |
55 |
56 | def _run_tests(
57 | session: nox.Session,
58 | *,
59 | install_args: Sequence[str] = (),
60 | extra_command: Sequence[str] = (),
61 | pytest_run_args: Sequence[str] = (),
62 | ) -> None:
63 | env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
64 | if shutil.which("cmake") is None and shutil.which("cmake3") is None:
65 | session.install("cmake")
66 | if shutil.which("ninja") is None:
67 | session.install("ninja")
68 |
69 | # install build and test dependencies on top of the existing environment
70 | session.run(
71 | "uv",
72 | "sync",
73 | "--inexact",
74 | "--only-group",
75 | "build",
76 | "--only-group",
77 | "test",
78 | *install_args,
79 | env=env,
80 | )
81 | print(session.python)
82 | add_tsplib = [] if session.python == "3.13" else ["--extra", "tsplib"]
83 |
84 | session.run(
85 | "uv",
86 | "sync",
87 | "--inexact",
88 | "--no-dev", # do not auto-install dev dependencies
89 | "--no-build-isolation-package",
90 | "mqt-qubomaker", # build the project without isolation
91 | "--extra",
92 | "check",
93 | *add_tsplib,
94 | *install_args,
95 | env=env,
96 | )
97 | if extra_command:
98 | session.run(*extra_command, env=env)
99 | if "--cov" in session.posargs:
100 | # try to use the lighter-weight `sys.monitoring` coverage core
101 | env["COVERAGE_CORE"] = "sysmon"
102 | session.run(
103 | "uv",
104 | "run",
105 | "--no-sync", # do not sync as everything is already installed
106 | *install_args,
107 | "pytest",
108 | *pytest_run_args,
109 | *session.posargs,
110 | "--cov-config=pyproject.toml",
111 | env=env,
112 | )
113 |
114 |
115 | @nox.session(reuse_venv=True, python=PYTHON_ALL_VERSIONS)
116 | def tests(session: nox.Session) -> None:
117 | """Run the test suite."""
118 | _run_tests(session)
119 |
120 |
121 | @nox.session(reuse_venv=True, venv_backend="uv", python=PYTHON_ALL_VERSIONS)
122 | def minimums(session: nox.Session) -> None:
123 | """Test the minimum versions of dependencies."""
124 | with preserve_lockfile():
125 | _run_tests(
126 | session,
127 | install_args=["--resolution=lowest-direct"],
128 | pytest_run_args=["-Wdefault"],
129 | )
130 | env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
131 | session.run("uv", "tree", "--frozen", env=env)
132 |
133 |
134 | @nox.session(reuse_venv=True, venv_backend="uv", python=PYTHON_ALL_VERSIONS)
135 | def qiskit(session: nox.Session) -> None:
136 | """Tests against the latest version of Qiskit."""
137 | with preserve_lockfile():
138 | _run_tests(
139 | session,
140 | extra_command=["uv", "pip", "install", "qiskit[qasm3-import] @ git+https://github.com/Qiskit/qiskit.git"],
141 | )
142 | env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
143 | session.run("uv", "pip", "show", "qiskit", env=env)
144 |
145 |
146 | @nox.session(reuse_venv=True)
147 | def docs(session: nox.Session) -> None:
148 | """Build the docs. Use "--non-interactive" to avoid serving. Pass "-b linkcheck" to check links."""
149 | parser = argparse.ArgumentParser()
150 | parser.add_argument("-b", dest="builder", default="html", help="Build target (default: html)")
151 | args, posargs = parser.parse_known_args(session.posargs)
152 |
153 | serve = args.builder == "html" and session.interactive
154 | if serve:
155 | session.install("sphinx-autobuild")
156 |
157 | env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
158 | # install build and docs dependencies on top of the existing environment
159 | session.run(
160 | "uv",
161 | "sync",
162 | "--inexact",
163 | "--only-group",
164 | "build",
165 | "--only-group",
166 | "docs",
167 | env=env,
168 | )
169 |
170 | shared_args = [
171 | "-n", # nitpicky mode
172 | "-T", # full tracebacks
173 | f"-b={args.builder}",
174 | "docs",
175 | f"docs/_build/{args.builder}",
176 | *posargs,
177 | ]
178 |
179 | session.run(
180 | "uv",
181 | "run",
182 | "--no-dev", # do not auto-install dev dependencies
183 | "--no-build-isolation-package",
184 | "mqt-qubomaker", # build the project without isolation
185 | "sphinx-autobuild" if serve else "sphinx-build",
186 | *shared_args,
187 | env=env,
188 | )
189 |
--------------------------------------------------------------------------------
/tests/test_circuit_generation.py:
--------------------------------------------------------------------------------
1 | """This module tests the generation of QAOA circuits (both the standard and heavy-hex optimized variants)."""
2 |
3 | from __future__ import annotations
4 |
5 | import pytest
6 | import sympy as sp
7 |
8 | from mqt.qubomaker import Calibration, QuboGenerator
9 |
10 |
11 | @pytest.fixture
12 | def simple_generator() -> QuboGenerator:
13 | """Provides a simple QuboGenerator instance for testing.
14 |
15 | Returns:
16 | QuboGenerator: An instance of QuboGenerator with the objective function x1 * x2 * x3 * x4 * x5.
17 | """
18 | symbols = [sp.Symbol(f"x_{i + 1}") for i in range(5)]
19 | obj = sp.Mul(*symbols)
20 | generator = QuboGenerator(obj)
21 | generator.disable_caching = True
22 | return generator
23 |
24 |
25 | @pytest.fixture
26 | def simple_device() -> Calibration:
27 | """Provides a test device calibration with 500 qubits and nearest-neighbor connectivity."""
28 | heavy_hex_coupling = []
29 |
30 | def get_qubit_index(row: int, cell: int, off: bool) -> int:
31 | """Converts a (row, cell, off) tuple to a qubit index.
32 |
33 | Args:
34 | row (int): The row of the qubit.
35 | cell (int): The cell of the qubit.
36 | off (bool): Whether the qubit is in a main row or an off row.
37 |
38 | Returns:
39 | int: The qubit index.
40 | """
41 | if not off:
42 | return 19 * row + cell
43 | return 19 * row + 15 + cell
44 |
45 | for row in range(7):
46 | for cell in range(15):
47 | current = get_qubit_index(row, cell, False)
48 | if cell != 14:
49 | neighbor = get_qubit_index(row, cell + 1, False)
50 | heavy_hex_coupling.append((current, neighbor))
51 | if cell % 4 == 0:
52 | if row % 2 == 0 and row != 6:
53 | below = get_qubit_index(row, cell // 4, True)
54 | heavy_hex_coupling.append((current, below))
55 | elif row % 2 == 1:
56 | above = get_qubit_index(row - 1, cell // 4, True)
57 | heavy_hex_coupling.append((current, above))
58 | if cell % 4 == 2:
59 | if row % 2 == 0 and row != 0:
60 | above = get_qubit_index(row - 1, cell // 4, True)
61 | heavy_hex_coupling.append((current, above))
62 | elif row % 2 == 1:
63 | below = get_qubit_index(row, cell // 4, True)
64 | heavy_hex_coupling.append((current, below))
65 |
66 | longest_hamiltonian_path = []
67 | for row in range(7):
68 | for cell in range(15):
69 | if row == 0 and cell == 0:
70 | continue # we skip the first qubit.
71 | current = get_qubit_index(row, cell if row % 2 == 1 else (14 - cell), False)
72 | longest_hamiltonian_path.append(current)
73 | if row != 6:
74 | longest_hamiltonian_path.append(get_qubit_index(row, 3 if row % 2 == 1 else 0, True))
75 |
76 | num_qubits = max(max(pair) for pair in heavy_hex_coupling) + 1
77 |
78 | return Calibration(
79 | num_qubits=num_qubits,
80 | one_qubit=dict.fromkeys(range(num_qubits), 0.99),
81 | two_qubit=dict.fromkeys(heavy_hex_coupling, 0.99),
82 | measurement_confidences=dict.fromkeys(range(num_qubits), 0.99),
83 | basis_gates=["cz", "id", "rx", "rz", "rzz", "sx", "x"],
84 | t1=dict.fromkeys(range(num_qubits), 0.0001),
85 | t2=dict.fromkeys(range(num_qubits), 0.0002),
86 | )
87 |
88 |
89 | def test_simple_qaoa(simple_generator: QuboGenerator) -> None:
90 | """Tests the construction of a simple QAOA circuit without qubit reuse and with barriers.
91 |
92 | Args:
93 | simple_generator (QuboGenerator): A simple QuboGenerator fixture.
94 | """
95 | circuit = simple_generator.construct_qaoa_circuit(do_reuse=False, include_barriers=True)
96 | expected_qubits = 8
97 | assert circuit.num_qubits == expected_qubits
98 | ops = circuit.count_ops()
99 | assert ops["barrier"] == 3
100 | assert ops["h"] == expected_qubits
101 | assert ops["rx"] == expected_qubits
102 | assert ops["rzz"] == 10
103 |
104 |
105 | def test_simple_qaoa_no_barriers(simple_generator: QuboGenerator) -> None:
106 | """Tests the construction of a simple QAOA circuit without qubit reuse and without barriers.
107 |
108 | Args:
109 | simple_generator (QuboGenerator): A simple QuboGenerator fixture.
110 | """
111 | circuit = simple_generator.construct_qaoa_circuit(do_reuse=False, include_barriers=False)
112 | expected_qubits = 8
113 | assert circuit.num_qubits == expected_qubits
114 | ops = circuit.count_ops()
115 | assert "barrier" not in ops
116 | assert ops["h"] == expected_qubits
117 | assert ops["rx"] == expected_qubits
118 | assert ops["rzz"] == 10
119 |
120 |
121 | def test_qaoa_with_reuse(simple_generator: QuboGenerator) -> None:
122 | """Tests the construction of a QAOA circuit with qubit reuse and without barriers.
123 |
124 | Args:
125 | simple_generator (QuboGenerator): A simple QuboGenerator fixture.
126 | """
127 | circuit = simple_generator.construct_qaoa_circuit(do_reuse=True, include_barriers=False)
128 | expected_qubits = 3
129 | expected_variables = 8
130 | assert circuit.num_qubits == expected_qubits
131 | ops = circuit.count_ops()
132 | assert "barrier" not in ops
133 | assert ops["h"] == expected_variables
134 | assert ops["rx"] == expected_variables
135 | assert ops["reset"] == expected_variables - expected_qubits
136 | assert ops["rzz"] == 10
137 |
138 |
139 | def test_heavy_hex_qaoa(simple_generator: QuboGenerator, simple_device: Calibration) -> None:
140 | """Tests the construction of a heavy-hex optimized QAOA circuit.
141 |
142 | Args:
143 | simple_generator (QuboGenerator): A simple QuboGenerator fixture.
144 | simple_device (Calibration): A simple device (heavy-hex) calibration fixture.
145 | """
146 | circuit = simple_generator.construct_embedded_qaoa_circuit(simple_device)
147 | assert circuit.num_qubits == simple_device.num_qubits
148 | ops = circuit.count_ops()
149 | expected_variables = 8
150 | assert ops["h"] == expected_variables
151 | assert ops["rx"] == expected_variables
152 | assert ops["rzz"] == 10
153 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > As of October 2025, this repository is no longer actively maintained. All code has been directly integrated into [MQT ProblemSolver](https://github.com/munich-quantum-toolkit/problemsolver).
3 | > Development is expected to continue there. No new contributions will be accepted here.
4 |
5 | [](https://pypi.org/project/mqt.qubomaker/)
6 | 
7 | [](https://opensource.org/licenses/MIT)
8 | [](https://github.com/cda-tum/mqt-qubomaker/actions/workflows/ci.yml)
9 | [](https://github.com/cda-tum/mqt-qubomaker/actions/workflows/cd.yml)
10 | [](https://mqt.readthedocs.io/projects/qubomaker)
11 | [](https://codecov.io/gh/cda-tum/mqt-qubomaker)
12 |
13 |
21 |
22 | # MQT QUBOMaker: Automatic Generation of QUBO Formulations from Optimization Problem Specifications
23 |
24 | MQT QUBOMaker is a framework that can be used to automatically generate QUBO formulations for various optimization problems based on a selection of constraints that define the problem.
25 | It is developed by the [Chair for Design Automation](https://www.cda.cit.tum.de/) at the [Technical University of Munich](https://www.tum.de/) as part of the _[Munich Quantum Toolkit](https://mqt.readthedocs.io/) (MQT)_.
26 |
27 | The tool allows users to create QUBO formulations, and, thus, interact with quantum algorithms, without requiring any background knowledge in the field of quantum computing. End-users can stay entirely within their domain of expertise while being shielded from the complex and error-prone mathematical tasks of QUBO reformulation.
28 |
29 | Furthermore, MQT QUBOMaker supports a variety of different encodings. End users can easily switch between the encodings for evaluation purposes without any additional effort, a task that would otherwise require a large amount of tedious mathematical reformulation.
30 |
31 | Currently, MQT QUBOMaker provides the following submodule:
32 |
33 | - [_Pathfinder_](./src/mqt/qubomaker/pathfinder/README.md): This submodule provides a specialization of the QUBOMaker class for the solution of optimization problems involving the search for paths in a directed graph. It provides a large set of pathfinding-related constraints that are used to define individual problem instances.
34 |
35 | The _Pathfinder_ submodule also has a supporting [GUI](https://cda-tum.github.io/mqt-qubomaker/) to further facilitate its use.
36 |
37 | For more details, please refer to:
38 |
39 |
44 |
45 | If you have any questions, feel free to create a [discussion](https://github.com/cda-tum/mqt-qubomaker/discussions) or an [issue](https://github.com/cda-tum/mqt-qubomaker/issues) on [GitHub](https://github.com/cda-tum/mqt-qubomaker).
46 |
47 | ## Getting Started
48 |
49 | `mqt-qubomaker` is available via [PyPI](https://pypi.org/project/mqt.qubomaker/).
50 |
51 | ```console
52 | (venv) $ pip install mqt.qubomaker
53 | ```
54 |
55 | The following code gives an example of the usage with the `pathfinder` submodule:
56 |
57 | ```python3
58 | import mqt.qubomaker as qm
59 | import mqt.qubomaker.pathfinder as pf
60 |
61 | # define an example graph to investigate.
62 | graph = qm.Graph.from_adjacency_matrix(
63 | [
64 | [0, 1, 3, 4],
65 | [2, 0, 4, 2],
66 | [1, 5, 0, 3],
67 | [3, 8, 1, 0],
68 | ]
69 | )
70 |
71 | # select the settings for the QUBO formulation.
72 | settings = pf.PathFindingQuboGeneratorSettings(
73 | encoding_type=pf.EncodingType.ONE_HOT, n_paths=1, max_path_length=4, loops=True
74 | )
75 |
76 | # define the generator to be used for the QUBO formulation.
77 | generator = pf.PathFindingQuboGenerator(
78 | objective_function=pf.MinimizePathLength(path_ids=[1]),
79 | graph=graph,
80 | settings=settings,
81 | )
82 |
83 | # add the constraints that define the problem instance.
84 | generator.add_constraint(pf.PathIsValid(path_ids=[1]))
85 | generator.add_constraint(
86 | pf.PathContainsVerticesExactlyOnce(vertex_ids=graph.all_vertices, path_ids=[1])
87 | )
88 |
89 | # generate and view the QUBO formulation as a QUBO matrix.
90 | print(generator.construct_qubo_matrix())
91 | ```
92 |
93 | **Detailed documentation and examples are available at [ReadTheDocs](https://mqt-qubomaker.readthedocs.io/en/latest/).**
94 |
95 | ## References
96 |
97 | MQT QUBOMaker has been developed based on methods proposed in the following paper:
98 |
99 | - D. Rovara, N. Quetschlich, and R. Wille "[A Framework to Formulate
100 | Pathfinding Problems for Quantum Computing](https://arxiv.org/abs/2404.10820)", arXiv, 2024
101 |
102 | ## Acknowledgements
103 |
104 | The Munich Quantum Toolkit has been supported by the European
105 | Research Council (ERC) under the European Union's Horizon 2020 research and innovation program (grant agreement
106 | No. 101001318), the Bavarian State Ministry for Science and Arts through the Distinguished Professorship Program, as well as the
107 | Munich Quantum Valley, which is supported by the Bavarian state government with funds from the Hightech Agenda Bayern Plus.
108 |
109 |