From af2da55196b61499bce3fd18ae588bfdbcf1dd3a Mon Sep 17 00:00:00 2001 From: JamesB-1qbit <84878946+JamesB-1qbit@users.noreply.github.com> Date: Wed, 15 Dec 2021 13:45:25 -0500 Subject: [PATCH] added inverse function to Circuit (#78) * Inverse function for Gate and Circuit class * Added phase gate support to allow T,S inverse * added invertible_gates list --- tangelo/linq/circuit.py | 11 ++++++ tangelo/linq/gate.py | 35 +++++++++++++++++-- tangelo/linq/tests/test_circuits.py | 17 +++++++++ tangelo/linq/tests/test_gates.py | 19 ++++++++++ tangelo/linq/tests/test_translator.py | 18 ++++++++++ .../linq/translator/translate_json_ionq.py | 14 +++++++- tangelo/linq/translator/translate_openqasm.py | 4 ++- tangelo/linq/translator/translate_projectq.py | 5 +-- 8 files changed, 117 insertions(+), 6 deletions(-) diff --git a/tangelo/linq/circuit.py b/tangelo/linq/circuit.py index 743fbbf8b..f4c904c4c 100644 --- a/tangelo/linq/circuit.py +++ b/tangelo/linq/circuit.py @@ -130,5 +130,16 @@ def check_index_valid(index): # Keep track of the total gate count self._gate_counts[gate.name] = self._gate_counts.get(gate.name, 0) + 1 + def inverse(self): + """Return the inverse (adjoint) of a circuit + + This is performed by reversing the Gate order and applying the inverse to each Gate. + + Returns: + Circuit: the inverted circuit + """ + gate_list = [gate.inverse() for gate in reversed(self._gates)] + return Circuit(gate_list) + def serialize(self): return {"type": "QuantumCircuit", "gates": [gate.serialize() for gate in self._gates]} diff --git a/tangelo/linq/gate.py b/tangelo/linq/gate.py index 41aa0c4ae..3da873183 100644 --- a/tangelo/linq/gate.py +++ b/tangelo/linq/gate.py @@ -16,15 +16,18 @@ gate operation, without tying it to a particular backend or an underlying mathematical operation. """ - +from math import pi from typing import Union -from numpy import integer, ndarray +from numpy import integer, ndarray, floating ONE_QUBIT_GATES = {"H", "X", "Y", "Z", "S", "T", "RX", "RY", "RZ", "PHASE"} TWO_QUBIT_GATES = {"CNOT", "CX", "CY", "CZ", "CRX", "CRY", "CRZ", "CPHASE", "XX", "SWAP"} THREE_QUBIT_GATES = {"CSWAP"} PARAMETERIZED_GATES = {"RX", "RY", "RZ", "PHASE", "CRX", "CRY", "CRZ", "CPHASE", "XX"} +INVERTIBLE_GATES = {"H", "X", "Y", "Z", "S", "T", "RX", "RY", "RZ", "CH", "PHASE", + "CNOT", "CX", "CY", "CZ", "CRX", "CRY", "CRZ", "CPHASE", "XX", "SWAP" + "CSWAP"} class Gate(dict): @@ -100,6 +103,34 @@ def __str__(self): return mystr + def inverse(self): + """Returns the inverse (adjoint) of a gate. + + Return: + Gate: the inverse of the gate. + """ + if self.name not in INVERTIBLE_GATES: + raise AttributeError(f"{self.gate} is not an invertible gate") + if self.parameter == "": + new_parameter = "" + elif isinstance(self.parameter, (float, floating, int, integer)): + new_parameter = -self.parameter + elif self.name in {"T", "S"}: + new_parameter = -pi / 2 if self.name == "T" else -pi / 4 + return Gate(name="PHASE", + target=self.target, + control=self.control, + parameter=new_parameter, + is_variational=self.is_variational) + + else: + raise AttributeError(f"{self.name} is not an invertible gate when parameter is {self.parameter}") + return Gate(name=self.name, + target=self.target, + control=self.control, + parameter=new_parameter, + is_variational=self.is_variational) + def serialize(self): return {"type": "Gate", "params": {"name": self.name, "target": self.target, diff --git a/tangelo/linq/tests/test_circuits.py b/tangelo/linq/tests/test_circuits.py index 5b5845d78..70cdf00f5 100644 --- a/tangelo/linq/tests/test_circuits.py +++ b/tangelo/linq/tests/test_circuits.py @@ -19,6 +19,7 @@ import unittest import copy +from math import pi from collections import Counter from tangelo.linq import Gate, Circuit @@ -98,6 +99,22 @@ def test_fixed_sized_circuit_below(self): circuit_fixed = Circuit([Gate("H", 0)], n_qubits=n_qubits) self.assertTrue(circuit_fixed.width == n_qubits) + def test_inverse(self): + """ Test if inverse function returns the proper set of gates by comparing print strings.""" + + mygates_inverse = list() + mygates_inverse.append(Gate("RX", 1, parameter=-2.)) + mygates_inverse.append(Gate("Y", 0)) + mygates_inverse.append(Gate("CNOT", 2, control=1)) + mygates_inverse.append(Gate("CNOT", 1, control=0)) + mygates_inverse.append(Gate("H", 2)) + circuit1_inverse = Circuit(mygates_inverse) + self.assertTrue(circuit1.inverse().__str__(), circuit1_inverse.__str__()) + + ts_circuit = Circuit([Gate("T", 0), Gate("S", 1)]) + ts_circuit_inverse = Circuit([Gate("PHASE", 0, parameter=-pi/4), Gate("PHASE", 0, parameter=-pi/2)]) + self.assertTrue(ts_circuit.inverse().__str__(), ts_circuit_inverse.__str__()) + if __name__ == "__main__": unittest.main() diff --git a/tangelo/linq/tests/test_gates.py b/tangelo/linq/tests/test_gates.py index 2c826d312..415a67973 100644 --- a/tangelo/linq/tests/test_gates.py +++ b/tangelo/linq/tests/test_gates.py @@ -43,6 +43,25 @@ def test_some_gates(self): for gate in [H_gate, CNOT_gate, RX_gate, RZ_gate, CCCX_gate]: print(gate) + def test_some_gates_inverse(self): + """ Test that some basic gates can be inverted with a few different parameters, and fails when non-invertible + parameters are passed""" + + # Create a Hadamard gate acting on qubit 2 + H_gate = Gate("H", 2) + H_gate_inverse = Gate("H", 2) + self.assertEqual(H_gate.inverse().__str__(), H_gate_inverse.__str__()) + + # Create a parameterized rotation on qubit 1 with angle 2 radians + RX_gate = Gate("RX", 1, parameter=2.) + RX_gate_inverse = Gate("RX", 1, parameter=-2.) + self.assertEqual(RX_gate.inverse().__str__(), RX_gate_inverse.__str__()) + + # Create a parameterized rotation on qubit 1 , with an undefined angle, that will be variational + RZ_gate = Gate("RZ", 1, parameter="an expression", is_variational=True) + with self.assertRaises(AttributeError): + RZ_gate.inverse() + def test_incorrect_gate(self): """ Test to catch a gate with inputs that do not make sense """ diff --git a/tangelo/linq/tests/test_translator.py b/tangelo/linq/tests/test_translator.py index 70dbd7e92..fa884a0d1 100644 --- a/tangelo/linq/tests/test_translator.py +++ b/tangelo/linq/tests/test_translator.py @@ -423,6 +423,24 @@ def test_unsupported_gate(self): circ = Circuit([Gate("Potato", 0)]) self.assertRaises(ValueError, translator.translate_qiskit, circ) + def test_translate_ionq_inverse(self): + """ Test that inverse of T and S circuits for ionQ return Tdag and Sdag after translation """ + + # Generate [Gate("Tdag", 0), Gate("Sdag", 0)] equivalent + circ = Circuit([Gate("PHASE", 0, parameter=-np.pi/4), Gate("PHASE", 0, parameter=-np.pi/2)]) + # Hard-coded inverse + inverse_circ = Circuit([Gate("S", 0), Gate("T", 0)]) + + ionq_circ_inverse = translator.translate_json_ionq(circ.inverse()) + ionq_inverse_circ = translator.translate_json_ionq(inverse_circ) + ionq_circ = translator.translate_json_ionq(circ) + # Hard-coded circuit dictionary + ionq_circ_dict = {'qubits': 1, 'circuit': [{'gate': 'ti', 'target': 0}, {'gate': 'si', 'target': 0}]} + + # ionq uses a dictionary to store circuits, convert to str and compare + self.assertEqual(str(ionq_inverse_circ), str(ionq_circ_inverse)) + self.assertEqual(str(ionq_circ), str(ionq_circ_dict)) + if __name__ == "__main__": unittest.main() diff --git a/tangelo/linq/translator/translate_json_ionq.py b/tangelo/linq/translator/translate_json_ionq.py index 78c981682..1393b3ee5 100644 --- a/tangelo/linq/translator/translate_json_ionq.py +++ b/tangelo/linq/translator/translate_json_ionq.py @@ -21,6 +21,7 @@ - how the order and conventions for some of the inputs to the gate operations may also differ. """ +from math import pi, isclose def get_ionq_gates(): @@ -31,7 +32,7 @@ def get_ionq_gates(): """ GATE_JSON_IONQ = dict() - for name in {"H", "X", "Y", "Z", "S", "T", "RX", "RY", "RZ", "CNOT"}: + for name in {"H", "X", "Y", "Z", "S", "T", "RX", "RY", "RZ", "CNOT", "PHASE"}: GATE_JSON_IONQ[name] = name.lower() return GATE_JSON_IONQ @@ -57,6 +58,17 @@ def translate_json_ionq(source_circuit): json_gates.append({'gate': GATE_JSON_IONQ[gate.name], 'target': gate.target[0]}) elif gate.name in {"RX", "RY", "RZ"}: json_gates.append({'gate': GATE_JSON_IONQ[gate.name], 'target': gate.target[0], 'rotation': gate.parameter}) + elif gate.name in {"PHASE"}: + if isclose(gate.parameter, pi / 2, abs_tol=1.e-7): + json_gates.append({'gate': 's', 'target': gate.target[0]}) + elif isclose(gate.parameter, - pi / 2, abs_tol=1.e-7): + json_gates.append({'gate': 'si', 'target': gate.target[0]}) + elif isclose(gate.parameter, pi / 4, abs_tol=1.e-7): + json_gates.append({'gate': 't', 'target': gate.target[0]}) + elif isclose(gate.parameter, - pi / 4, abs_tol=1.e-7): + json_gates.append({'gate': 'ti', 'target': gate.target[0]}) + else: + raise ValueError(f"Only phases of pi/2, -pi/2, pi/4, -pi/4 are supported with JSON IonQ translation") elif gate.name in {"CNOT"}: json_gates.append({'gate': GATE_JSON_IONQ[gate.name], 'target': gate.target[0], 'control': gate.control[0]}) else: diff --git a/tangelo/linq/translator/translate_openqasm.py b/tangelo/linq/translator/translate_openqasm.py index ad58c1f86..a5cb97ad7 100644 --- a/tangelo/linq/translator/translate_openqasm.py +++ b/tangelo/linq/translator/translate_openqasm.py @@ -39,7 +39,7 @@ def get_openqasm_gates(): GATE_OPENQASM = dict() for name in {"H", "X", "Y", "Z", "S", "T"}: GATE_OPENQASM[name] = name.lower() - for name in {"RX", "RY", "RZ", "MEASURE"}: + for name in {"RX", "RY", "RZ", "MEASURE", "PHASE"}: GATE_OPENQASM[name] = name.lower() GATE_OPENQASM["CNOT"] = "cx" @@ -110,6 +110,8 @@ def parse_param(s): # TODO: Rethink the use of enums for gates to set the equality CX=CNOT and enable other refactoring elif gate_name in {"cx"}: gate = Gate("CNOT", qubit_indices[1], control=qubit_indices[0]) + elif gate_name in {"p"}: + gate = Gate("PHASE", qubit_indices[0], parameter=eval(str(parameters[0]))) else: raise ValueError(f"Gate '{gate_name}' not supported with openqasm translation") abs_circ.add_gate(gate) diff --git a/tangelo/linq/translator/translate_projectq.py b/tangelo/linq/translator/translate_projectq.py index ec8ebdd27..32739c334 100644 --- a/tangelo/linq/translator/translate_projectq.py +++ b/tangelo/linq/translator/translate_projectq.py @@ -38,6 +38,7 @@ def get_projectq_gates(): for name in {"RX", "RY", "RZ", "MEASURE"}: GATE_PROJECTQ[name] = name[0] + name[1:].lower() GATE_PROJECTQ["CNOT"] = "CX" + GATE_PROJECTQ["PHASE"] = "R" return GATE_PROJECTQ @@ -63,7 +64,7 @@ def translate_projectq(source_circuit): for gate in source_circuit._gates: if gate.name in {"H", "X", "Y", "Z", "S", "T", "MEASURE"}: projectq_circuit += f"{GATE_PROJECTQ[gate.name]} | Qureg[{gate.target[0]}]\n" - elif gate.name in {"RX", "RY", "RZ"}: + elif gate.name in {"RX", "RY", "RZ", "PHASE"}: projectq_circuit += f"{GATE_PROJECTQ[gate.name]}({gate.parameter}) | Qureg[{gate.target[0]}]\n" elif gate.name in {"CNOT"}: projectq_circuit += f"{GATE_PROJECTQ[gate.name]} | ( Qureg[{gate.control[0]}], Qureg[{gate.target[0]}] )\n" @@ -112,7 +113,7 @@ def _translate_projectq2abs(projectq_str): if gate_name in {"H", "X", "Y", "Z", "S", "T"}: gate = Gate(gate_mapping[gate_name], qubit_indices[0]) - elif gate_name in {"Rx", "Ry", "Rz"}: + elif gate_name in {"Rx", "Ry", "Rz", "PHASE"}: gate = Gate(gate_mapping[gate_name], qubit_indices[0], parameter=parameters[0]) # #TODO: Rethink the use of enums for gates to set the equality CX=CNOT and enable other refactoring elif gate_name in {"CX"}: