Skip to content

Commit

Permalink
✨ Portable Layout Information (#76)
Browse files Browse the repository at this point in the history
* ✨ return `QuantumCircuit` if Qiskit is available (including Layout information)
* 🙈 add virtual environments to .gitignore file
* 🔧🐍 make `qiskit-terra` a project dependency
* 🚨 fix LGTM warning
* ♻️ replace soon to be deprecated `qiskit.test.mock` imports
* 📝 update documentation
* ⬆️ QFR 📦 with a 🐛 fix for Qiskit `Layout` import
* 🐛 fix output permutation bug in heuristic mapper
* ⬆️ QFR 📦 with a 🐛 fix for the `cancelCNOTs` optimization
* ✅ add tests verifying the correctness of the circuits produced by both mappers
  • Loading branch information
burgholzer authored Jun 25, 2022
1 parent e6475a1 commit 108dd52
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 71 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@ var/
.installed.cfg
*.egg
.pytest_cache/

.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ MQT QMAP is developed as a C++ library with an easy to use Python interface.
- Once installed, start using it in Python:
```python
from mqt import qmap
results = qmap.compile(circ, arch)
circ_mapped, results = qmap.compile(circ, arch)
```

where `circ` is either a Qiskit `QuantumCircuit` object or the path to an input file (in any of the formats listed above)
and `arch` is either

- a Qiskit `Backend` instance such as those defined under `qiskit.test.mock` **(recommended)**,
- a Qiskit `Backend` instance such as those defined under `qiskit.providers.fake_provider` **(recommended)**,
- one of the pre-defined architectures (see below), or
- the path to a file containing the number of qubits and a line-by-line enumeration of the qubit connections.

Expand Down
2 changes: 1 addition & 1 deletion extern/qfr
Submodule qfr updated from 92bbd8 to 2c0c3d
2 changes: 1 addition & 1 deletion jkq/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import warnings
from mqt import *
from mqt import qmap

warnings.simplefilter('always', DeprecationWarning)
warnings.warn('Usage via `import jkq` is deprecated in favor of the new prefix. Please use `import mqt` instead.', DeprecationWarning)
101 changes: 60 additions & 41 deletions mqt/qmap/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,42 @@
#
import pickle
from pathlib import Path
from typing import Union, Optional, Set
from typing import Union, Optional, Set, List, Tuple

from qiskit import QuantumCircuit, QuantumRegister
from qiskit.providers import Backend
from qiskit.providers.models import BackendProperties
from qiskit.transpiler.target import Target
from qiskit.transpiler import Layout

from mqt.qmap.pyqmap import map, Method, InitialLayout, Layering, Arch, Encoding, CommanderGrouping, SwapReduction, Configuration, MappingResults, Architecture

try:
from qiskit.providers import Backend
from qiskit.providers.models import BackendProperties
from qiskit.transpiler.target import Target

PossibleArchitectureTypes = Union[str, Arch, Architecture, Backend]
PossibleCalibrationTypes = Union[str, BackendProperties, Target]
except ModuleNotFoundError:
PossibleArchitectureTypes = Union[str, Arch, Architecture]
PossibleCalibrationTypes = Union[str]
def extract_initial_layout_from_qasm(qasm: str, qregs: List[QuantumRegister]) -> Layout:
"""
Extracts the initial layout resulting from compiling a circuit from a QASM file.
:param qasm: QASM file
:type qasm: str
:param qregs: The quantum registers to apply the layout to.
:type qregs: List[QuantumRegister]
:return: layout to be used in Qiskit
"""
for line in qasm.split("\n"):
if line.startswith("// i "):
# strip away initial part of line
line = line[5:]
# split line into tokens
tokens = line.split(" ")
# convert tokens to integers
tokens = [int(token) for token in tokens]
# create an empty layout
layout = Layout().from_intlist(tokens, *qregs)
return layout


def compile(circ, arch: Optional[PossibleArchitectureTypes],
calibration: Optional[PossibleCalibrationTypes] = None,
def compile(circ: Union[QuantumCircuit, str],
arch: Optional[Union[str, Arch, Architecture, Backend]],
calibration: Optional[Union[str, BackendProperties, Target]] = None,
method: Union[str, Method] = "heuristic",
initial_layout: Union[str, InitialLayout] = "dynamic",
layering: Union[str, Layering] = "individual_gates",
Expand All @@ -38,14 +57,15 @@ def compile(circ, arch: Optional[PossibleArchitectureTypes],
pre_mapping_optimizations: bool = True,
post_mapping_optimizations: bool = True,
verbose: bool = False
) -> MappingResults:
) -> Tuple[QuantumCircuit, MappingResults]:
"""Interface to the MQT QMAP tool for mapping quantum circuits
:param circ: Path to circuit file, path to Qiskit QuantumCircuit pickle, or Qiskit QuantumCircuit object
:param circ: Qiskit QuantumCircuit object, path to circuit file, or path to Qiskit QuantumCircuit pickle
:type circ: Union[QuantumCircuit, str]
:param arch: Architecture to map to. Either a path to a file with architecture information, one of the available architectures (Arch), qmap.Architecture, or `qiskit.providers.backend` (if Qiskit is installed)
:type arch: Optional[PossibleArchitectureTypes]
:type arch: Optional[Union[str, Arch, Architecture, Backend]]
:param calibration: Path to file containing calibration information, `qiskit.providers.models.BackendProperties` object (if Qiskit is installed), or `qiskit.transpiler.target.Target` object (if Qiskit is installed)
:type calibration: Optional[PossibleCalibrationTypes]
:type calibration: Optional[Union[str, BackendProperties, Target]]
:param method: Mapping technique to use (*heuristic* | exact)
:type method: Union[str, Method]
:param initial_layout: Strategy to use for determining initial layout in heuristic mapper (identity | static | *dynamic*)
Expand Down Expand Up @@ -77,12 +97,13 @@ def compile(circ, arch: Optional[PossibleArchitectureTypes],
:type post_mapping_optimizations: bool
:param verbose: Print more detailed information during the mapping process
:type verbose: bool
:return: Object containing all the results
:rtype: MappingResults
:return: Mapped circuit (as Qiskit `QuantumCircuit`) and results
:rtype: Tuple[QuantumCircuit, MappingResults]
"""

if subgraph is None:
subgraph = set()

if type(circ) == str and Path(circ).suffix == '.pickle':
circ = pickle.load(open(circ, "rb"))

Expand All @@ -100,34 +121,26 @@ def compile(circ, arch: Optional[PossibleArchitectureTypes],
architecture.load_coupling_map(arch)
elif isinstance(arch, Architecture):
architecture = arch
elif isinstance(arch, Backend):
from mqt.qmap.qiskit.backend import import_backend

architecture = import_backend(arch)
else:
try:
from qiskit.providers.backend import Backend
from mqt.qmap.qiskit.backend import import_backend
if isinstance(arch, Backend):
architecture = import_backend(arch)
else:
raise ValueError("No compatible type for architecture:", type(arch))
except ModuleNotFoundError:
raise ValueError("No compatible type for architecture:", type(arch))
raise ValueError("No compatible type for architecture:", type(arch))

if calibration is not None:
if type(calibration) == str:
architecture.load_properties(calibration)
elif isinstance(calibration, BackendProperties):
from mqt.qmap.qiskit.backend import import_backend_properties

architecture.load_properties(import_backend_properties(calibration))
elif isinstance(calibration, Target):
from mqt.qmap.qiskit.backend import import_target

architecture.load_properties(import_target(calibration))
else:
try:
from qiskit.providers.models import BackendProperties
from qiskit.transpiler.target import Target
from mqt.qmap.qiskit.backend import import_backend_properties, import_target

if isinstance(calibration, BackendProperties):
architecture.load_properties(import_backend_properties(calibration))
elif isinstance(calibration, Target):
architecture.load_properties(import_target(calibration))
else:
raise ValueError("No compatible type for calibration:", type(calibration))
except ModuleNotFoundError:
raise ValueError("No compatible type for calibration:", type(calibration))
raise ValueError("No compatible type for calibration:", type(calibration))

config = Configuration()
config.method = Method(method)
Expand All @@ -148,4 +161,10 @@ def compile(circ, arch: Optional[PossibleArchitectureTypes],
config.post_mapping_optimizations = post_mapping_optimizations
config.verbose = verbose

return map(circ, architecture, config)
results = map(circ, architecture, config)

circ = QuantumCircuit.from_qasm_str(results.mapped_circuit)
layout = extract_initial_layout_from_qasm(results.mapped_circuit, circ.qregs)
circ._layout = layout

return circ, results
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ build = "cp3*"
archs = "auto64"
skip = "*-musllinux*"
test-skip = "*-macosx_arm64 *-musllinux* *aarch64"
test-extras = ["tests"]
test-extras = ["test"]
test-command = "python -m pytest {project}/test/python"
environment = { DEPLOY = "ON" }
build-frontend = "build"
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ def build_extension(self, ext):
cmdclass={"build_ext": CMakeBuild},
zip_safe=False,
packages=find_namespace_packages(include=['mqt.*']),
install_requires=["qiskit-terra~=0.20.2"],
extras_require={
"tests": ["pytest~=7.1.1", "qiskit-terra>=0.19.2,<0.21.0"],
"test": ["pytest~=7.1.1", "mqt.qcec~=2.0.0rc4"],
"dev": ["mqt.qmap[test]"] # requires Pip 21.2 or newer
},
classifiers=[
'Development Status :: 5 - Production/Stable',
Expand Down
24 changes: 12 additions & 12 deletions src/heuristic/HeuristicMapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ void HeuristicMapper::map(const Configuration& ms) {
}
}

// infer output permutation from qubit locations
qcMapped.outputPermutation.clear();
std::size_t count = 0U;
for (std::size_t i = 0U; i < architecture.getNqubits(); ++i) {
if (qubits[i] != -1) {
qcMapped.outputPermutation[static_cast<dd::Qubit>(i)] = static_cast<dd::Qubit>(qubits[i]);
} else {
qcMapped.setLogicalQubitGarbage(qc.getNqubits() + count);
++count;
}
}

// fix single qubit gates
if (!gatesToAdjust.empty()) {
gateidx--; // index of last operation
Expand Down Expand Up @@ -153,18 +165,6 @@ void HeuristicMapper::map(const Configuration& ms) {
}
}

// infer output permutation from qubit locations
qcMapped.outputPermutation.clear();
std::size_t count = 0U;
for (std::size_t i = 0U; i < architecture.getNqubits(); ++i) {
if (qubits[i] != -1) {
qcMapped.outputPermutation[static_cast<dd::Qubit>(i)] = static_cast<dd::Qubit>(qubits[i]);
} else {
qcMapped.setLogicalQubitGarbage(qc.getNqubits() + count);
++count;
}
}

postMappingOptimizations(config);
countGates(qcMapped, results.output);
finalizeMappedCircuit();
Expand Down
7 changes: 2 additions & 5 deletions test/python/test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from qiskit import QuantumCircuit
from qiskit.test.mock.backends import FakeLondon
from qiskit.providers.fake_provider import FakeLondon

from mqt import qmap

Expand All @@ -12,8 +12,5 @@
print(qc.draw(fold=-1))

# compile the circuit
results = qmap.compile(qc, arch=FakeLondon())

# get the mapped circuit
qc_mapped = QuantumCircuit.from_qasm_str(results.mapped_circuit)
qc_mapped, results = qmap.compile(qc, arch=FakeLondon())
print(qc_mapped.draw(fold=-1))
60 changes: 60 additions & 0 deletions test/python/test_exact_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pytest
from qiskit import QuantumCircuit
from qiskit.providers.fake_provider import FakeLondon

from mqt import qmap, qcec


def test_exact_no_swaps_trivial_layout():
"""Verify that the exact mapper works on a simple circuit that requires no swaps on a trivial initial layout."""
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.measure_all()

qc_mapped, results = qmap.compile(qc, arch=FakeLondon(), method="exact")
assert results.timeout is False
assert results.mapped_circuit != ""
assert results.output.swaps == 0

result = qcec.verify(qc, qc_mapped)
assert result.considered_equivalent() is True


def test_exact_no_swaps_non_trivial_layout():
"""Verify that the exact mapper works on a simple circuit that requires a non-trivial layout to achieve no swaps."""
qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.cx(0, 3)
qc.measure_all()

qc_mapped, results = qmap.compile(qc, arch=FakeLondon(), method="exact")

assert results.timeout is False
assert results.mapped_circuit != ""
assert results.output.swaps == 0

result = qcec.verify(qc, qc_mapped)
assert result.considered_equivalent() is True


def test_exact_non_trivial_swaps():
"""Verify that the exact mapper works on a simple circuit that requires at least a single SWAP."""
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.cx(2, 0)
qc.measure_all()

qc_mapped, results = qmap.compile(qc, arch=FakeLondon(), method="exact")

assert results.timeout is False
assert results.mapped_circuit != ""
assert results.output.swaps == 1

result = qcec.verify(qc, qc_mapped)
assert result.considered_equivalent() is True
67 changes: 67 additions & 0 deletions test/python/test_heuristic_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest
from qiskit import QuantumCircuit
from qiskit.providers.fake_provider import FakeLondon

from mqt import qmap, qcec


def test_heuristic_no_swaps_trivial_layout():
"""Verify that the heuristic mapper works on a simple circuit that requires no swaps on a trivial initial layout."""
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.measure_all()

qc_mapped, results = qmap.compile(qc, arch=FakeLondon())
assert results.timeout is False
assert results.mapped_circuit != ""
# assert results.output.swaps == 0

result = qcec.verify(qc, qc_mapped)
assert result.considered_equivalent() is True


def test_heuristic_no_swaps_non_trivial_layout():
"""Verify that the heuristic mapper works on a simple circuit that requires a non-trivial layout to achieve no swaps."""
qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.cx(0, 3)
qc.measure_all()

qc_mapped, results = qmap.compile(qc, arch=FakeLondon())

assert results.timeout is False
assert results.mapped_circuit != ""
# assert results.output.swaps == 0

result = qcec.verify(qc, qc_mapped)
assert result.considered_equivalent() is True


def test_heuristic_non_trivial_swaps():
"""Verify that the heuristic mapper works on a simple circuit that requires at least a single SWAP."""
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.cx(2, 0)
qc.measure_all()

qc_mapped, results = qmap.compile(qc, arch=FakeLondon())

assert results.timeout is False
assert results.mapped_circuit != ""
assert results.output.swaps == 1

print('\n')
print(qc_mapped)

config = qcec.Configuration()
config.execution.run_alternating_checker = False
result = qcec.verify(qc, qc_mapped, config=config)
print(result)

assert result.considered_equivalent() is True
Loading

0 comments on commit 108dd52

Please sign in to comment.