Skip to content

Creating a new QPE framework to fix bugs and enable new features - #105

Merged
BrendanKKrueger merged 28 commits into
mainfrom
bkk_tQPE
Oct 13, 2026
Merged

BrendanKKrueger merged 28 commits into
mainfrom
bkk_tQPE

Conversation

@BrendanKKrueger

Copy link
Copy Markdown
Collaborator

Work being done with Claude Code. Not yet complete (missing tapering QPE implementations). Not yet reviewed by a human.

@BrendanKKrueger
BrendanKKrueger added this pull request to stack #106 October 2, 2026 22:39
@BrendanKKrueger BrendanKKrueger self-assigned this Oct 2, 2026
Base automatically changed from bkk_pow to main October 5, 2026 17:20
@BrendanKKrueger
BrendanKKrueger force-pushed the bkk_tQPE branch 5 times, most recently from 991afeb to fd52f92 Compare October 5, 2026 22:07
@BrendanKKrueger
BrendanKKrueger marked this pull request as ready for review October 12, 2026 15:31
@cferenba

Copy link
Copy Markdown
Member

I can't say I understand all of the tests, but the basic algorithm looks sound to me.

Comment thread common/flexible_qpe.py
@BrendanKKrueger

Copy link
Copy Markdown
Collaborator Author

Fixes #16

…ction

Problem: Qualtran 0.4.0's TextbookQPE builds controlled U^(2^j) via
cirq.pow on the unitary's cirq operation. For unitaries that are plain
Bloqs (e.g. QHAT's Trotterization) this hits an assertion in
BloqAsCirqGate.__pow__, so decompose_bloq(), tensor_contract(), and the
Qualtran QubitCount resource estimate all fail. "QPE: qualtran textbook"
therefore cannot produce an algorithm matrix, run numerical simulation,
or (with the qualtran estimator) report a qubit count for ramped-Trotter
unitaries.

Solution: add FlexibleQPE (common/flexible_qpe.py), a textbook QPE bloq
whose interface mirrors TextbookQPE in newer Qualtran releases (fields
unitary, ctrl_state_prep, qft_inv; registers qpe_reg + the unitary's
registers) so QHAT can adopt the upstream framework when it upgrades.
QPEWindowStateBase and RectangularWindowState are backported in
common/qpe_window_state.py. The one intentional deviation from upstream
is that qpe_reg is THRU rather than allocating, so the QPE matrix stays
square as with 0.4.0.

FlexibleQPE decomposes natively as a Bloq and uses the structure of QPE:
- U^(2^j) uses the unitary's __pow__ when available (fast-forwarding;
  relies on the Trotterization __pow__ fix), else Power(U, 2^j).
- The call graph has one controlled-power entry per phase qubit.
- QubitCount is computed statically from the components.
- tensor_contract builds one dense tensor from the block-diagonal
  structure diag_k(U^k) instead of contracting ~2^m copies of U.

It is selected with algorithm.method = "QPE: qhat textbook". The new
optional algorithm.ctrl_state_prep / algorithm.qft_inv settings
currently accept only "rectangular" / "textbook" (the defaults) and are
the hook for alternative window states and inverse QFTs later.

Verification: common/tests/test_flexible_qpe.py and
analysis/tests/test_flexible_qpe_algorithm.py (42 tests) check the
optimized tensor against the generic decomposition contraction, against
0.4.0 TextbookQPE (for ZPowGate, where it works), and against a dense
reference built from U's matrix; exact-phase estimation; T-complexity
and qubit counts; and config routing. The full common/tests and
analysis/tests suites pass (588 tests). End-to-end on the Be-H example
(6 system + 3 phase qubits, ramped Trotter) the driver writes the
512x512 algorithm matrix (unitarity error 9e-14) and qualtran resource
estimates; the matrix matches the dense reference to 8e-16, and QPE on
every eigenvector of U returns the nearest 3-bit phase. The T-count is
35400 vs 35560 for TextbookQPE, the difference coming from terms merged
at step boundaries inside fast-forwarded U^k.
…gets

Problem: QPE register sizing lived in analysis/algorithm.py as a single
textbook formula (P0 + confidence bits), which cannot accommodate QPE
variants whose sizing depends on their components (e.g. FlexibleQPE with
other window states). The evolution time t was rounded so the energy
range was 2^P0 * algorithm.energy_error and then shrunk by
phase_scale_factor (default 1.01). That margin was a ratio, not tied to
the errors, so for small registers it did not keep the top and bottom
of the spectrum distinguishable, and the register only met the energy
target to within the scale factor.

Solution:
- Uniform construction interface for non-qubitized QPE:
  from_requirements(unitary, phase_error, probability_of_failure, ...)
  and from_num_phase_qubits(unitary, P, ...). Phase is in turns, [0, 1).
  FlexibleQPE dispatches sizing to its ctrl_state_prep class;
  QPEWindowStateBase declares both as abstract classmethods and
  RectangularWindowState implements them (precision bits + Nielsen &
  Chuang Eq. 5.35 confidence bits). "QPE: qualtran textbook" now builds
  NewTextbookQPE, a TextbookQPE subclass with the same interface. An
  explicit algorithm.num_phase_qubits is used as given. Qubitized QPE is
  unchanged.
- Trotterization and RampedTrotterizedUnitary gain
  phase_error_from_energy_error(), which build_algorithm uses to turn
  algorithm.energy_error into the phase-error requirement.
- The driver chooses t/hbar = 2 pi / (W + 2 dE_enc + 2 dE_qpe), where W
  is the width of the energy bounds, dE_enc is unitary.energy_error and
  dE_qpe is algorithm.energy_error (zero for non-QPE algorithms). A user
  timestep is used as given, with a warning if it is too long to prevent
  aliasing. phase_scale_factor is removed (setting it now raises), along
  with compute_initial_phase_qubits and the P0 argument.
- Rename "QPE: qhat textbook" to "QPE: QHAT flexible" to match the
  README. error_budget.txt records the evolution-time rule and how it
  interacts with QHAT splitting a total error budget itself.

Behavior change: evolution times change for all ramped-Trotter runs,
which can change Trotter step counts and QPE register sizes relative to
earlier results. QPE sized from energy_error now requires
algorithm.probability_of_failure.

Verification: new tests cover from_requirements and
from_num_phase_qubits (including a worst-case check that the rectangular
window meets the failure probability), dispatch to the window-state
class and qft_inv factory, energy-to-phase conversion against U's
eigenphases, the evolution-time rule, and identical sizing for both
non-qubitized methods. The full common/tests and analysis/tests suites
pass (612 tests). End-to-end on the Be-H example (dE_qpe = 0.6,
probability_of_failure = 0.25), the driver builds a 6-qubit FlexibleQPE;
over all 64 eigenvectors of U the worst probability of a phase error
beyond the target is 0.033, and the most likely outcome's energy is
within 0.046 of the true energy.
Problem: the previous commit replaced the driver's evolution-time rule
with t/hbar = 2 pi / (W + 2 dE_enc + 2 dE_qpe). We want to keep that
idea for a later change (the analysis is in an untracked planning note),
but not to change evolution times, Trotter step counts, or QPE register
sizes now.

Solution: restore compute_initial_phase_qubits and the driver's
power-of-two rounding, P0 = ceil(log2(W / dE_qpe)) and
t/hbar = 2 pi / (2^P0 dE_qpe) (or 2 pi / W without a QPE energy error),
but without phase_scale_factor, which stays removed. Remove
compute_evolution_time, the user-timestep aliasing warning, and the
README/operators.py text describing the margin rule. The uniform QPE
construction interface (from_requirements / from_num_phase_qubits) and
phase_error_from_energy_error are kept: with this t the unitary's phase
error is exactly 2^-P0, so both non-qubitized QPE methods again use P0
plus the textbook confidence bits.

Verification: a new test checks, for both "QPE: QHAT flexible" and
"QPE: qualtran textbook", that a Trotter unitary built with the driver's
t gives a register of P0 plus the confidence bits. The full common/tests
and analysis/tests suites pass (612 tests).
…ult is the rectangular window state that gives you 'textbook' phase estimation.
Override tensor_contract to fill the output one phase-register column at a
time, so peak memory stays near the output size (was ~2.5x), and build
U^(2^j) by squaring the matrix of U instead of contracting each power bloq.
add_my_tensors now wraps tensor_contract so nested FlexibleQPEs use it too.
Fold textbook_confidence_bits into RectangularWindowState.from_precision_and_delta,
which now validates numeric inputs and still accepts symbolic ones, and have
from_requirements call it. NewTextbookQPE sizes itself from
RectangularWindowState.from_requirements, since TextbookQPE uses that window.
Observed round-off in the driver chain puts log2(1/phase_error) at most about 1e-15 above n, so
1e-12 leaves ample margin while being closer to what the tolerance is for.
Sizing the rectangular window to a standard-deviation target costs O(1/eps^2) unitary
applications, losing the quadratic advantage of QPE; QHAT sizes QPE by error and failure
probability instead.
Removing phase_scale_factor changes evolution times independently of the QPE work, so it is
split out. This restores the option with its validation and TOML output, its use in the driver
evolution time, its README and operators.py documentation, and its tests, all as on main. The
QPE change to build_algorithm (no P0 argument) is kept.
"QPE: qualtran textbook" now sizes from requirements via TextbookQPE.from_precision_and_delta
with precision_bits, so no window state is built just to read its size. The shared checks
move to qpe_requirements, which also rejects probability_of_failure outside (0, 1).
The old round-off test used an input that evaluated to exactly 2^-7, so it passed without the
tolerance. Use values a few ulps below 2^-n, plus a driver-chain case (W=1, dE=0.165) that
produces one, and clarify that t in the driver test assumes phase_scale_factor = 1.
P0 is no longer used now that QPE is sized by precision_bits, so return only the energy window
mapped onto one phase turn. The extra width is split evenly about the middle of the range, so
the window matches the one the phases decode against (needed once the energy shift is removed,
see issue 103). The evolution time is unchanged.
The default algorithm.method was "qualtran textbook", which build_algorithm rejects, so configs
that did not set the method failed. Use "QPE: qualtran textbook" and add a test. List all six
methods in the example config and README, and clarify how the QPE sizing parameters interact.
build_algorithm converted algorithm.energy_error to a phase error before checking
num_phase_qubits, so setting both raised for unitaries without phase_error_from_energy_error
(e.g. PauliStringEvolution), even though the conversion was not needed. Convert only when sizing
from requirements, inside qpe_requirements, and add a regression test for both QPE methods.
…ailure

End to end: take the window from qpe_energy_window, set t = 2 pi / (hi - lo), size the QPE from
(energy_error, probability_of_failure), simulate it on each eigenstate of an exactly Trotterized
Hamiltonian, decode phases to energies, and check Pr[|E_est - E| > dE] <= p_fail. Covers centered
and off-center windows. Replaces test_tensor_contract_shape, which this subsumes.
Check that algorithm.ancilla_prep and algorithm.qft_inv are written to the TOML summary only
when set, and that the pyLIQTR and qualtran resource estimators accept a FlexibleQPE.
…ization

The default prep and inverse QFT have symmetric matrices, so a transposed component in the
optimized tensor_contract would go unnoticed.  Add a test with non-symmetric components, and run
the decomposition and matrix-power tests on the original Trotterization as well.
Cover the error for a unitary register named qpe_reg, the error when decomposing with a symbolic
register size, and the symbolic call graph (2^m - 1 controlled unitaries).
precision_bits is shared by the qualtran textbook and QHAT flexible QPE builders; test its
rounding (smallest n with 2^-n at most the phase error) and its range check.
…file

Diagonal Hamiltonians with eigenphases 0, 1/2 (one qubit) and 0, 1/4, 1/2, 3/4 (two qubits) are
written as I/Z Pauli strings to a file, loaded, Trotterized, and run through FlexibleQPE with one
phase qubit per target qubit.  Numerical simulation of each basis state must give exactly
|expected bits> x |target>, both with the optimized tensor_contract and with the decomposed circuit.

Energies are scaled by 1.7 (t = 2 pi / 1.7) rather than using t = 2 pi, where integer coefficient
errors would cancel, and three Trotter steps check the per-step time.
@BrendanKKrueger
BrendanKKrueger merged commit 5aa3fe5 into main Oct 13, 2026
14 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.

3 participants