From c69ffeef28d8ad3ae543f720ebe4b8b315d36c31 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Fri, 3 Apr 2026 15:07:00 -0400 Subject: [PATCH 1/9] fix(library): clifford_6_4 template missing global_phase causes silent 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 #14538. Co-Authored-By: Claude Sonnet 4.6 --- .../templates/clifford/clifford_6_4.py | 5 +++ .../optimization/template_optimization.py | 2 -- ...-clifford-6-4-template-identity-14538.yaml | 20 +++++++++++ .../transpiler/test_template_matching.py | 33 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml diff --git a/qiskit/circuit/library/templates/clifford/clifford_6_4.py b/qiskit/circuit/library/templates/clifford/clifford_6_4.py index 5377a99b413c..2c92ad1ab04a 100644 --- a/qiskit/circuit/library/templates/clifford/clifford_6_4.py +++ b/qiskit/circuit/library/templates/clifford/clifford_6_4.py @@ -11,6 +11,8 @@ # that they have been altered from the originals. +from math import pi + from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -34,4 +36,7 @@ def clifford_6_4(): qc.h(0) 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. + qc.global_phase = -pi / 4 return qc diff --git a/qiskit/transpiler/passes/optimization/template_optimization.py b/qiskit/transpiler/passes/optimization/template_optimization.py index 3f8cb49182aa..51b0e26819b2 100644 --- a/qiskit/transpiler/passes/optimization/template_optimization.py +++ b/qiskit/transpiler/passes/optimization/template_optimization.py @@ -112,9 +112,7 @@ def run(self, dag): data = Operator(dagdependency_to_circuit(template)).data else: data = Operator(template).data - comparison = np.allclose(data, identity) - if not comparison: raise TranspilerError( "A template is a QuantumCircuit() that performs the identity." diff --git a/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml new file mode 100644 index 000000000000..7e8bad6d8ee6 --- /dev/null +++ b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml @@ -0,0 +1,20 @@ +--- +fixes: + - | + Fixed :func:`.clifford_6_4` template so that :class:`.TemplateOptimization` + now accepts and applies it. + + The ``clifford_6_4`` template (SHSHSH) has gate unitary ``e^{i*pi/4} * I`` + but was missing the compensating ``global_phase = -pi/4``, so + ``Operator(clifford_6_4())`` was not the identity matrix and the template + was silently rejected by :class:`.TemplateOptimization`. + + ``clifford_6_4`` now sets ``global_phase = -pi/4`` so that its full + operator is exactly ``I``. + + This fix depends on the global-phase propagation fixes introduced for + :issue:`14537` (``circuit_to_dagdependency`` now copies ``global_phase`` + and ``TemplateSubstitution`` now adjusts the circuit's ``global_phase`` + for each template substitution applied). + + Fixes :issue:`14538`. diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 6d5916b0985e..522e699e277a 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -29,6 +29,7 @@ clifford_3_1, clifford_4_1, clifford_4_2, + clifford_6_4, ) from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency @@ -762,6 +763,38 @@ def test_clifford_templates(self): # All of these gates are in the commutation library, i.e. the cache should not be used self.assertEqual(scc.num_cached_entries(), 0) + def test_clifford_6_4_is_identity(self): + """Test that clifford_6_4 is exactly the identity (#14538). + + SHSHSH has gate unitary e^{i*pi/4} * I; the template previously lacked the + compensating global_phase = -pi/4, so Operator(clifford_6_4()) was not I. + """ + self.assertTrue(np.allclose(Operator(clifford_6_4()).data, np.identity(2, dtype=complex))) + + def test_template_optimization_accepts_clifford_6_4(self): + """Test that TemplateOptimization accepts and applies clifford_6_4 (#14538). + + Before the fix, clifford_6_4 was missing global_phase = -pi/4, so + Operator(clifford_6_4()) was e^{i*pi/4}*I rather than I and the identity check + in TemplateOptimization silently rejected the template. + """ + qr = QuantumRegister(1, "qr") + circuit_in = QuantumCircuit(qr) + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + + result = PassManager(TemplateOptimization([clifford_6_4()])).run(circuit_in) + + # All six gates must be cancelled. + self.assertEqual(result.count_ops(), {}) + # Full phase equality (not just equiv) is verified here because the + # per-match global_phase accumulation fix from #14537 is included in this branch. + self.assertTrue(Operator(circuit_in) == Operator(result)) + def test_circuit_global_phase_preserved_after_single_and_multiple_template_match(self): """Test that circuit global_phase survives template optimization (#14537).""" From 096a4acd4031112c7052c81ba84ed4036dd61db2 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Fri, 3 Apr 2026 15:21:01 -0400 Subject: [PATCH 2/9] docs: use RST hyperlinks for issue references in release notes 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 --- .../notes/fix-clifford-6-4-template-identity-14538.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml index 7e8bad6d8ee6..6f2ae313aad2 100644 --- a/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml +++ b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml @@ -13,8 +13,9 @@ fixes: operator is exactly ``I``. This fix depends on the global-phase propagation fixes introduced for - :issue:`14537` (``circuit_to_dagdependency`` now copies ``global_phase`` + `#14537 `__ + (``circuit_to_dagdependency`` now copies ``global_phase`` and ``TemplateSubstitution`` now adjusts the circuit's ``global_phase`` for each template substitution applied). - Fixes :issue:`14538`. + Fixes `#14538 `__. From e31b1c8166732b17cb4a9d9f5075df3da18f4252 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Mon, 6 Apr 2026 12:11:44 -0400 Subject: [PATCH 3/9] fix(library): extend global_phase fix to rzx_xz, rzx_zz1, rzx_zz2, rzx_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 --- .../circuit/library/templates/rzx/rzx_xz.py | 5 ++ .../circuit/library/templates/rzx/rzx_zz1.py | 5 ++ .../circuit/library/templates/rzx/rzx_zz2.py | 5 ++ .../circuit/library/templates/rzx/rzx_zz3.py | 5 ++ ...-clifford-6-4-template-identity-14538.yaml | 33 ++++++--- test/python/circuit/test_templates.py | 8 ++- .../transpiler/test_template_matching.py | 68 +++++++++++-------- 7 files changed, 89 insertions(+), 40 deletions(-) diff --git a/qiskit/circuit/library/templates/rzx/rzx_xz.py b/qiskit/circuit/library/templates/rzx/rzx_xz.py index b3c8d3adbbc0..ba6f53fa5f94 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_xz.py +++ b/qiskit/circuit/library/templates/rzx/rzx_xz.py @@ -13,6 +13,8 @@ from __future__ import annotations +from math import pi + import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -50,4 +52,7 @@ def rzx_xz(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 0) qc.rx(np.pi / 2, 0) qc.rz(np.pi / 2, 0) + # The gate content has unitary e^{i*pi} * I == -I; global_phase = pi corrects this + # so that Operator(rzx_xz()) == I exactly, as required by TemplateOptimization. + qc.global_phase = pi return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz1.py b/qiskit/circuit/library/templates/rzx/rzx_zz1.py index 007717ea80f5..5ae0a58a4f5d 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz1.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz1.py @@ -13,6 +13,8 @@ from __future__ import annotations +from math import pi + import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -64,5 +66,8 @@ def rzx_zz1(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 1) qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) + # The gate content has unitary e^{-i*pi/2} * I; global_phase = pi/2 corrects this + # so that Operator(rzx_zz1()) == I exactly, as required by TemplateOptimization. + qc.global_phase = pi / 2 return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz2.py b/qiskit/circuit/library/templates/rzx/rzx_zz2.py index 0bb4db011476..ffded123fece 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz2.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz2.py @@ -13,6 +13,8 @@ from __future__ import annotations +from math import pi + import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -54,5 +56,8 @@ def rzx_zz2(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 1) qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) + # The gate content has unitary e^{i*pi} * I == -I; global_phase = pi corrects this + # so that Operator(rzx_zz2()) == I exactly, as required by TemplateOptimization. + qc.global_phase = pi return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz3.py b/qiskit/circuit/library/templates/rzx/rzx_zz3.py index d62a0ff6b268..2c88c26eef5e 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz3.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz3.py @@ -13,6 +13,8 @@ from __future__ import annotations +from math import pi + import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -54,5 +56,8 @@ def rzx_zz3(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 1) qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) + # The gate content has unitary e^{i*pi} * I == -I; global_phase = pi corrects this + # so that Operator(rzx_zz3()) == I exactly, as required by TemplateOptimization. + qc.global_phase = pi return qc diff --git a/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml index 6f2ae313aad2..62ff8c3d8642 100644 --- a/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml +++ b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml @@ -1,18 +1,33 @@ --- fixes: - | - Fixed :func:`.clifford_6_4` template so that :class:`.TemplateOptimization` - now accepts and applies it. + Fixed :func:`.clifford_6_4` and four RZX templates so that + :class:`.TemplateOptimization` now accepts and applies them. - The ``clifford_6_4`` template (SHSHSH) has gate unitary ``e^{i*pi/4} * I`` - but was missing the compensating ``global_phase = -pi/4``, so - ``Operator(clifford_6_4())`` was not the identity matrix and the template - was silently rejected by :class:`.TemplateOptimization`. + 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. - ``clifford_6_4`` now sets ``global_phase = -pi/4`` so that its full - operator is exactly ``I``. + The affected templates and their corrections: - This fix depends on the global-phase propagation fixes introduced for + - ``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``. + + 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. + + These fixes depend on the global-phase propagation fixes introduced for `#14537 `__ (``circuit_to_dagdependency`` now copies ``global_phase`` and ``TemplateSubstitution`` now adjusts the circuit's ``global_phase`` diff --git a/test/python/circuit/test_templates.py b/test/python/circuit/test_templates.py index a7085aeafafe..29c5448ae462 100644 --- a/test/python/circuit/test_templates.py +++ b/test/python/circuit/test_templates.py @@ -37,11 +37,15 @@ class TestTemplates(QiskitTestCase): @combine(template_circuit=circuits) def test_template(self, template_circuit): - """test to verify that all templates are equivalent to the identity""" + """Test that all templates are exactly equal to the identity (including global phase). + Uses strict Operator equality rather than equiv() so that a template whose + gate content carries a residual global phase (i.e. global_phase is not set + to compensate) is caught as a failure rather than silently accepted. + """ target = Operator(template_circuit) value = Operator(np.eye(2**template_circuit.num_qubits)) - self.assertTrue(target.equiv(value)) + self.assertTrue(target == value) if __name__ == "__main__": diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 522e699e277a..26ff9980bbc2 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -14,6 +14,7 @@ """Test the TemplateOptimization pass.""" import unittest +from inspect import getmembers, isfunction import numpy as np from qiskit.circuit.commutation_library import SessionCommutationChecker as scc @@ -31,6 +32,7 @@ clifford_4_2, clifford_6_4, ) +import qiskit.circuit.library.templates as templib from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency from qiskit.transpiler import PassManager @@ -763,37 +765,45 @@ def test_clifford_templates(self): # All of these gates are in the commutation library, i.e. the cache should not be used self.assertEqual(scc.num_cached_entries(), 0) - def test_clifford_6_4_is_identity(self): - """Test that clifford_6_4 is exactly the identity (#14538). + def test_template_optimization_accepts_all_templates(self): + """Test that TemplateOptimization accepts and applies every template (#14538). - SHSHSH has gate unitary e^{i*pi/4} * I; the template previously lacked the - compensating global_phase = -pi/4, so Operator(clifford_6_4()) was not I. + Uses each template as its own input circuit: since every template implements + the identity, running TemplateOptimization with the template against a copy of + itself must cancel all gates and produce an operator-equal output. This catches + any template whose global_phase is not set correctly (causing silent rejection by + the identity check) as well as any per-match phase accumulation errors. """ - self.assertTrue(np.allclose(Operator(clifford_6_4()).data, np.identity(2, dtype=complex))) - - def test_template_optimization_accepts_clifford_6_4(self): - """Test that TemplateOptimization accepts and applies clifford_6_4 (#14538). - - Before the fix, clifford_6_4 was missing global_phase = -pi/4, so - Operator(clifford_6_4()) was e^{i*pi/4}*I rather than I and the identity check - in TemplateOptimization silently rejected the template. - """ - qr = QuantumRegister(1, "qr") - circuit_in = QuantumCircuit(qr) - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - - result = PassManager(TemplateOptimization([clifford_6_4()])).run(circuit_in) - - # All six gates must be cancelled. - self.assertEqual(result.count_ops(), {}) - # Full phase equality (not just equiv) is verified here because the - # per-match global_phase accumulation fix from #14537 is included in this branch. - self.assertTrue(Operator(circuit_in) == Operator(result)) + all_templates = [] + for _, fn in getmembers(templib, isfunction): + result = fn() + if isinstance(result, QuantumCircuit): + all_templates.append(result) + # Build a cost dict covering every gate name that appears in any template, + # supplementing the default dict which is missing some gates (e.g. sx, p, cz). + all_gate_names = { + instr.operation.name for template in all_templates for instr in template.data + } + extra_costs = dict.fromkeys(all_gate_names, 1) + for template in all_templates: + # Bind any free parameters to an arbitrary value. + if template.parameters: + template = template.assign_parameters(dict.fromkeys(template.parameters, 0.2)) + with self.subTest(template=template.name): + result = PassManager( + TemplateOptimization([template], user_cost_dict=extra_costs) + ).run(template.copy()) + # All gates must be cancelled — the template matched itself fully. + # Assumption: the matching algorithm finds a complete self-match for + # every template in the library. This holds for all current templates + # (verified empirically) but if a future template is added for which + # TemplateOptimization cannot find a complete self-match, this assertion + # will fail spuriously and should be replaced with a weaker check such + # as asserting that result.num_operations < template.num_operations. + self.assertEqual(result.count_ops(), {}) + # Full phase equality (not just equiv) confirms that global_phase is + # propagated correctly through the substitution pipeline. + self.assertTrue(Operator(template) == Operator(result)) def test_circuit_global_phase_preserved_after_single_and_multiple_template_match(self): """Test that circuit global_phase survives template optimization (#14537).""" From e5830926e05d78ebca4d1fedf227fdcb802cfc44 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Tue, 7 Apr 2026 09:44:07 -0400 Subject: [PATCH 4/9] fix(library): add global_phase to five identity-only-up-to-phase templates 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 #14538. Co-Authored-By: Claude Sonnet 4.6 --- .../templates/clifford/clifford_6_4.py | 10 ++-- .../circuit/library/templates/rzx/rzx_xz.py | 12 ++-- .../circuit/library/templates/rzx/rzx_zz1.py | 12 ++-- .../circuit/library/templates/rzx/rzx_zz2.py | 10 ++-- .../circuit/library/templates/rzx/rzx_zz3.py | 10 ++-- ...-clifford-6-4-template-identity-14538.yaml | 37 ++---------- test/python/circuit/test_templates.py | 2 +- .../transpiler/test_template_matching.py | 58 +++++++++++++++---- 8 files changed, 80 insertions(+), 71 deletions(-) diff --git a/qiskit/circuit/library/templates/clifford/clifford_6_4.py b/qiskit/circuit/library/templates/clifford/clifford_6_4.py index 2c92ad1ab04a..4fd1f4ea71af 100644 --- a/qiskit/circuit/library/templates/clifford/clifford_6_4.py +++ b/qiskit/circuit/library/templates/clifford/clifford_6_4.py @@ -22,9 +22,10 @@ def clifford_6_4(): .. code-block:: text - ┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐ - q_0: ┤ S ├┤ H ├┤ S ├┤ H ├┤ S ├┤ H ├ - └───┘└───┘└───┘└───┘└───┘└───┘ + global phase: 7π/4 + ┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐ + q: ┤ S ├┤ H ├┤ S ├┤ H ├┤ S ├┤ H ├ + └───┘└───┘└───┘└───┘└───┘└───┘ Returns: QuantumCircuit: template as a quantum circuit. @@ -36,7 +37,6 @@ def clifford_6_4(): qc.h(0) 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. + # SHSHSH has gate unitary e^{i*pi/4} * I; global_phase = -pi/4 makes Operator(qc) == I exactly. qc.global_phase = -pi / 4 return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_xz.py b/qiskit/circuit/library/templates/rzx/rzx_xz.py index ba6f53fa5f94..a6652ab15599 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_xz.py +++ b/qiskit/circuit/library/templates/rzx/rzx_xz.py @@ -26,13 +26,14 @@ def rzx_xz(theta: ParameterValueType | None = None): .. code-block:: text + global phase: π ┌───┐ ┌───┐┌─────────┐┌─────────┐┌─────────┐┌──────────┐» - 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 ├» └───────┘ └──────────┘» « ┌─────────┐┌─────────┐┌─────────┐ - «q_0: ┤ RZ(π/2) ├┤ RX(π/2) ├┤ RZ(π/2) ├ + «q_0: ┤ Rz(π/2) ├┤ Rx(π/2) ├┤ Rz(π/2) ├ « └─────────┘└─────────┘└─────────┘ «q_1: ───────────────────────────────── « @@ -52,7 +53,6 @@ def rzx_xz(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 0) qc.rx(np.pi / 2, 0) qc.rz(np.pi / 2, 0) - # The gate content has unitary e^{i*pi} * I == -I; global_phase = pi corrects this - # so that Operator(rzx_xz()) == I exactly, as required by TemplateOptimization. + # Gate content has unitary e^{i*pi} * I == -I; global_phase = pi makes Operator(qc) == I exactly. qc.global_phase = pi return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz1.py b/qiskit/circuit/library/templates/rzx/rzx_zz1.py index 5ae0a58a4f5d..d8d330a48f54 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz1.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz1.py @@ -26,20 +26,21 @@ def rzx_zz1(theta: ParameterValueType | None = None): .. code-block:: text + global phase: π/2 » q_0: ──■────────────────────────────────────────────■───────────────────────» ┌─┴─┐┌───────┐┌────┐┌───────┐┌────┐┌────────┐┌─┴─┐┌────────┐┌─────────┐» - q_1: ┤ X ├┤ RZ(ϴ) ├┤ √X ├┤ RZ(π) ├┤ √X ├┤ RZ(3π) ├┤ X ├┤ RZ(-ϴ) ├┤ RZ(π/2) ├» + q_1: ┤ X ├┤ Rz(ϴ) ├┤ √X ├┤ Rz(π) ├┤ √X ├┤ Rz(3π) ├┤ X ├┤ Rz(-ϴ) ├┤ Rz(π/2) ├» └───┘└───────┘└────┘└───────┘└────┘└────────┘└───┘└────────┘└─────────┘» « ┌──────────┐ » «q_0: ───────────────────────────────┤0 ├──────────────────────» - « ┌─────────┐┌─────────┐┌───────┐│ RZX(-ϴ) │┌─────────┐┌─────────┐» - «q_1: ┤ RX(π/2) ├┤ RZ(π/2) ├┤ RX(ϴ) ├┤1 ├┤ RZ(π/2) ├┤ RX(π/2) ├» + « ┌─────────┐┌─────────┐┌───────┐│ Rzx(-ϴ) │┌─────────┐┌─────────┐» + «q_1: ┤ Rx(π/2) ├┤ Rz(π/2) ├┤ Rx(ϴ) ├┤1 ├┤ Rz(π/2) ├┤ Rx(π/2) ├» « └─────────┘└─────────┘└───────┘└──────────┘└─────────┘└─────────┘» « «q_0: ─────────── « ┌─────────┐ - «q_1: ┤ RZ(π/2) ├ + «q_1: ┤ Rz(π/2) ├ « └─────────┘ """ if theta is None: @@ -66,8 +67,7 @@ def rzx_zz1(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 1) qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) - # The gate content has unitary e^{-i*pi/2} * I; global_phase = pi/2 corrects this - # so that Operator(rzx_zz1()) == I exactly, as required by TemplateOptimization. + # Gate content has unitary e^{-i*pi/2} * I; global_phase = pi/2 makes Operator(qc) == I exactly. qc.global_phase = pi / 2 return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz2.py b/qiskit/circuit/library/templates/rzx/rzx_zz2.py index ffded123fece..2cfd406c038f 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz2.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz2.py @@ -26,15 +26,16 @@ def rzx_zz2(theta: ParameterValueType | None = None): .. code-block:: text + global phase: π » q_0: ──■────────────■─────────────────────────────────────────────────────» ┌─┴─┐┌──────┐┌─┴─┐┌───────┐┌─────────┐┌─────────┐┌─────────┐┌───────┐» - q_1: ┤ X ├┤ P(ϴ) ├┤ X ├┤ P(-ϴ) ├┤ RZ(π/2) ├┤ RX(π/2) ├┤ RZ(π/2) ├┤ RX(ϴ) ├» + q_1: ┤ X ├┤ P(ϴ) ├┤ X ├┤ P(-ϴ) ├┤ Rz(π/2) ├┤ Rx(π/2) ├┤ Rz(π/2) ├┤ Rx(ϴ) ├» └───┘└──────┘└───┘└───────┘└─────────┘└─────────┘└─────────┘└───────┘» « ┌──────────┐ «q_0: ┤0 ├───────────────────────────────── - « │ RZX(-ϴ) │┌─────────┐┌─────────┐┌─────────┐ - «q_1: ┤1 ├┤ RZ(π/2) ├┤ RX(π/2) ├┤ RZ(π/2) ├ + « │ Rzx(-ϴ) │┌─────────┐┌─────────┐┌─────────┐ + «q_1: ┤1 ├┤ Rz(π/2) ├┤ Rx(π/2) ├┤ Rz(π/2) ├ « └──────────┘└─────────┘└─────────┘└─────────┘ """ if theta is None: @@ -56,8 +57,7 @@ def rzx_zz2(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 1) qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) - # The gate content has unitary e^{i*pi} * I == -I; global_phase = pi corrects this - # so that Operator(rzx_zz2()) == I exactly, as required by TemplateOptimization. + # Gate content has unitary e^{i*pi} * I == -I; global_phase = pi makes Operator(qc) == I exactly. qc.global_phase = pi return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz3.py b/qiskit/circuit/library/templates/rzx/rzx_zz3.py index 2c88c26eef5e..b1b1f19f90be 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz3.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz3.py @@ -26,15 +26,16 @@ def rzx_zz3(theta: ParameterValueType | None = None): .. code-block:: text + global phase: π » q_0: ──■─────────────■──────────────────────────────────────────────────────» ┌─┴─┐┌───────┐┌─┴─┐┌────────┐┌─────────┐┌─────────┐┌─────────┐┌───────┐» - q_1: ┤ X ├┤ RZ(ϴ) ├┤ X ├┤ RZ(-ϴ) ├┤ RZ(π/2) ├┤ RX(π/2) ├┤ RZ(π/2) ├┤ RX(ϴ) ├» + q_1: ┤ X ├┤ Rz(ϴ) ├┤ X ├┤ Rz(-ϴ) ├┤ Rz(π/2) ├┤ Rx(π/2) ├┤ Rz(π/2) ├┤ Rx(ϴ) ├» └───┘└───────┘└───┘└────────┘└─────────┘└─────────┘└─────────┘└───────┘» « ┌──────────┐ «q_0: ┤0 ├───────────────────────────────── - « │ RZX(-ϴ) │┌─────────┐┌─────────┐┌─────────┐ - «q_1: ┤1 ├┤ RZ(π/2) ├┤ RX(π/2) ├┤ RZ(π/2) ├ + « │ Rzx(-ϴ) │┌─────────┐┌─────────┐┌─────────┐ + «q_1: ┤1 ├┤ Rz(π/2) ├┤ Rx(π/2) ├┤ Rz(π/2) ├ « └──────────┘└─────────┘└─────────┘└─────────┘ """ if theta is None: @@ -56,8 +57,7 @@ def rzx_zz3(theta: ParameterValueType | None = None): qc.rz(np.pi / 2, 1) qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) - # The gate content has unitary e^{i*pi} * I == -I; global_phase = pi corrects this - # so that Operator(rzx_zz3()) == I exactly, as required by TemplateOptimization. + # Gate content has unitary e^{i*pi} * I == -I; global_phase = pi makes Operator(qc) == I exactly. qc.global_phase = pi return qc diff --git a/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml index 62ff8c3d8642..931fae54e90d 100644 --- a/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml +++ b/releasenotes/notes/fix-clifford-6-4-template-identity-14538.yaml @@ -1,36 +1,9 @@ --- fixes: - | - 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``. - - 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. - - These fixes depend on the global-phase propagation fixes introduced for - `#14537 `__ - (``circuit_to_dagdependency`` now copies ``global_phase`` - and ``TemplateSubstitution`` now adjusts the circuit's ``global_phase`` - for each template substitution applied). - + 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 `__. diff --git a/test/python/circuit/test_templates.py b/test/python/circuit/test_templates.py index 29c5448ae462..076d35f67e50 100644 --- a/test/python/circuit/test_templates.py +++ b/test/python/circuit/test_templates.py @@ -45,7 +45,7 @@ def test_template(self, template_circuit): """ target = Operator(template_circuit) value = Operator(np.eye(2**template_circuit.num_qubits)) - self.assertTrue(target == value) + self.assertEqual(target, value) if __name__ == "__main__": diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 26ff9980bbc2..6012720e77d1 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -766,14 +766,7 @@ def test_clifford_templates(self): self.assertEqual(scc.num_cached_entries(), 0) def test_template_optimization_accepts_all_templates(self): - """Test that TemplateOptimization accepts and applies every template (#14538). - - Uses each template as its own input circuit: since every template implements - the identity, running TemplateOptimization with the template against a copy of - itself must cancel all gates and produce an operator-equal output. This catches - any template whose global_phase is not set correctly (causing silent rejection by - the identity check) as well as any per-match phase accumulation errors. - """ + """Test that TemplateOptimization accepts and applies every template (#14538).""" all_templates = [] for _, fn in getmembers(templib, isfunction): result = fn() @@ -803,7 +796,7 @@ def test_template_optimization_accepts_all_templates(self): self.assertEqual(result.count_ops(), {}) # Full phase equality (not just equiv) confirms that global_phase is # propagated correctly through the substitution pipeline. - self.assertTrue(Operator(template) == Operator(result)) + self.assertEqual(Operator(template), Operator(result)) def test_circuit_global_phase_preserved_after_single_and_multiple_template_match(self): """Test that circuit global_phase survives template optimization (#14537).""" @@ -837,7 +830,6 @@ def test_circuit_global_phase_preserved_after_single_and_multiple_template_match def test_template_nonzero_global_phase_applied_to_circuit(self): """Test the template's global phase is respected (#14537).""" - template = QuantumCircuit(1) template.h(0) template.s(0) @@ -856,6 +848,9 @@ def test_template_nonzero_global_phase_applied_to_circuit(self): 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) + self.assertEqual(result.count_ops(), {}) self.assertEqual(Operator(circuit_in), Operator(result)) def test_circuit_and_template_both_have_nonzero_global_phase(self): @@ -883,10 +878,51 @@ def test_circuit_and_template_both_have_nonzero_global_phase(self): # 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) self.assertEqual(result.count_ops(), {}) self.assertEqual(Operator(circuit_in), Operator(result)) + def test_two_global_phase_carrying_template_matches_accumulate_phase(self): + """Test that phase contributions accumulate correctly when a template matches twice (#14538). + + Uses clifford_6_4 (SHSHSH, global_phase = -pi/4) as the template. A circuit + containing two SHSHSH blocks separated by a T gate produces two matches; each + match contributes +pi/4 to the circuit's global_phase, leaving pi/2 total. + """ + from qiskit.circuit.library.templates.clifford.clifford_6_4 import clifford_6_4 + + template = clifford_6_4() + + qr = QuantumRegister(1, "qr") + circuit_in = QuantumCircuit(qr) + # First SHSHSH block — matches clifford_6_4 + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + # T gate separates the two matches + circuit_in.t(qr[0]) + # Second SHSHSH block — matches clifford_6_4 again + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + circuit_in.s(qr[0]) + circuit_in.h(qr[0]) + + result = PassManager( + TemplateOptimization([template], user_cost_dict={"s": 1, "h": 1, "t": 1}) + ).run(circuit_in) + + # Both SHSHSH blocks cancelled; only T remains. + self.assertEqual(result.count_ops(), {"t": 1}) + # Each of the two matches contributes +pi/4; total accumulated phase = pi/2. + self.assertAlmostEqual(float(result.global_phase) % (2 * np.pi), np.pi / 2) + # Authoritative check: full unitary (including phase) must be identical. + self.assertEqual(Operator(circuit_in), Operator(result)) + if __name__ == "__main__": unittest.main() From 3d12ff53925ff8f6b47dba1691aee3f9b0d9d049 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Tue, 7 Apr 2026 10:17:16 -0400 Subject: [PATCH 5/9] fix(lint): remove redundant local import of clifford_6_4 (ruff F811) 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 --- test/python/transpiler/test_template_matching.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 6012720e77d1..1c60dc98f140 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -889,8 +889,6 @@ def test_two_global_phase_carrying_template_matches_accumulate_phase(self): containing two SHSHSH blocks separated by a T gate produces two matches; each match contributes +pi/4 to the circuit's global_phase, leaving pi/2 total. """ - from qiskit.circuit.library.templates.clifford.clifford_6_4 import clifford_6_4 - template = clifford_6_4() qr = QuantumRegister(1, "qr") From 3e86327647f7edf2043b4d89baebccf27810b051 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Tue, 7 Apr 2026 11:10:43 -0400 Subject: [PATCH 6/9] style(test): simplify all_templates list to one-liner (Shelly Garion review) Co-Authored-By: Claude Sonnet 4.6 --- test/python/transpiler/test_template_matching.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 1c60dc98f140..d64dbaa066b7 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -767,11 +767,7 @@ def test_clifford_templates(self): def test_template_optimization_accepts_all_templates(self): """Test that TemplateOptimization accepts and applies every template (#14538).""" - all_templates = [] - for _, fn in getmembers(templib, isfunction): - result = fn() - if isinstance(result, QuantumCircuit): - all_templates.append(result) + all_templates = [o[1]() for o in getmembers(templib) if isfunction(o[1])] # Build a cost dict covering every gate name that appears in any template, # supplementing the default dict which is missing some gates (e.g. sx, p, cz). all_gate_names = { From b941044578059c9d1acb25b4f9222076074afb43 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Mon, 13 Apr 2026 11:38:33 -0400 Subject: [PATCH 7/9] test: use Operator equivalence in test_template_nonzero_global_phase_applied_to_circuit --- test/python/transpiler/test_template_matching.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index d64dbaa066b7..d893e37cb28e 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -844,9 +844,6 @@ def test_template_nonzero_global_phase_applied_to_circuit(self): 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) - self.assertEqual(result.count_ops(), {}) self.assertEqual(Operator(circuit_in), Operator(result)) def test_circuit_and_template_both_have_nonzero_global_phase(self): From 3547dd74c798cad800b5f138682fd035bb3ea451 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Mon, 13 Apr 2026 13:08:53 -0400 Subject: [PATCH 8/9] Addressing Julien review points --- .../templates/clifford/clifford_6_4.py | 2 +- .../circuit/library/templates/rzx/rzx_xz.py | 4 +- .../circuit/library/templates/rzx/rzx_zz1.py | 4 +- .../circuit/library/templates/rzx/rzx_zz2.py | 4 +- .../circuit/library/templates/rzx/rzx_zz3.py | 4 +- .../optimization/template_optimization.py | 2 + .../transpiler/test_template_matching.py | 74 +------------------ 7 files changed, 8 insertions(+), 86 deletions(-) diff --git a/qiskit/circuit/library/templates/clifford/clifford_6_4.py b/qiskit/circuit/library/templates/clifford/clifford_6_4.py index 4fd1f4ea71af..db7c0f2e8f14 100644 --- a/qiskit/circuit/library/templates/clifford/clifford_6_4.py +++ b/qiskit/circuit/library/templates/clifford/clifford_6_4.py @@ -37,6 +37,6 @@ def clifford_6_4(): 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. + # Add a global phase of -pi/4 to get Operator(qc) == I qc.global_phase = -pi / 4 return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_xz.py b/qiskit/circuit/library/templates/rzx/rzx_xz.py index a6652ab15599..372501ab4cdc 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_xz.py +++ b/qiskit/circuit/library/templates/rzx/rzx_xz.py @@ -13,8 +13,6 @@ from __future__ import annotations -from math import pi - import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -54,5 +52,5 @@ def rzx_xz(theta: ParameterValueType | None = None): 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 + qc.global_phase = np.pi return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz1.py b/qiskit/circuit/library/templates/rzx/rzx_zz1.py index d8d330a48f54..2532f9c6b23b 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz1.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz1.py @@ -13,8 +13,6 @@ from __future__ import annotations -from math import pi - import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -68,6 +66,6 @@ def rzx_zz1(theta: ParameterValueType | None = None): qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) # Gate content has unitary e^{-i*pi/2} * I; global_phase = pi/2 makes Operator(qc) == I exactly. - qc.global_phase = pi / 2 + qc.global_phase = np.pi / 2 return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz2.py b/qiskit/circuit/library/templates/rzx/rzx_zz2.py index 2cfd406c038f..13e8c2bf306b 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz2.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz2.py @@ -13,8 +13,6 @@ from __future__ import annotations -from math import pi - import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -58,6 +56,6 @@ def rzx_zz2(theta: ParameterValueType | None = None): qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) # Gate content has unitary e^{i*pi} * I == -I; global_phase = pi makes Operator(qc) == I exactly. - qc.global_phase = pi + qc.global_phase = np.pi return qc diff --git a/qiskit/circuit/library/templates/rzx/rzx_zz3.py b/qiskit/circuit/library/templates/rzx/rzx_zz3.py index b1b1f19f90be..a233bf86382f 100644 --- a/qiskit/circuit/library/templates/rzx/rzx_zz3.py +++ b/qiskit/circuit/library/templates/rzx/rzx_zz3.py @@ -13,8 +13,6 @@ from __future__ import annotations -from math import pi - import numpy as np from qiskit.circuit import Parameter, QuantumCircuit @@ -58,6 +56,6 @@ def rzx_zz3(theta: ParameterValueType | None = None): qc.rx(np.pi / 2, 1) qc.rz(np.pi / 2, 1) # Gate content has unitary e^{i*pi} * I == -I; global_phase = pi makes Operator(qc) == I exactly. - qc.global_phase = pi + qc.global_phase = np.pi return qc diff --git a/qiskit/transpiler/passes/optimization/template_optimization.py b/qiskit/transpiler/passes/optimization/template_optimization.py index 51b0e26819b2..d95c970a235b 100644 --- a/qiskit/transpiler/passes/optimization/template_optimization.py +++ b/qiskit/transpiler/passes/optimization/template_optimization.py @@ -112,7 +112,9 @@ def run(self, dag): data = Operator(dagdependency_to_circuit(template)).data else: data = Operator(template).data + comparison = np.allclose(data, identity) + if not comparison: raise TranspilerError( "A template is a QuantumCircuit() that performs the identity." diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index d893e37cb28e..e9acfb66634f 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -14,7 +14,6 @@ """Test the TemplateOptimization pass.""" import unittest -from inspect import getmembers, isfunction import numpy as np from qiskit.circuit.commutation_library import SessionCommutationChecker as scc @@ -30,9 +29,7 @@ clifford_3_1, clifford_4_1, clifford_4_2, - clifford_6_4, ) -import qiskit.circuit.library.templates as templib from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency from qiskit.transpiler import PassManager @@ -765,35 +762,6 @@ def test_clifford_templates(self): # All of these gates are in the commutation library, i.e. the cache should not be used self.assertEqual(scc.num_cached_entries(), 0) - def test_template_optimization_accepts_all_templates(self): - """Test that TemplateOptimization accepts and applies every template (#14538).""" - all_templates = [o[1]() for o in getmembers(templib) if isfunction(o[1])] - # Build a cost dict covering every gate name that appears in any template, - # supplementing the default dict which is missing some gates (e.g. sx, p, cz). - all_gate_names = { - instr.operation.name for template in all_templates for instr in template.data - } - extra_costs = dict.fromkeys(all_gate_names, 1) - for template in all_templates: - # Bind any free parameters to an arbitrary value. - if template.parameters: - template = template.assign_parameters(dict.fromkeys(template.parameters, 0.2)) - with self.subTest(template=template.name): - result = PassManager( - TemplateOptimization([template], user_cost_dict=extra_costs) - ).run(template.copy()) - # All gates must be cancelled — the template matched itself fully. - # Assumption: the matching algorithm finds a complete self-match for - # every template in the library. This holds for all current templates - # (verified empirically) but if a future template is added for which - # TemplateOptimization cannot find a complete self-match, this assertion - # will fail spuriously and should be replaced with a weaker check such - # as asserting that result.num_operations < template.num_operations. - self.assertEqual(result.count_ops(), {}) - # Full phase equality (not just equiv) confirms that global_phase is - # propagated correctly through the substitution pipeline. - self.assertEqual(Operator(template), Operator(result)) - def test_circuit_global_phase_preserved_after_single_and_multiple_template_match(self): """Test that circuit global_phase survives template optimization (#14537).""" @@ -871,49 +839,9 @@ def test_circuit_and_template_both_have_nonzero_global_phase(self): # All gates cancelled; total phase = circuit phase + template compensation # = pi/3 + pi/4 = 7*pi/12. - self.assertAlmostEqual(float(result.global_phase) % (2 * np.pi), 7 * np.pi / 12) + self.assertAlmostEqual(result.global_phase, 7 * np.pi / 12) self.assertEqual(result.count_ops(), {}) self.assertEqual(Operator(circuit_in), Operator(result)) - def test_two_global_phase_carrying_template_matches_accumulate_phase(self): - """Test that phase contributions accumulate correctly when a template matches twice (#14538). - - Uses clifford_6_4 (SHSHSH, global_phase = -pi/4) as the template. A circuit - containing two SHSHSH blocks separated by a T gate produces two matches; each - match contributes +pi/4 to the circuit's global_phase, leaving pi/2 total. - """ - template = clifford_6_4() - - qr = QuantumRegister(1, "qr") - circuit_in = QuantumCircuit(qr) - # First SHSHSH block — matches clifford_6_4 - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - # T gate separates the two matches - circuit_in.t(qr[0]) - # Second SHSHSH block — matches clifford_6_4 again - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - circuit_in.s(qr[0]) - circuit_in.h(qr[0]) - - result = PassManager( - TemplateOptimization([template], user_cost_dict={"s": 1, "h": 1, "t": 1}) - ).run(circuit_in) - - # Both SHSHSH blocks cancelled; only T remains. - self.assertEqual(result.count_ops(), {"t": 1}) - # Each of the two matches contributes +pi/4; total accumulated phase = pi/2. - self.assertAlmostEqual(float(result.global_phase) % (2 * np.pi), np.pi / 2) - # Authoritative check: full unitary (including phase) must be identical. - self.assertEqual(Operator(circuit_in), Operator(result)) - - if __name__ == "__main__": unittest.main() From 8244352cf5c164ab6eb8627ab274df0e98988326 Mon Sep 17 00:00:00 2001 From: adcorcol Date: Tue, 14 Apr 2026 07:43:01 -0400 Subject: [PATCH 9/9] black reformat --- qiskit/transpiler/passes/optimization/template_optimization.py | 2 +- test/python/transpiler/test_template_matching.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/template_optimization.py b/qiskit/transpiler/passes/optimization/template_optimization.py index d95c970a235b..3f8cb49182aa 100644 --- a/qiskit/transpiler/passes/optimization/template_optimization.py +++ b/qiskit/transpiler/passes/optimization/template_optimization.py @@ -114,7 +114,7 @@ def run(self, dag): data = Operator(template).data comparison = np.allclose(data, identity) - + if not comparison: raise TranspilerError( "A template is a QuantumCircuit() that performs the identity." diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index e9acfb66634f..fcb8fd717297 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -843,5 +843,6 @@ def test_circuit_and_template_both_have_nonzero_global_phase(self): self.assertEqual(result.count_ops(), {}) self.assertEqual(Operator(circuit_in), Operator(result)) + if __name__ == "__main__": unittest.main()