From d50b7e0a82dff795844f5d213f3594ec6c5c940e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 10 Jun 2021 11:21:14 +1200 Subject: [PATCH 01/30] fix 1q optimization heuristic --- .../optimization/optimize_1q_decomposition.py | 21 +----- ...ping-short-sequences-044a64740bf414a7.yaml | 65 +++++++++++++++++++ .../test_optimize_1q_decomposition.py | 15 +++++ 3 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index fd3ea68ec358..a198042510f7 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -75,25 +75,6 @@ def run(self, dag): runs = dag.collect_1q_runs() identity_matrix = np.eye(2) for run in runs: - single_u3 = False - # Don't try to optimize a single 1q gate, except for U3 - if len(run) <= 1: - params = run[0].op.params - # Remove single identity gates - if len(params) > 0 and np.array_equal(run[0].op.to_matrix(), identity_matrix): - dag.remove_op_node(run[0]) - continue - if isinstance(run[0].op, U3Gate): - param = float(params[0]) - if math.isclose(param, 0, rel_tol=0, abs_tol=1e-12) or math.isclose( - param, np.pi / 2, abs_tol=1e-12, rel_tol=0 - ): - single_u3 = True - else: - continue - else: - continue - new_circs = [] operator = run[0].op.to_matrix() for gate in run[1:]: @@ -102,7 +83,7 @@ def run(self, dag): new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) - if len(run) > len(new_circ) or (single_u3 and new_circ.data[0][0].name != "u3"): + if any([g.name not in self.basis for g in run]) or len(run) > len(new_circ): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) # Delete the other nodes in the run diff --git a/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml new file mode 100644 index 000000000000..5e93448a33ce --- /dev/null +++ b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml @@ -0,0 +1,65 @@ +--- +prelude: > + Replace this text with content to appear at the top of the section for this + release. All of the prelude content is merged together and then rendered + separately from the items listed in other parts of the file, so the text + needs to be worded so that both the prelude and the other items make sense + when read independently. This may mean repeating some details. Not every + release note requires a prelude. Usually only notes describing major + features or adding release theme details should have a prelude. +features: + - | + List new features here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +issues: + - | + List known issues here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +upgrade: + - | + List upgrade notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +deprecations: + - | + List deprecations notes here, or remove this section. All of the list + items in this section are combined when the release notes are rendered, so + the text needs to be worded so that it does not depend on any information + only available in another section, such as the prelude. This may mean + repeating some details. +critical: + - | + Add critical notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +security: + - | + Add security notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +fixes: + - | + Add normal bug fixes here, or remove this section. All of the list items + in this section are combined when the release notes are rendered, so the + text needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +other: + - | + Add other notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 32e5d191db5b..2a102b107d64 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -444,6 +444,21 @@ def test_y_simplification_rz_sx_x(self): msg = f"expected:\n{expected}\nresult:\n{result}" self.assertEqual(expected, result, msg=msg) + def test_short_string(self): + """Test that a shorter-than-universal string is still rewritten.""" + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(np.pi/2, 0) + basis = ["sx", "x", "rz"] + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(qc) + expected = QuantumCircuit(1) + expected.sx(0) + expected.sx(0) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) + if __name__ == "__main__": unittest.main() From a863bbacc32959d777cb7d2a4c29d249486cb859 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 10 Jun 2021 16:22:05 +1200 Subject: [PATCH 02/30] drop unnecessary brackets Co-authored-by: Ali Javadi-Abhari --- .../transpiler/passes/optimization/optimize_1q_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index a198042510f7..e5cb28ef525d 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -83,7 +83,7 @@ def run(self, dag): new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) - if any([g.name not in self.basis for g in run]) or len(run) > len(new_circ): + if any(g.name not in self.basis for g in run) or len(run) > len(new_circ): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) # Delete the other nodes in the run From 0666a656622b2c6f4e09d34e8e11598935c47cad Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 10 Jun 2021 16:26:31 +1200 Subject: [PATCH 03/30] actually fill out reno template :P --- ...ping-short-sequences-044a64740bf414a7.yaml | 64 +------------------ 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml index 5e93448a33ce..44e15f890c2f 100644 --- a/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml +++ b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml @@ -1,65 +1,5 @@ --- -prelude: > - Replace this text with content to appear at the top of the section for this - release. All of the prelude content is merged together and then rendered - separately from the items listed in other parts of the file, so the text - needs to be worded so that both the prelude and the other items make sense - when read independently. This may mean repeating some details. Not every - release note requires a prelude. Usually only notes describing major - features or adding release theme details should have a prelude. -features: - - | - List new features here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -issues: - - | - List known issues here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -upgrade: - - | - List upgrade notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -deprecations: - - | - List deprecations notes here, or remove this section. All of the list - items in this section are combined when the release notes are rendered, so - the text needs to be worded so that it does not depend on any information - only available in another section, such as the prelude. This may mean - repeating some details. -critical: - - | - Add critical notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -security: - - | - Add security notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. fixes: - | - Add normal bug fixes here, or remove this section. All of the list items - in this section are combined when the release notes are rendered, so the - text needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -other: - - | - Add other notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. + Fixes a bug in `Optimize1qGatesDecomposition` previously causing certain + short sequences of gates to erroneously not be rewritten. From 657de35b2180bfc993fbf6cb4b0d6d3c514297e6 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 10 Jun 2021 16:34:25 +1200 Subject: [PATCH 04/30] maker linter happier --- test/python/transpiler/test_optimize_1q_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 2a102b107d64..305772eaf113 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -448,7 +448,7 @@ def test_short_string(self): """Test that a shorter-than-universal string is still rewritten.""" qc = QuantumCircuit(1) qc.h(0) - qc.ry(np.pi/2, 0) + qc.ry(np.pi / 2, 0) basis = ["sx", "x", "rz"] passmanager = PassManager() passmanager.append(Optimize1qGatesDecomposition(basis)) From ef39e8d063162900c3d152b600871e8edc1e09fc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 10 Jun 2021 16:34:35 +1200 Subject: [PATCH 05/30] make lev happier --- .../passes/optimization/optimize_1q_decomposition.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index e5cb28ef525d..3139e7391c23 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -15,6 +15,7 @@ import copy import logging import math +import warnings import numpy as np @@ -83,6 +84,12 @@ def run(self, dag): new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) + if all(g.name in self.basis for g in run) and len(run) < len(new_circ): + warnings.warn(f"Resynthesized {run} and got {new_circ}, " + f"but the original was native and the new " + f"value is longer. This indicates an " + f"efficiency bug in synthesis. Please " + f"report it!") if any(g.name not in self.basis for g in run) or len(run) > len(new_circ): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) From 8cd954cc39e06f116c0f93a5ad25690b7e33e133 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 11 Jun 2021 05:58:34 +1200 Subject: [PATCH 06/30] add a GH link to the sloppy synth warning Co-authored-by: Matthew Treinish --- .../passes/optimization/optimize_1q_decomposition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 3139e7391c23..4740f53a1a69 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -89,7 +89,8 @@ def run(self, dag): f"but the original was native and the new " f"value is longer. This indicates an " f"efficiency bug in synthesis. Please " - f"report it!") + f"report it by opening an issue here:" + f"https://github.com/Qiskit/qiskit-terra/issues/new/choose") if any(g.name not in self.basis for g in run) or len(run) > len(new_circ): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) From f13e554c45119106025715a165082c70d78799c1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 11 Jun 2021 05:59:12 +1200 Subject: [PATCH 07/30] improve source linking in changelog Co-authored-by: Matthew Treinish --- ...Decomposition-skipping-short-sequences-044a64740bf414a7.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml index 44e15f890c2f..336895951a90 100644 --- a/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml +++ b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml @@ -1,5 +1,5 @@ --- fixes: - | - Fixes a bug in `Optimize1qGatesDecomposition` previously causing certain + Fixes a bug in :func:`~qiskit.transpiler.passes.Optimize1qGatesDecomposition` previously causing certain short sequences of gates to erroneously not be rewritten. From c274e89fceeae98436bac1c7bda0b3878d6de0f4 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 11 Jun 2021 13:16:56 +1200 Subject: [PATCH 08/30] remember target basis name --- .../passes/optimization/optimize_1q_decomposition.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 4740f53a1a69..ba9903b83e50 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -39,6 +39,7 @@ def __init__(self, basis=None): and the Euler basis. """ super().__init__() + self._target_basis = basis self.basis = None if basis: self.basis = [] @@ -84,14 +85,15 @@ def run(self, dag): new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) - if all(g.name in self.basis for g in run) and len(run) < len(new_circ): + if all(g.name in self._target_basis for g in run) and len(run) < len(new_circ): warnings.warn(f"Resynthesized {run} and got {new_circ}, " f"but the original was native and the new " f"value is longer. This indicates an " f"efficiency bug in synthesis. Please " f"report it by opening an issue here:" - f"https://github.com/Qiskit/qiskit-terra/issues/new/choose") - if any(g.name not in self.basis for g in run) or len(run) > len(new_circ): + f"https://github.com/Qiskit/qiskit-terra/issues/new/choose", + stacklevel=2) + if any(g.name not in self._target_basis for g in run) or len(run) >= len(new_circ): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) # Delete the other nodes in the run From d4549a81e84e7deb3f2e0ab6950c0335582bc462 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 11 Jun 2021 13:17:04 +1200 Subject: [PATCH 09/30] update circuit definitions --- test/python/opflow/test_pauli_sum_op.py | 5 +++-- test/python/pulse/test_builder.py | 12 ++++++------ test/python/transpiler/test_basis_translator.py | 3 ++- .../transpiler/test_optimize_1q_decomposition.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/test/python/opflow/test_pauli_sum_op.py b/test/python/opflow/test_pauli_sum_op.py index 8214fb9c1e15..4b03b2cbcf84 100644 --- a/test/python/opflow/test_pauli_sum_op.py +++ b/test/python/opflow/test_pauli_sum_op.py @@ -211,8 +211,9 @@ def test_to_instruction(self): """test for to_instruction""" target = ((X + Z) / np.sqrt(2)).to_instruction() qc = QuantumCircuit(1) - qc.u(np.pi / 2, 0, np.pi, 0) - self.assertEqual(transpile(target.definition, basis_gates=["u"]), qc) + qc.u(np.pi / 2, 0, -np.pi, 0) + qc_out = transpile(target.definition, basis_gates=["u"]) + self.assertEqual(qc_out, qc) def test_to_pauli_op(self): """test to_pauli_op method""" diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 86969e64329c..712289fb0d8d 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -817,10 +817,10 @@ def test_cx(self): def test_u1(self): """Test u1 gate.""" with pulse.build(self.backend) as schedule: - pulse.u1(np.pi, 0) + pulse.u1(np.pi / 2, 0) reference_qc = circuit.QuantumCircuit(1) - reference_qc.append(circuit.library.U1Gate(np.pi), [0]) + reference_qc.append(circuit.library.U1Gate(np.pi / 2), [0]) reference = compiler.schedule(reference_qc, self.backend) self.assertScheduleEqual(schedule, reference) @@ -828,10 +828,10 @@ def test_u1(self): def test_u2(self): """Test u2 gate.""" with pulse.build(self.backend) as schedule: - pulse.u2(np.pi, 0, 0) + pulse.u2(np.pi / 2, 0, 0) reference_qc = circuit.QuantumCircuit(1) - reference_qc.append(circuit.library.U2Gate(np.pi, 0), [0]) + reference_qc.append(circuit.library.U2Gate(np.pi / 2, 0), [0]) reference = compiler.schedule(reference_qc, self.backend) self.assertScheduleEqual(schedule, reference) @@ -839,10 +839,10 @@ def test_u2(self): def test_u3(self): """Test u3 gate.""" with pulse.build(self.backend) as schedule: - pulse.u3(np.pi, 0, np.pi / 2, 0) + pulse.u3(np.pi / 8, np.pi / 16, np.pi / 4, 0) reference_qc = circuit.QuantumCircuit(1) - reference_qc.append(circuit.library.U3Gate(np.pi, 0, np.pi / 2), [0]) + reference_qc.append(circuit.library.U3Gate(np.pi / 8, np.pi / 16, np.pi / 4), [0]) reference = compiler.schedule(reference_qc, self.backend) self.assertScheduleEqual(schedule, reference) diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 4fc0b0fdb2a3..8332186eed10 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -811,10 +811,11 @@ def test_condition_set_substitute_node(self): qr = QuantumRegister(2, "q") cr = ClassicalRegister(2, "c") expected = QuantumCircuit(qr, cr) - expected.u2(0, pi, 0) + expected.u2(0, -pi, 0) expected.cx(0, 1) expected.measure(1, 1) expected.u2(0, pi, 0).c_if(cr, 1) + self.assertEqual(circ_transpiled, expected) def test_skip_target_basis_equivalences_1(self): diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 305772eaf113..b7f5a416ccea 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -449,7 +449,7 @@ def test_short_string(self): qc = QuantumCircuit(1) qc.h(0) qc.ry(np.pi / 2, 0) - basis = ["sx", "x", "rz"] + basis = ["sx", "rz"] passmanager = PassManager() passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) From 93dcb6f8f4ab4a0a6add687bff47da5832d3655d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 11 Jun 2021 13:43:19 +1200 Subject: [PATCH 10/30] improve linter cheerfulness --- .../passes/optimization/optimize_1q_decomposition.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index ba9903b83e50..f62855f2555e 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -14,14 +14,10 @@ import copy import logging -import math import warnings -import numpy as np - from qiskit.transpiler.basepasses import TransformationPass from qiskit.quantum_info.synthesis import one_qubit_decompose -from qiskit.circuit.library.standard_gates import U3Gate from qiskit.converters import circuit_to_dag logger = logging.getLogger(__name__) @@ -75,7 +71,6 @@ def run(self, dag): logger.info("Skipping pass because no basis is set") return dag runs = dag.collect_1q_runs() - identity_matrix = np.eye(2) for run in runs: new_circs = [] operator = run[0].op.to_matrix() From 764a30e1091f9b9e6ea80d731fedf27ae0c506c0 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 12 Jun 2021 07:26:45 +1200 Subject: [PATCH 11/30] increase reluctance to decompose calibrated gates --- .../optimization/optimize_1q_decomposition.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index f62855f2555e..a43282dbfb83 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -80,15 +80,28 @@ def run(self, dag): new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) - if all(g.name in self._target_basis for g in run) and len(run) < len(new_circ): + if (all(g.name in self._target_basis and not dag.has_calibration_for(g) + for g in run) + and len(run) < len(new_circ)): + # NOTE: This is short-circuited on calibrated gates, which we're timid about + # reducing. warnings.warn(f"Resynthesized {run} and got {new_circ}, " - f"but the original was native and the new " - f"value is longer. This indicates an " - f"efficiency bug in synthesis. Please " - f"report it by opening an issue here:" + f"but the original was native and the new value is longer. This " + f"indicates an efficiency bug in synthesis. Please report it by " + f"opening an issue here: " f"https://github.com/Qiskit/qiskit-terra/issues/new/choose", stacklevel=2) - if any(g.name not in self._target_basis for g in run) or len(run) >= len(new_circ): + # does this run have uncalibrated gates? + uncalibrated_p = any(not dag.has_calibration_for(g) for g in run) + # does this run have gates which are not in the image of .basis _and_ uncalibrated? + uncalibrated_and_not_basis_p = any( + g.name not in self._target_basis and not dag.has_calibration_for(g) + for g in run + ) + # if we're outside of the basis set, we're obligated to logically decompose. + # if we're outside of the set of gates for which we have physical definitions, + # then we _try_ to decompose, using the results if we see improvement. + if uncalibrated_and_not_basis_p or (uncalibrated_p and len(run) >= len(new_circ)): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) # Delete the other nodes in the run From 147eee1cd42b9023584a98f3ab69ee1960ee1c0e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 12 Jun 2021 07:28:36 +1200 Subject: [PATCH 12/30] change .basis slot to ._decomposers --- .../optimization/optimize_1q_decomposition.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index a43282dbfb83..5f4d929745c0 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -36,25 +36,25 @@ def __init__(self, basis=None): """ super().__init__() self._target_basis = basis - self.basis = None + self._decomposers = None if basis: - self.basis = [] + self._decomposers = [] basis_set = set(basis) euler_basis_gates = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES for euler_basis_name, gates in euler_basis_gates.items(): if set(gates).issubset(basis_set): - basis_copy = copy.copy(self.basis) + basis_copy = copy.copy(self._decomposers) for base in basis_copy: # check if gates are a superset of another basis # and if so, remove that basis if set(euler_basis_gates[base.basis]).issubset(set(gates)): - self.basis.remove(base) + self._decomposers.remove(base) # check if the gates are a subset of another basis elif set(gates).issubset(set(euler_basis_gates[base.basis])): break # if not a subset, add it to the list else: - self.basis.append( + self._decomposers.append( one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name) ) @@ -67,7 +67,7 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - if not self.basis: + if not self._decomposers: logger.info("Skipping pass because no basis is set") return dag runs = dag.collect_1q_runs() @@ -76,7 +76,7 @@ def run(self, dag): operator = run[0].op.to_matrix() for gate in run[1:]: operator = gate.op.to_matrix().dot(operator) - for decomposer in self.basis: + for decomposer in self._decomposers: new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) @@ -93,7 +93,7 @@ def run(self, dag): stacklevel=2) # does this run have uncalibrated gates? uncalibrated_p = any(not dag.has_calibration_for(g) for g in run) - # does this run have gates which are not in the image of .basis _and_ uncalibrated? + # does this run have gates not in the image of ._decomposers _and_ uncalibrated? uncalibrated_and_not_basis_p = any( g.name not in self._target_basis and not dag.has_calibration_for(g) for g in run From 64f78fd207bfe57da479ea3a5a217afe256e1578 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 12 Jun 2021 08:06:30 +1200 Subject: [PATCH 13/30] my local linter thinks everything is fine :/ --- .../optimization/optimize_1q_decomposition.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 5f4d929745c0..32963073dbab 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -80,9 +80,21 @@ def run(self, dag): new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) - if (all(g.name in self._target_basis and not dag.has_calibration_for(g) - for g in run) - and len(run) < len(new_circ)): + + # is this run all in the target set and also uncalibrated? + rewriteable_and_in_basis_p = all( + g.name in self._target_basis and not dag.has_calibration_for(g) + for g in run + ) + # does this run have uncalibrated gates? + uncalibrated_p = any(not dag.has_calibration_for(g) for g in run) + # does this run have gates not in the image of ._decomposers _and_ uncalibrated? + uncalibrated_and_not_basis_p = any( + g.name not in self._target_basis and not dag.has_calibration_for(g) + for g in run + ) + + if (rewriteable_and_in_basis_p and len(run) < len(new_circ)): # NOTE: This is short-circuited on calibrated gates, which we're timid about # reducing. warnings.warn(f"Resynthesized {run} and got {new_circ}, " @@ -91,13 +103,6 @@ def run(self, dag): f"opening an issue here: " f"https://github.com/Qiskit/qiskit-terra/issues/new/choose", stacklevel=2) - # does this run have uncalibrated gates? - uncalibrated_p = any(not dag.has_calibration_for(g) for g in run) - # does this run have gates not in the image of ._decomposers _and_ uncalibrated? - uncalibrated_and_not_basis_p = any( - g.name not in self._target_basis and not dag.has_calibration_for(g) - for g in run - ) # if we're outside of the basis set, we're obligated to logically decompose. # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. From 9327d05544191ad0dce728af5fa01db5f6cd7e4b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 15 Jun 2021 08:09:33 +1200 Subject: [PATCH 14/30] add some U3 special cases --- .../optimization/optimize_1q_decomposition.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 32963073dbab..49692e4c94b4 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -16,6 +16,9 @@ import logging import warnings +import numpy as np + +from qiskit.circuit.library import U3Gate from qiskit.transpiler.basepasses import TransformationPass from qiskit.quantum_info.synthesis import one_qubit_decompose from qiskit.converters import circuit_to_dag @@ -72,6 +75,17 @@ def run(self, dag): return dag runs = dag.collect_1q_runs() for run in runs: + # SPECIAL CASE: Don't bother to optimize single U3 gates which are in the basis set. + # The U3 decomposer is only going to emit a sequence of length 1 anyhow. + if ('u3' in self._target_basis and len(run) == 1 and isinstance(run[0].op, U3Gate)): + # Toss U3 gates equivalent to the identity; there we get off easy. + if np.array_equal(run[0].op.to_matrix(), np.eye(2)): + dag.remove_op_node(run[0]) + continue + # We might rewrite into lower `u`s if they're available. + if 'u2' not in self._target_basis and 'u1' not in self._target_basis: + continue + new_circs = [] operator = run[0].op.to_matrix() for gate in run[1:]: @@ -81,16 +95,19 @@ def run(self, dag): if new_circs: new_circ = min(new_circs, key=len) + # do we even have calibrations? + has_cals_p = dag.calibrations is not None and len(dag.calibrations) > 0 # is this run all in the target set and also uncalibrated? rewriteable_and_in_basis_p = all( - g.name in self._target_basis and not dag.has_calibration_for(g) + g.name in self._target_basis and (not has_cals_p or not dag.has_calibration_for(g)) for g in run ) # does this run have uncalibrated gates? - uncalibrated_p = any(not dag.has_calibration_for(g) for g in run) + uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in run) # does this run have gates not in the image of ._decomposers _and_ uncalibrated? uncalibrated_and_not_basis_p = any( - g.name not in self._target_basis and not dag.has_calibration_for(g) + g.name not in self._target_basis and + (not has_cals_p or not dag.has_calibration_for(g)) for g in run ) From 7ed9d8c8f83a3679f3046163244d025f0683e69a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 15 Jun 2021 08:42:23 +1200 Subject: [PATCH 15/30] make black happy --- .../optimization/optimize_1q_decomposition.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 49692e4c94b4..b3c963528ae4 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -77,13 +77,13 @@ def run(self, dag): for run in runs: # SPECIAL CASE: Don't bother to optimize single U3 gates which are in the basis set. # The U3 decomposer is only going to emit a sequence of length 1 anyhow. - if ('u3' in self._target_basis and len(run) == 1 and isinstance(run[0].op, U3Gate)): + if "u3" in self._target_basis and len(run) == 1 and isinstance(run[0].op, U3Gate): # Toss U3 gates equivalent to the identity; there we get off easy. if np.array_equal(run[0].op.to_matrix(), np.eye(2)): dag.remove_op_node(run[0]) continue # We might rewrite into lower `u`s if they're available. - if 'u2' not in self._target_basis and 'u1' not in self._target_basis: + if "u2" not in self._target_basis and "u1" not in self._target_basis: continue new_circs = [] @@ -99,27 +99,30 @@ def run(self, dag): has_cals_p = dag.calibrations is not None and len(dag.calibrations) > 0 # is this run all in the target set and also uncalibrated? rewriteable_and_in_basis_p = all( - g.name in self._target_basis and (not has_cals_p or not dag.has_calibration_for(g)) + g.name in self._target_basis + and (not has_cals_p or not dag.has_calibration_for(g)) for g in run ) # does this run have uncalibrated gates? uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in run) # does this run have gates not in the image of ._decomposers _and_ uncalibrated? uncalibrated_and_not_basis_p = any( - g.name not in self._target_basis and - (not has_cals_p or not dag.has_calibration_for(g)) + g.name not in self._target_basis + and (not has_cals_p or not dag.has_calibration_for(g)) for g in run ) - if (rewriteable_and_in_basis_p and len(run) < len(new_circ)): + if rewriteable_and_in_basis_p and len(run) < len(new_circ): # NOTE: This is short-circuited on calibrated gates, which we're timid about # reducing. - warnings.warn(f"Resynthesized {run} and got {new_circ}, " - f"but the original was native and the new value is longer. This " - f"indicates an efficiency bug in synthesis. Please report it by " - f"opening an issue here: " - f"https://github.com/Qiskit/qiskit-terra/issues/new/choose", - stacklevel=2) + warnings.warn( + f"Resynthesized {run} and got {new_circ}, " + f"but the original was native and the new value is longer. This " + f"indicates an efficiency bug in synthesis. Please report it by " + f"opening an issue here: " + f"https://github.com/Qiskit/qiskit-terra/issues/new/choose", + stacklevel=2, + ) # if we're outside of the basis set, we're obligated to logically decompose. # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. From bccbf63e86326dc60a4ec72d5be8feeabe9c0981 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 15 Jun 2021 09:32:45 +1200 Subject: [PATCH 16/30] fix a claimed circular import --- .../transpiler/passes/optimization/optimize_1q_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index b3c963528ae4..78882a86b54b 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -18,7 +18,7 @@ import numpy as np -from qiskit.circuit.library import U3Gate +from qiskit.circuit.library.standard_gates import U3Gate from qiskit.transpiler.basepasses import TransformationPass from qiskit.quantum_info.synthesis import one_qubit_decompose from qiskit.converters import circuit_to_dag From 2fddee1e67448a752a302cec1d5ea14bd35f921c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 16 Jun 2021 12:15:01 +1200 Subject: [PATCH 17/30] add a couple more rewrite tests --- .../test_optimize_1q_decomposition.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index b7f5a416ccea..9d15ec151f8f 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -459,6 +459,31 @@ def test_short_string(self): msg = f"expected:\n{expected}\nresult:\n{result}" self.assertEqual(expected, result, msg=msg) + def test_u_rewrites_to_rz(self): + """Test that a phase-like U-gate gets rewritten into an RZ gate.""" + qc = QuantumCircuit(1) + qc.u(0, 0, np.pi / 6, 0) + basis = ["sx", "rz"] + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(qc) + expected = QuantumCircuit(1, global_phase=np.pi / 12) + expected.rz(np.pi / 6, 0) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) + + def test_u_rewrites_to_phase(self): + """Test that a phase-like U-gate gets rewritten into an RZ gate.""" + qc = QuantumCircuit(1) + qc.u(0, 0, np.pi / 6, 0) + basis = ["sx", "p"] + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(qc) + expected = QuantumCircuit(1) + expected.p(np.pi / 6, 0) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) if __name__ == "__main__": unittest.main() From 0a1febd3e7ad747ee7436911cb01faa05219ef3e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 17 Jun 2021 12:53:57 +1200 Subject: [PATCH 18/30] satisfy black --- test/python/transpiler/test_optimize_1q_decomposition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 9d15ec151f8f..f4212f3e00c9 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -485,5 +485,6 @@ def test_u_rewrites_to_phase(self): msg = f"expected:\n{expected}\nresult:\n{result}" self.assertEqual(expected, result, msg=msg) + if __name__ == "__main__": unittest.main() From 2038df7effdd67b30ab8284f4bc3a87cd0c906b3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 22 Jun 2021 06:39:46 +1200 Subject: [PATCH 19/30] =?UTF-8?q?avoid=20lambda=20=3D=20=C2=B1=C2=A0pi=20i?= =?UTF-8?q?n=201Q=20KAK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 73e94cbbc1b8..ff1ab7cf1701 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -298,6 +298,8 @@ def _circuit_zyz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): gphase += phi lam, phi = lam - phi, 0 lam = _mod_2pi(lam, atol) + if abs(_mod_2pi(lam + np.pi)) < atol: + lam, theta, phi = 0, -theta, lam + phi if abs(lam) > atol: gphase += lam / 2 circuit._append(RZGate(lam), [qr[0]], []) @@ -327,6 +329,8 @@ def _circuit_zxz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): gphase += phi lam, phi = lam - phi, 0 lam = _mod_2pi(lam, atol) + if abs(_mod_2pi(lam + np.pi)) < atol: + lam, theta, phi = 0, -theta, lam + phi if abs(lam) > atol: gphase += lam / 2 circuit._append(RZGate(lam), [qr[0]], []) @@ -356,6 +360,8 @@ def _circuit_xyx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): gphase += phi lam, phi = lam - phi, 0 lam = _mod_2pi(lam, atol) + if abs(_mod_2pi(lam + np.pi)) < atol: + lam, theta, phi = 0, -theta, lam + phi if abs(lam) > atol: gphase += lam / 2 circuit._append(RXGate(lam), [qr[0]], []) From 7b7c363ae6bd8fc99a15893b3153809b8b744348 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 23 Jun 2021 09:19:17 +1200 Subject: [PATCH 20/30] optimize on strict length --- .../optimization/optimize_1q_decomposition.py | 15 ++++++++++++++- test/python/opflow/test_pauli_sum_op.py | 2 +- test/python/transpiler/test_basis_translator.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 78882a86b54b..44959d48ba40 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -126,7 +126,20 @@ def run(self, dag): # if we're outside of the basis set, we're obligated to logically decompose. # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. - if uncalibrated_and_not_basis_p or (uncalibrated_p and len(run) >= len(new_circ)): + # NOTE: Here we use circuit length as a weak proxy for "improvement"; in reality, + # we care about something more like fidelity at runtime, which would mean, + # e.g., a preference for `RZGate`s over `RXGate`s. In fact, users sometimes + # express a preference for a "canonical form" of a circuit, which may come in + # the form of some parameter values, also not visible at the level of circuit + # length. Since we don't have a framework for the caller to programmatically + # express what they want here, we include some special casing for particular + # gates which we've promised to normalize --- but this is fragile and should + # ultimately be done away with. + if ( + uncalibrated_and_not_basis_p + or (uncalibrated_p and len(run) > len(new_circ)) + or isinstance(run[0].op, U3Gate) + ): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) # Delete the other nodes in the run diff --git a/test/python/opflow/test_pauli_sum_op.py b/test/python/opflow/test_pauli_sum_op.py index 4b03b2cbcf84..e0a25901e05e 100644 --- a/test/python/opflow/test_pauli_sum_op.py +++ b/test/python/opflow/test_pauli_sum_op.py @@ -211,7 +211,7 @@ def test_to_instruction(self): """test for to_instruction""" target = ((X + Z) / np.sqrt(2)).to_instruction() qc = QuantumCircuit(1) - qc.u(np.pi / 2, 0, -np.pi, 0) + qc.u(np.pi / 2, 0, np.pi, 0) qc_out = transpile(target.definition, basis_gates=["u"]) self.assertEqual(qc_out, qc) diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 8332186eed10..540df7d20ae9 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -811,7 +811,7 @@ def test_condition_set_substitute_node(self): qr = QuantumRegister(2, "q") cr = ClassicalRegister(2, "c") expected = QuantumCircuit(qr, cr) - expected.u2(0, -pi, 0) + expected.u2(0, pi, 0) expected.cx(0, 1) expected.measure(1, 1) expected.u2(0, pi, 0).c_if(cr, 1) From 1c5b531930066bfbac2b5e8f91afb1d3e6d5c8f0 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 24 Jun 2021 11:03:52 +1200 Subject: [PATCH 21/30] more thorough gate elision during 1Q synthesis --- .../synthesis/one_qubit_decompose.py | 104 +++++++++++------- test/python/compiler/test_transpiler.py | 6 +- .../test_optimize_1q_decomposition.py | 45 +++++++- 3 files changed, 108 insertions(+), 47 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index ff1ab7cf1701..807b88e5ba1e 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -287,28 +287,30 @@ def _circuit_zyz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(qr) if not simplify: atol = -1.0 - if abs(theta) < atol: - tot = _mod_2pi(phi + lam, atol) - if abs(tot) > atol: - circuit._append(RZGate(tot), [qr[0]], []) - gphase += tot / 2 + if abs(_mod_2pi(theta)) < atol: + lam, phi = lam + phi, 0 + if abs(_mod_2pi(lam)) > atol: + circuit._append(RZGate(lam), [qr[0]], []) + gphase += lam / 2 circuit.global_phase = gphase return circuit - if abs(theta - np.pi) < atol: + if abs(_mod_2pi(theta - np.pi)) < atol: gphase += phi lam, phi = lam - phi, 0 - lam = _mod_2pi(lam, atol) if abs(_mod_2pi(lam + np.pi)) < atol: lam, theta, phi = 0, -theta, lam + phi - if abs(lam) > atol: + if abs(_mod_2pi(phi + np.pi)) < atol: + lam, theta, phi = lam + phi, -theta, 0 + if abs(_mod_2pi(lam)) > atol: gphase += lam / 2 circuit._append(RZGate(lam), [qr[0]], []) - circuit._append(RYGate(theta), [qr[0]], []) + if abs(theta) > atol: + circuit._append(RYGate(theta), [qr[0]], []) phi = _mod_2pi(phi, atol) if abs(phi) > atol: gphase += phi / 2 circuit._append(RZGate(phi), [qr[0]], []) - circuit.global_phase = gphase + circuit.global_phase = _mod_2pi(gphase) return circuit @staticmethod @@ -318,28 +320,30 @@ def _circuit_zxz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(qr) if not simplify: atol = -1.0 - if abs(theta) < atol: - tot = _mod_2pi(phi + lam) - if abs(tot) > atol: - circuit._append(RZGate(tot), [qr[0]], []) - gphase += tot / 2 + if abs(_mod_2pi(theta)) < atol: + lam, phi = lam + phi, 0 + if abs(_mod_2pi(lam)) > atol: + circuit._append(RZGate(lam), [qr[0]], []) + gphase += lam / 2 circuit.global_phase = gphase return circuit - if abs(theta - np.pi) < atol: + if abs(_mod_2pi(theta - np.pi)) < atol: gphase += phi lam, phi = lam - phi, 0 - lam = _mod_2pi(lam, atol) if abs(_mod_2pi(lam + np.pi)) < atol: lam, theta, phi = 0, -theta, lam + phi - if abs(lam) > atol: + if abs(_mod_2pi(phi + np.pi)) < atol: + lam, theta, phi = lam + phi, -theta, 0 + if abs(_mod_2pi(lam)) > atol: gphase += lam / 2 circuit._append(RZGate(lam), [qr[0]], []) - circuit._append(RXGate(theta), [qr[0]], []) + if abs(_mod_2pi(theta)) > atol: + circuit._append(RXGate(theta), [qr[0]], []) phi = _mod_2pi(phi, atol) if abs(phi) > atol: gphase += phi / 2 circuit._append(RZGate(phi), [qr[0]], []) - circuit.global_phase = gphase + circuit.global_phase = _mod_2pi(gphase) return circuit @staticmethod @@ -349,23 +353,25 @@ def _circuit_xyx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(qr) if not simplify: atol = -1.0 - if abs(theta) < atol: - tot = _mod_2pi(phi + lam, atol) - if abs(tot) > atol: - circuit._append(RXGate(tot), [qr[0]], []) - gphase += tot / 2 + if abs(_mod_2pi(theta)) < atol: + lam, phi = lam + phi, 0 + if abs(_mod_2pi(lam)) > atol: + circuit._append(RXGate(lam), [qr[0]], []) + gphase += lam / 2 circuit.global_phase = gphase return circuit - if abs(theta - np.pi) < atol: + if abs(_mod_2pi(theta - np.pi)) < atol: gphase += phi lam, phi = lam - phi, 0 - lam = _mod_2pi(lam, atol) if abs(_mod_2pi(lam + np.pi)) < atol: lam, theta, phi = 0, -theta, lam + phi - if abs(lam) > atol: + if abs(_mod_2pi(phi + np.pi)) < atol: + lam, theta, phi = lam + phi, -theta, 0 + if abs(_mod_2pi(lam)) > atol: gphase += lam / 2 circuit._append(RXGate(lam), [qr[0]], []) - circuit._append(RYGate(theta), [qr[0]], []) + if abs(theta) > atol: + circuit._append(RYGate(theta), [qr[0]], []) phi = _mod_2pi(phi, atol) if abs(phi) > atol: gphase += phi / 2 @@ -416,32 +422,46 @@ def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None): """Generic X90, phase decomposition""" qr = QuantumRegister(1, "qr") circuit = QuantumCircuit(qr, global_phase=phase) - # Check for decomposition into minimimal number required SX pulses + # Early return for zero SX decomposition if np.abs(theta) < atol: - # Zero SX gate decomposition - pfun(circuit, qr, lam + phi) + if abs(_mod_2pi(lam + phi)) > atol: + pfun(circuit, qr, lam + phi) return circuit + # Early return for single SX decomposition if abs(theta - np.pi / 2) < atol: - # Single SX gate decomposition - pfun(circuit, qr, lam - np.pi / 2) + if abs(_mod_2pi(lam - np.pi / 2)) > atol: + pfun(circuit, qr, lam - np.pi / 2) xfun(circuit, qr) - pfun(circuit, qr, phi + np.pi / 2) + if abs(_mod_2pi(phi + np.pi / 2)) > atol: + pfun(circuit, qr, phi + np.pi / 2) return circuit - # General two-SX gate decomposition - # Shift theta and phi so decomposition is - # P(phi).SX.P(theta).SX.P(lam) + # General double SX decomposition if abs(theta - np.pi) < atol: circuit.global_phase += lam phi, lam = phi - lam, 0 + if abs(_mod_2pi(lam + np.pi)) < atol: + lam, theta, phi = 0, -theta, phi + lam + circuit.global_phase -= theta + if abs(_mod_2pi(phi)) < atol: + lam, theta, phi = lam - np.pi, -theta, phi + np.pi + circuit.global_phase -= theta + # Shift theta and phi to turn the decomposition from + # RZ(phi).RY(theta).RZ(lam) = RZ(phi).RX(-pi/2).RZ(theta).RX(pi/2).RZ(lam) + # into RZ(phi+pi).SX.RZ(theta+pi).SX.RZ(lam) . + theta, phi = theta + np.pi, phi + np.pi circuit.global_phase -= np.pi / 2 - pfun(circuit, qr, lam) - if xpifun and abs(_mod_2pi(theta + np.pi)) < atol: + # Emit circuit + if abs(lam) > atol: + pfun(circuit, qr, lam) + if xpifun and abs(_mod_2pi(theta)) < atol: xpifun(circuit, qr) else: xfun(circuit, qr) - pfun(circuit, qr, theta + np.pi) + if abs(_mod_2pi(theta)) > atol: + pfun(circuit, qr, theta) xfun(circuit, qr) - pfun(circuit, qr, phi + np.pi) + if abs(phi) > atol: + pfun(circuit, qr, phi) return circuit diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 7be923e812c5..e6f93a830892 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1212,12 +1212,12 @@ def test_no_infinite_loop(self, optimization_level): # a -0.5 * theta phase for RZ to P twice, once at theta, and once at 3 pi # for the second and third RZ gates in the U3 decomposition. expected = QuantumCircuit( - 1, global_phase=-np.pi / 2 - 0.5 * (0.2 + np.pi) - 0.5 * 3 * np.pi + 1, global_phase=-np.pi / 2 - 0.5 * (-0.2 + np.pi) - 0.5 * 3 * np.pi ) + expected.p(-np.pi, 0) expected.sx(0) - expected.p(-np.pi + 0.2, 0) + expected.p(np.pi - 0.2, 0) expected.sx(0) - expected.p(-np.pi, 0) error_message = ( f"\nOutput circuit:\n{out!s}\n{Operator(out).data}\n" diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index f4212f3e00c9..86e9d379aaa1 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -354,6 +354,48 @@ def test_euler_decomposition_worse(self): # assert optimization pass doesn't use it. self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") + def test_euler_decomposition_worse_2(self): + """Ensure we don't decompose to a deeper circuit in an edge case.""" + circuit = QuantumCircuit(1) + circuit.rz(0.13, 0) + circuit.ry(-0.14, 0) + basis = ["ry", "rz"] + passmanager = PassManager() + passmanager.append(BasisTranslator(sel, basis)) + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(circuit) + self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") + + def test_euler_decomposition_zsx(self): + """Ensure we don't decompose to a deeper circuit in the ZSX basis.""" + circuit = QuantumCircuit(1) + circuit.rz(0.3, 0) + circuit.sx(0) + circuit.rz(0.2, 0) + circuit.sx(0) + + basis = ["sx", "rz"] + passmanager = PassManager() + passmanager.append(BasisTranslator(sel, basis)) + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(circuit) + self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") + + def test_euler_decomposition_zsx_2(self): + """Ensure we don't decompose to a deeper circuit in the ZSX basis.""" + circuit = QuantumCircuit(1) + circuit.sx(0) + circuit.rz(0.2, 0) + circuit.sx(0) + circuit.rz(0.3, 0) + + basis = ["sx", "rz"] + passmanager = PassManager() + passmanager.append(BasisTranslator(sel, basis)) + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(circuit) + self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") + def test_optimize_u_to_phase_gate(self): """U(0, 0, pi/4) -> p(pi/4). Basis [p, sx].""" qr = QuantumRegister(2, "qr") @@ -438,9 +480,8 @@ def test_y_simplification_rz_sx_x(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) expected = QuantumCircuit(1) - expected.x(0) expected.rz(-np.pi, 0) - expected.global_phase += np.pi + expected.x(0) msg = f"expected:\n{expected}\nresult:\n{result}" self.assertEqual(expected, result, msg=msg) From fd605a4a3844764bfcfd05d78798cd6d7d5e7d8f Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 25 Jun 2021 09:00:47 +1200 Subject: [PATCH 22/30] respond to most of Lev's feedback --- .../synthesis/one_qubit_decompose.py | 144 ++++++------------ 1 file changed, 48 insertions(+), 96 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 807b88e5ba1e..b34fbd86d823 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -281,104 +281,60 @@ def _params_u1x(mat): return theta, phi, lam, phase - 0.5 * (theta + phi + lam) @staticmethod - def _circuit_zyz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - gphase = phase - (phi + lam) / 2 - qr = QuantumRegister(1, "qr") - circuit = QuantumCircuit(qr) - if not simplify: - atol = -1.0 - if abs(_mod_2pi(theta)) < atol: - lam, phi = lam + phi, 0 - if abs(_mod_2pi(lam)) > atol: - circuit._append(RZGate(lam), [qr[0]], []) - gphase += lam / 2 - circuit.global_phase = gphase - return circuit - if abs(_mod_2pi(theta - np.pi)) < atol: - gphase += phi - lam, phi = lam - phi, 0 - if abs(_mod_2pi(lam + np.pi)) < atol: - lam, theta, phi = 0, -theta, lam + phi - if abs(_mod_2pi(phi + np.pi)) < atol: - lam, theta, phi = lam + phi, -theta, 0 - if abs(_mod_2pi(lam)) > atol: - gphase += lam / 2 - circuit._append(RZGate(lam), [qr[0]], []) - if abs(theta) > atol: - circuit._append(RYGate(theta), [qr[0]], []) - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - gphase += phi / 2 - circuit._append(RZGate(phi), [qr[0]], []) - circuit.global_phase = _mod_2pi(gphase) - return circuit - - @staticmethod - def _circuit_zxz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - gphase = phase - (phi + lam) / 2 - qr = QuantumRegister(1, "qr") - circuit = QuantumCircuit(qr) - if not simplify: - atol = -1.0 - if abs(_mod_2pi(theta)) < atol: - lam, phi = lam + phi, 0 - if abs(_mod_2pi(lam)) > atol: - circuit._append(RZGate(lam), [qr[0]], []) - gphase += lam / 2 - circuit.global_phase = gphase - return circuit - if abs(_mod_2pi(theta - np.pi)) < atol: - gphase += phi - lam, phi = lam - phi, 0 - if abs(_mod_2pi(lam + np.pi)) < atol: - lam, theta, phi = 0, -theta, lam + phi - if abs(_mod_2pi(phi + np.pi)) < atol: - lam, theta, phi = lam + phi, -theta, 0 - if abs(_mod_2pi(lam)) > atol: - gphase += lam / 2 - circuit._append(RZGate(lam), [qr[0]], []) - if abs(_mod_2pi(theta)) > atol: - circuit._append(RXGate(theta), [qr[0]], []) - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - gphase += phi / 2 - circuit._append(RZGate(phi), [qr[0]], []) - circuit.global_phase = _mod_2pi(gphase) - return circuit + def _circuit_kak( + theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, k_gate=RZGate, a_gate=RYGate + ): + """ + Installs the angles phi, theta, and lam into a KAK-type decomposition of + the form K(phi) . A(theta) . K(lam) , where K and A are an orthogonal + pair drawn from RZGate, RYGate, and RXGate. - @staticmethod - def _circuit_xyx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + NOTE: `theta` is expected to lie in [0, pi). + """ gphase = phase - (phi + lam) / 2 qr = QuantumRegister(1, "qr") circuit = QuantumCircuit(qr) if not simplify: atol = -1.0 - if abs(_mod_2pi(theta)) < atol: + # Early return for the middle-gate-free case + if abs(theta) < atol: lam, phi = lam + phi, 0 - if abs(_mod_2pi(lam)) > atol: - circuit._append(RXGate(lam), [qr[0]], []) + if abs(_mod_2pi(lam, atol)) > atol: + circuit._append(k_gate(lam), [qr[0]], []) gphase += lam / 2 circuit.global_phase = gphase return circuit - if abs(_mod_2pi(theta - np.pi)) < atol: + if abs(theta - np.pi) < atol: gphase += phi lam, phi = lam - phi, 0 - if abs(_mod_2pi(lam + np.pi)) < atol: - lam, theta, phi = 0, -theta, lam + phi - if abs(_mod_2pi(phi + np.pi)) < atol: - lam, theta, phi = lam + phi, -theta, 0 + if abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol: + lam, theta, phi = lam + np.pi, -theta, phi + np.pi if abs(_mod_2pi(lam)) > atol: gphase += lam / 2 - circuit._append(RXGate(lam), [qr[0]], []) - if abs(theta) > atol: - circuit._append(RYGate(theta), [qr[0]], []) + circuit._append(k_gate(lam), [qr[0]], []) + circuit._append(a_gate(theta), [qr[0]], []) phi = _mod_2pi(phi, atol) if abs(phi) > atol: gphase += phi / 2 - circuit._append(RXGate(phi), [qr[0]], []) + circuit._append(k_gate(phi), [qr[0]], []) circuit.global_phase = gphase return circuit + def _circuit_zyz(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + return self._circuit_kak( + theta, phi, lam, phase, simplify=simplify, atol=atol, k_gate=RZGate, a_gate=RYGate + ) + + def _circuit_zxz(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + return self._circuit_kak( + theta, phi, lam, phase, simplify=simplify, atol=atol, k_gate=RZGate, a_gate=RXGate + ) + + def _circuit_xyx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + return self._circuit_kak( + theta, phi, lam, phase, simplify=simplify, atol=atol, k_gate=RXGate, a_gate=RYGate + ) + @staticmethod def _circuit_u3(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): qr = QuantumRegister(1, "qr") @@ -419,31 +375,30 @@ def _circuit_u(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): @staticmethod def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None): - """Generic X90, phase decomposition""" + """ + Generic X90, phase decomposition + + NOTE: `pfun`, `xfun`, and `xpifun` are responsible for eliding gates + where appropriate (e.g., at angle value 0). + """ qr = QuantumRegister(1, "qr") circuit = QuantumCircuit(qr, global_phase=phase) # Early return for zero SX decomposition if np.abs(theta) < atol: - if abs(_mod_2pi(lam + phi)) > atol: - pfun(circuit, qr, lam + phi) + pfun(circuit, qr, lam + phi) return circuit # Early return for single SX decomposition if abs(theta - np.pi / 2) < atol: - if abs(_mod_2pi(lam - np.pi / 2)) > atol: - pfun(circuit, qr, lam - np.pi / 2) + pfun(circuit, qr, lam - np.pi / 2) xfun(circuit, qr) - if abs(_mod_2pi(phi + np.pi / 2)) > atol: - pfun(circuit, qr, phi + np.pi / 2) + pfun(circuit, qr, phi + np.pi / 2) return circuit # General double SX decomposition if abs(theta - np.pi) < atol: circuit.global_phase += lam phi, lam = phi - lam, 0 - if abs(_mod_2pi(lam + np.pi)) < atol: - lam, theta, phi = 0, -theta, phi + lam - circuit.global_phase -= theta - if abs(_mod_2pi(phi)) < atol: - lam, theta, phi = lam - np.pi, -theta, phi + np.pi + if abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi)) < atol: + lam, theta, phi = lam + np.pi, -theta, phi + np.pi circuit.global_phase -= theta # Shift theta and phi to turn the decomposition from # RZ(phi).RY(theta).RZ(lam) = RZ(phi).RX(-pi/2).RZ(theta).RX(pi/2).RZ(lam) @@ -451,17 +406,14 @@ def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None): theta, phi = theta + np.pi, phi + np.pi circuit.global_phase -= np.pi / 2 # Emit circuit - if abs(lam) > atol: - pfun(circuit, qr, lam) + pfun(circuit, qr, lam) if xpifun and abs(_mod_2pi(theta)) < atol: xpifun(circuit, qr) else: xfun(circuit, qr) - if abs(_mod_2pi(theta)) > atol: - pfun(circuit, qr, theta) + pfun(circuit, qr, theta) xfun(circuit, qr) - if abs(phi) > atol: - pfun(circuit, qr, phi) + pfun(circuit, qr, phi) return circuit From 2c4de1f571ac828f7ca77cc90200afe60e2026c1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 25 Jun 2021 11:50:52 +1200 Subject: [PATCH 23/30] add an 'allow_non_canonical' parameters --- .../synthesis/one_qubit_decompose.py | 76 +++++++++++++++---- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index b34fbd86d823..d6e866ee69b7 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -282,14 +282,29 @@ def _params_u1x(mat): @staticmethod def _circuit_kak( - theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, k_gate=RZGate, a_gate=RYGate + theta, + phi, + lam, + phase, + simplify=True, + atol=DEFAULT_ATOL, + allow_non_canonical=True, + k_gate=RZGate, + a_gate=RYGate, ): """ - Installs the angles phi, theta, and lam into a KAK-type decomposition of - the form K(phi) . A(theta) . K(lam) , where K and A are an orthogonal - pair drawn from RZGate, RYGate, and RXGate. - - NOTE: `theta` is expected to lie in [0, pi). + Installs the angles phi, theta, and lam into a KAK-type decomposition of the form + K(phi) . A(theta) . K(lam) , where K and A are an orthogonal pair drawn from RZGate, RYGate, + and RXGate. + + Behavior flags: + `simplify` indicates whether gates should be elided / coalesced where possible. + `allow_non_canonical` indicates whether we are permitted to reverse the sign of the + middle parameter, theta, in the output. When this and `simplify` are both enabled, + we take the opportunity to commute half-rotations in the outer gates past the middle + gate, which permits us to coalesce them at the cost of reversing the sign of theta. + + NOTE: The input value of `theta` is expected to lie in [0, pi). """ gphase = phase - (phi + lam) / 2 qr = QuantumRegister(1, "qr") @@ -307,7 +322,9 @@ def _circuit_kak( if abs(theta - np.pi) < atol: gphase += phi lam, phi = lam - phi, 0 - if abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol: + if allow_non_canonical and ( + abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol + ): lam, theta, phi = lam + np.pi, -theta, phi + np.pi if abs(_mod_2pi(lam)) > atol: gphase += lam / 2 @@ -320,19 +337,49 @@ def _circuit_kak( circuit.global_phase = gphase return circuit - def _circuit_zyz(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + def _circuit_zyz( + self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True + ): return self._circuit_kak( - theta, phi, lam, phase, simplify=simplify, atol=atol, k_gate=RZGate, a_gate=RYGate + theta, + phi, + lam, + phase, + simplify=simplify, + atol=atol, + allow_non_canonical=allow_non_canonical, + k_gate=RZGate, + a_gate=RYGate, ) - def _circuit_zxz(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + def _circuit_zxz( + self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True + ): return self._circuit_kak( - theta, phi, lam, phase, simplify=simplify, atol=atol, k_gate=RZGate, a_gate=RXGate + theta, + phi, + lam, + phase, + simplify=simplify, + atol=atol, + allow_non_canonical=allow_non_canonical, + k_gate=RZGate, + a_gate=RXGate, ) - def _circuit_xyx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + def _circuit_xyx( + self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True + ): return self._circuit_kak( - theta, phi, lam, phase, simplify=simplify, atol=atol, k_gate=RXGate, a_gate=RYGate + theta, + phi, + lam, + phase, + simplify=simplify, + atol=atol, + allow_non_canonical=allow_non_canonical, + k_gate=RXGate, + a_gate=RYGate, ) @staticmethod @@ -378,8 +425,7 @@ def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None): """ Generic X90, phase decomposition - NOTE: `pfun`, `xfun`, and `xpifun` are responsible for eliding gates - where appropriate (e.g., at angle value 0). + NOTE: `pfun` is responsible for eliding gates where appropriate (e.g., at angle value 0). """ qr = QuantumRegister(1, "qr") circuit = QuantumCircuit(qr, global_phase=phase) From fbdf39af3d9f3ac3b16db14cad834a835c55759b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 2 Jul 2021 06:36:49 +1200 Subject: [PATCH 24/30] normalize K rolls --- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index d6e866ee69b7..07eeacefa6fb 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -315,6 +315,10 @@ def _circuit_kak( if abs(theta) < atol: lam, phi = lam + phi, 0 if abs(_mod_2pi(lam, atol)) > atol: + # NOTE: The following normalization is safe, because the gphase correction below + # fixes a particular diagonal entry to 1, which prevents any potential phase + # slippage coming from _mod_2pi injecting multiples of 2pi. + lam = _mod_2pi(lam) circuit._append(k_gate(lam), [qr[0]], []) gphase += lam / 2 circuit.global_phase = gphase @@ -326,7 +330,8 @@ def _circuit_kak( abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol ): lam, theta, phi = lam + np.pi, -theta, phi + np.pi - if abs(_mod_2pi(lam)) > atol: + lam = _mod_2pi(lam) + if abs(lam) > atol: gphase += lam / 2 circuit._append(k_gate(lam), [qr[0]], []) circuit._append(a_gate(theta), [qr[0]], []) From f8f5e5fc94e17367afc218c139d991d1d170fe7e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 7 Jul 2021 05:08:33 +1200 Subject: [PATCH 25/30] Update qiskit/quantum_info/synthesis/one_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 07eeacefa6fb..215ef3487e68 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -330,7 +330,7 @@ def _circuit_kak( abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol ): lam, theta, phi = lam + np.pi, -theta, phi + np.pi - lam = _mod_2pi(lam) + lam = _mod_2pi(lam, atol) if abs(lam) > atol: gphase += lam / 2 circuit._append(k_gate(lam), [qr[0]], []) From a747b5ae39599ef37e4dc201f0ae978e5ff58c1c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 7 Jul 2021 05:09:00 +1200 Subject: [PATCH 26/30] Update qiskit/quantum_info/synthesis/one_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 215ef3487e68..a616367682bc 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -314,11 +314,12 @@ def _circuit_kak( # Early return for the middle-gate-free case if abs(theta) < atol: lam, phi = lam + phi, 0 - if abs(_mod_2pi(lam, atol)) > atol: - # NOTE: The following normalization is safe, because the gphase correction below - # fixes a particular diagonal entry to 1, which prevents any potential phase - # slippage coming from _mod_2pi injecting multiples of 2pi. - lam = _mod_2pi(lam) + # NOTE: The following normalization is safe, because the gphase correction below + # fixes a particular diagonal entry to 1, which prevents any potential phase + # slippage coming from _mod_2pi injecting multiples of 2pi. + lam = _mod_2pi(lam, atol) + if abs(lam) > atol: + circuit._append(k_gate(lam), [qr[0]], []) gphase += lam / 2 circuit.global_phase = gphase From a5272ac9c06edb7bbc9fc78fff5b0038f94f13c5 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 7 Jul 2021 05:43:35 +1200 Subject: [PATCH 27/30] add some Euler special case tests for pushing a K(pi) through an A(alpha) --- test/python/quantum_info/test_synthesis.py | 35 ++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 52c7d9c3fd2e..eade1def95ff 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -262,7 +262,18 @@ def test_euler_angles_1q_random(self, seed): self.check_one_qubit_euler_angles(unitary) -ANGEXP_ZYZ = [ # Special cases for ZYZ type expansions +""" +Special cases for ZYZ type expansions. Each list entry is of the format + + (alpha, beta, gamma, delta), (r, s), + +and encodes the assertion that + + (K(b) @ A(a) @ K(c), global_phase=d) + +re-synthesizes to have r applications of the K gate and s of the A gate. +""" +ANGEXP_ZYZ = [ [(1.0e-13, 0.1, -0.1, 0), (0, 0)], [(1.0e-13, 0.2, -0.1, 0), (1, 0)], [(1.0e-13, np.pi, np.pi, 0), (0, 0)], @@ -276,8 +287,24 @@ def test_euler_angles_1q_random(self, seed): [(0.1, 0.0, 0.0, 0), (0, 1)], [(0.1, 1.0e-13, 0.2, 0), (1, 1)], [(0.1, 0.2, 0.3, 0), (2, 1)], + [(0.1, 0.2, np.pi, 0), (1, 1)], + [(0.1, np.pi, 0.1, 0), (1, 1)], + [(0.1, np.pi, np.pi, 0), (0, 1)], ] -ANGEXP_PSX = [ # Special cases for Z.X90.Z.X90.Z type expansions + + +""" +Special cases for Z.X90.Z.X90.Z type expansions. Each list entry is of the format + + (alpha, beta, gamma), (r, s), + +and encodes the assertion that + + U3(alpha, beta, gamma) + +re-synthesizes to have r applications of the P gate and s of the SX gate. +""" +ANGEXP_PSX = [ [(0.0, 0.1, -0.1), (0, 0)], [(0.0, 0.1, 0.2), (1, 0)], [(-np.pi / 2, 0.2, 0.0), (2, 1)], @@ -288,6 +315,10 @@ def test_euler_angles_1q_random(self, seed): [(np.pi, np.pi + 0.1, 0.1), (0, 2)], [(np.pi, np.pi + 0.2, -0.1), (1, 2)], [(0.1, 0.2, 0.3), (3, 2)], + [(0.1, np.pi, 0.2), (2, 2)], + [(0.1, 0.2, 0.0), (2, 2)], + [(0.1, 0.2, np.pi), (2, 2)], + [(0.1, np.pi, 0), (1, 2)], ] From d808985cd8eadf274ede0e4ab7bc0c3faddd1f31 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 7 Jul 2021 05:43:35 +1200 Subject: [PATCH 28/30] add some Euler special case tests for pushing a K(pi) through an A(alpha) --- test/python/quantum_info/test_synthesis.py | 35 ++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 52c7d9c3fd2e..b6b282752946 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -262,7 +262,7 @@ def test_euler_angles_1q_random(self, seed): self.check_one_qubit_euler_angles(unitary) -ANGEXP_ZYZ = [ # Special cases for ZYZ type expansions +ANGEXP_ZYZ = [ [(1.0e-13, 0.1, -0.1, 0), (0, 0)], [(1.0e-13, 0.2, -0.1, 0), (1, 0)], [(1.0e-13, np.pi, np.pi, 0), (0, 0)], @@ -276,8 +276,24 @@ def test_euler_angles_1q_random(self, seed): [(0.1, 0.0, 0.0, 0), (0, 1)], [(0.1, 1.0e-13, 0.2, 0), (1, 1)], [(0.1, 0.2, 0.3, 0), (2, 1)], + [(0.1, 0.2, np.pi, 0), (1, 1)], + [(0.1, np.pi, 0.1, 0), (1, 1)], + [(0.1, np.pi, np.pi, 0), (0, 1)], ] -ANGEXP_PSX = [ # Special cases for Z.X90.Z.X90.Z type expansions +""" +Special cases for ZYZ type expansions. Each list entry is of the format + + (alpha, beta, gamma, delta), (r, s), + +and encodes the assertion that + + (K(b) @ A(a) @ K(c), global_phase=d) + +re-synthesizes to have r applications of the K gate and s of the A gate. +""" + + +ANGEXP_PSX = [ [(0.0, 0.1, -0.1), (0, 0)], [(0.0, 0.1, 0.2), (1, 0)], [(-np.pi / 2, 0.2, 0.0), (2, 1)], @@ -288,7 +304,22 @@ def test_euler_angles_1q_random(self, seed): [(np.pi, np.pi + 0.1, 0.1), (0, 2)], [(np.pi, np.pi + 0.2, -0.1), (1, 2)], [(0.1, 0.2, 0.3), (3, 2)], + [(0.1, np.pi, 0.2), (2, 2)], + [(0.1, 0.2, 0.0), (2, 2)], + [(0.1, 0.2, np.pi), (2, 2)], + [(0.1, np.pi, 0), (1, 2)], ] +""" +Special cases for Z.X90.Z.X90.Z type expansions. Each list entry is of the format + + (alpha, beta, gamma), (r, s), + +and encodes the assertion that + + U3(alpha, beta, gamma) + +re-synthesizes to have r applications of the P gate and s of the SX gate. +""" @ddt From c3e7d6ca70e6105f09246b0ddd5a653b11fe08f9 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 6 Jul 2021 13:30:13 -0700 Subject: [PATCH 29/30] Update one_qubit_decompose.py a by-hand attempt at reformatting the docstring --- .../synthesis/one_qubit_decompose.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index a616367682bc..a6ae78876323 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -297,14 +297,22 @@ def _circuit_kak( K(phi) . A(theta) . K(lam) , where K and A are an orthogonal pair drawn from RZGate, RYGate, and RXGate. - Behavior flags: - `simplify` indicates whether gates should be elided / coalesced where possible. - `allow_non_canonical` indicates whether we are permitted to reverse the sign of the - middle parameter, theta, in the output. When this and `simplify` are both enabled, - we take the opportunity to commute half-rotations in the outer gates past the middle - gate, which permits us to coalesce them at the cost of reversing the sign of theta. - - NOTE: The input value of `theta` is expected to lie in [0, pi). + Args: + theta (float): The middle KAK parameter. Expected to lie in [0, pi). + phi (float): The first KAK parameter. + lam (float): The final KAK parameter. + phase (float): The input global phase. + k_gate (Callable): The constructor for the K gate Instruction. + a_gate (Callable): The constructor for the A gate Instruction. + simplify (bool): Indicates whether gates should be elided / coalesced where possible. + allow_non_canonical (bool): Indicates whether we are permitted to reverse the sign of + the middle parameter, theta, in the output. When this and `simplify` are both + enabled, we take the opportunity to commute half-rotations in the outer gates past + the middle gate, which permits us to coalesce them at the cost of reversing the sign + of theta. + + Returns: + QuantumCircuit: The assembled circuit. """ gphase = phase - (phi + lam) / 2 qr = QuantumRegister(1, "qr") From 75acef023f06c5d24433568642ddadbd3fd6d3f7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 7 Jul 2021 09:30:42 +1200 Subject: [PATCH 30/30] ok linter --- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index a6ae78876323..dfa60b75c17f 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -310,7 +310,7 @@ def _circuit_kak( enabled, we take the opportunity to commute half-rotations in the outer gates past the middle gate, which permits us to coalesce them at the cost of reversing the sign of theta. - + Returns: QuantumCircuit: The assembled circuit. """