Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning], with the exception that minor rel

### Added

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

### Changed

- ♻️ Build all builtin QDMI devices as shared libraries ([#1694]) ([**@burgholzer**])
Comment thread
burgholzer marked this conversation as resolved.
- ⬆️ Update the minimum supported Qiskit version to `1.1.0` ([#1694]) ([**@burgholzer**])
- ⬆️ Require LLVM 22.1 for C++ library builds ([#1549]) ([**@burgholzer**], [**@denialhaag**])
- 📦 Build MLIR by default for C++ library builds ([#1356]) ([**@burgholzer**], [**@denialhaag**])

### Fixed

- 🐛 Fix segfault in DD `sample` method when idle classical bits are present ([#1694]) ([**@burgholzer**])

### Removed

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

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

<!-- PR links -->

[#1694]: https://github.com/munich-quantum-toolkit/core/pull/1694
[#1676]: https://github.com/munich-quantum-toolkit/core/pull/1676
[#1675]: https://github.com/munich-quantum-toolkit/core/pull/1675
[#1674]: https://github.com/munich-quantum-toolkit/core/pull/1674
Expand Down
19 changes: 19 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ This document describes breaking changes and how to upgrade. For a complete list

## [Unreleased]

### Changes to builtin QDMI devices

The builtin QDMI devices (with prefixes `MQT_SC`, `MQT_NA`, and `MQT_DDSIM`) are now all built as shared libraries by default.
In turn, the shared library wrappers (with prefixes `MQT_SC_DYN` and `MQT_NA_DYN`) have been removed entirely.
MQT Core's QDMI driver will automatically load the shared libraries of the builtin devices if they are available in the library search path.
If you were previously using the statically builtin devices, no changes should be necessary as the shared libraries are now the default.
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.

### Broader operation support in QDMI Qiskit converter

The QDMI Qiskit converter now supports a broader range of operations, including multi-controlled gates such as `mcx`, `mcz`, `mcrx`, and more.
As a consequence, these operations can now be directly used without requiring decomposition, for example, with the builtin `DDSIM` QDMI device.

### Minimum supported Qiskit version

From this release onwards, MQT Core requires Qiskit version `1.1.0` or higher.
This is due to the fact that we are relying on some fixes to Qiskit primitives that were introduced in that version.
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.

### MLIR enabled by default for C++ builds

The MLIR-based functionality within MQT Core has long been experimental and opt-in.
Expand Down
14 changes: 12 additions & 2 deletions cmake/AddMQTCoreLibrary.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ function(kebab_to_camel output input)
endfunction()

function(add_mqt_core_library name)
cmake_parse_arguments(ARG "" "ALIAS_NAME" "" ${ARGN})
if(BUILD_MQT_CORE_SHARED_LIBS)
cmake_parse_arguments(ARG "FORCE_SHARED;HIDDEN_VISIBILITY" "ALIAS_NAME" "" ${ARGN})

if(ARG_FORCE_SHARED OR BUILD_MQT_CORE_SHARED_LIBS)
add_library(${name} SHARED ${ARG_UNPARSED_ARGUMENTS})
else()
add_library(${name} ${ARG_UNPARSED_ARGUMENTS})
endif()

if(NOT ARG_ALIAS_NAME)
# remove prefix 'mqt-' from target name if exists
string(REGEX REPLACE "^${MQT_CORE_TARGET_NAME}" "" ALIAS_NAME_ARG ${name})
Expand All @@ -41,6 +43,14 @@ function(add_mqt_core_library name)
# Add link libraries for warnings and options
target_link_libraries(${name} PRIVATE MQT::ProjectWarnings MQT::ProjectOptions)

if(ARG_HIDDEN_VISIBILITY)
set_target_properties(
${name}
PROPERTIES C_VISIBILITY_PRESET hidden
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN 1)
endif()

# Set versioning information
set_target_properties(
${name}
Expand Down
4 changes: 4 additions & 0 deletions cmake/PackageAddTest.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ macro(PACKAGE_ADD_TEST testname linklibs)
if(NOT TARGET ${testname})
# create an executable in which the tests will be stored
add_executable(${testname} ${ARGN})
# Ensure test executables remain runnable from the build tree during GoogleTest discovery
set_property(TARGET ${testname} PROPERTY BUILD_WITH_INSTALL_RPATH FALSE)
# link the Google test infrastructure and a default main function to the test executable.
target_link_libraries(${testname} PRIVATE ${linklibs} GTest::gmock GTest::gtest_main
MQT::ProjectOptions MQT::ProjectWarnings)
Expand All @@ -30,6 +32,8 @@ macro(PACKAGE_ADD_TEST_WITH_WORKING_DIR testname linklibs test_working_directory
if(NOT TARGET ${testname})
# create an executable in which the tests will be stored
add_executable(${testname} ${ARGN})
# Ensure test executables remain runnable from the build tree during GoogleTest discovery
set_property(TARGET ${testname} PROPERTY BUILD_WITH_INSTALL_RPATH FALSE)
# link the Google test infrastructure and a default main function to the test executable.
target_link_libraries(${testname} PRIVATE ${linklibs} GTest::gmock GTest::gtest_main
MQT::ProjectOptions MQT::ProjectWarnings)
Expand Down
5 changes: 3 additions & 2 deletions docs/qdmi/driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ mystnb:

## Objective

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/).
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/).
It is responsible for loading the device, forwarding requests from the client to the device, and sending back the results.
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.
MQT Core's QDMI Driver, {cpp:class}`qdmi::Driver`, comes with several preloaded devices that can be used directly.
Other devices can be loaded dynamically at runtime via {cpp:func}`qdmi::Driver::addDynamicDeviceLibrary`.

## Python Bindings
Expand All @@ -38,4 +38,5 @@ available_devices = session.get_devices()
# Print the name of every device
for device in available_devices:
print(device.name())

```
24 changes: 0 additions & 24 deletions include/mqt-core/qdmi/driver/Driver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,22 +146,6 @@ class DynamicDeviceLibrary final : public DeviceLibrary {
~DynamicDeviceLibrary() override;
};

// Macro to define a static library class that inherits from DeviceLibrary.
// It binds all device library functions to the functions of the static library.
// @param prefix is the prefix used for the function names in the library.
#define DECLARE_STATIC_LIBRARY(prefix) \
class prefix##DeviceLibrary final : public DeviceLibrary { \
public: \
prefix##DeviceLibrary(); \
\
~prefix##DeviceLibrary() override; \
};

// Call the above macro for all static libraries that we want to support.
DECLARE_STATIC_LIBRARY(MQT_NA)
DECLARE_STATIC_LIBRARY(MQT_DDSIM)
DECLARE_STATIC_LIBRARY(MQT_SC)

/**
* @brief The status of a session.
* @details This enum defines the possible states of a session in the QDMI
Expand All @@ -178,9 +162,6 @@ enum class SessionStatus : uint8_t {
*/
struct QDMI_Device_impl_d {
private:
// Since we treat this struct as a class, we apply also the naming scheme for
// classes, i.e., an underscore at the end of member names.

/**
* @brief The device library that provides the device interface functions.
* @note This must be a pointer type as we need access to dynamic and static
Expand Down Expand Up @@ -265,9 +246,6 @@ struct QDMI_Device_impl_d {
*/
struct QDMI_Job_impl_d {
private:
// Since we treat this struct as a class, we apply also the naming scheme for
// classes, i.e., an underscore at the end of member names.

/// @brief The device job handle.
QDMI_Device_Job deviceJob_ = nullptr;
/// @brief The device associated with the job.
Expand Down Expand Up @@ -350,8 +328,6 @@ struct QDMI_Job_impl_d {
/**
* @brief Definition of the QDMI Session.
*/
// Since we treat this struct as a class, we apply also the naming scheme for
// classes, i.e., an underscore at the end of member names.
struct QDMI_Session_impl_d {
private:
/// @brief The status of the session.
Expand Down
3 changes: 2 additions & 1 deletion json/sc/device.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
],
"operations": [
{ "name": "r", "numParameters": 2, "numQubits": 1 },
{ "name": "cz", "numParameters": 0, "numQubits": 2 }
{ "name": "cz", "numParameters": 0, "numQubits": 2 },
{ "name": "measure", "numParameters": 0, "numQubits": 1 }
]
}
8 changes: 3 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ dynamic = ["version"]

[project.optional-dependencies]
qiskit = [
"qiskit[qasm3-import]>=1.0.0",
"qiskit[qasm3-import]>=1.1.0",
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[project.scripts]
Expand Down Expand Up @@ -97,9 +97,7 @@ build.targets = [
"mqt-core-na-bindings",
"mqt-core-qdmi-ddsim-device",
"mqt-core-qdmi-na-device",
"mqt-core-qdmi-na-device-dyn",
"mqt-core-qdmi-sc-device",
"mqt-core-qdmi-sc-device-dyn",
]

install.components = [
Expand Down Expand Up @@ -344,7 +342,7 @@ docs = [
"sphinxcontrib-bibtex>=2.6.5",
"sphinxcontrib-svg2pdfconverter>=1.3.0",
"sphinxext-opengraph>=0.13.0",
"qiskit[qasm3-import,visualization]>=1.0.0",
"qiskit[qasm3-import,visualization]>=1.1.0",
"openqasm-pygments>=0.2.0",
"breathe>=4.36.0",
"graphviz>=0.21.0",
Expand All @@ -358,7 +356,7 @@ test = [
"pytest-cov>=7.0.0",
"pytest-sugar>=1.1.1",
"pytest-xdist>=3.8.0",
"qiskit[qasm3-import]>=1.0.0",
"qiskit[qasm3-import]>=1.1.0",
"numpy>=2.1; python_version >= '3.13'",
"numpy>=2.3.2; python_version >= '3.14'",
]
Expand Down
56 changes: 45 additions & 11 deletions python/mqt/core/plugins/qiskit/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@

from __future__ import annotations

import inspect
import itertools
import warnings
from typing import TYPE_CHECKING, Any, ClassVar

from qiskit import qasm2, qasm3
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import get_standard_gate_name_mapping
from qiskit.circuit.library import (
MCPhaseGate,
MCXGate,
get_standard_gate_name_mapping,
)
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import InstructionProperties, Target

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

def _build_gate_mappings_for_backend(
gate_aliases: dict[str, set[str]],
) -> tuple[dict[str, set[str]], dict[str, Instruction]]:
) -> tuple[dict[str, set[str]], dict[str, Instruction | type[Instruction]]]:
"""Build both forward (Qiskit→QDMI) and inverse (QDMI→Gate) mappings.

Uses Qiskit's standard gate mapping as the canonical source of truth,
Expand All @@ -70,11 +75,18 @@ def _build_gate_mappings_for_backend(
# Get Qiskit's standard gate name mapping as our canonical source
canonical_gates = get_standard_gate_name_mapping()

# Augment the canonical mapping with any additional gates that may not be in Qiskit's standard library
canonical_gates.update({
"mcx": MCXGate,
"mcphase": MCPhaseGate,
"mcp": MCPhaseGate,
})

qiskit_to_qdmi: dict[str, set[str]] = {}
operation_to_gate: dict[str, Instruction] = {}
operation_to_gate: dict[str, Instruction | type[Instruction]] = {}

# Process each canonical gate from Qiskit's standard library
for canonical_name, gate_instance in canonical_gates.items():
for canonical_name, gate in canonical_gates.items():
# Get all names for this gate (canonical + aliases)
all_names = {canonical_name}
if canonical_name in gate_aliases:
Expand All @@ -83,7 +95,7 @@ def _build_gate_mappings_for_backend(
# For each name, map it to all names (bidirectional aliases)
for name in all_names:
qiskit_to_qdmi[name] = all_names.copy()
operation_to_gate[name] = gate_instance
operation_to_gate[name] = gate

return qiskit_to_qdmi, operation_to_gate

Expand Down Expand Up @@ -125,13 +137,25 @@ def is_convertible(device: fomac.Device) -> bool:
"p": {"phase"}, # Phase gate can also be called 'phase'
"r": {"prx"}, # R gate can also be called 'prx' (IQM naming)
"u": {"u3"}, # U and U3 are the same gate
"cu": {"cu3"}, # CU and CU3 are the same gate
"cx": {"cnot"}, # CX and CNOT are the same gate
"global_phase": {"gphase"}, # Qiskit canonical name
"gphase": {"global_phase"}, # OpenQASM canonical name
"mcphase": {"mcp"}, # Qiskit canonical name
"mcp": {"mcphase"}, # OpenQASM canonical name
}

_QDMI_TO_QISKIT_GATE_MAP: ClassVar[dict[str, str]] = {
"i": "id",
"prx": "r",
"mcp": "mcphase",
"u3": "u",
"gphase": "global_phase",
"cu3": "cu",
}

_QISKIT_TO_QDMI_GATE_MAP: ClassVar[dict[str, set[str]]]
_OPERATION_TO_GATE_MAP: ClassVar[dict[str, Instruction]]
_OPERATION_TO_GATE_MAP: ClassVar[dict[str, Instruction | type[Instruction]]]

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

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

if op_name in self._QDMI_TO_QISKIT_GATE_MAP:
op_name = self._QDMI_TO_QISKIT_GATE_MAP[op_name]

gate = self._map_operation_to_gate(op_name)
if gate is None:
warnings.warn(
Expand All @@ -219,15 +246,22 @@ def _build_target(self) -> Target:
)
continue

is_class = inspect.isclass(gate)

# Skip if we've already added this Qiskit gate to the target
gate_name = gate.name
gate_name = op_name if is_class else gate.name
if gate_name in seen_gate_names:
continue
seen_gate_names.add(gate_name)

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

# Globally supported gates (such as MCX) must specify a name and no properties
if is_class:
target.add_instruction(gate, name=op_name)
continue

# If qargs is [None], it means the operation is available on all qubits
if qargs == [None]:
# Create instruction properties
Expand Down Expand Up @@ -280,15 +314,15 @@ def _build_target(self) -> Target:
# Check if the measurement operation is defined
if "measure" not in seen_gate_names:
warnings.warn(
"Device does not define a measurement operation. This may limit practical usage.",
f"{self._device.name()} does not define a measurement operation. This may limit practical usage.",
UserWarning,
stacklevel=2,
)

return target

@staticmethod
def _map_operation_to_gate(op_name: str) -> Instruction | None:
def _map_operation_to_gate(op_name: str) -> Instruction | type[Instruction] | None:
"""Map a device operation name to a Qiskit gate.

Args:
Expand Down
8 changes: 5 additions & 3 deletions src/dd/Simulation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ std::map<std::string, std::size_t> sample(const qc::QuantumComputation& qc,
}

// correct permutation if necessary
changePermutation(e, permutation, qc.outputPermutation, dd);
e = dd.reduceGarbage(e, qc.getGarbage());
if (!hasMeasurements) {
changePermutation(e, permutation, qc.outputPermutation, dd);
e = dd.reduceGarbage(e, qc.getGarbage());
}

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