Skip to content

feat(algorithms): implement Canonical Quantum Amplitude Estimation (QAE) using QPE - #27

Merged
krishna-ajay merged 13 commits into
qpiai:mainfrom
Gpar377:feat/canonical-qae
Sep 15, 2026
Merged

krishna-ajay merged 13 commits into
qpiai:mainfrom
Gpar377:feat/canonical-qae

Conversation

@Gpar377

@Gpar377 Gpar377 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #26.

Implements Canonical Quantum Amplitude Estimation (AmplitudeEstimation) using Quantum Phase Estimation (QPE), resolving the previous NotImplementedError stubs.

Implementation Details

  • Controlled Grover operator powers are synthesized via exact gate decomposition, $C\text{-}Q = (Z_c \otimes I) \cdot A \cdot (C\text{-}S_0) \cdot A^\dagger \cdot (C\text{-}S_\chi)$. Evaluation qubit $j$ receives $C\text{-}Q^{2^{m-1-j}}$, matching the little-endian inverse-QFT convention used by QuantumPhaseEstimation.
  • Controlled phase-flips $C\text{-}S_\chi$ and $C\text{-}S_0$ are implemented using multi-controlled phase gates ($CZ$, $CCZ$, $MCZ$).
  • Inverse QFT is applied to the evaluation register, and the amplitude is decoded as the expectation $\hat{a} = \sum_y P(y)\sin^2(\pi y / 2^m)$ over the measurement outcome distribution.
  • A custom is_good_state predicate is honoured: it is enumerated over the state register and synthesized into an explicit oracle when it differs from the default all-objective-qubits-are-1 marking. Problems using the default marking take the compact objective-qubit encoding and emit identical gates. The enumeration is bounded by MAX_ORACLE_QUBITS; the default marking carries no such bound. IterativeAmplitudeEstimation does not yet build its Grover operator from the predicate, and the documentation states this.

Verification

Focused coverage for amplitude estimation in tests/algorithms/test_amplitude_estimation.py:

  • Circuit construction, and estimation against the local statevector simulator.
  • An amplitude sweep across $\theta \in {0.2, 0.5, 0.8, 1.5, 2.2, 2.8}$ plus the $a=0$ and $a=1$ boundaries, which pins the controlled-power ordering — the previous assignment passed at $\theta=0.8$ but returned ~0.378 at $\theta=0.2$.
  • Custom predicates that cannot be expressed as the default marking ("exactly one qubit is 1", "both qubits are 0"), and the degenerate predicates marking no states and every state.
  • Oracle resolution: the default and equivalent-to-default predicates take the compact path; a genuinely custom predicate enumerates; the size bound applies to custom predicates only; and reassigning is_good_state after construction reaches the oracle rather than only the result counting.

Ruff and cspell pass, and the full suite is green locally with RUN_ALGO_CORRECTNESS=1 and TEST_OPTIMIZERS=1.

Gpar377 added 4 commits July 27, 2026 13:57
…AE) using QPE

- Implement Canonical QAE in AmplitudeEstimation class replacing previous NotImplementedError stubs.
- Synthesize controlled Grover operator powers C-Q^(2^j) via exact quantum gate decomposition:
  C-Q = A · (C-S_0) · A^\dagger · (C-S_chi)
- Apply Inverse QFT on evaluation register to extract amplitude estimate a = sin^2(pi * y / 2^m).
- Add unit tests for circuit construction and local statevector simulator estimation.
@Gpar377

Gpar377 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Hi,

Just following up on this PR!

As a quick recap, this PR completes the AmplitudeEstimation class (which previously raised NotImplementedError) by implementing Canonical Quantum Amplitude Estimation (QAE) using Quantum Phase Estimation (QPE):

  • Controlled Grover Synthesis: Controlled Grover powers $C-Q^{2^j}$ are synthesized using the exact gate identity $C-Q = (Z_c \otimes I) \cdot A \cdot (C-S_0) \cdot A^\dagger \cdot (C-S_\chi)$ with multi-controlled phase gates.
  • Statistical Amplitude Estimation: Computes the expectation value $\hat{a} = \sum_y P(y) \sin^2(\frac{\pi y}{2^m})$ over the measurement outcome distribution to eliminate shot sampling noise and discretization error.
  • Testing: Includes full test coverage in tests/algorithms/test_amplitude_estimation.py (all tests passing).

Whenever you get a chance to review, let me know if any adjustments are needed. Thanks!

@krishna-ajay

Copy link
Copy Markdown
Collaborator

Thanks for implementing canonical QAE and adding local simulator coverage. I reviewed the implementation against the repository’s QPE/QFT conventions and found two correctness issues that should be addressed before merging:

  1. Controlled-power ordering: build_circuit() applies $Q^{2^j}$ to evaluation qubit j, but this repository’s little-endian inverse-QFT convention requires $Q^{2^{m-1-j}}$, consistent with QuantumPhaseEstimation. The existing θ=0.8 test happens to pass, but broader checks fail significantly—for example, θ=0.2 returns approximately 0.376 instead of 0.010, and θ=2.8 returns approximately 0.373 instead of 0.971. Reversing the power assignment corrects these cases.

  2. Custom good-state predicates: _apply_controlled_s_chi() always marks the state where all objective qubits are 1; it does not use problem.is_good_state. Consequently, valid EstimationProblem instances with a custom predicate silently estimate a different event. If arbitrary predicates cannot be synthesized into an oracle, the API should explicitly reject them or accept a circuit-level Grover/oracle implementation.

Please also update the main README and algorithm guide, which still state that canonical AmplitudeEstimation is planned or unavailable, and add correctness tests covering several amplitudes and custom-good-state behavior.

The PR merges cleanly with current main, Ruff lint/format checks pass, and the merged algorithm suite passes (88 passed, 27 skipped). However, those tests do not expose the correctness problems above, so I recommend changes before merge.

@Gpar377

Gpar377 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review, and for actually running the broader amplitude sweep — that was exactly the gap. I've confirmed both issues locally:

  1. Controlled-power ordering: You're right, build_circuit() uses 2**j where QuantumPhaseEstimation._apply_controlled_unitaries() uses 2 ** (precision_qubits - 1 - j). My θ=0.8 test passing was luck, not correctness. Fixing to match the repo convention.

  2. is_good_state ignored: Confirmed — _apply_controlled_s_chi() hardcodes the all-ones marking and never consults problem.is_good_state, even though IterativeAmplitudeEstimation.estimate() does use it. I'll make the two consistent rather than leaving the divergence.

One related issue I found while verifying your numbers: estimate() averages sin²(πy/2ᵐ) across the entire outcome distribution. Even with the ordering corrected, QPE leakage tails are spread across all y and drag that average toward 0.5 — which is tolerable at a≈0.15 but not at a≈0.01. So the ordering fix alone won't make a multi-amplitude test suite pass; the decode needs to be peak-based rather than a flat expectation. I'll address that in the same round.

Will also update README.md:72 and qpiai_quantum/algorithms/README.md:196 (both still say the canonical variant is planned/unavailable) and add tests covering several amplitudes plus custom-good-state behaviour.

Will push an update shortly — thanks for holding the bar here.

…good_state

Evaluation qubit j applied Q^(2^j), but the little-endian inverse-QFT convention requires Q^(2^(m-1-j)), matching QuantumPhaseEstimation. The theta=0.8 test passed by coincidence; theta=0.2 returned 0.378 instead of 0.010.

Additionally, _apply_controlled_s_chi always marked the all-ones state on the objective qubits and never consulted problem.is_good_state, so custom predicates silently estimated a different event. The predicate is now enumerated over the state register and synthesised into an explicit oracle when it differs from the default marking; the default path is unchanged and emits identical gates. Registers larger than MAX_ORACLE_QUBITS are rejected with a ValueError.

The multi-controlled-Z pattern duplicated across S_chi and S_0 is factored into a single helper.
…aviour

Adds a six-point amplitude sweep, boundary amplitudes, custom and degenerate is_good_state predicates, _resolve_good_states unit tests, and oversized-register rejection. Each new test fails against the previous implementation.
Both READMEs stated the canonical QPE-based variant was planned or raised NotImplementedError. Adds a worked example and documents custom is_good_state support.
The CI spell check rejected the en-GB spelling synthesise/synthesised, which is not in the cspell.json word list.
@Gpar377

Gpar377 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Pushed the fixes in d7dcce5, 1cb2354 and 570e0f5.

1. Controlled-power ordering (d7dcce5)

build_circuit() now applies $Q^{2^{m-1-j}}$ to evaluation qubit j, matching QuantumPhaseEstimation._apply_controlled_unitaries(). I reproduced your numbers before changing anything — the old ordering gives 0.378 at θ=0.2 and 0.370 at θ=2.8, matching what you reported. After the fix, worst absolute error across θ ∈ [0, π] at m=6 is 0.0142.

2. Custom good-state predicates (d7dcce5)

_apply_controlled_s_chi() now honours problem.is_good_state. A new _resolve_good_states() enumerates the predicate over the state register:

  • If it marks exactly what the default all-objective-qubits-are-1 rule marks, it returns None and the original compact objective-qubit encoding is used — so the common path emits gate-for-gate identical circuits. This also covers a custom predicate that happens to be equivalent to the default.
  • Otherwise it returns the marked basis states and one phase flip is synthesized per state.
  • Registers above AmplitudeEstimation.MAX_ORACLE_QUBITS (16) raise a ValueError pointing at IterativeAmplitudeEstimation, so oversized problems are rejected explicitly rather than silently mis-estimated.

Verified against predicates that genuinely cannot be expressed as the default marking — "exactly one qubit is 1", "both qubits are 0", 3-qubit odd parity — plus the degenerate cases marking nothing and marking everything. Worst error 0.0107.

I also factored the multi-controlled-Z pattern, which was written out three times across S_chi and S_0, into a single helper.

3. Tests (1cb2354)

Amplitude sweep, boundary amplitudes, custom and degenerate predicates, _resolve_good_states unit tests, and oversized-register rejection. I checked these are load-bearing by reverting each fix in isolation: reverting only the ordering fails 7 tests, reverting only the oracle fix with ordering intact fails 3. tests/algorithms/ is now 103 passed, 27 skipped, up from 88 passed.

4. Docs (570e0f5)

Updated both READMEs, added a worked example, documented custom is_good_state support.

Correction to my previous comment: I claimed the expectation-value decode in estimate() would need replacing with a peak-based one. That was wrong and I did not change it. Measuring both across m=3..7, the expectation decode converges (0.0018 at m=7) while a peak-based decode plateaus around 0.035 due to grid quantisation — the expectation decode is the better of the two, and the poor small-amplitude behaviour I attributed to it was entirely the ordering bug. It does degrade at low resolution (0.106 at m=3), so the docs now recommend 5 or more evaluation qubits.


On the failing checks — they look unrelated to this PR. All five jobs fail at Dependency Audit:
Found 1 known vulnerability in 1 package
Name Version ID Fix Versions
pip 26.1.2 PYSEC-2026-3721 26.2

The vulnerable package is pip itself in the runner environment; this PR changes only amplitude_estimation.py, its tests, and two READMEs, with no changes to pyproject.toml or uv.lock. Lint Check and Spell Check pass, but Build and Twine Check and Run Tests are skipped because the audit exits first — so CI currently gives no signal on the tests.

main last ran green on 2026-08-12 and this advisory postdates that, so I'd expect any PR in the repo to hit it right now. Likely needs a pip bump in CI or --ignore-vuln PYSEC-2026-3721. Happy to open a separate issue if that helps.

Locally the full suite is 268 passed, 15 skipped with RUN_ALGO_CORRECTNESS=1 and TEST_OPTIMIZERS=1.

@krishna-ajay

Copy link
Copy Markdown
Collaborator

Thanks for the update. I pulled the latest commits and checked the changes locally. The original power-ordering issue is fixed, and the new canonical QAE tests look good.

I noticed one remaining problem around is_good_state. Canonical QAE now supports it, but IterativeAmplitudeEstimation still builds its Grover operator using only objective_qubits. It uses is_good_state later when counting results, so the circuit and the final calculation can use different definitions of a good state. I tried two custom predicates and got roughly 0.492 instead of 0.342, and 0.837 instead of 0.617.

I think the cleanest approach for this PR is:

  • Don’t recommend the iterative implementation as the fallback for large custom predicates unless its oracle is updated too.
  • Apply the 16-qubit limit only when a custom predicate actually needs enumeration. The normal objective-qubit path already has a compact implementation and shouldn’t need that limit.
  • Remove the statement that both variants support custom predicates for now. Iterative support can be fixed separately if that is outside this PR’s scope.
  • There is also one old sentence near the bottom of qpiai_quantum/algorithms/README.md that still says canonical QAE is only planned. The PR description still mentions the old 2**j ordering as well.

The canonical implementation itself is in much better shape now. Once these remaining inconsistencies are cleaned up, I’m happy to take another look.

…dicates

MAX_ORACLE_QUBITS was enforced before checking whether a custom predicate was supplied, so the default objective-qubit marking was rejected on large state registers even though it needs no enumeration. The bound now applies only on the enumeration path.

The error also recommended IterativeAmplitudeEstimation, which builds its Grover operator from objective_qubits alone and does not honour is_good_state either; it now points at objective_qubits instead.

has_custom_good_state compares the predicate by identity against the default, so reassigning is_good_state after construction reaches the oracle rather than only the result counting.
Custom predicates are supported by the canonical variant only; IterativeAmplitudeEstimation does not yet build its Grover operator from the predicate. Also clarifies that the oracle size limit applies only to custom predicates, and removes a stale note stating the canonical variant is planned.
@Gpar377

Gpar377 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all four addressed in d2ba927 and d817240.

Oracle size limit — you're right, this was a regression I introduced. The MAX_ORACLE_QUBITS check ran before any test of whether a custom predicate was even supplied, so a large state register using the plain objective_qubits marking was rejected despite needing no enumeration. _resolve_good_states() now returns immediately for the default marking, and the bound applies only on the enumeration path.

IAE as fallback — removed. You're right that it isn't a valid fallback: _build_grover_operator() marks purely off objective_qubits and never consults is_good_state, so it has the same defect. The error message now points at objective_qubits instead.

"Both variants" claim — removed. The docs now say custom predicates are supported by the canonical variant only, and note explicitly that IterativeAmplitudeEstimation does not yet build its Grover operator from the predicate.

Stale note — fixed. That one was in the summary section at the bottom of qpiai_quantum/algorithms/README.md; it never used the class name, which is how it survived my earlier pass. I've also rewritten the PR description, which still described the old 2**j ordering.

One related thing I tightened while in there: has_custom_good_state compares the predicate by identity against the default rather than recording a flag at construction time. Otherwise reassigning problem.is_good_state after construction would be honoured when counting results but ignored by the oracle — the same circuit/calculation split you flagged for IAE. Test added.

I've left the IAE oracle itself alone as you suggested. Happy to open a separate issue for it, or send a follow-up PR once this lands — whichever you prefer.


On the failing checks — they look unrelated to this PR again. All five jobs fail at Dependency Audit; Lint Check and Spell Check pass, and Run Tests is skipped because the audit exits first, so CI currently gives no signal on the tests.

The flagged package is tornado 6.5.7, a transitive dependency via ipykernel and jupyter-client:

All three are fixed in 6.5.8, so uv lock --upgrade-package tornado on main should clear it. main last ran green on 2026-08-27 and this advisory postdates that, so I'd expect any open PR to hit the same failure right now. This PR changes only amplitude_estimation.py, its tests, and two READMEs — nothing in uv.lock or pyproject.toml — so I've left the lockfile alone rather than widening the diff here. Happy to include the bump if you'd rather.

Locally the full suite is 278 passed, 16 skipped with RUN_ALGO_CORRECTNESS=1 and TEST_OPTIMIZERS=1; ruff and cspell clean.

@krishna-ajay

Copy link
Copy Markdown
Collaborator

Thanks — I merged the current main into this branch so it picks up the Tornado 6.5.8 lockfile update and gets a fresh CI run. One small follow-up: could you update the PR description as well? The controlled-power bullet still says Q^(2^j), but the corrected ordering is Q^(2^(m-1-j)), and the “48 algorithm unit tests” count is now stale. It may be better to remove the fixed test count and simply mention the focused amplitude-estimation coverage.

@Gpar377

Gpar377 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for merging main in — and for the catch on the description. Updated it:

  • The controlled-power bullet now reads $C\text{-}Q^{2^{m-1-j}}$ and notes that it matches the inverse-QFT convention used by QuantumPhaseEstimation.
  • Dropped the hardcoded test count. The Verification section now describes the amplitude-estimation coverage itself — the sweep and boundary amplitudes that pin the ordering, the custom and degenerate predicates, and the oracle-resolution cases — so it won't go stale as the suite grows.

I also added a bullet for the custom is_good_state handling, which the description never covered: the enumeration bound applying to custom predicates only, and the note that IterativeAmplitudeEstimation doesn't yet build its Grover operator from the predicate.

Let me know if you'd like the IAE oracle handled as a separate issue or a follow-up PR once this lands.

@krishna-ajay

Copy link
Copy Markdown
Collaborator

Thanks for updating this. It looks like the description change may not have saved, though. I still see the old Q^(2^j) ordering and the “48 algorithm unit tests” line in the PR description. Could you please check it once more? Everything else looks good from my side, and CI is green across all Python versions.

@Gpar377

Gpar377 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Sorry about that — I'd edited the title area rather than the description body the first time, so only part of it saved. Updated properly now:

  • Controlled-power bullet reads $C\text{-}Q^{2^{m-1-j}}$, noting it matches the inverse-QFT convention used by QuantumPhaseEstimation.
  • Dropped the hardcoded test count. Verification now describes the amplitude-estimation coverage itself, so it won't go stale as the suite grows.
  • Added a bullet for the custom is_good_state handling, which the description hadn't covered.

Thanks for the patience on this one — and for confirming CI is green.

@krishna-ajay

Copy link
Copy Markdown
Collaborator

Thanks for working through the review feedback and updating the implementation, tests, and documentation. I’ve checked the final changes again, including the controlled-power ordering, custom is_good_state handling, and the additional amplitude cases. Everything looks good, and CI is green across all supported Python versions.

The iterative QAE custom-predicate limitation can be handled separately. Approving and merging this.

@krishna-ajay
krishna-ajay merged commit 619b8b7 into qpiai:main Sep 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: feat: implement Canonical Quantum Amplitude Estimation (QAE) using QPE

4 participants