diff --git a/pyproject.toml b/pyproject.toml index cd8c051e1..a6a347181 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dynamic = ["version"] dependencies = [ # Qiskit 1.3.2 contains some fixes for exporting OpenQASM 3 files - "qiskit[qasm3-import]>=1.3.2", + "qiskit[qasm3-import]>=2.0.0", "networkx>=2.8.8", "numpy>=1.22", "numpy>=1.24; python_version >= '3.11'", diff --git a/src/mqt/bench/benchmarks/iqpe_exact.py b/src/mqt/bench/benchmarks/iqpe_exact.py new file mode 100644 index 000000000..229378dc6 --- /dev/null +++ b/src/mqt/bench/benchmarks/iqpe_exact.py @@ -0,0 +1,89 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Iterative Quantum Phase Estimation (IQPE).""" + +from __future__ import annotations + +import random +from fractions import Fraction + +import numpy as np +from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister + +from ._registry import register_benchmark + + +@register_benchmark("iqpe_exact", description="Iterative Quantum Phase Estimation (IQPE)") +def create_circuit(num_qubits: int) -> QuantumCircuit: + """Returns a dynamic quantum circuit implementing the Iterative Quantum Phase Estimation algorithm. + + Arguments: + num_qubits: total number of qubits. + + Returns: + QuantumCircuit: a dynamic quantum circuit implementing the Iterative Quantum Phase Estimation algorithm for a phase which can be exactly estimated + + """ + # We just use 2 quantum registers: q[1] is the main register where + # the eigenstate will be encoded, q[0] is the ancillary qubit where the phase + # will be encoded. Only the ancillary qubit is measured, and the result will + # be stored in "c", the classical register. + if num_qubits < 2: + msg = "num_qubits must be >= 2 (1 ancilla + at least 1 phase bit)" + raise ValueError(msg) + num_bits = num_qubits - 1 # because of ancilla qubit + q = QuantumRegister(num_qubits, "q") + c = ClassicalRegister(num_bits, "c") + qc = QuantumCircuit(q, c, name="iqpe_exact") + + # Generate a random n-bit integer as a target phase "theta". The phase can be exactly + # estimated + rng = random.Random(10) + theta = 0 + while theta == 0: + theta = rng.getrandbits(num_bits) + lam = Fraction(0, 1) + for i in range(num_bits): + if theta & (1 << (num_bits - i - 1)): + lam += Fraction(1, (1 << i)) + + # We apply an X gate to the q[1] qubit, to prepare the target qubit in the + # |1> state + qc.x(q[1]) + + for k in range(num_bits): + index = num_bits - 1 - k + # We reset the ancillary qubit in order to reuse in each iteration + qc.measure(q[0], c[0]) + with qc.if_test((c[0], 1)): + qc.x(q[0]) + qc.h(q[0]) + + # Controlled unitary. The angle is normalized from + # [0, 2] to [-1, 1], which minimize the errors because uses shortest rotations + angle = float((lam * (1 << index)) % 2) + if angle > 1: + angle -= 2 + + # We use pi convention for simplicity + qc.cp(angle * np.pi, q[0], q[1]) + + # We apply phase corrections based on previous measurements. + for i in range(k): + m_index = num_bits - 1 - i + true_angle = -1.0 / (1 << (k - i)) + + with qc.if_test((c[m_index], 1)): + qc.p(true_angle * np.pi, q[0]) + + qc.h(q[0]) + # We measure and store the result for future corrections + qc.measure(q[0], c[index]) + + return qc diff --git a/tests/test_bench.py b/tests/test_bench.py index ea46dba1a..c0dd92793 100644 --- a/tests/test_bench.py +++ b/tests/test_bench.py @@ -145,6 +145,34 @@ def test_adder_circuits(benchmark_name: str, input_value: int, kind: str) -> Non assert qc.num_qubits == input_value +def test_iqpe_exact_structure() -> None: + """Test that the Iterative Quantum Phase Estimation code circuit has the expected structure. + + Verifies (for a 5-qubit input): + - Quantum registers: 4 target qubits and 1 ancillary qubit + - Classical register: num_qubits - 1 (4 bits) + - 8 measurements (4 resets + 4 final measurements) + - 10 conditional operations (4 reset conditionals + 6 correction conditionals) + """ + qc = create_circuit("iqpe_exact", 5) + + assert qc.num_qubits == 5 + assert qc.num_clbits == 4 + + ops = qc.count_ops() + ops = dict(ops) # type: dict[str, int] + assert ops.get("measure") == 8 + + if_else_count = sum(1 for inst in qc.data if inst.operation.name == "if_else") + assert if_else_count == 10, f"Expected 10 conditional operations, found {if_else_count}" + + +def test_iqpe_exact_invalid_qubit_number() -> None: + """Test that the Iterative Quantum Phase Estimation code circuit raises an error for invalid qubit numbers.""" + with pytest.raises(ValueError, match=r"num_qubits must be >= 2"): + create_circuit("iqpe_exact", 1) + + @pytest.mark.parametrize( ("benchmark_name", "input_value", "kind", "msg"), [ diff --git a/uv.lock b/uv.lock index 9e4114d99..6d6de626c 100644 --- a/uv.lock +++ b/uv.lock @@ -1529,7 +1529,7 @@ requires-dist = [ { name = "numpy", marker = "python_full_version >= '3.12'", specifier = ">=1.26" }, { name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1" }, { name = "numpy", marker = "python_full_version >= '3.14'", specifier = ">=2.3.2" }, - { name = "qiskit", extras = ["qasm3-import"], specifier = ">=1.3.2" }, + { name = "qiskit", extras = ["qasm3-import"], specifier = ">=2.0.0" }, { name = "scikit-learn", specifier = ">=1.5.2" }, { name = "scikit-learn", marker = "python_full_version >= '3.14'", specifier = ">=1.7.2" }, ]