Skip to content

Add IQPE benchmark#806

Open
carlareciofdz wants to merge 26 commits intomunich-quantum-toolkit:mainfrom
carlareciofdz:carla-IQPE
Open

Add IQPE benchmark#806
carlareciofdz wants to merge 26 commits intomunich-quantum-toolkit:mainfrom
carlareciofdz:carla-IQPE

Conversation

@carlareciofdz
Copy link

@carlareciofdz carlareciofdz commented Jan 23, 2026

Description

Please include a summary of the change and, if applicable, which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.

Fixes #(issue)

Checklist:

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

@carlareciofdz
Copy link
Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Adds a new deterministic iterative quantum phase estimation (IQPE) circuit generator function create_circuit(num_qubits: int), accompanying tests, and a Qiskit dependency bump to pyproject.toml.

Changes

Cohort / File(s) Summary
IQPE Exact Circuit Module
src/mqt/bench/benchmarks/iqpe_exact.py
Adds create_circuit(num_qubits: int) -> QuantumCircuit. Validates num_qubits >= 2. Builds a two-register dynamic IQPE circuit (ancilla + target) with a classical register, deterministic RNG for exact n-bit phase, iterative reset/H/controlled-CP/measurement-driven conditional phase corrections, and registers the benchmark.
Tests
tests/test_bench.py
Adds test_iqpe_exact_structure (asserts circuit structure for 5 qubits: qubit/classical counts, measurement and conditional op counts) and test_iqpe_exact_invalid_qubit_number (asserts ValueError for num_qubits < 2).
Dependency / Manifest
pyproject.toml
Bumps Qiskit requirement from qiskit[qasm3-import]>=1.3.2 to qiskit[qasm3-import]>=2.0.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through phases, bit by bit,
Reset, H, measure — then wisdom to fit,
Seeded seeds yield binary light,
Iterations dance through quantum night,
Carrots, circuits, small rabbit delight. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete and largely unchanged from the template. No actual summary, motivation, context, dependencies, or issue reference are provided. Fill in the Description section with a summary of changes (IQPE benchmark implementation), motivation, context, and dependencies (Qiskit >=2.0.0). Replace or remove the issue placeholder if not applicable.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a new IQPE (Iterative Quantum Phase Estimation) benchmark to the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/mqt/bench/benchmarks/iqpe_exact.py`:
- Around line 41-44: The code currently calls random.seed(10) which mutates the
global RNG; replace this with a local RNG instance to avoid side effects by
creating rng = random.Random(10) and then use rng.getrandbits(num_qubits) in the
theta generation loop (i.e., replace random.getrandbits(num_qubits) with
rng.getrandbits(num_qubits) and remove random.seed(10)), keeping the same loop
logic that ensures theta != 0.
- Around line 20-34: The function create_circuit should guard against too-small
inputs: after you subtract the ancilla (num_qubits = num_qubits - 1) ensure
there are at least 1 data qubit (original num_qubits >= 2) to avoid
num_qubits==0 which makes random.getrandbits(0) and the while theta==0 loop
non-terminating; add a benchmark-specific check at the start of create_circuit
(e.g., if original num_qubits < 2: raise ValueError with a clear message) so
callers get a deterministic error instead of hanging.
- Around line 56-81: The measurement feedback uses classical bits mismatched to
where results are stored: qc.measure(q[0], c[k]) stores into c[k] but the
correction loop reads c[m_index] where m_index = num_qubits-1-i, causing
corrections to read future/unset bits; fix by storing the measurement to the
same index the feedback expects (replace qc.measure(q[0], c[k]) with
qc.measure(q[0], c[index])) or alternatively change m_index computation in the
correction loop to match c[k] storage (e.g., set m_index = k - 1 - i); update
qc.measure and/or m_index logic so qc.measure, the correction loop (with
qc.if_test), and indexing all refer to the same classical bit indices.
- Around line 35-37: The circuit currently allocates a fixed 2-qubit register
(q) causing qc.num_qubits to not match the input num_qubits; change the register
allocation to mirror qpeexact.py by creating the main quantum register with
QuantumRegister(num_qubits - 1, "q"), a single-qubit ancilla register (e.g.,
QuantumRegister(1, "anc")), keep the classical register as
ClassicalRegister(num_qubits, "c"), and construct the circuit with qc =
QuantumCircuit(q, anc, c, name="iqpeexact") so qc.num_qubits == num_qubits.

Comment on lines 35 to 37
q = QuantumRegister(2, "q")
c = ClassicalRegister(num_qubits, "c")
qc = QuantumCircuit(q, c, name="iqpeexact")
Copy link

@coderabbitai coderabbitai bot Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the create_circuit function and its contract
cat -n src/mqt/bench/benchmarks/__init__.py | head -100

Repository: munich-quantum-toolkit/bench

Length of output: 3894


🏁 Script executed:

# Examine the full iqpe_exact.py file to understand the algorithm
cat -n src/mqt/bench/benchmarks/iqpe_exact.py

Repository: munich-quantum-toolkit/bench

Length of output: 3413


🏁 Script executed:

# Search for how other benchmarks use QuantumRegister and num_qubits
rg -A5 'def create_circuit' src/mqt/bench/benchmarks/ -g'*.py' | head -150

Repository: munich-quantum-toolkit/bench

Length of output: 10853


🏁 Script executed:

# Check if num_qubits is used elsewhere in iqpe_exact.py
rg -n 'num_qubits' src/mqt/bench/benchmarks/iqpe_exact.py

Repository: munich-quantum-toolkit/bench

Length of output: 538


🏁 Script executed:

# Get the full create_circuit function in __init__.py
sed -n '96,130p' src/mqt/bench/benchmarks/__init__.py

Repository: munich-quantum-toolkit/bench

Length of output: 1565


🏁 Script executed:

# Search for validation in create_circuit
rg -A15 'def create_circuit' src/mqt/bench/benchmarks/__init__.py

Repository: munich-quantum-toolkit/bench

Length of output: 972


🏁 Script executed:

# Check if iqpe_exact circuit.num_qubits equals the input parameter
rg -B2 -A10 'qc.num_qubits|circuit_size.*>.*0' src/mqt/bench/benchmarks/__init__.py

Repository: munich-quantum-toolkit/bench

Length of output: 54


🏁 Script executed:

# Compare with another algorithm like DJ to understand the pattern
cat -n src/mqt/bench/benchmarks/dj.py | sed -n '20,50p'

Repository: munich-quantum-toolkit/bench

Length of output: 1146


🏁 Script executed:

# Search for usages of create_circuit to understand the contract
rg -n 'create_circuit\(' -B2 -A3 src/mqt/bench/ --glob='*.py' | head -100

Repository: munich-quantum-toolkit/bench

Length of output: 7056


🏁 Script executed:

# Look for any assertions or validations on circuit.num_qubits after creation
rg -n '\.num_qubits\s*(==|!=|<|>|<=|>=)' src/mqt/bench/ --glob='*.py' | head -50

Repository: munich-quantum-toolkit/bench

Length of output: 54


🏁 Script executed:

# Check the test file to understand expected behavior
fd -e test.py -o -e _test.py | head -20

Repository: munich-quantum-toolkit/bench

Length of output: 188


🏁 Script executed:

# Look at benchmark_generation.py to see what's done with the returned circuit
cat -n src/mqt/bench/benchmark_generation.py | sed -n '70,100p'

Repository: munich-quantum-toolkit/bench

Length of output: 1791


🏁 Script executed:

# Search for any assertions or validations after circuit creation
rg -A10 'qc = create_circuit' src/mqt/bench/benchmark_generation.py

Repository: munich-quantum-toolkit/bench

Length of output: 487


🏁 Script executed:

# Check if there's a test file that validates the contract
find . -name '*test*.py' -type f | xargs grep -l 'iqpe\|create_circuit' 2>/dev/null | head -5

Repository: munich-quantum-toolkit/bench

Length of output: 93


🏁 Script executed:

# Check qpeexact to compare - it should have similar issues if this is an algorithm property
cat -n src/mqt/bench/benchmarks/qpeexact.py | head -50

Repository: munich-quantum-toolkit/bench

Length of output: 2080


🏁 Script executed:

# Check the test file for iqpe_exact tests
grep -n 'iqpe' tests/test_bench.py -i

Repository: munich-quantum-toolkit/bench

Length of output: 54


🏁 Script executed:

# Look for any size validation or assertions in tests
cat -n tests/test_bench.py | head -100

Repository: munich-quantum-toolkit/bench

Length of output: 3481


🏁 Script executed:

# Check if there's documentation or comments about the expected circuit size
rg -B5 -A5 'iqpe_exact|iqpeinexact' docs/ README* 2>/dev/null || echo "No docs found"

Repository: munich-quantum-toolkit/bench

Length of output: 85


🏁 Script executed:

# Verify by checking qpeexact structure more carefully - how many total qubits?
python3 << 'EOF'
# Parse both files to show the total qubit count formula
import re

files = {
    "qpeexact": "src/mqt/bench/benchmarks/qpeexact.py",
    "iqpe_exact": "src/mqt/bench/benchmarks/iqpe_exact.py"
}

for name, path in files.items():
    with open(path) as f:
        content = f.read()
        
    # Find the qubit allocation section
    match = re.search(r'num_qubits = num_qubits - 1.*?qc = QuantumCircuit\([^)]+\)', content, re.DOTALL)
    if match:
        print(f"\n{name}:")
        print(match.group(0))
EOF

Repository: munich-quantum-toolkit/bench

Length of output: 158


🏁 Script executed:

# Check if iqpe_exact or iqpeinexact is in the SPECIAL_QUBIT_COUNTS
grep -E 'iqpe|iqpe_exact' tests/test_bench.py

Repository: munich-quantum-toolkit/bench

Length of output: 54


🏁 Script executed:

# Let's verify the actual total qubit count for iqpe_exact
python3 << 'EOF'
# Simulate what iqpe_exact.py does
def simulate_iqpe_exact(num_qubits_input):
    num_qubits = num_qubits_input - 1
    # QuantumRegister(2, "q") creates 2 qubits
    q_qubits = 2
    # ClassicalRegister doesn't add qubits, only classical bits
    c_bits = num_qubits
    # Total qubits in circuit
    total = q_qubits
    return total

# Test with typical input
for input_val in [3, 5, 10]:
    result = simulate_iqpe_exact(input_val)
    print(f"Input: {input_val}, Circuit qubits: {result}, Contract expects: {input_val}, Match: {result == input_val}")
EOF

Repository: munich-quantum-toolkit/bench

Length of output: 262


🏁 Script executed:

# Compare with qpeexact
python3 << 'EOF'
def simulate_qpeexact(num_qubits_input):
    num_qubits = num_qubits_input - 1
    # QuantumRegister(num_qubits, "q")
    q_qubits = num_qubits
    # QuantumRegister(1, "psi")
    psi_qubits = 1
    # ClassicalRegister doesn't add qubits
    total = q_qubits + psi_qubits
    return total

for input_val in [3, 5, 10]:
    result = simulate_qpeexact(input_val)
    print(f"Input: {input_val}, Circuit qubits: {result}, Contract expects: {input_val}, Match: {result == input_val}")
EOF

Repository: munich-quantum-toolkit/bench

Length of output: 260


🏁 Script executed:

# Verify iqpe_exact is tested and will fail the contract
rg -n 'iqpe' tests/test_bench.py -i -A3 -B3

Repository: munich-quantum-toolkit/bench

Length of output: 54


🏁 Script executed:

# Check if there's an iqpeinexact as well
fd -name '*iqpe*' src/mqt/bench/benchmarks/

Repository: munich-quantum-toolkit/bench

Length of output: 302


🏁 Script executed:

# Let's verify by actually running the test assertion logic
python3 << 'EOF'
# Simulate the test that will run on iqpe_exact
benchmark_name = "iqpe_exact"
SPECIAL_QUBIT_COUNTS = {
    "shor": 18,
    "hrs_cumulative_multiplier": 5,
    "bmw_quark_copula": 4,
    "cdkm_ripple_carry_adder": 4,
    "draper_qft_adder": 4,
    "full_adder": 4,
    "multiplier": 4,
    "modular_adder": 4,
    "rg_qft_multiplier": 4,
    "vbe_ripple_carry_adder": 4,
}

# This is what test_quantumcircuit_levels does
input_value = SPECIAL_QUBIT_COUNTS.get(benchmark_name, 3)
print(f"Benchmark: {benchmark_name}")
print(f"Input value (circuit_size): {input_value}")

# What iqpe_exact creates
num_qubits = input_value - 1  # line 34
q_qubits = 2  # QuantumRegister(2, "q") line 35
# Final qc has only 2 qubits
qc_num_qubits = 2

print(f"Circuit qubits allocated: {qc_num_qubits}")
print(f"Test expects qc.num_qubits == input_value: {qc_num_qubits} == {input_value}")
print(f"Test will PASS: {qc_num_qubits == input_value}")
EOF

Repository: munich-quantum-toolkit/bench

Length of output: 223


🏁 Script executed:

# Verify iqpe_exact would be tested with default input
python3 << 'EOF'
# Check what benchmarks are available and would be tested
import sys
sys.path.insert(0, 'src')

from mqt.bench.benchmarks import get_available_benchmark_names

names = get_available_benchmark_names()
print("iqpe_exact in available benchmarks:", "iqpe_exact" in names)
print("Total benchmarks:", len(names))

# Show that it would use default=3 since not in SPECIAL_QUBIT_COUNTS
SPECIAL_QUBIT_COUNTS = {
    "shor": 18,
    "hrs_cumulative_multiplier": 5,
    "bmw_quark_copula": 4,
    "cdkm_ripple_carry_adder": 4,
    "draper_qft_adder": 4,
    "full_adder": 4,
    "multiplier": 4,
    "modular_adder": 4,
    "rg_qft_multiplier": 4,
    "vbe_ripple_carry_adder": 4,
}
print("iqpe_exact in SPECIAL_QUBIT_COUNTS:", "iqpe_exact" in SPECIAL_QUBIT_COUNTS)
print("Would use input_value =", SPECIAL_QUBIT_COUNTS.get("iqpe_exact", 3))
EOF

Repository: munich-quantum-toolkit/bench

Length of output: 425


Fix circuit qubit allocation to match the benchmark contract.

The circuit allocates exactly 2 qubits regardless of the num_qubits parameter, violating the contract enforced by the test suite (tests/test_bench.py:95): assert qc.num_qubits == input_value. With default test input of 3 qubits, the circuit will have 2 qubits, causing test failure. Allocate num_qubits - 1 qubits for the main register and 1 for the ancilla at line 35, matching the pattern in qpeexact.py.

🤖 Prompt for AI Agents
In `@src/mqt/bench/benchmarks/iqpe_exact.py` around lines 35 - 37, The circuit
currently allocates a fixed 2-qubit register (q) causing qc.num_qubits to not
match the input num_qubits; change the register allocation to mirror qpeexact.py
by creating the main quantum register with QuantumRegister(num_qubits - 1, "q"),
a single-qubit ancilla register (e.g., QuantumRegister(1, "anc")), keep the
classical register as ClassicalRegister(num_qubits, "c"), and construct the
circuit with qc = QuantumCircuit(q, anc, c, name="iqpeexact") so qc.num_qubits
== num_qubits.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlareciofdz the suggestion by @coderabbitai here is actually wrong. The circuit should only ever have two qubits. The input parameter to the create_circuit function only really configures the precision to be used.
I see two ways forward here:

  • either we relax the test suite slightly so that it does not assume that the final circuit has n qubits.
  • or we fix the number of qubits in the create_circuit function to 2; error for everything else; and introduce an additional parameter precision to the function that regulates the number of iterations etc.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain


🏁 Script executed:

#!/bin/bash

# First, let's examine the test file to understand SPECIAL_QUBIT_COUNTS and how tests work
echo "=== Examining test_bench.py for SPECIAL_QUBIT_COUNTS and test structure ==="
cat tests/test_bench.py | grep -A 20 "SPECIAL_QUBIT_COUNTS"

echo -e "\n=== Looking at test_quantumcircuit_levels function ==="
sed -n '/def test_quantumcircuit_levels/,/^def /p' tests/test_bench.py | head -30

echo -e "\n=== Checking if any benchmarks have additional parameters beyond num_qubits ==="
rg 'def create_circuit\(' src/mqt/bench/benchmarks/ -A 1 | head -80

Length of output: 8063


🏁 Script executed:

#!/bin/bash

# Check the create_circuit signature in __init__.py to understand the contract
echo "=== Checking create_circuit function signature in __init__.py ==="
sed -n '/^def create_circuit/,/^def /p' src/mqt/bench/benchmarks/__init__.py | head -40

echo -e "\n=== Looking for benchmarks with extra parameters ==="
rg 'def create_circuit\([^)]*,[^)]*\)' src/mqt/bench/benchmarks/ -g '*.py'

Length of output: 2911


🏁 Script executed:

#!/bin/bash

# Let's see the actual validation in test_bench.py
echo "=== Full context of test_quantumcircuit_levels ==="
sed -n '/def test_quantumcircuit_levels/,/^def [a-z]/p' tests/test_bench.py

echo -e "\n=== Looking for iqpe_exact tests ==="
grep -n "iqpe" tests/test_bench.py

Length of output: 2124


@burgholzer Thank you for the correction! You're absolutely right - I misunderstood the algorithm. IQPE is iterative and only needs 2 qubits regardless of precision.

Looking at the codebase architecture, I recommend Option 2 (fix to 2 qubits + add precision parameter) because:

  1. Maintains test contract consistency: The existing test suite asserts qc.num_qubits == input_value for all benchmarks, and Option 2 preserves this uniform contract.

  2. Has clear precedent: Several benchmarks already use additional parameters beyond num_qubits:

    • qaoa: repetitions, seed
    • graphstate: degree, seed
    • dj: balanced
    • vqe_real_amp: entanglement, reps
  3. More explicit and maintainable: Separating qubit count from precision makes the API clearer - create_circuit("iqpe_exact", 2, precision=5) is self-documenting.

Concrete steps:

  1. Add "iqpe_exact": 2 to SPECIAL_QUBIT_COUNTS in tests/test_bench.py
  2. Change signature to: def create_circuit(num_qubits: int, precision: int = 3) -> QuantumCircuit:
  3. Validate num_qubits == 2 (raise ValueError otherwise)
  4. Use precision instead of num_qubits - 1 for iterations (line 34, line 36)
  5. Update test_iqpe_exact_structure to call create_circuit("iqpe_exact", 2, precision=4)

This approach keeps the architecture clean and consistent across all benchmarks.

🐰💭


🧠 Learnings used
Learnt from: adnathanail
Repo: munich-quantum-toolkit/bench PR: 803
File: src/mqt/bench/benchmarks/shors_nine_qubit_code.py:306-313
Timestamp: 2026-01-20T01:27:48.658Z
Learning: In the munich-quantum-toolkit/bench repository, the central `create_circuit` function in `src/mqt/bench/benchmarks/__init__.py` validates that `circuit_size > 0` before calling benchmark-specific factory functions. Therefore, individual benchmark implementations only need to validate benchmark-specific constraints (e.g., divisibility requirements) and don't need to re-check for positive values.

Comment on lines 56 to 81
for k in range(num_qubits):
index = num_qubits - 1 - k
# We reset the ancillary qubit in order to reuse in each iteration
qc.reset(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_qubits - 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[k])
Copy link

@coderabbitai coderabbitai bot Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Feedback reads the wrong classical bit indices.

Measurements are stored in c[k], but corrections read c[num_qubits-1-i]. For k > 0, this targets bits that haven’t been measured yet, so feedback is incorrect. Store measurements at c[index] (or adjust m_index accordingly).

🐛 Proposed fix (store measurements at the same index used by feedback)
-        for i in range(k):
-            m_index = num_qubits - 1 - i
+        for i in range(k):
+            m_index = num_qubits - 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.measure(q[0], c[k])
+        qc.measure(q[0], c[index])
🤖 Prompt for AI Agents
In `@src/mqt/bench/benchmarks/iqpe_exact.py` around lines 56 - 81, The measurement
feedback uses classical bits mismatched to where results are stored:
qc.measure(q[0], c[k]) stores into c[k] but the correction loop reads c[m_index]
where m_index = num_qubits-1-i, causing corrections to read future/unset bits;
fix by storing the measurement to the same index the feedback expects (replace
qc.measure(q[0], c[k]) with qc.measure(q[0], c[index])) or alternatively change
m_index computation in the correction loop to match c[k] storage (e.g., set
m_index = k - 1 - i); update qc.measure and/or m_index logic so qc.measure, the
correction loop (with qc.if_test), and indexing all refer to the same classical
bit indices.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai please check if this is still true

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@burgholzer, let me trace through the indexing to verify:

For num_qubits = 4 (after the decrement, so 3 classical bits):

Iteration k=0:

  • index = 2
  • No feedback loop (range(0) is empty)
  • Measure into c[0]

Iteration k=1:

  • index = 1
  • Feedback tries to read: m_index = 3 - 0 = 3reads c[3] (not measured yet!)
  • Measure into c[1]

Iteration k=2:

  • index = 0
  • Feedback tries to read:
    • i=0: m_index = 3reads c[3] (not measured yet!)
    • i=1: m_index = 2reads c[2] (not measured yet!)
  • Measure into c[2]

The issue is still present. Measurements are stored at c[0], c[1], c[2], ... but the feedback loop tries to read from c[num_qubits-1], c[num_qubits-2], ... which haven't been measured yet.

The fix is to store measurements at c[index] instead of c[k], so the sequence becomes:

  • k=0: measure into c[2] (index=2)
  • k=1: measure into c[1] (index=1), read from c[2] ✓
  • k=2: measure into c[0] (index=0), read from c[2], c[1] ✓

This aligns measurements with the feedback loop's expectations.


🧠 Learnings used
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp:0-0
Timestamp: 2025-12-10T02:20:01.189Z
Learning: In QIRProgramBuilder::measure(Value, int64_t), the intentional design prevents duplicate measurements: the early return when `registerResultMap.find(key)` succeeds avoids generating multiple `mz` calls to the same classical bit index and ensures output recording contains only one entry per index. This implements an "override" semantic where repeated measurements to the same resultIndex reuse the cached pointer without additional side effects.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1283
File: src/qir/runtime/QIR.cpp:196-201
Timestamp: 2025-11-01T15:57:31.153Z
Learning: In the QIR runtime (src/qir/runtime/QIR.cpp), the PRX gate (__quantum__qis__prx__body) is an alias for the R gate (Phased X-Rotation) and should call runtime.apply<qc::R>(theta, phi, qubit), not runtime.apply<qc::RX>() which is a single-parameter rotation gate.

Learnt from: adnathanail
Repo: munich-quantum-toolkit/bench PR: 803
File: src/mqt/bench/benchmarks/shors_nine_qubit_code.py:306-313
Timestamp: 2026-01-20T01:27:48.658Z
Learning: In the munich-quantum-toolkit/bench repository, the central `create_circuit` function in `src/mqt/bench/benchmarks/__init__.py` validates that `circuit_size > 0` before calling benchmark-specific factory functions. Therefore, individual benchmark implementations only need to validate benchmark-specific constraints (e.g., divisibility requirements) and don't need to re-check for positive values.

@codecov
Copy link

codecov bot commented Jan 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@carlareciofdz carlareciofdz marked this pull request as ready for review January 31, 2026 13:00
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tests/test_bench.py`:
- Around line 148-167: The test_iqpe_exact_structure docstring and assertion
message are inconsistent with the actual check for 10 if_else operations; update
the docstring to state "4 reset conditionals + 6 correction conditionals = 10
total" (or equivalent) and change the assertion error string in the
if_else_count check to "Expected 10 conditional operations (4 reset + 6
correction), found {if_else_count}" so the message matches the assertion that
if_else_count == 10; locate these edits around the test_iqpe_exact_structure
function and the if_else_count assertion that references qc.data / if_else.
- Line 163: Remove the redundant inline type comment on the ops assignment:
replace the line "ops = dict(ops)  # type: dict[str, int]" with just "ops =
dict(ops)"; the dict conversion already yields a plain dict from the return of
qc.count_ops(), so delete the "# type: dict[str, int]" annotation to clean up
the code (referencing the ops variable and qc.count_ops() return).

@carlareciofdz
Copy link
Author

@burgholzer @flowerthrower Dear Lukas and Patrick the errors from the rabbit ai are solved. Whenever you have time, you can review it.

@burgholzer burgholzer added python Pull requests that update Python code feature New feature or request labels Feb 3, 2026
Copy link
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks for the contribution here!
I just had a quick look through the implementation and notices some things that could potentially be improved for this benchmark.
You'll find these suggestions in the comments inline with the code.

Additionally, it would be great if you could also add a corresponding changelog entry to the respective file 🙏🏼

Don't hesitate to reach out in case of questions!

Comment on lines +63 to +65
qc.measure(q[0], c[0])
with qc.if_test((c[0], 1)):
qc.x(q[0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qiskit has a dedicated reset instruction that can be used here.
Furthermore, the first reset can be avoided as the qubit will be initialized in |0>.

Comment on lines 35 to 37
q = QuantumRegister(2, "q")
c = ClassicalRegister(num_qubits, "c")
qc = QuantumCircuit(q, c, name="iqpeexact")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlareciofdz the suggestion by @coderabbitai here is actually wrong. The circuit should only ever have two qubits. The input parameter to the create_circuit function only really configures the precision to be used.
I see two ways forward here:

  • either we relax the test suite slightly so that it does not assume that the final circuit has n qubits.
  • or we fix the number of qubits in the create_circuit function to 2; error for everything else; and introduce an additional parameter precision to the function that regulates the number of iterations etc.

Comment on lines 56 to 81
for k in range(num_qubits):
index = num_qubits - 1 - k
# We reset the ancillary qubit in order to reuse in each iteration
qc.reset(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_qubits - 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[k])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai please check if this is still true

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This algorithm will probably need a safeguard to limit it to something like 60 qubits because otherwise the shift operations are no longer well defined and will cause issues.
A corresponding check should be added, the docstrings should mention this limitation, and a test is probably also in order.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only covering the exact version of iQPE. Is there any particular reason for that?
It could make sense to add one more parameter to the create_circuit function that controls whether the phase is exactly representable or not.
Then, the naming and docstrings can also be adjusted accordingly to refer to this benchmark as IQPE (without the exact or inexact).

Comment on lines +148 to +167
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}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit hard-coded to be honest. Can we generalize this slightly to cover a few more cases?

@flowerthrower
Copy link
Collaborator

Dear @carlareciofdz, may I ask for the current status of this PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments