diff --git a/tangelo/linq/tests/data/H2_JW_occfirst.data b/tangelo/linq/tests/data/H2_JW_occfirst.data new file mode 100644 index 000000000..0902dc2f8 --- /dev/null +++ b/tangelo/linq/tests/data/H2_JW_occfirst.data @@ -0,0 +1,16 @@ +QubitOperator: +(-0.10973055606700678+0j) [] + +(-0.0454428841443262+0j) [X0 X1 Y2 Y3] + +(0.0454428841443262+0j) [X0 Y1 Y2 X3] + +(0.0454428841443262+0j) [Y0 X1 X2 Y3] + +(-0.0454428841443262+0j) [Y0 Y1 X2 X3] + +(0.16988452027940382+0j) [Z0] + +(0.16821198673715723+0j) [Z0 Z1] + +(0.12005143072546026+0j) [Z0 Z2] + +(0.16549431486978647+0j) [Z0 Z3] + +(0.16988452027940382+0j) [Z1] + +(0.16549431486978647+0j) [Z1 Z2] + +(0.12005143072546026+0j) [Z1 Z3] + +(-0.21886306781219628+0j) [Z2] + +(0.17395378776494128+0j) [Z2 Z3] + +(-0.21886306781219633+0j) [Z3] \ No newline at end of file diff --git a/tangelo/linq/tests/test_simulator.py b/tangelo/linq/tests/test_simulator.py index 6b8297627..efa50f170 100644 --- a/tangelo/linq/tests/test_simulator.py +++ b/tangelo/linq/tests/test_simulator.py @@ -33,26 +33,31 @@ # Simple circuit for superposition, also tells us qubit ordering as well immediately from the statevector # probabilities : |00> = 0.5 |01> = 0.5 circuit1 = Circuit([Gate("H", 0)], n_qubits=2) + # 2-qubit circuit checking all the basic gates that are not defined up to a convention (e.g unambiguous) mygates = [Gate("H", 0), Gate("S", 0), Gate("X", 0), Gate("T", 1), Gate("Y", 1), Gate("Z", 1)] mygates += [Gate("CNOT", 1, control=0)] circuit2 = Circuit(mygates) + # Circuit for the parametrized rotation gates Rx and Ry. Some convention about the sign of theta or a phase may appear circuit3 = Circuit([Gate("RX", 0, parameter=2.), Gate("RY", 1, parameter=-1.)]) + # Circuit for the parametrized rotation gate Rz. Some convention about the sign of theta or a phase may appear circuit4 = Circuit([Gate("RZ", 0, parameter=2.)], n_qubits=2) + # Circuit that tests all gates that are supported on all simulators init_gates = [Gate('H', 0), Gate('X', 1), Gate('H', 2)] one_qubit_gate_names = ["H", "X", "Y", "Z", "S", "T", "RX", "RY", "RZ", "PHASE"] one_qubit_gates = [Gate(name, target=0) if name not in PARAMETERIZED_GATES else Gate(name, target=0, parameter=0.5) - for name in one_qubit_gate_names] + for name in one_qubit_gate_names] one_qubit_gates += [Gate(name, target=1) if name not in PARAMETERIZED_GATES else Gate(name, target=1, parameter=0.2) - for name in one_qubit_gate_names] + for name in one_qubit_gate_names] two_qubit_gate_names = ["CNOT", "CH", "CX", "CY", "CZ", "CRX", "CRY", "CRZ", "CPHASE"] two_qubit_gates = [Gate(name, target=1, control=0) if name not in PARAMETERIZED_GATES - else Gate(name, target=1, control=0, parameter=0.5) for name in two_qubit_gate_names] + else Gate(name, target=1, control=0, parameter=0.5) for name in two_qubit_gate_names] swap_gates = [Gate('SWAP', target=[1, 0]), Gate('CSWAP', target=[1, 2], control=0)] circuit5 = Circuit(init_gates + one_qubit_gates + two_qubit_gates + swap_gates) + # Circuit preparing a mixed-state (e.g containing a MEASURE instruction in the middle of the circuit) circuit_mixed = Circuit([Gate("RX", 0, parameter=2.), Gate("RY", 1, parameter=-1.), Gate("MEASURE", 0), Gate("X", 0)]) diff --git a/tangelo/linq/tests/test_translator_qubitop.py b/tangelo/linq/tests/test_translator_qubitop.py new file mode 100644 index 000000000..61ab9f5c6 --- /dev/null +++ b/tangelo/linq/tests/test_translator_qubitop.py @@ -0,0 +1,112 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import unittest +import time +from itertools import product + +import numpy as np +from openfermion.utils import load_operator +from openfermion.linalg import eigenspectrum + +from tangelo.helpers.utils import installed_backends +from tangelo.linq import translate_operator +from tangelo.toolboxes.operators import QubitOperator + +# For openfermion.load_operator function. +pwd_this_test = os.path.dirname(os.path.abspath(__file__)) + +tangelo_op = QubitOperator("X0 Y1 Z2", 1.) + + +class TranslateOperatorTest(unittest.TestCase): + + def test_unsupported_source(self): + """Test error with an unsuported source.""" + + with self.assertRaises(NotImplementedError): + translate_operator(tangelo_op, source="sourcenotsupported", target="tangelo") + + def test_unsupported_target(self): + """Test error with an unsuported target.""" + + with self.assertRaises(NotImplementedError): + translate_operator(tangelo_op, source="tangelo", target="targetnotsupported") + + @unittest.skipIf("qiskit" not in installed_backends, "Test Skipped: Qiskit not available \n") + def test_qiskit_to_tangelo(self): + """Test translation from a qiskit to a tangelo operator.""" + + from qiskit.opflow.primitive_ops import PauliSumOp + qiskit_op = PauliSumOp.from_list([("ZYX", 1.)]) + + test_op = translate_operator(qiskit_op, source="qiskit", target="tangelo") + self.assertEqual(test_op, tangelo_op) + + @unittest.skipIf("qiskit" not in installed_backends, "Test Skipped: Qiskit not available \n") + def test_tangelo_to_qiskit(self): + """Test translation from a tangelo to a qiskit operator.""" + + from qiskit.opflow.primitive_ops import PauliSumOp + qiskit_op = PauliSumOp.from_list([("ZYX", 1.)]) + + test_op = translate_operator(tangelo_op, source="tangelo", target="qiskit") + self.assertEqual(qiskit_op, test_op) + + @unittest.skipIf("qiskit" not in installed_backends, "Test Skipped: Qiskit not available \n") + def test_tangelo_to_qiskit_H2_eigenvalues(self): + """Test eigenvalues resulting from a tangelo to qiskit translation.""" + + from qiskit.algorithms import NumPyEigensolver + + qu_op = load_operator("H2_JW_occfirst.data", data_directory=pwd_this_test+"/data", plain_text=True) + test_op = translate_operator(qu_op, source="tangelo", target="qiskit") + + eigenvalues_tangelo = eigenspectrum(qu_op) + + qiskit_solver = NumPyEigensolver(2**4) + eigenvalues_qiskit = qiskit_solver.compute_eigenvalues(test_op) + + np.testing.assert_array_almost_equal(eigenvalues_tangelo, eigenvalues_qiskit.eigenvalues) + + @unittest.skipIf("qiskit" not in installed_backends, "Test Skipped: Qiskit not available \n") + def test_tangelo_to_qiskit_big(self): + """Test translation from a tangelo to a qiskit operator, for a large input""" + + n_qubits = 10 + n_terms = 3**n_qubits + + # Build large operator made of all possible "full" Pauli words (no 'I') of length n_qubits + terms = {tuple(zip(range(n_qubits), pw)): 1.0 for pw in product(['X', 'Y', 'Z'], repeat=n_qubits)} + q_op = QubitOperator() + q_op.terms = terms + + s, t = "tangelo", "qiskit" + tstart1 = time.time() + tmp_op = translate_operator(q_op, source=s, target=t) + tstop1 = time.time() + print(f"Qubit operator conversion {s} to {t}: {tstop1 - tstart1:.1f} (terms = {n_terms})") + + t, s = s, t + tstart2 = time.time() + q_op2 = translate_operator(tmp_op, source=s, target=t) + tstop2 = time.time() + print(f"Qubit operator conversion {s} to {t}: {tstop2 - tstart2:.1f} (terms = {n_terms})") + + assert(q_op == q_op2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tangelo/linq/translator/__init__.py b/tangelo/linq/translator/__init__.py index 02da3c729..dde9b355a 100644 --- a/tangelo/linq/translator/__init__.py +++ b/tangelo/linq/translator/__init__.py @@ -22,6 +22,7 @@ from .translate_qdk import translate_qsharp, get_qdk_gates from .translate_projectq import translate_projectq, _translate_projectq2abs, get_projectq_gates from .translate_openqasm import translate_openqasm, _translate_openqasm2abs, get_openqasm_gates +from .translate_qubitop import translate_operator # List all supported gates for all backends found diff --git a/tangelo/linq/translator/translate_qiskit.py b/tangelo/linq/translator/translate_qiskit.py index eb972bd53..29eedb9f9 100644 --- a/tangelo/linq/translator/translate_qiskit.py +++ b/tangelo/linq/translator/translate_qiskit.py @@ -12,16 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Functions helping with quantum circuit format conversion between abstract -format and qiskit format. +"""Functions helping with quantum circuit and operator format conversion between +Tangelo format and qiskit format. In order to produce an equivalent circuit for the target backend, it is necessary to account for: - how the gate names differ between the source backend to the target backend. - how the order and conventions for some of the inputs to the gate operations may also differ. + +The module also enables bidirectional conversion between qiskit and Tangelo +qubit operators (linear combination of Pauli operators) """ +from tangelo.toolboxes.operators import QubitOperator +from tangelo.linq.helpers import pauli_of_to_string, pauli_string_to_of + def get_qiskit_gates(): """Map gate name of the abstract format to the equivalent add_gate method of @@ -98,3 +104,58 @@ def translate_qiskit(source_circuit): else: raise ValueError(f"Gate '{gate.name}' not supported on backend qiskit") return target_circuit + + +def translate_op_to_qiskit(qubit_operator, n_qubits): + """Helper function to translate a Tangelo QubitOperator to a qiskit + PauliSumOp. Qiskit must be installed for the function to work. + + Args: + qubit_operator (tangelo.toolboxes.operators.QubitOperator): Self-explanatory. + n_qubits (int): Number of qubits relevant to the operator. + + Returns: + (qiskit.opflow.primitive_ops.PauliSumOp): Qiskit qubit operator. + """ + + # Import qiskit qubit operator. + from qiskit.opflow.primitive_ops import PauliSumOp + + # Convert each term sequencially. + term_list = list() + for term_tuple, coeff in qubit_operator.terms.items(): + term_string = pauli_of_to_string(term_tuple, n_qubits) + + # Reverse the string because of qiskit convention. + term_list += [(term_string[::-1], coeff)] + + return PauliSumOp.from_list(term_list) + + +def translate_op_from_qiskit(qubit_operator): + """Helper function to translate a qiskit PauliSumOp to a Tangelo + QubitOperator. + + Args: + qubit_operator (qiskit.opflow.primitive_ops.PauliSumOp): Self-explanatory. + + Returns: + (tangelo.toolboxes.operators.QubitOperator): Tangelo qubit operator. + """ + + # Create a dictionary to append all terms at once. + terms_dict = dict() + for pauli_word in qubit_operator: + # Inversion of the string because of qiskit ordering. + term_string = pauli_word.to_pauli_op().primitive.to_label()[::-1] + term_tuple = pauli_string_to_of(term_string) + terms_dict[tuple(term_tuple)] = pauli_word.coeff + + # Create and copy the information into a new QubitOperator. + tangelo_op = QubitOperator() + tangelo_op.terms = terms_dict + + # Clean the QubitOperator. + tangelo_op.compress() + + return tangelo_op diff --git a/tangelo/linq/translator/translate_qubitop.py b/tangelo/linq/translator/translate_qubitop.py new file mode 100644 index 000000000..667fd7aea --- /dev/null +++ b/tangelo/linq/translator/translate_qubitop.py @@ -0,0 +1,59 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module to convert qubit operators to different formats.""" + +from tangelo.toolboxes.operators import count_qubits +from tangelo.linq.translator.translate_qiskit import translate_op_from_qiskit, translate_op_to_qiskit + + +FROM_TANGELO = { + "qiskit": translate_op_to_qiskit +} + +TO_TANGELO = { + "qiskit": translate_op_from_qiskit +} + + +def translate_operator(qubit_operator, source, target, n_qubits=None): + """Function to convert a qubit operator defined within the "source" format + to a "target" format. + + Args: + qubit_operator (source format): Self-explanatory. + source (string): Identifier for the source format. + target (string): Identifier for the target format. + n_qubits (int): Number of qubits relevant to the operator. + + Returns: + (operator in target format): Translated qubit operator. + """ + + source = source.lower() + target = target.lower() + + if source == target: + return qubit_operator + if source != "tangelo": + if source not in TO_TANGELO: + raise NotImplementedError(f"Qubit operator conversion from {source} to {target} is not supported.") + qubit_operator = TO_TANGELO[source](qubit_operator) + if target != "tangelo": + if target not in FROM_TANGELO: + raise NotImplementedError(f"Qubit operator conversion from {source} to {target} is not supported.") + n_qubits = count_qubits(qubit_operator) if n_qubits is None else n_qubits + qubit_operator = FROM_TANGELO[target](qubit_operator, n_qubits) + + return qubit_operator