fix(library): clifford_6_4 template missing global_phase causes silent rejection#15944
fix(library): clifford_6_4 template missing global_phase causes silent rejection#15944adcorcol wants to merge 9 commits intoQiskit:mainfrom
Conversation
|
One or more of the following people are relevant to this code:
|
662d5c1 to
bf03e41
Compare
ShellyGarion
left a comment
There was a problem hiding this comment.
Thanks for the fix!
Note that except of the clifford template which is not global phase correct, there are also some RZXGate template circuits which are also not global phase correct.
Therefore, it's worth to extend the templates library and the tests to include these cases as well.
…x_zz3 templates
The same missing global_phase bug present in clifford_6_4 also affected four
RZX templates. Each had gate content that implemented a non-trivial phase
times I, but lacked the compensating global_phase field, causing
TemplateOptimization to silently reject them.
Corrections:
- rzx_xz: global_phase = pi (gate content e^{i*pi} * I)
- rzx_zz1: global_phase = pi/2 (gate content e^{-i*pi/2} * I)
- rzx_zz2: global_phase = pi (gate content e^{i*pi} * I)
- rzx_zz3: global_phase = pi (gate content e^{i*pi} * I)
Also:
- Strengthen test_templates.py identity check from equiv() to strict ==,
making it a universal regression guard for all templates.
- Replace test_template_optimization_accepts_clifford_6_4 with
test_template_optimization_accepts_all_templates, covering every
template in the library.
- Move test_clifford_6_4_is_identity out of test_template_matching.py
(now covered universally by test_templates.py).
Pointed out by Shelly Garion in review of Qiskit#15944.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Fixed in this PR: |
ShellyGarion
left a comment
There was a problem hiding this comment.
Thanks for adding the RZX circuits. I have some minor comments on the documentation and tests.
alexanderivrii
left a comment
There was a problem hiding this comment.
Thanks for doing this, it always bothered me that some of the templates are not global-phase correct.
Generally this looks great, I have left a few minor comments. I am wondering how much of the work was done just by Claude and how much you had to do on top of that (or how much you had to instruct Claude to do what you want). Towards the end of the review, I have realized that this is a chain of two PRs and some of the comments apply to the previous PR in the chain.
One nitpick: I think the generated release notes are way too detailed and verbose, we usually don't document every line that got changed.
There was a problem hiding this comment.
I am wondering if we should update the circuit in the docstring to include the global phase? (Btw, I am not sure that the formatting in the suggestion below is fully correct).
| .. code-block:: text | |
| global phase: 7π/4 | |
| ┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐ | |
| q_0: ┤ S ├┤ H ├┤ S ├┤ H ├┤ S ├┤ H ├ | |
| └───┘└───┘└───┘└───┘└───┘└───┘ |
There was a problem hiding this comment.
Added global phase: 7π/4 to the docstring. Verified that the formatting matches what the text drawer produces by running the circuit through the text renderer locally
| qc.s(0) | ||
| qc.h(0) | ||
| # SHSHSH has gate unitary e^{i*pi/4} * I; the global_phase corrects this | ||
| # so that Operator(clifford_6_4()) == I exactly, as required by TemplateOptimization. |
There was a problem hiding this comment.
I don't know if we should mention TemplateOptimization pass in this comment: a template is any circuit that is equivalent to identity and it should not matter how this information can be used by the rest of Qiskit.
There was a problem hiding this comment.
Note that the two comments in this file also apply to other modified templates.
There was a problem hiding this comment.
Done. The inline comments no longer reference TemplateOptimization. Applied to all five templates (clifford_6_4, rzx_xz, rzx_zz1, rzx_zz2, rzx_zz3) both this edit and the adding the global phase to the docstring
| # copy metadata | ||
| dagdependency.global_phase = circuit.global_phase | ||
|
|
There was a problem hiding this comment.
I believe we might be also not propagating the global phase in dagdependency_to_circuit. While this is not strictly necessary for this PR, I am wondering if it makes sense to make that change as well.
There was a problem hiding this comment.
Thanks. Does it make sense to address this in a separate follow-up PR to keep this one focused on the global_phase fixes? If so, I can open a new issue to track it
| def test_circuit_global_phase_preserved_with_multiple_template_matches(self): | ||
| """Regression test for #14537: circuit global_phase is preserved across multiple matches. | ||
|
|
||
| When a template matches more than once in the circuit, the original circuit's | ||
| global_phase must appear exactly once in the output — not zeroed out and not | ||
| multiplied by the number of matches. | ||
| """ |
There was a problem hiding this comment.
The added tests look very nice, thank you. Maybe the only other test worth adding is for a circuit where two different global phase changing templates apply, to see that both global phase corrections are taken into account. Maybe something like "SHSHSH T HSHSHS"?
There was a problem hiding this comment.
Added test_two_global_phase_carrying_template_matches_accumulate_phase which uses SHSHSH–T–SHSHSH with clifford_6_4 as the template.
| Fixed :func:`.clifford_6_4` and four RZX templates so that | ||
| :class:`.TemplateOptimization` now accepts and applies them. | ||
|
|
||
| Each of these templates implements the identity only up to a global phase in | ||
| their gate content, but was missing the compensating ``global_phase`` field. | ||
| As a result ``Operator(template)`` was not the identity matrix and the | ||
| template was silently rejected by :class:`.TemplateOptimization` on every run. | ||
|
|
||
| The affected templates and their corrections: | ||
|
|
||
| - ``clifford_6_4`` (SHSHSH): gate unitary ``e^{i*pi/4} * I``, | ||
| fixed by setting ``global_phase = -pi/4``. | ||
| - ``rzx_xz``: gate unitary ``e^{i*pi} * I``, | ||
| fixed by setting ``global_phase = pi``. | ||
| - ``rzx_zz1``: gate unitary ``e^{-i*pi/2} * I``, | ||
| fixed by setting ``global_phase = pi/2``. | ||
| - ``rzx_zz2``: gate unitary ``e^{i*pi} * I``, | ||
| fixed by setting ``global_phase = pi``. | ||
| - ``rzx_zz3``: gate unitary ``e^{i*pi} * I``, | ||
| fixed by setting ``global_phase = pi``. |
There was a problem hiding this comment.
Can we shorten this, just listing the templates that are now global-phase correct.
There was a problem hiding this comment.
Done. The release note now just lists the five fixed templates and states what was wrong. No details of every code change.
| The test suite's template sanity check (``test_templates``) is also | ||
| strengthened: it now uses strict ``Operator ==`` instead of | ||
| ``Operator.equiv()`` so that any future template with a missing | ||
| ``global_phase`` is caught immediately at the unit level. |
There was a problem hiding this comment.
I don't think we usually document changes to testing, as these are not user-facing.
| These fixes depend on the global-phase propagation fixes introduced for | ||
| `#14537 <https://github.com/Qiskit/qiskit/issues/14537>`__ | ||
| (``circuit_to_dagdependency`` now copies ``global_phase`` | ||
| and ``TemplateSubstitution`` now adjusts the circuit's ``global_phase`` | ||
| for each template substitution applied). |
There was a problem hiding this comment.
I have initially missed that this PR is based on another PR under review. Since it has its own release notes, can we remove this paragraph?
There was a problem hiding this comment.
Removed the paragraph referencing PR #15943. This PR's release note now only covers the template global_phase fixes
Regarding working with Claude on this: it requires a good amount of hand holding but after a few iterations I think the results are not too terrible. I have written zero code in these PRs but I have questioned most of the outputs and optimized the way I work with the tool in -I think- interesting ways. Happy to expand on it, but for now I'll say that deploying agents in non-trivial ways tends to result in better context for some of the directions the output could take. |
alexanderivrii
left a comment
There was a problem hiding this comment.
LGTM, modulo one sentence in the release note which seems a bit unclear. That's impressive that all the code writing was done by Claude (with hand-holding). I dare to ask, who wrote the replies on the PR -- I wouldn't be surprised if one could use Clause for that as well. 😅
| q_0: ┤ X ├─────────┤ X ├┤ RZ(π/2) ├┤ RX(π/2) ├┤ RZ(π/2) ├┤0 ├» | ||
| └─┬─┘┌───────┐└─┬─┘└─────────┘└─────────┘└─────────┘│ RZX(-ϴ) │» | ||
| q_1: ──■──┤ RX(ϴ) ├──■───────────────────────────────────┤1 ├» | ||
| q_0: ┤ X ├─────────┤ X ├┤ Rz(π/2) ├┤ Rx(π/2) ├┤ Rz(π/2) ├┤0 ├» | ||
| └─┬─┘┌───────┐└─┬─┘└─────────┘└─────────┘└─────────┘│ Rzx(-ϴ) │» | ||
| q_1: ──■──┤ Rx(ϴ) ├──■───────────────────────────────────┤1 ├» | ||
| └───────┘ └──────────┘» |
There was a problem hiding this comment.
Nice! The circuit drawer indeed draws "Rz" instead of "RZ".
There was a problem hiding this comment.
I wonder if this drawings should be fixed in all the RZX templates, not only the ones that had global phase issues.
There was a problem hiding this comment.
@ShellyGarion, would you like to open a "good first issue" about this?
| Fixed :func:`.clifford_6_4`, :func:`.rzx_xz`, :func:`.rzx_zz1`, :func:`.rzx_zz2`, | ||
| and :func:`.rzx_zz3` templates so that :class:`.TemplateOptimization` now accepts | ||
| and applies them. Each of these templates implements the identity only up to a | ||
| global phase in their gate content but was missing the compensating ``global_phase`` | ||
| field, causing :class:`.TemplateOptimization` to silently reject them on every run. | ||
| Fixes `#14538 <https://github.com/Qiskit/qiskit/issues/14538>`__. |
There was a problem hiding this comment.
Regarding the sentence: Each of these templates implements the identity only up to a global phase..., is it clear that "only up to the global phase" was before the fix, while now the templates implement the identity exactly?
That's a very fair question 😅 I did run the questions by Claude but typed the answers myself. More like a sanity check than anything else. But there is one -at least to me- illuminating example in the question about |
|
Could you rebase this on |
…t rejection
The SHSHSH template has gate unitary e^{i*pi/4}*I but lacked global_phase=-pi/4,
so Operator(clifford_6_4()) was not the identity matrix and TemplateOptimization
silently discarded the template on every run.
Set global_phase = -pi/4 so the full operator is exactly I.
Fixes Qiskit#14538.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The :issue: role is not registered in this project's Sphinx config. Use inline hyperlinks matching the existing convention in releasenotes/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…x_zz3 templates
The same missing global_phase bug present in clifford_6_4 also affected four
RZX templates. Each had gate content that implemented a non-trivial phase
times I, but lacked the compensating global_phase field, causing
TemplateOptimization to silently reject them.
Corrections:
- rzx_xz: global_phase = pi (gate content e^{i*pi} * I)
- rzx_zz1: global_phase = pi/2 (gate content e^{-i*pi/2} * I)
- rzx_zz2: global_phase = pi (gate content e^{i*pi} * I)
- rzx_zz3: global_phase = pi (gate content e^{i*pi} * I)
Also:
- Strengthen test_templates.py identity check from equiv() to strict ==,
making it a universal regression guard for all templates.
- Replace test_template_optimization_accepts_clifford_6_4 with
test_template_optimization_accepts_all_templates, covering every
template in the library.
- Move test_clifford_6_4_is_identity out of test_template_matching.py
(now covered universally by test_templates.py).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lates clifford_6_4, rzx_xz, rzx_zz1, rzx_zz2, rzx_zz3 all implement the identity only up to a global phase in their gate content. Without the compensating global_phase field, TemplateOptimization rejects them silently on every run (the identity check sees a scalar != I). - Add the exact compensating global_phase to each template so Operator(qc) == I exactly. - Tighten test_templates.py to use Operator equality (not equiv()) so the same class of bug is caught in future. - Add test_template_optimization_accepts_all_templates to test_template_matching.py to verify that TemplateOptimization accepts and fully applies every template in the library. - Add test_two_global_phase_carrying_template_matches_accumulate_phase to verify that phase contributions from multiple matches accumulate correctly. - Shorten new test docstrings to concise one-liners (Alexander Ivrii review). - Use assertEqual(Operator(x), Operator(y)) instead of assertTrue for better failure messages. - Update template docstrings to show the global phase line and use gate names matching the text renderer (Rz/Rx/Rzx). Fixes Qiskit#14538. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
clifford_6_4 is already imported at the top of the test file; the local import inside test_two_global_phase_carrying_template_matches_accumulate_phase triggered ruff F811 (redefinition of unused name). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…review) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8f8417e to
3e86327
Compare
| result = TemplateOptimization([template])(circuit_in) | ||
|
|
||
| # All gates cancelled; global_phase must be pi/4 to match the gate unitary. | ||
| self.assertAlmostEqual(float(result.global_phase) % (2 * np.pi), np.pi / 4) |
There was a problem hiding this comment.
seems that this test is failing CI checks now
There was a problem hiding this comment.
Yeah, I think I need to be more careful before pushing changes. Let me look at this with some time throughout the week rather than blindly pushing this forward
There was a problem hiding this comment.
Ok, I think here it needs to assertEqual Operators, rather than the specific phases. We did that for 15943. Let's see if I manage to fix it 😅
…applied_to_circuit
| qc.rx(np.pi / 2, 0) | ||
| qc.rz(np.pi / 2, 0) | ||
| # Gate content has unitary e^{i*pi} * I == -I; global_phase = pi makes Operator(qc) == I exactly. | ||
| qc.global_phase = pi |
There was a problem hiding this comment.
Can we just use np.pi instead of a new import, since that's already used (same in the other files)?
There was a problem hiding this comment.
Yeah, changed to np.pi here and also in rzx_zz1, rzx_zz2, and rzx_zz3
| qc.h(0) | ||
| qc.s(0) | ||
| qc.h(0) | ||
| # SHSHSH has gate unitary e^{i*pi/4} * I; global_phase = -pi/4 makes Operator(qc) == I exactly. |
There was a problem hiding this comment.
Since the order of gates is reversed to the order of operators, I think putting SHSHSH is misleading here since it's unclear what it refers to -- can we just remove that in favor of something simpler like
| # SHSHSH has gate unitary e^{i*pi/4} * I; global_phase = -pi/4 makes Operator(qc) == I exactly. | |
| # Add a global phase of -pi/4 to get Operator(qc) == I |
There was a problem hiding this comment.
Ok, yes, changed to your suggestion.
|
|
||
| comparison = np.allclose(data, identity) | ||
|
|
There was a problem hiding this comment.
Could we undo these unrelated changes?
| all_gate_names = { | ||
| instr.operation.name for template in all_templates for instr in template.data | ||
| } | ||
| extra_costs = dict.fromkeys(all_gate_names, 1) |
There was a problem hiding this comment.
Why is this necessary to test that the template optimization accepts all templates?
| with self.subTest(template=template.name): | ||
| result = PassManager( | ||
| TemplateOptimization([template], user_cost_dict=extra_costs) | ||
| ).run(template.copy()) |
There was a problem hiding this comment.
The explicit copy shouldn't be necessary, since the PassManager returns a copy already
| ).run(template.copy()) | |
| ).run(template) |
There was a problem hiding this comment.
Not applicable after removing the test for all templates
| # All gates cancelled; total phase = circuit phase + template compensation | ||
| # = pi/3 + pi/4 = 7*pi/12. | ||
| self.assertAlmostEqual(result.global_phase, 7 * np.pi / 12) | ||
| self.assertAlmostEqual(float(result.global_phase) % (2 * np.pi), 7 * np.pi / 12) |
There was a problem hiding this comment.
This manual modulo shouldn't be necessary (this might be rebasing gone wrong, since we already undid this in the last PR)
There was a problem hiding this comment.
Removed modulo here and in another point in this file
| self.assertEqual(result.count_ops(), {}) | ||
| self.assertEqual(Operator(circuit_in), Operator(result)) | ||
|
|
||
| def test_two_global_phase_carrying_template_matches_accumulate_phase(self): |
There was a problem hiding this comment.
I think we can remove this test, all templates are already tested in another test and previous tests (from the last PR) already check that global phases are now handled correctly
|
seems that there is a black failure |
ShellyGarion
left a comment
There was a problem hiding this comment.
LGTM. @alexanderivrii @Cryoris - do you have additional comments?
Summary
Fixes #14538.
The
clifford_6_4template (SHSHSH — six gates: S, H, S, H, S, H) has gateunitary
e^{i*π/4} * Ibut was missingglobal_phase = -π/4. As a result,Operator(clifford_6_4())wase^{i*π/4} * Irather thanI, andTemplateOptimizationsilently rejected the template on every run because itsidentity check requires the full operator to be exactly the identity matrix.
Fix: set
global_phase = -π/4inclifford_6_4()so thatOperator(clifford_6_4()) == Iexactly.Dependency on #15943
This branch is built on top of #15943 (fix for #14537) and includes those
commits in its diff. The incremental changes introduced by this PR are:
qiskit/circuit/library/templates/clifford/clifford_6_4.py: addglobal_phase = -pi/4(usingfrom math import pi).test/python/transpiler/test_template_matching.py: two new regression tests.releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml: release note.The dependency is load-bearing: the second regression test asserts strict
operator equality (
Operator(circuit_in) == Operator(result), including globalphase), which only holds because #15943 fixes
TemplateSubstitutionto subtractthe template's
global_phasefrom the output circuit for each match applied.Once #15943 merges, this branch can be rebased onto
mainwith no conflicts andthe diff will show only the three files above.
Test plan
test_clifford_6_4_is_identity— direct regression guard: assertsOperator(clifford_6_4()).datais the 2×2 identity matrix.test_template_optimization_accepts_clifford_6_4— end-to-end: the SHSHSHpattern is cancelled (zero gates remaining) and the output unitary equals the
input (strict
Operator ==, not justequiv).🤖 Generated with Claude Code