diff --git a/qiskit_research/utils/pauli_twirling.py b/qiskit_research/utils/pauli_twirling.py index 7126ff9..685e76e 100644 --- a/qiskit_research/utils/pauli_twirling.py +++ b/qiskit_research/utils/pauli_twirling.py @@ -11,14 +11,27 @@ """Pauli twirling.""" from typing import Any, Iterable, Optional +import itertools +import cmath import numpy as np -from qiskit.circuit import QuantumRegister +from qiskit.circuit import QuantumRegister, QuantumCircuit from qiskit.circuit.library import ( IGate, XGate, YGate, ZGate, + CXGate, + CYGate, + CZGate, + CHGate, + CSGate, + DCXGate, + CSXGate, + CSdgGate, + ECRGate, + iSwapGate, + SwapGate, ) from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import BasePass, TransformationPass @@ -26,14 +39,28 @@ CXCancellation, Optimize1qGatesDecomposition, ) -from qiskit.quantum_info import Pauli, pauli_basis +from qiskit.quantum_info import Pauli, Operator, pauli_basis from qiskit_research.utils.pulse_scaling import BASIS_GATES +# Single qubit Pauli gates I = IGate() X = XGate() Y = YGate() Z = ZGate() +# 2Q entangling gates +CX = CXGate() # cnot; controlled-X +CY = CYGate() # controlled-Y +CZ = CZGate() # controlled-Z +CH = CHGate() # controlled-Hadamard +CS = CSGate() # controlled-S +DCX = DCXGate() # double cnot +CSX = CSXGate() # controlled sqrt X +CSdg = CSdgGate() # controlled S^dagger +ECR = ECRGate() # echoed cross-resonance +Swap = SwapGate() # swap +iSwap = iSwapGate() # imaginary swap + # this list consists of the 2-qubit rotation gates TWO_QUBIT_PAULI_GENERATORS = { "rxx": Pauli("XX"), @@ -43,6 +70,74 @@ "secr": Pauli("XZ"), } + +def match_global_phase(a, b): + """Phase the given arrays so that their phases match at one entry. + + Args: + a: A Numpy array. + b: Another Numpy array. + + Returns: + A pair of arrays (a', b') that are equal if and only if a == b * exp(i phi) + for some real number phi. + """ + if a.shape != b.shape: + return a, b + # use the largest entry of one of the matrices to maximize precision + index = max(np.ndindex(*a.shape), key=lambda i: abs(b[i])) + phase_a = cmath.phase(a[index]) + phase_b = cmath.phase(b[index]) + return a * cmath.rect(1, -phase_a), b * cmath.rect(1, -phase_b) + + +def allclose_up_to_global_phase(a, b, rtol=1e-05, atol=1e-08, equal_nan=False): + """Check if two operators are close up to a global phase.""" + # Phase both operators to match their phases + phased_op1, phased_op2 = match_global_phase(a, b) + return np.allclose(phased_op1, phased_op2, rtol, atol, equal_nan) + + +def create_pauli_twirling_sets(two_qubit_gate): + """Generate the Pauli twirling sets for a given 2Q gate. + + Sets are ordered such that gate[0] and gate[1] are pre-rotations + applied to control and target, respectively. gate[2] and gate[3] + are post-rotations for control and target, respectively. + + Parameters: + two_qubit_gate (Gate): Input two-qubit gate + + Returns: + tuple: Tuple of all twirling gate sets + """ + + target_unitary = np.array(two_qubit_gate) + twirling_sets = [] + + # Generate combinations of 4 gates from the operator list + for gates in itertools.product(itertools.product([I, X, Y, Z], repeat=2), repeat=2): + qc = _build_twirl_circuit(gates, two_qubit_gate) + qc_array = Operator.from_circuit(qc).to_matrix() + if allclose_up_to_global_phase(qc_array, target_unitary): + twirling_sets.append(gates) + + return tuple(twirling_sets) + + +def _build_twirl_circuit(gates, two_qubit_gate): + """Build the twirled quantum circuit with specified gates.""" + qc = QuantumCircuit(2) + + qc.append(gates[0][0], [0]) + qc.append(gates[0][1], [1]) + qc.append(two_qubit_gate, [0, 1]) + qc.append(gates[1][0], [0]) + qc.append(gates[1][1], [1]) + + return qc + + # this dictionary stores the twirl sets for each supported gate # each key is the name of a supported gate # each value is a tuple that represents the twirl set for the gate @@ -50,24 +145,17 @@ # "before" and "after" are tuples of single-qubit gates to be applied # before and after the gate to be twirled TWIRL_GATES = { - "cx": ( - ((I, I), (I, I)), - ((I, X), (I, X)), - ((I, Y), (Z, Y)), - ((I, Z), (Z, Z)), - ((X, I), (X, X)), - ((X, X), (X, I)), - ((X, Y), (Y, Z)), - ((X, Z), (Y, Y)), - ((Y, I), (Y, X)), - ((Y, X), (Y, I)), - ((Y, Y), (X, Z)), - ((Y, Z), (X, Y)), - ((Z, I), (Z, I)), - ((Z, X), (Z, X)), - ((Z, Y), (I, Y)), - ((Z, Z), (I, Z)), - ), + "cx": create_pauli_twirling_sets(CX), + "cy": create_pauli_twirling_sets(CY), + "cz": create_pauli_twirling_sets(CZ), + "ch": create_pauli_twirling_sets(CH), + "cs": create_pauli_twirling_sets(CS), + "dcx": create_pauli_twirling_sets(DCX), + "csx": create_pauli_twirling_sets(CSX), + "csdg": create_pauli_twirling_sets(CSdg), + "ecr": create_pauli_twirling_sets(ECR), + "swap": create_pauli_twirling_sets(Swap), + "iswap": create_pauli_twirling_sets(iSwap), } diff --git a/test/utils/test_pauli_twirling.py b/test/utils/test_pauli_twirling.py index 4bbdb4e..1173d89 100644 --- a/test/utils/test_pauli_twirling.py +++ b/test/utils/test_pauli_twirling.py @@ -14,7 +14,19 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.library import CXGate +from qiskit.circuit.library import ( + CXGate, + CYGate, + CZGate, + CHGate, + CSGate, + DCXGate, + CSXGate, + CSdgGate, + ECRGate, + iSwapGate, + SwapGate, +) from qiskit.quantum_info import Operator from qiskit_research.utils.convenience import add_pauli_twirls from qiskit_research.utils.gates import SECRGate @@ -27,6 +39,7 @@ class TestPauliTwirling(unittest.TestCase): def test_twirl_gates_cnot(self): """Test twirling CNOT.""" twirl_gates = TWIRL_GATES["cx"] + self.assertEqual(len(twirl_gates), 16) operator = Operator(CXGate()) for (a, b), (c, d) in twirl_gates: circuit = QuantumCircuit(2) @@ -37,6 +50,146 @@ def test_twirl_gates_cnot(self): circuit.append(d, [1]) self.assertTrue(Operator(circuit).equiv(operator)) + def test_twirl_gates_cy(self): + """Test twirling CY""" + twirl_gates = TWIRL_GATES["cy"] + self.assertEqual(len(twirl_gates), 16) + operator = Operator(CYGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(CYGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_cz(self): + """Test twirling CZ.""" + twirl_gates = TWIRL_GATES["cz"] + self.assertEqual(len(twirl_gates), 16) + operator = Operator(CZGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(CZGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_ch(self): + """Test twirling CH.""" + twirl_gates = TWIRL_GATES["ch"] + self.assertEqual(len(twirl_gates), 4) + operator = Operator(CHGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(CHGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_cs(self): + """Test twirling CS.""" + twirl_gates = TWIRL_GATES["cs"] + self.assertEqual(len(twirl_gates), 4) + operator = Operator(CSGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(CSGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_dcx(self): + """Test twirling DCX.""" + twirl_gates = TWIRL_GATES["dcx"] + self.assertEqual(len(twirl_gates), 16) + operator = Operator(DCXGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(DCXGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_csx(self): + """Test twirling CSX.""" + twirl_gates = TWIRL_GATES["csx"] + self.assertEqual(len(twirl_gates), 4) + operator = Operator(CSXGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(CSXGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_csdg(self): + """Test twirling CSdg.""" + twirl_gates = TWIRL_GATES["csdg"] + self.assertEqual(len(twirl_gates), 4) + operator = Operator(CSdgGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(CSdgGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_ecr(self): + """Test twirling ECR.""" + twirl_gates = TWIRL_GATES["ecr"] + self.assertEqual(len(twirl_gates), 16) + operator = Operator(ECRGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(ECRGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_swap(self): + """Test twirling Swap.""" + twirl_gates = TWIRL_GATES["swap"] + self.assertEqual(len(twirl_gates), 16) + operator = Operator(SwapGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(SwapGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + + def test_twirl_gates_iswap(self): + """Test twirling iSwap.""" + twirl_gates = TWIRL_GATES["iswap"] + self.assertEqual(len(twirl_gates), 16) + operator = Operator(iSwapGate()) + for (a, b), (c, d) in twirl_gates: + circuit = QuantumCircuit(2) + circuit.append(a, [0]) + circuit.append(b, [1]) + circuit.append(iSwapGate(), [0, 1]) + circuit.append(c, [0]) + circuit.append(d, [1]) + self.assertTrue(Operator(circuit).equiv(operator)) + def test_add_pauli_twirls(self): """Test adding Pauli twirls.""" rng = np.random.default_rng()