Skip to content

Commit b3c501d

Browse files
committed
♻️ make qiskit an optional dependency
Signed-off-by: burgholzer <burgholzer@me.com>
1 parent 156b9ce commit b3c501d

8 files changed

+605
-457
lines changed

docs/source/library/VerifyCompilation.rst

+1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ Compilation Flow Profile Generation
2222
QCEC provides dedicated compilation flow profiles for IBM Qiskit which can be used to efficiently verify the results of compilation flow results :cite:p:`burgholzer2020verifyingResultsIBM`.
2323
These profiles are generated from IBM Qiskit using the :func:`.generate_profile` method.
2424

25+
.. currentmodule:: mqt.qcec.compilation_flow_profiles
2526
.. autofunction:: generate_profile

pyproject.toml

+9-4
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ dependencies = [
4343
"mqt.core>=3.0.0b4",
4444
"importlib_resources>=5.0; python_version < '3.10'",
4545
"typing_extensions>=4.2; python_version < '3.11'", # used for typing.Unpack
46-
"qiskit[qasm3-import]>=1.0.0",
46+
"numpy>=2.1; python_version >= '3.13'",
47+
"numpy>=1.26; python_version >= '3.12'",
48+
"numpy>=1.24; python_version >= '3.11'",
49+
"numpy>=1.22",
4750
]
4851
dynamic = ["version"]
4952

53+
[project.optional-dependencies]
54+
qiskit = ["qiskit[qasm3-import]>=1.0.0"]
55+
5056
[project.urls]
5157
Homepage = "https://github.com/cda-tum/mqt-qcec"
5258
Documentation = "https://mqt.readthedocs.io/projects/qcec"
@@ -310,7 +316,7 @@ build = [
310316
]
311317
docs = [
312318
"furo>=2024.8.6",
313-
"qiskit[visualization]>=1.0.0",
319+
"qiskit[qasm3-import,visualization]>=1.0.0",
314320
"setuptools-scm>=8.1",
315321
"sphinx-autoapi>=3.4.0",
316322
"sphinx-copybutton>=0.5.2",
@@ -321,12 +327,11 @@ docs = [
321327
"ipykernel>=6.29.5",
322328
"nbsphinx>=0.9.6",
323329
"sphinx-autodoc-typehints>=2.3.0",
324-
"mqt-core[qiskit]>=3.0.0b1",
325330
]
326331
test = [
327332
"pytest>=8.3.4",
328333
"pytest-cov>=6",
329-
"mqt-core[qiskit]>=3.0.0b1",
334+
"qiskit[qasm3-import]>=1.0.0",
330335
]
331336
dev = [
332337
{include-group = "build"},

src/mqt/qcec/__init__.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from __future__ import annotations
88

99
from ._version import version as __version__
10-
from .compilation_flow_profiles import AncillaMode, generate_profile
10+
from .compilation_flow_profiles import AncillaMode
1111
from .pyqcec import (
1212
ApplicationScheme,
1313
Configuration,
@@ -26,7 +26,6 @@
2626
"EquivalenceCriterion",
2727
"StateType",
2828
"__version__",
29-
"generate_profile",
3029
"verify",
3130
"verify_compilation",
3231
]

src/mqt/qcec/_compat/optional.py

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Optional dependency tester.
2+
3+
Inspired by Qiskit's LazyDependencyManager
4+
https://github.com/Qiskit/qiskit/blob/f13673b05edf98263f80a174d2e13a118b4acda7/qiskit/utils/lazy_tester.py#L44
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import contextlib
10+
import importlib
11+
import typing
12+
import warnings
13+
14+
15+
class OptionalDependencyTester:
16+
"""A manager for optional dependencies to assert their availability.
17+
18+
This class is used to lazily test for the availability of optional dependencies.
19+
It can be used in Boolean contexts to check if the dependency is available.
20+
"""
21+
22+
def __init__(self, module: str, *, msg: str | None = None) -> None:
23+
"""Construct a new optional dependency tester.
24+
25+
Args:
26+
module: the name of the module to test for.
27+
msg: an extra message to include in the error raised if this is required.
28+
"""
29+
self._module = module
30+
self._bool: bool | None = None
31+
self._msg = msg
32+
33+
def _is_available(self) -> bool:
34+
"""Test the availability of the module.
35+
36+
Returns:
37+
``True`` if the module is available, ``False`` otherwise.
38+
"""
39+
try:
40+
importlib.import_module(self._module)
41+
except ImportError as exc: # pragma: no cover
42+
warnings.warn(
43+
f"Module '{self._module}' failed to import with: {exc!r}",
44+
category=UserWarning,
45+
stacklevel=2,
46+
)
47+
return False
48+
else:
49+
return True
50+
51+
def __bool__(self) -> bool:
52+
"""Check if the dependency is available.
53+
54+
Returns:
55+
``True`` if the dependency is available, ``False`` otherwise.
56+
"""
57+
if self._bool is None:
58+
self._bool = self._is_available()
59+
return self._bool
60+
61+
def require_now(self, feature: str) -> None:
62+
"""Eagerly attempt to import the dependency and raise an exception if it cannot be imported.
63+
64+
Args:
65+
feature: the feature that is requiring this dependency.
66+
67+
Raises:
68+
ImportError: if the dependency cannot be imported.
69+
"""
70+
if self:
71+
return
72+
message = f"The '{self._module}' library is required to {feature}."
73+
if self._msg:
74+
message += f" {self._msg}."
75+
raise ImportError(message)
76+
77+
@contextlib.contextmanager
78+
def disable_locally(self) -> typing.Generator[None, None, None]:
79+
"""Create a context during which the value of the dependency manager will be ``False``.
80+
81+
Yields:
82+
None
83+
"""
84+
previous = self._bool
85+
self._bool = False
86+
try:
87+
yield
88+
finally:
89+
self._bool = previous
90+
91+
92+
HAS_QISKIT = OptionalDependencyTester(
93+
"qiskit",
94+
msg="Please install the `mqt.qcec[qiskit]` extra or a compatible version of Qiskit to use functionality related to its functionality.",
95+
)

src/mqt/qcec/compilation_flow_profiles.py

+36-26
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
from typing import TYPE_CHECKING, Any
88

99
import numpy as np
10-
from qiskit import QuantumCircuit, transpile
11-
from qiskit import __version__ as qiskit_version
10+
11+
from ._compat.optional import HAS_QISKIT
1212

1313
if TYPE_CHECKING:
1414
from numpy.typing import NDArray
15+
from qiskit.circuit import QuantumCircuit
1516

1617

1718
@unique
@@ -154,8 +155,10 @@ def __str__(self) -> str:
154155
multi_controlled_gates_v_chain = [mcx_v_chain]
155156

156157

157-
def create_general_gate(qubits: int, params: int, controls: int, identifier: str) -> QuantumCircuit:
158+
def __create_general_gate(qubits: int, params: int, controls: int, identifier: str) -> QuantumCircuit:
158159
"""Create a ``QuantumCircuit`` containing a single gate ``identifier`` with the given number of ``qubits``, ``params``, and ``controls``."""
160+
from qiskit.circuit import QuantumCircuit
161+
159162
required_qubits = qubits + controls
160163
qc = QuantumCircuit(required_qubits)
161164
gate_identifier = "c" * controls + identifier
@@ -167,7 +170,7 @@ def create_general_gate(qubits: int, params: int, controls: int, identifier: str
167170
return qc
168171

169172

170-
def create_multi_controlled_gate(
173+
def __create_multi_controlled_gate(
171174
qubits: int,
172175
params: int,
173176
controls: int,
@@ -176,6 +179,8 @@ def create_multi_controlled_gate(
176179
identifier: str,
177180
) -> QuantumCircuit:
178181
"""Create a ``QuantumCircuit`` containing a single multi-controlled gate ``identifier`` with the given number of ``qubits``, ``params``, and ``controls`` using ``ancilla_qubits`` ancilla qubits and the given ancilla ``mode``."""
182+
from qiskit.circuit import QuantumCircuit
183+
179184
required_qubits = qubits + controls
180185

181186
# special handling for v-chain mode which is indicated by the ancilla_qubits being None
@@ -208,12 +213,14 @@ def create_multi_controlled_gate(
208213
return qc
209214

210215

211-
def compute_cost(
216+
def __compute_cost(
212217
qc: QuantumCircuit,
213218
basis_gates: list[str],
214219
optimization_level: int = 1,
215220
) -> int:
216221
"""Compute the cost of a circuit by transpiling the circuit to a given ``basis_gates`` gate set and a certain ``optimization_level``."""
222+
from qiskit import transpile
223+
217224
transpiled_circuit = transpile(
218225
qc, basis_gates=basis_gates, optimization_level=optimization_level, seed_transpiler=12345
219226
)
@@ -228,7 +235,7 @@ class GateType(Enum):
228235
MULTI_CONTROLLED = 2
229236

230237

231-
def create_gate_profile_data(
238+
def __create_gate_profile_data(
232239
gate_collection: list[dict[str, Any]],
233240
gate_type: GateType,
234241
basis_gates: list[str] | None = None,
@@ -254,9 +261,9 @@ def create_gate_profile_data(
254261
qc = None
255262
# create the gate
256263
if gate_type == GateType.GENERAL:
257-
qc = create_general_gate(qubits, params, control, gate)
264+
qc = __create_general_gate(qubits, params, control, gate)
258265
elif gate_type == GateType.MULTI_CONTROLLED:
259-
qc = create_multi_controlled_gate(
266+
qc = __create_multi_controlled_gate(
260267
qubits,
261268
params,
262269
control,
@@ -265,14 +272,14 @@ def create_gate_profile_data(
265272
gate,
266273
)
267274
# compute the cost
268-
cost = compute_cost(qc, basis_gates, optimization_level)
275+
cost = __compute_cost(qc, basis_gates, optimization_level)
269276

270277
# add the cost to the profile data
271278
profile_data[gate, control] = cost
272279
return profile_data
273280

274281

275-
def add_special_case_data(
282+
def __add_special_case_data(
276283
profile_data: dict[tuple[str, int], int],
277284
special_cases: dict[str, Any] | None = None,
278285
) -> None:
@@ -292,19 +299,17 @@ def add_special_case_data(
292299
profile_data.setdefault((gate, nc), cost)
293300

294301

295-
def generate_profile_name(optimization_level: int = 1, mode: AncillaMode = AncillaMode.NO_ANCILLA) -> str:
296-
"""Generate a profile name based on the given optimization level and ancilla mode."""
297-
return "qiskit_O" + str(optimization_level) + "_" + str(mode) + ".profile"
298-
299-
300-
def write_profile_data_to_file(profile_data: dict[tuple[str, int], int], filename: Path) -> None:
302+
def __write_profile_data_to_file(profile_data: dict[tuple[str, int], int], filename: Path) -> None:
301303
"""Write the profile data to a file."""
304+
HAS_QISKIT.require_now("generate compilation flow profiles")
305+
from qiskit import __version__ as qiskit_version
306+
302307
with Path(filename).open("w+", encoding="utf-8") as f:
303308
f.write(f"# {filename}, Qiskit version: {qiskit_version}\n")
304309
f.writelines(f"{gate} {controls} {cost}\n" for (gate, controls), cost in profile_data.items())
305310

306311

307-
def check_recurrence(seq: list[int], order: int = 2) -> list[int] | None:
312+
def __check_recurrence(seq: list[int], order: int = 2) -> list[int] | None:
308313
"""Determine a recurrence relation with a given ``order`` in ``sequence`` and return the corresponding coefficients or ``None`` if no relation was determined."""
309314
if len(seq) < (2 * order + 1):
310315
return None
@@ -325,7 +330,7 @@ def check_recurrence(seq: list[int], order: int = 2) -> list[int] | None:
325330
return list(coefficients)
326331

327332

328-
def find_continuation(
333+
def __find_continuation(
329334
profile_data: dict[tuple[str, int], int],
330335
gate: str,
331336
cutoff: int = 5,
@@ -345,7 +350,7 @@ def find_continuation(
345350

346351
coeffs = None
347352
for order in range(1, max_order + 1):
348-
coeffs = check_recurrence(sequence, order)
353+
coeffs = __check_recurrence(sequence, order)
349354
if coeffs is not None:
350355
break
351356

@@ -371,6 +376,11 @@ def find_continuation(
371376
default_profile_path = Path(__file__).resolve().parent.joinpath("profiles")
372377

373378

379+
def generate_profile_name(optimization_level: int = 1, mode: AncillaMode = AncillaMode.NO_ANCILLA) -> str:
380+
"""Generate a profile name based on the given optimization level and ancilla mode."""
381+
return "qiskit_O" + str(optimization_level) + "_" + str(mode) + ".profile"
382+
383+
374384
def generate_profile(
375385
optimization_level: int = 1,
376386
mode: AncillaMode = AncillaMode.NO_ANCILLA,
@@ -392,34 +402,34 @@ def generate_profile(
392402
filepath = default_profile_path
393403

394404
# generate general profile data
395-
profile = create_gate_profile_data(general_gates, GateType.GENERAL, optimization_level=optimization_level)
405+
profile = __create_gate_profile_data(general_gates, GateType.GENERAL, optimization_level=optimization_level)
396406

397407
# add multi-controlled gates
398408
profile.update(
399-
create_gate_profile_data(
409+
__create_gate_profile_data(
400410
multi_controlled_gates,
401411
GateType.MULTI_CONTROLLED,
402412
optimization_level=optimization_level,
403413
)
404414
)
405-
find_continuation(profile, gate="p", max_control=max_controls)
415+
__find_continuation(profile, gate="p", max_control=max_controls)
406416

407417
gate_collection = gate_collection_for_mode[mode]
408418

409419
# add multi-controlled gates with specific mode
410420
profile.update(
411-
create_gate_profile_data(
421+
__create_gate_profile_data(
412422
gate_collection,
413423
GateType.MULTI_CONTROLLED,
414424
optimization_level=optimization_level,
415425
)
416426
)
417-
find_continuation(profile, gate="x", max_control=max_controls)
427+
__find_continuation(profile, gate="x", max_control=max_controls)
418428

419429
# add special case data
420-
add_special_case_data(profile)
430+
__add_special_case_data(profile)
421431

422432
# write profile data to file
423433
filename = generate_profile_name(optimization_level=optimization_level, mode=mode)
424434
filepath = filepath.joinpath(filename)
425-
write_profile_data_to_file(profile, filepath)
435+
__write_profile_data_to_file(profile, filepath)

0 commit comments

Comments
 (0)