diff --git a/CHANGELOG.md b/CHANGELOG.md index 991a365b4..953740c8e 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 ([#916]) ([**@kpassito**]) + ### Fixed - 🐛 Make IQM Crystal device connectivity bidirectional ([#914]) ([**@flowerthrower**]) diff --git a/src/mqt/bench/benchmarks/iqpe.py b/src/mqt/bench/benchmarks/iqpe.py new file mode 100644 index 000000000..298a7a1a7 --- /dev/null +++ b/src/mqt/bench/benchmarks/iqpe.py @@ -0,0 +1,67 @@ +# 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. + + Arguments: + 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: The constructed IQPE circuit. + """ + 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." + 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(num_qubits - 1, "target") + phase_bits = ClassicalRegister(num_bits, "phase") + qc = QuantumCircuit(measurement, target, phase_bits, name="iqpe") + + 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]) + + # 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)): + 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..a8e1b0327 100644 --- a/tests/test_bench.py +++ b/tests/test_bench.py @@ -203,6 +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", 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\)."), @@ -252,6 +253,70 @@ 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", 4, num_bits=4, phase=0.625) + + assert qc.name == "iqpe" + 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) == 3 + assert ops.get("h", 0) == 8 + 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 + + +def test_iqpe_scales_with_num_bits() -> None: + """IQPE precision scales via classical bits and dynamic iterations.""" + 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 == 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"), + [ + (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: