Skip to content

Commit 4fe097c

Browse files
🚸 Improve QDMI Integration (#1694)
## Description This PR originally only aimed to address #1352. However, working on it prompted several follow-ups and improvements for the QDMI support layer as well as a couple of bugfixes. The main change here is still that all QDMI devices are now built as shared libraries by default and that the respective dynamic wrappers have been removed. The QDMI Qiskit wrapper now supports multi-controlled gates as exposed by the DDSIM device. The QDMI Python tests have been significantly streamlined, and a lot of the mocking code is replaced with actual device executions. I will leave this in draft until CI is green because I expect Windows to cause some problems. Edit: As expected, Windows is acting up big again. Edit2: I think I have a working version now. Only took 10h 🫠 Fixes #1352 ## Checklist <!--- This checklist serves as a reminder of a couple of things that ensure your pull request will be merged swiftly. --> - [x] The pull request only contains commits that are focused and relevant to this change. - [x] I have added appropriate tests that cover the new/changed functionality. - [x] I have updated the documentation to reflect these changes. - [x] I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals. - [x] I have added migration instructions to the upgrade guide (if needed). - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes. **If PR contains AI-assisted content:** - [x] I have disclosed the use of AI tools in the PR description as per our [AI Usage Guidelines](https://github.com/munich-quantum-toolkit/core/blob/main/docs/ai_usage.md). - [x] AI-assisted commits include an `Assisted-by: [Model Name] via [Tool Name]` footer. - [x] I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it. --------- Signed-off-by: burgholzer <burgholzer@me.com> Co-authored-by: Daniel Haag <121057143+denialhaag@users.noreply.github.com>
1 parent afbda84 commit 4fe097c

34 files changed

Lines changed: 1370 additions & 2020 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning], with the exception that minor rel
1111

1212
### Added
1313

14+
- 🚸 Add a measurement instruction to the default SC QDMI device ([#1694]) ([**@burgholzer**])
15+
- ✨ Add support for multi-controlled gates to the QDMI Qiskit backend converter ([#1694]) ([**@burgholzer**])
1416
- ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates ([#1605]) ([**@lirem101**], [**@burgholzer**])
1517
- ✨ Add a `merge-single-qubit-rotation-gates` pass for merging consecutive rotation gates using quaternions ([#1407], [#1674]) ([**@J4MMlE**], [**@denialhaag**], [**@MatthiasReumann**])
1618
- ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676]) ([**@denialhaag**], [**@burgholzer**])
@@ -21,11 +23,18 @@ This project adheres to [Semantic Versioning], with the exception that minor rel
2123

2224
### Changed
2325

26+
- ♻️ Build all builtin QDMI devices as shared libraries ([#1694]) ([**@burgholzer**])
27+
- ⬆️ Update the minimum supported Qiskit version to `1.1.0` ([#1694]) ([**@burgholzer**])
2428
- ⬆️ Require LLVM 22.1 for C++ library builds ([#1549]) ([**@burgholzer**], [**@denialhaag**])
2529
- 📦 Build MLIR by default for C++ library builds ([#1356]) ([**@burgholzer**], [**@denialhaag**])
2630

31+
### Fixed
32+
33+
- 🐛 Fix segfault in DD `sample` method when idle classical bits are present ([#1694]) ([**@burgholzer**])
34+
2735
### Removed
2836

37+
- 🔥 Remove shared library wrappers for QDMI devices ([#1694]) ([**@burgholzer**])
2938
- 🔥 Remove the density matrix support from the MQT Core DD package ([#1466]) ([**@burgholzer**])
3039
- 🔥 Remove `datastructures` (`ds`) (sub)library from MQT Core ([#1458]) ([**@burgholzer**])
3140

@@ -362,6 +371,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool
362371

363372
<!-- PR links -->
364373

374+
[#1694]: https://github.com/munich-quantum-toolkit/core/pull/1694
365375
[#1676]: https://github.com/munich-quantum-toolkit/core/pull/1676
366376
[#1675]: https://github.com/munich-quantum-toolkit/core/pull/1675
367377
[#1674]: https://github.com/munich-quantum-toolkit/core/pull/1674

UPGRADING.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ This document describes breaking changes and how to upgrade. For a complete list
44

55
## [Unreleased]
66

7+
### Changes to builtin QDMI devices
8+
9+
The builtin QDMI devices (with prefixes `MQT_SC`, `MQT_NA`, and `MQT_DDSIM`) are now all built as shared libraries by default.
10+
In turn, the shared library wrappers (with prefixes `MQT_SC_DYN` and `MQT_NA_DYN`) have been removed entirely.
11+
MQT Core's QDMI driver will automatically load the shared libraries of the builtin devices if they are available in the library search path.
12+
If you were previously using the statically builtin devices, no changes should be necessary as the shared libraries are now the default.
13+
If you were previously using the shared library wrappers, you should switch to using the builtin devices instead, which are now shared libraries by default.
14+
15+
### Broader operation support in QDMI Qiskit converter
16+
17+
The QDMI Qiskit converter now supports a broader range of operations, including multi-controlled gates such as `mcx`, `mcz`, `mcrx`, and more.
18+
As a consequence, these operations can now be directly used without requiring decomposition, for example, with the builtin `DDSIM` QDMI device.
19+
20+
### Minimum supported Qiskit version
21+
22+
From this release onwards, MQT Core requires Qiskit version `1.1.0` or higher.
23+
This is due to the fact that we are relying on some fixes to Qiskit primitives that were introduced in that version.
24+
If you are using MQT Core with Qiskit, please ensure that you have updated to Qiskit `1.1.0` or higher to avoid any compatibility issues.
25+
726
### MLIR enabled by default for C++ builds
827

928
The MLIR-based functionality within MQT Core has long been experimental and opt-in.

cmake/AddMQTCoreLibrary.cmake

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ function(kebab_to_camel output input)
2121
endfunction()
2222

2323
function(add_mqt_core_library name)
24-
cmake_parse_arguments(ARG "" "ALIAS_NAME" "" ${ARGN})
25-
if(BUILD_MQT_CORE_SHARED_LIBS)
24+
cmake_parse_arguments(ARG "FORCE_SHARED;HIDDEN_VISIBILITY" "ALIAS_NAME" "" ${ARGN})
25+
26+
if(ARG_FORCE_SHARED OR BUILD_MQT_CORE_SHARED_LIBS)
2627
add_library(${name} SHARED ${ARG_UNPARSED_ARGUMENTS})
2728
else()
2829
add_library(${name} ${ARG_UNPARSED_ARGUMENTS})
2930
endif()
31+
3032
if(NOT ARG_ALIAS_NAME)
3133
# remove prefix 'mqt-' from target name if exists
3234
string(REGEX REPLACE "^${MQT_CORE_TARGET_NAME}" "" ALIAS_NAME_ARG ${name})
@@ -41,6 +43,14 @@ function(add_mqt_core_library name)
4143
# Add link libraries for warnings and options
4244
target_link_libraries(${name} PRIVATE MQT::ProjectWarnings MQT::ProjectOptions)
4345

46+
if(ARG_HIDDEN_VISIBILITY)
47+
set_target_properties(
48+
${name}
49+
PROPERTIES C_VISIBILITY_PRESET hidden
50+
CXX_VISIBILITY_PRESET hidden
51+
VISIBILITY_INLINES_HIDDEN 1)
52+
endif()
53+
4454
# Set versioning information
4555
set_target_properties(
4656
${name}

cmake/PackageAddTest.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ macro(PACKAGE_ADD_TEST testname linklibs)
1111
if(NOT TARGET ${testname})
1212
# create an executable in which the tests will be stored
1313
add_executable(${testname} ${ARGN})
14+
# Ensure test executables remain runnable from the build tree during GoogleTest discovery
15+
set_property(TARGET ${testname} PROPERTY BUILD_WITH_INSTALL_RPATH FALSE)
1416
# link the Google test infrastructure and a default main function to the test executable.
1517
target_link_libraries(${testname} PRIVATE ${linklibs} GTest::gmock GTest::gtest_main
1618
MQT::ProjectOptions MQT::ProjectWarnings)
@@ -30,6 +32,8 @@ macro(PACKAGE_ADD_TEST_WITH_WORKING_DIR testname linklibs test_working_directory
3032
if(NOT TARGET ${testname})
3133
# create an executable in which the tests will be stored
3234
add_executable(${testname} ${ARGN})
35+
# Ensure test executables remain runnable from the build tree during GoogleTest discovery
36+
set_property(TARGET ${testname} PROPERTY BUILD_WITH_INSTALL_RPATH FALSE)
3337
# link the Google test infrastructure and a default main function to the test executable.
3438
target_link_libraries(${testname} PRIVATE ${linklibs} GTest::gmock GTest::gtest_main
3539
MQT::ProjectOptions MQT::ProjectWarnings)

docs/qdmi/driver.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ mystnb:
1010

1111
## Objective
1212

13-
A QDMI Driver manages the communication between QDMI devices, such as [MQT Core's NA QDMI Device](na_device.md), and QDMI clients, see the [QDMI specification](https://munich-quantum-software-stack.github.io/QDMI/).
13+
A QDMI Driver manages the communication between QDMI devices, such as [MQT Core's NA QDMI Device](na_device.md) or [MQT Core's DDSIM QDMI Device](ddsim_device.md), and QDMI clients, see the [QDMI specification](https://munich-quantum-software-stack.github.io/QDMI/).
1414
It is responsible for loading the device, forwarding requests from the client to the device, and sending back the results.
15-
The MQT Core's QDMI Driver, {cpp:class}`qdmi::Driver`, comes with the [MQT Core's NA QDMI Device](na_device.md) that is already statically linked into the driver and can directly be used.
15+
MQT Core's QDMI Driver, {cpp:class}`qdmi::Driver`, comes with several preloaded devices that can be used directly.
1616
Other devices can be loaded dynamically at runtime via {cpp:func}`qdmi::Driver::addDynamicDeviceLibrary`.
1717

1818
## Python Bindings
@@ -38,4 +38,5 @@ available_devices = session.get_devices()
3838
# Print the name of every device
3939
for device in available_devices:
4040
print(device.name())
41+
4142
```

include/mqt-core/qdmi/driver/Driver.hpp

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,6 @@ class DynamicDeviceLibrary final : public DeviceLibrary {
146146
~DynamicDeviceLibrary() override;
147147
};
148148

149-
// Macro to define a static library class that inherits from DeviceLibrary.
150-
// It binds all device library functions to the functions of the static library.
151-
// @param prefix is the prefix used for the function names in the library.
152-
#define DECLARE_STATIC_LIBRARY(prefix) \
153-
class prefix##DeviceLibrary final : public DeviceLibrary { \
154-
public: \
155-
prefix##DeviceLibrary(); \
156-
\
157-
~prefix##DeviceLibrary() override; \
158-
};
159-
160-
// Call the above macro for all static libraries that we want to support.
161-
DECLARE_STATIC_LIBRARY(MQT_NA)
162-
DECLARE_STATIC_LIBRARY(MQT_DDSIM)
163-
DECLARE_STATIC_LIBRARY(MQT_SC)
164-
165149
/**
166150
* @brief The status of a session.
167151
* @details This enum defines the possible states of a session in the QDMI
@@ -178,9 +162,6 @@ enum class SessionStatus : uint8_t {
178162
*/
179163
struct QDMI_Device_impl_d {
180164
private:
181-
// Since we treat this struct as a class, we apply also the naming scheme for
182-
// classes, i.e., an underscore at the end of member names.
183-
184165
/**
185166
* @brief The device library that provides the device interface functions.
186167
* @note This must be a pointer type as we need access to dynamic and static
@@ -265,9 +246,6 @@ struct QDMI_Device_impl_d {
265246
*/
266247
struct QDMI_Job_impl_d {
267248
private:
268-
// Since we treat this struct as a class, we apply also the naming scheme for
269-
// classes, i.e., an underscore at the end of member names.
270-
271249
/// @brief The device job handle.
272250
QDMI_Device_Job deviceJob_ = nullptr;
273251
/// @brief The device associated with the job.
@@ -350,8 +328,6 @@ struct QDMI_Job_impl_d {
350328
/**
351329
* @brief Definition of the QDMI Session.
352330
*/
353-
// Since we treat this struct as a class, we apply also the naming scheme for
354-
// classes, i.e., an underscore at the end of member names.
355331
struct QDMI_Session_impl_d {
356332
private:
357333
/// @brief The status of the session.

json/sc/device.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@
185185
],
186186
"operations": [
187187
{ "name": "r", "numParameters": 2, "numQubits": 1 },
188-
{ "name": "cz", "numParameters": 0, "numQubits": 2 }
188+
{ "name": "cz", "numParameters": 0, "numQubits": 2 },
189+
{ "name": "measure", "numParameters": 0, "numQubits": 1 }
189190
]
190191
}

pyproject.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ dynamic = ["version"]
4949

5050
[project.optional-dependencies]
5151
qiskit = [
52-
"qiskit[qasm3-import]>=1.0.0",
52+
"qiskit[qasm3-import]>=1.1.0",
5353
]
5454

5555
[project.scripts]
@@ -97,9 +97,7 @@ build.targets = [
9797
"mqt-core-na-bindings",
9898
"mqt-core-qdmi-ddsim-device",
9999
"mqt-core-qdmi-na-device",
100-
"mqt-core-qdmi-na-device-dyn",
101100
"mqt-core-qdmi-sc-device",
102-
"mqt-core-qdmi-sc-device-dyn",
103101
]
104102

105103
install.components = [
@@ -344,7 +342,7 @@ docs = [
344342
"sphinxcontrib-bibtex>=2.6.5",
345343
"sphinxcontrib-svg2pdfconverter>=1.3.0",
346344
"sphinxext-opengraph>=0.13.0",
347-
"qiskit[qasm3-import,visualization]>=1.0.0",
345+
"qiskit[qasm3-import,visualization]>=1.1.0",
348346
"openqasm-pygments>=0.2.0",
349347
"breathe>=4.36.0",
350348
"graphviz>=0.21.0",
@@ -358,7 +356,7 @@ test = [
358356
"pytest-cov>=7.0.0",
359357
"pytest-sugar>=1.1.1",
360358
"pytest-xdist>=3.8.0",
361-
"qiskit[qasm3-import]>=1.0.0",
359+
"qiskit[qasm3-import]>=1.1.0",
362360
"numpy>=2.1; python_version >= '3.13'",
363361
"numpy>=2.3.2; python_version >= '3.14'",
364362
]

python/mqt/core/plugins/qiskit/backend.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@
1313

1414
from __future__ import annotations
1515

16+
import inspect
1617
import itertools
1718
import warnings
1819
from typing import TYPE_CHECKING, Any, ClassVar
1920

2021
from qiskit import qasm2, qasm3
2122
from qiskit.circuit import QuantumCircuit
22-
from qiskit.circuit.library import get_standard_gate_name_mapping
23+
from qiskit.circuit.library import (
24+
MCPhaseGate,
25+
MCXGate,
26+
get_standard_gate_name_mapping,
27+
)
2328
from qiskit.providers import BackendV2, Options
2429
from qiskit.transpiler import InstructionProperties, Target
2530

@@ -55,7 +60,7 @@ def __dir__() -> list[str]:
5560

5661
def _build_gate_mappings_for_backend(
5762
gate_aliases: dict[str, set[str]],
58-
) -> tuple[dict[str, set[str]], dict[str, Instruction]]:
63+
) -> tuple[dict[str, set[str]], dict[str, Instruction | type[Instruction]]]:
5964
"""Build both forward (Qiskit→QDMI) and inverse (QDMI→Gate) mappings.
6065
6166
Uses Qiskit's standard gate mapping as the canonical source of truth,
@@ -70,11 +75,18 @@ def _build_gate_mappings_for_backend(
7075
# Get Qiskit's standard gate name mapping as our canonical source
7176
canonical_gates = get_standard_gate_name_mapping()
7277

78+
# Augment the canonical mapping with any additional gates that may not be in Qiskit's standard library
79+
canonical_gates.update({
80+
"mcx": MCXGate,
81+
"mcphase": MCPhaseGate,
82+
"mcp": MCPhaseGate,
83+
})
84+
7385
qiskit_to_qdmi: dict[str, set[str]] = {}
74-
operation_to_gate: dict[str, Instruction] = {}
86+
operation_to_gate: dict[str, Instruction | type[Instruction]] = {}
7587

7688
# Process each canonical gate from Qiskit's standard library
77-
for canonical_name, gate_instance in canonical_gates.items():
89+
for canonical_name, gate in canonical_gates.items():
7890
# Get all names for this gate (canonical + aliases)
7991
all_names = {canonical_name}
8092
if canonical_name in gate_aliases:
@@ -83,7 +95,7 @@ def _build_gate_mappings_for_backend(
8395
# For each name, map it to all names (bidirectional aliases)
8496
for name in all_names:
8597
qiskit_to_qdmi[name] = all_names.copy()
86-
operation_to_gate[name] = gate_instance
98+
operation_to_gate[name] = gate
8799

88100
return qiskit_to_qdmi, operation_to_gate
89101

@@ -125,13 +137,25 @@ def is_convertible(device: fomac.Device) -> bool:
125137
"p": {"phase"}, # Phase gate can also be called 'phase'
126138
"r": {"prx"}, # R gate can also be called 'prx' (IQM naming)
127139
"u": {"u3"}, # U and U3 are the same gate
140+
"cu": {"cu3"}, # CU and CU3 are the same gate
128141
"cx": {"cnot"}, # CX and CNOT are the same gate
129142
"global_phase": {"gphase"}, # Qiskit canonical name
130143
"gphase": {"global_phase"}, # OpenQASM canonical name
144+
"mcphase": {"mcp"}, # Qiskit canonical name
145+
"mcp": {"mcphase"}, # OpenQASM canonical name
146+
}
147+
148+
_QDMI_TO_QISKIT_GATE_MAP: ClassVar[dict[str, str]] = {
149+
"i": "id",
150+
"prx": "r",
151+
"mcp": "mcphase",
152+
"u3": "u",
153+
"gphase": "global_phase",
154+
"cu3": "cu",
131155
}
132156

133157
_QISKIT_TO_QDMI_GATE_MAP: ClassVar[dict[str, set[str]]]
134-
_OPERATION_TO_GATE_MAP: ClassVar[dict[str, Instruction]]
158+
_OPERATION_TO_GATE_MAP: ClassVar[dict[str, Instruction | type[Instruction]]]
135159

136160
# Initialize derived mappings at class definition time
137161
_QISKIT_TO_QDMI_GATE_MAP, _OPERATION_TO_GATE_MAP = _build_gate_mappings_for_backend(_GATE_ALIASES)
@@ -203,13 +227,16 @@ def _build_target(self) -> Target:
203227
# Add operations from device
204228
for op in self._device.operations():
205229
# Map known operations to Qiskit gates
206-
op_name = op.name()
230+
op_name = op.name().lower()
207231

208232
# Skip control flow operations that don't belong in the Target
209233
# (barrier is handled separately by Qiskit, if_else is a circuit construct)
210-
if op_name.lower() in {"barrier", "if_else"}:
234+
if op_name in {"barrier", "if_else"}:
211235
continue
212236

237+
if op_name in self._QDMI_TO_QISKIT_GATE_MAP:
238+
op_name = self._QDMI_TO_QISKIT_GATE_MAP[op_name]
239+
213240
gate = self._map_operation_to_gate(op_name)
214241
if gate is None:
215242
warnings.warn(
@@ -219,15 +246,22 @@ def _build_target(self) -> Target:
219246
)
220247
continue
221248

249+
is_class = inspect.isclass(gate)
250+
222251
# Skip if we've already added this Qiskit gate to the target
223-
gate_name = gate.name
252+
gate_name = op_name if is_class else gate.name
224253
if gate_name in seen_gate_names:
225254
continue
226255
seen_gate_names.add(gate_name)
227256

228257
# Determine which qubits this operation applies to
229258
qargs = self._get_operation_qargs(op)
230259

260+
# Globally supported gates (such as MCX) must specify a name and no properties
261+
if is_class:
262+
target.add_instruction(gate, name=op_name)
263+
continue
264+
231265
# If qargs is [None], it means the operation is available on all qubits
232266
if qargs == [None]:
233267
# Create instruction properties
@@ -280,15 +314,15 @@ def _build_target(self) -> Target:
280314
# Check if the measurement operation is defined
281315
if "measure" not in seen_gate_names:
282316
warnings.warn(
283-
"Device does not define a measurement operation. This may limit practical usage.",
317+
f"{self._device.name()} does not define a measurement operation. This may limit practical usage.",
284318
UserWarning,
285319
stacklevel=2,
286320
)
287321

288322
return target
289323

290324
@staticmethod
291-
def _map_operation_to_gate(op_name: str) -> Instruction | None:
325+
def _map_operation_to_gate(op_name: str) -> Instruction | type[Instruction] | None:
292326
"""Map a device operation name to a Qiskit gate.
293327
294328
Args:

src/dd/Simulation.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ std::map<std::string, std::size_t> sample(const qc::QuantumComputation& qc,
109109
}
110110

111111
// correct permutation if necessary
112-
changePermutation(e, permutation, qc.outputPermutation, dd);
113-
e = dd.reduceGarbage(e, qc.getGarbage());
112+
if (!hasMeasurements) {
113+
changePermutation(e, permutation, qc.outputPermutation, dd);
114+
e = dd.reduceGarbage(e, qc.getGarbage());
115+
}
114116

115117
// measure all qubits
116118
std::map<std::string, std::size_t> counts{};
@@ -134,7 +136,7 @@ std::map<std::string, std::size_t> sample(const qc::QuantumComputation& qc,
134136
// measurement map specifies that the circuit `qubit` is measured into
135137
// a certain `bit`
136138
measurement[numBits - 1U - bit] =
137-
bitstring[bitstring.size() - 1U - qc.outputPermutation.at(qubit)];
139+
bitstring[bitstring.size() - 1U - permutation.at(qubit)];
138140
}
139141
} else {
140142
// otherwise, we consider the output permutation for determining where

0 commit comments

Comments
 (0)