From a83b61c0b0358aeeec008859d046782774952756 Mon Sep 17 00:00:00 2001 From: kpassito Date: Wed, 3 Jun 2026 10:38:46 +0200 Subject: [PATCH 1/2] feat: add IQPE benchmark Assisted-by: GPT-5 Codex via Codex --- CHANGELOG.md | 4 ++ docs/benchmark_selection.md | 7 ++++ src/mqt/bench/benchmarks/iqpe.py | 70 ++++++++++++++++++++++++++++++++ tests/test_bench.py | 49 ++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 src/mqt/bench/benchmarks/iqpe.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 991a365b4..21e5224d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- ✨ Add Iterative Quantum Phase Estimation (IQPE) benchmark. + ### Fixed - 🐛 Make IQM Crystal device connectivity bidirectional ([#914]) ([**@flowerthrower**]) diff --git a/docs/benchmark_selection.md b/docs/benchmark_selection.md index 471aa79b1..f9b6dce72 100644 --- a/docs/benchmark_selection.md +++ b/docs/benchmark_selection.md @@ -52,3 +52,10 @@ HTML(html) ``` See the [benchmark description](https://www.cda.cit.tum.de/mqtbench/benchmark_description) for further details on the individual benchmarks. + +## Dynamic Iterative Phase Estimation + +The `iqpe` benchmark implements Iterative Quantum Phase Estimation as a dynamic circuit. +Unlike the standard QPE benchmarks, it reuses one measurement ancilla and one target eigenstate qubit instead of allocating a full quantum phase register. +Use `circuit_size=2`; the optional `num_bits` argument controls the number of mid-circuit measurement and classically controlled feedback iterations. +The benchmark reinitializes the reusable ancilla with measurement-conditioned feedback so it remains compatible with targets that do not expose a native reset operation. diff --git a/src/mqt/bench/benchmarks/iqpe.py b/src/mqt/bench/benchmarks/iqpe.py new file mode 100644 index 000000000..e35682edc --- /dev/null +++ b/src/mqt/bench/benchmarks/iqpe.py @@ -0,0 +1,70 @@ +# 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 benchmark definition.""" + +from __future__ import annotations + +import math + +from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister + +from ._registry import register_benchmark + + +@register_benchmark("iqpe", description="Iterative Quantum Phase Estimation (IQPE)") +def create_circuit(num_qubits: int, num_bits: int = 3, phase: float = 0.625) -> QuantumCircuit: + """Returns a dynamic circuit implementing Iterative Quantum Phase Estimation. + + IQPE estimates the phase of a unitary eigenvalue using one reusable measurement + ancilla instead of a full QFT phase register. This benchmark uses a single-qubit + phase gate as the target unitary and prepares the target eigenstate ``|1>``. + The requested precision is controlled by ``num_bits``. + + Arguments: + num_qubits: Number of qubits in the returned circuit. Must be exactly 2: + one reusable measurement ancilla and one target eigenstate qubit. + num_bits: Number of phase-estimation iterations/classical bits. + phase: Target phase as a fraction of one full turn. Must be in ``[0, 1)``. + + Returns: + QuantumCircuit: A dynamic IQPE circuit with mid-circuit measurements, + reset-by-feedback operations, and classically controlled rotations. + """ + if num_qubits != 2: + msg = "Number of qubits must be exactly 2 for IQPE." + raise ValueError(msg) + if num_bits < 1: + msg = "num_bits must be at least 1 for IQPE." + raise ValueError(msg) + if not 0 <= phase < 1: + msg = "phase must be in the interval [0, 1)." + raise ValueError(msg) + + measurement = QuantumRegister(1, "measurement") + target = QuantumRegister(1, "target") + phase_bits = ClassicalRegister(num_bits, "phase") + qc = QuantumCircuit(measurement, target, phase_bits, name="iqpe") + + # Prepare the |1> eigenstate of the single-qubit phase unitary. + qc.x(target[0]) + + for bit in reversed(range(num_bits)): + qc.h(measurement[0]) + qc.cp(2 * math.pi * phase * (2**bit), measurement[0], target[0]) + + for correction_bit in range(bit + 1, num_bits): + with qc.if_test((phase_bits[correction_bit], 1)): + qc.rz(-math.pi / (2 ** (correction_bit - bit)), measurement[0]) + + qc.h(measurement[0]) + qc.measure(measurement[0], phase_bits[bit]) + with qc.if_test((phase_bits[bit], 1)): + qc.x(measurement[0]) + + return qc diff --git a/tests/test_bench.py b/tests/test_bench.py index 850fef287..567fa9ba6 100644 --- a/tests/test_bench.py +++ b/tests/test_bench.py @@ -74,6 +74,7 @@ SPECIAL_QUBIT_COUNTS: dict[str, int] = { "shor": 18, "hrs_cumulative_multiplier": 5, + "iqpe": 2, "bmw_quark_copula": 4, "cdkm_ripple_carry_adder": 4, "draper_qft_adder": 4, @@ -203,6 +204,7 @@ def test_arithmetic_circuits(benchmark_name: str, input_value: int) -> None: ), ("vbe_ripple_carry_adder", 3, "unknown_adder", "kind must be 'full', 'half', or 'fixed'."), ("hhl", 2, None, "Number of qubits must be at least 3 for HHL."), + ("iqpe", 3, None, "Number of qubits must be exactly 2 for IQPE."), ("qpeexact", 1, None, "Number of qubits must be at least 2 for QPE exact."), ("bmw_quark_copula", 3, None, "Number of qubits must be divisible by 2."), ("ae", 1, None, r"Number of qubits must be at least 2 \(1 evaluation \+ 1 target\)."), @@ -252,6 +254,53 @@ def test_graphstate_seed() -> None: assert qc_no_seed.name == "graphstate" +def test_iqpe_circuit_structure() -> None: + """Verify the dynamic IQPE benchmark structure.""" + qc = create_circuit("iqpe", 2, num_bits=4, phase=0.625) + + assert qc.name == "iqpe" + assert qc.num_qubits == 2 + assert qc.num_clbits == 4 + assert [reg.name for reg in qc.qregs] == ["measurement", "target"] + assert [reg.name for reg in qc.cregs] == ["phase"] + + ops = qc.count_ops() + assert ops.get("x", 0) == 1 + assert ops.get("h", 0) == 8 + assert ops.get("cp", 0) == 4 + assert ops.get("measure", 0) == 4 + assert ops.get("reset", 0) == 0 + assert ops.get("if_else", 0) == 10 + + +def test_iqpe_scales_with_num_bits() -> None: + """IQPE precision scales via classical bits and dynamic iterations.""" + three_bits = create_circuit("iqpe", 2, num_bits=3) + five_bits = create_circuit("iqpe", 2, num_bits=5) + + assert three_bits.num_qubits == five_bits.num_qubits == 2 + assert three_bits.num_clbits == 3 + assert five_bits.num_clbits == 5 + assert three_bits.count_ops().get("measure", 0) == 3 + assert five_bits.count_ops().get("measure", 0) == 5 + assert three_bits.count_ops().get("if_else", 0) == 6 + assert five_bits.count_ops().get("if_else", 0) == 15 + + +@pytest.mark.parametrize( + ("num_bits", "phase", "msg"), + [ + (0, 0.625, "num_bits must be at least 1 for IQPE."), + (3, -0.1, r"phase must be in the interval \[0, 1\)."), + (3, 1.0, r"phase must be in the interval \[0, 1\)."), + ], +) +def test_iqpe_parameter_validation(num_bits: int, phase: float, msg: str) -> None: + """Invalid IQPE parameters are rejected.""" + with pytest.raises(ValueError, match=msg): + create_circuit("iqpe", 2, num_bits=num_bits, phase=phase) + + # Test the dynamic GHZ circuit @pytest.mark.parametrize("num_qubits", [1, 2, 3, 7, 10]) def test_dynamic_ghz_circuit_structure(num_qubits: int) -> None: From 862e9a8ce1b0eb6a2566b30eea7127803e036535 Mon Sep 17 00:00:00 2001 From: kpassito Date: Wed, 3 Jun 2026 23:07:09 +0200 Subject: [PATCH 2/2] address IQPE review feedback Assisted-by: GPT-5 Codex via Codex --- CHANGELOG.md | 2 +- docs/benchmark_selection.md | 7 ------- src/mqt/bench/benchmarks/iqpe.py | 29 ++++++++++++--------------- tests/test_bench.py | 34 +++++++++++++++++++++++--------- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e5224d8..953740c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added -- ✨ Add Iterative Quantum Phase Estimation (IQPE) benchmark. +- ✨ Add Iterative Quantum Phase Estimation (IQPE) benchmark ([#916]) ([**@kpassito**]) ### Fixed diff --git a/docs/benchmark_selection.md b/docs/benchmark_selection.md index f9b6dce72..471aa79b1 100644 --- a/docs/benchmark_selection.md +++ b/docs/benchmark_selection.md @@ -52,10 +52,3 @@ HTML(html) ``` See the [benchmark description](https://www.cda.cit.tum.de/mqtbench/benchmark_description) for further details on the individual benchmarks. - -## Dynamic Iterative Phase Estimation - -The `iqpe` benchmark implements Iterative Quantum Phase Estimation as a dynamic circuit. -Unlike the standard QPE benchmarks, it reuses one measurement ancilla and one target eigenstate qubit instead of allocating a full quantum phase register. -Use `circuit_size=2`; the optional `num_bits` argument controls the number of mid-circuit measurement and classically controlled feedback iterations. -The benchmark reinitializes the reusable ancilla with measurement-conditioned feedback so it remains compatible with targets that do not expose a native reset operation. diff --git a/src/mqt/bench/benchmarks/iqpe.py b/src/mqt/bench/benchmarks/iqpe.py index e35682edc..298a7a1a7 100644 --- a/src/mqt/bench/benchmarks/iqpe.py +++ b/src/mqt/bench/benchmarks/iqpe.py @@ -21,23 +21,16 @@ def create_circuit(num_qubits: int, num_bits: int = 3, phase: float = 0.625) -> QuantumCircuit: """Returns a dynamic circuit implementing Iterative Quantum Phase Estimation. - IQPE estimates the phase of a unitary eigenvalue using one reusable measurement - ancilla instead of a full QFT phase register. This benchmark uses a single-qubit - phase gate as the target unitary and prepares the target eigenstate ``|1>``. - The requested precision is controlled by ``num_bits``. - Arguments: - num_qubits: Number of qubits in the returned circuit. Must be exactly 2: - one reusable measurement ancilla and one target eigenstate qubit. - num_bits: Number of phase-estimation iterations/classical bits. + num_qubits: Number of qubits of the returned quantum circuit. Must be at least 2. + num_bits: Number of measured phase bits. phase: Target phase as a fraction of one full turn. Must be in ``[0, 1)``. Returns: - QuantumCircuit: A dynamic IQPE circuit with mid-circuit measurements, - reset-by-feedback operations, and classically controlled rotations. + QuantumCircuit: The constructed IQPE circuit. """ - if num_qubits != 2: - msg = "Number of qubits must be exactly 2 for IQPE." + if num_qubits < 2: + msg = "Number of qubits must be at least 2 for IQPE." raise ValueError(msg) if num_bits < 1: msg = "num_bits must be at least 1 for IQPE." @@ -47,16 +40,20 @@ def create_circuit(num_qubits: int, num_bits: int = 3, phase: float = 0.625) -> raise ValueError(msg) measurement = QuantumRegister(1, "measurement") - target = QuantumRegister(1, "target") + target = QuantumRegister(num_qubits - 1, "target") phase_bits = ClassicalRegister(num_bits, "phase") qc = QuantumCircuit(measurement, target, phase_bits, name="iqpe") - # Prepare the |1> eigenstate of the single-qubit phase unitary. - qc.x(target[0]) + phase_per_target = phase / len(target) + for target_qubit in target: + qc.x(target_qubit) for bit in reversed(range(num_bits)): qc.h(measurement[0]) - qc.cp(2 * math.pi * phase * (2**bit), measurement[0], target[0]) + + # Split the phase over the target register so |1...1> has the requested eigenphase. + for target_qubit in target: + qc.cp(2 * math.pi * phase_per_target * (2**bit), measurement[0], target_qubit) for correction_bit in range(bit + 1, num_bits): with qc.if_test((phase_bits[correction_bit], 1)): diff --git a/tests/test_bench.py b/tests/test_bench.py index 567fa9ba6..a8e1b0327 100644 --- a/tests/test_bench.py +++ b/tests/test_bench.py @@ -74,7 +74,6 @@ SPECIAL_QUBIT_COUNTS: dict[str, int] = { "shor": 18, "hrs_cumulative_multiplier": 5, - "iqpe": 2, "bmw_quark_copula": 4, "cdkm_ripple_carry_adder": 4, "draper_qft_adder": 4, @@ -204,7 +203,7 @@ def test_arithmetic_circuits(benchmark_name: str, input_value: int) -> None: ), ("vbe_ripple_carry_adder", 3, "unknown_adder", "kind must be 'full', 'half', or 'fixed'."), ("hhl", 2, None, "Number of qubits must be at least 3 for HHL."), - ("iqpe", 3, None, "Number of qubits must be exactly 2 for IQPE."), + ("iqpe", 1, None, "Number of qubits must be at least 2 for IQPE."), ("qpeexact", 1, None, "Number of qubits must be at least 2 for QPE exact."), ("bmw_quark_copula", 3, None, "Number of qubits must be divisible by 2."), ("ae", 1, None, r"Number of qubits must be at least 2 \(1 evaluation \+ 1 target\)."), @@ -256,18 +255,19 @@ def test_graphstate_seed() -> None: def test_iqpe_circuit_structure() -> None: """Verify the dynamic IQPE benchmark structure.""" - qc = create_circuit("iqpe", 2, num_bits=4, phase=0.625) + qc = create_circuit("iqpe", 4, num_bits=4, phase=0.625) assert qc.name == "iqpe" - assert qc.num_qubits == 2 + assert qc.num_qubits == 4 assert qc.num_clbits == 4 assert [reg.name for reg in qc.qregs] == ["measurement", "target"] + assert qc.qregs[1].size == 3 assert [reg.name for reg in qc.cregs] == ["phase"] ops = qc.count_ops() - assert ops.get("x", 0) == 1 + assert ops.get("x", 0) == 3 assert ops.get("h", 0) == 8 - assert ops.get("cp", 0) == 4 + assert ops.get("cp", 0) == 12 assert ops.get("measure", 0) == 4 assert ops.get("reset", 0) == 0 assert ops.get("if_else", 0) == 10 @@ -275,18 +275,34 @@ def test_iqpe_circuit_structure() -> None: def test_iqpe_scales_with_num_bits() -> None: """IQPE precision scales via classical bits and dynamic iterations.""" - three_bits = create_circuit("iqpe", 2, num_bits=3) - five_bits = create_circuit("iqpe", 2, num_bits=5) + three_bits = create_circuit("iqpe", 3, num_bits=3) + five_bits = create_circuit("iqpe", 3, num_bits=5) - assert three_bits.num_qubits == five_bits.num_qubits == 2 + assert three_bits.num_qubits == five_bits.num_qubits == 3 assert three_bits.num_clbits == 3 assert five_bits.num_clbits == 5 assert three_bits.count_ops().get("measure", 0) == 3 assert five_bits.count_ops().get("measure", 0) == 5 + assert three_bits.count_ops().get("cp", 0) == 6 + assert five_bits.count_ops().get("cp", 0) == 10 assert three_bits.count_ops().get("if_else", 0) == 6 assert five_bits.count_ops().get("if_else", 0) == 15 +def test_iqpe_scales_with_num_qubits() -> None: + """IQPE circuit width scales with the requested number of qubits.""" + two_qubits = create_circuit("iqpe", 2, num_bits=3) + five_qubits = create_circuit("iqpe", 5, num_bits=3) + + assert two_qubits.num_qubits == 2 + assert five_qubits.num_qubits == 5 + assert two_qubits.num_clbits == five_qubits.num_clbits == 3 + assert two_qubits.count_ops().get("x", 0) == 1 + assert five_qubits.count_ops().get("x", 0) == 4 + assert two_qubits.count_ops().get("cp", 0) == 3 + assert five_qubits.count_ops().get("cp", 0) == 12 + + @pytest.mark.parametrize( ("num_bits", "phase", "msg"), [