Skip to content

Commit

Permalink
Allow to apply unitary qubit operators to qubits (#263)
Browse files Browse the repository at this point in the history
  • Loading branch information
damiansteiger committed Aug 26, 2018
1 parent 3eaab56 commit 371ead6
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 4 deletions.
4 changes: 2 additions & 2 deletions projectq/backends/_sim/_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ def collapse_wavefunction(self, qureg, values):
Args:
qureg (Qureg|list[Qubit]): Qubits to collapse.
values (list[bool|int]|string[0|1]): Measurement outcome for each of the qubits
in `qureg`.
values (list[bool|int]|string[0|1]): Measurement outcome for each
of the qubits in `qureg`.
Raises:
RuntimeError: If an outcome has probability (approximately) 0 or
Expand Down
1 change: 1 addition & 0 deletions projectq/libs/revkit/_control_function_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def test_control_function_majority():

assert len(saving_backend.received_commands) == 7


def test_control_function_majority_from_python():
dormouse = pytest.importorskip('dormouse')

Expand Down
167 changes: 166 additions & 1 deletion projectq/ops/_qubit_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@
# limitations under the License.

"""QubitOperator stores a sum of Pauli operators acting on qubits."""
import cmath
import copy
import itertools

import numpy

from ._basics import BasicGate, NotInvertible, NotMergeable
from ._command import apply_command
from ._gates import Ph, X, Y, Z


EQ_TOLERANCE = 1e-12

Expand Down Expand Up @@ -45,7 +50,7 @@ class QubitOperatorError(Exception):
pass


class QubitOperator(object):
class QubitOperator(BasicGate):
"""
A sum of terms acting on qubits, e.g., 0.5 * 'X0 X5' + 0.3 * 'Z1 Z2'.
Expand All @@ -68,6 +73,25 @@ class QubitOperator(object):
hamiltonian = 0.5 * QubitOperator('X0 X5') + 0.3 * QubitOperator('Z0')
Our Simulator takes a hermitian QubitOperator to directly calculate the
expectation value (see Simulator.get_expectation_value) of this observable.
A hermitian QubitOperator can also be used as input for the
TimeEvolution gate.
If the QubitOperator is unitary, i.e., it contains only one term with a
coefficient, whose absolute value is 1, then one can apply it directly to
qubits:
.. code-block:: python
eng = projectq.MainEngine()
qureg = eng.allocate_qureg(6)
QubitOperator('X0 X5', 1.j) | qureg # Applies X to qubit 0 and 5
# with an additional global phase
# of 1.j
Attributes:
terms (dict): **key**: A term represented by a tuple containing all
non-trivial local Pauli operators ('X', 'Y', or 'Z').
Expand Down Expand Up @@ -128,6 +152,7 @@ def __init__(self, term=None, coefficient=1.):
Raises:
QubitOperatorError: Invalid operators provided to QubitOperator.
"""
BasicGate.__init__(self)
if not isinstance(coefficient, (int, float, complex)):
raise ValueError('Coefficient must be a numeric type.')
self.terms = {}
Expand Down Expand Up @@ -226,6 +251,143 @@ def isclose(self, other, rel_tol=1e-12, abs_tol=1e-12):
return False
return True

def __or__(self, qubits):
"""
Operator| overload which enables the following syntax:
.. code-block:: python
QubitOperator(...) | qureg
QubitOperator(...) | (qureg,)
QubitOperator(...) | qubit
QubitOperator(...) | (qubit,)
Unlike other gates, this gate is only allowed to be applied to one
quantum register or one qubit and only if the QubitOperator is
unitary, i.e., consists of one term with a coefficient whose absolute
values is 1.
Example:
.. code-block:: python
eng = projectq.MainEngine()
qureg = eng.allocate_qureg(6)
QubitOperator('X0 X5', 1.j) | qureg # Applies X to qubit 0 and 5
# with an additional global
# phase of 1.j
While in the above example the QubitOperator gate is applied to 6
qubits, it only acts non-trivially on the two qubits qureg[0] and
qureg[5]. Therefore, the operator| will create a new rescaled
QubitOperator, i.e, it sends the equivalent of the following new gate
to the MainEngine:
.. code-block:: python
QubitOperator('X0 X1', 1.j) | [qureg[0], qureg[5]]
which is only a two qubit gate.
Args:
qubits: one Qubit object, one list of Qubit objects, one Qureg
object, or a tuple of the former three cases.
Raises:
TypeError: If QubitOperator is not unitary or applied to more than
one quantum register.
ValueError: If quantum register does not have enough qubits
"""
# Check that input is only one qureg or one qubit
qubits = self.make_tuple_of_qureg(qubits)
if len(qubits) != 1:
raise TypeError("Only one qubit or qureg allowed.")
# Check that operator is unitary
if not len(self.terms) == 1:
raise TypeError("Too many terms. Only QubitOperators consisting "
"of a single term (single n-qubit Pauli operator) "
"with a coefficient of unit length can be applied "
"to qubits with this function.")
(term, coefficient), = self.terms.items()
phase = cmath.phase(coefficient)
if (abs(coefficient) < 1 - EQ_TOLERANCE or
abs(coefficient) > 1 + EQ_TOLERANCE):
raise TypeError("abs(coefficient) != 1. Only QubitOperators "
"consisting of a single term (single n-qubit "
"Pauli operator) with a coefficient of unit "
"length can be applied to qubits with this "
"function.")
# Test if we need to apply only Ph
if term == ():
Ph(phase) | qubits[0][0]
return
# Check that Qureg has enough qubits:
num_qubits = len(qubits[0])
non_trivial_qubits = set()
for index, action in term:
non_trivial_qubits.add(index)
if max(non_trivial_qubits) >= num_qubits:
raise ValueError("QubitOperator acts on more qubits than the gate "
"is applied to.")
# Apply X, Y, Z, if QubitOperator acts only on one qubit
if len(term) == 1:
if term[0][1] == "X":
X | qubits[0][term[0][0]]
elif term[0][1] == "Y":
Y | qubits[0][term[0][0]]
elif term[0][1] == "Z":
Z | qubits[0][term[0][0]]
Ph(phase) | qubits[0][term[0][0]]
return
# Create new QubitOperator gate with rescaled qubit indices in
# 0,..., len(non_trivial_qubits) - 1
new_index = dict()
non_trivial_qubits = sorted(list(non_trivial_qubits))
for i in range(len(non_trivial_qubits)):
new_index[non_trivial_qubits[i]] = i
new_qubitoperator = QubitOperator()
assert len(new_qubitoperator.terms) == 0
new_term = tuple([(new_index[index], action)
for index, action in term])
new_qubitoperator.terms[new_term] = coefficient
new_qubits = [qubits[0][i] for i in non_trivial_qubits]
# Apply new gate
cmd = new_qubitoperator.generate_command(new_qubits)
apply_command(cmd)

def get_inverse(self):
"""
Return the inverse gate of a QubitOperator if applied as a gate.
Raises:
NotInvertible: Not implemented for QubitOperators which have
multiple terms or a coefficient with absolute value
not equal to 1.
"""

if len(self.terms) == 1:
(term, coefficient), = self.terms.items()
if (not abs(coefficient) < 1 - EQ_TOLERANCE and not
abs(coefficient) > 1 + EQ_TOLERANCE):
return QubitOperator(term, coefficient**(-1))
raise NotInvertible("BasicGate: No get_inverse() implemented.")

def get_merged(self, other):
"""
Return this gate merged with another gate.
Standard implementation of get_merged:
Raises:
NotMergeable: merging is not possible
"""
if (isinstance(other, self.__class__) and
len(other.terms) == 1 and
len(self.terms) == 1):
return self * other
else:
raise NotMergeable()

def __imul__(self, multiplier):
"""
In-place multiply (*=) terms with scalar or QubitOperator.
Expand Down Expand Up @@ -463,3 +625,6 @@ def __str__(self):

def __repr__(self):
return str(self)

def __hash__(self):
return hash(str(self))
104 changes: 104 additions & 0 deletions projectq/ops/_qubit_operator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@
# limitations under the License.

"""Tests for _qubit_operator.py."""
import cmath
import copy
import math

import numpy
import pytest

from projectq import MainEngine
from projectq.cengines import DummyEngine
from ._basics import NotInvertible, NotMergeable
from ._gates import Ph, T, X, Y, Z

from projectq.ops import _qubit_operator as qo


Expand Down Expand Up @@ -184,6 +191,98 @@ def test_isclose_different_num_terms():
assert not a.isclose(b, rel_tol=1e-12, abs_tol=0.05)


def test_get_inverse():
qo0 = qo.QubitOperator("X1 Z2", cmath.exp(0.6j))
qo1 = qo.QubitOperator("", 1j)
assert qo0.get_inverse().isclose(
qo.QubitOperator("X1 Z2", cmath.exp(-0.6j)))
assert qo1.get_inverse().isclose(qo.QubitOperator("", -1j))
qo0 += qo1
with pytest.raises(NotInvertible):
qo0.get_inverse()


def test_get_merged():
qo0 = qo.QubitOperator("X1 Z2", 1j)
qo1 = qo.QubitOperator("Y3", 1j)
merged = qo0.get_merged(qo1)
assert qo0.isclose(qo.QubitOperator("X1 Z2", 1j))
assert qo1.isclose(qo.QubitOperator("Y3", 1j))
assert qo0.get_merged(qo1).isclose(qo.QubitOperator("X1 Z2 Y3", -1))
with pytest.raises(NotMergeable):
qo1.get_merged(T)
qo2 = qo0 + qo1
with pytest.raises(NotMergeable):
qo2.get_merged(qo0)
with pytest.raises(NotMergeable):
qo0.get_merged(qo2)


def test_or_one_qubit():
saving_backend = DummyEngine(save_commands=True)
eng = MainEngine(backend=saving_backend, engine_list=[])
qureg = eng.allocate_qureg(3)
eng.flush()
identity = qo.QubitOperator("", 1j)
x = qo.QubitOperator("X1", cmath.exp(0.5j))
y = qo.QubitOperator("Y2", cmath.exp(0.6j))
z = qo.QubitOperator("Z0", cmath.exp(4.5j))
identity | qureg
eng.flush()
x | qureg
eng.flush()
y | qureg
eng.flush()
z | qureg
eng.flush()
assert saving_backend.received_commands[4].gate == Ph(math.pi/2.)

assert saving_backend.received_commands[6].gate == X
assert saving_backend.received_commands[6].qubits == ([qureg[1]],)
assert saving_backend.received_commands[7].gate == Ph(0.5)
assert saving_backend.received_commands[7].qubits == ([qureg[1]],)

assert saving_backend.received_commands[9].gate == Y
assert saving_backend.received_commands[9].qubits == ([qureg[2]],)
assert saving_backend.received_commands[10].gate == Ph(0.6)
assert saving_backend.received_commands[10].qubits == ([qureg[2]],)

assert saving_backend.received_commands[12].gate == Z
assert saving_backend.received_commands[12].qubits == ([qureg[0]],)
assert saving_backend.received_commands[13].gate == Ph(4.5)
assert saving_backend.received_commands[13].qubits == ([qureg[0]],)


def test_wrong_input():
eng = MainEngine()
qureg = eng.allocate_qureg(3)
op0 = qo.QubitOperator("X1", 0.99)
with pytest.raises(TypeError):
op0 | qureg
op1 = qo.QubitOperator("X2", 1)
with pytest.raises(ValueError):
op1 | qureg[1]
with pytest.raises(TypeError):
op0 | (qureg[1], qureg[2])
op2 = op0 + op1
with pytest.raises(TypeError):
op2 | qureg


def test_rescaling_of_indices():
saving_backend = DummyEngine(save_commands=True)
eng = MainEngine(backend=saving_backend, engine_list=[])
qureg = eng.allocate_qureg(4)
eng.flush()
op = qo.QubitOperator("X0 Y1 Z3", 1j)
op | qureg
eng.flush()
assert saving_backend.received_commands[5].gate.isclose(
qo.QubitOperator("X0 Y1 Z2", 1j))
# test that gate creates a new QubitOperator
assert op.isclose(qo.QubitOperator("X0 Y1 Z3", 1j))


def test_imul_inplace():
qubit_op = qo.QubitOperator("X1")
prev_id = id(qubit_op)
Expand Down Expand Up @@ -444,6 +543,11 @@ def test_str():
assert str(op2) == "2 I"


def test_hash():
op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5)
assert hash(op) == hash("0.5 X1 Y3 Z8")


def test_str_empty():
op = qo.QubitOperator()
assert str(op) == '0'
Expand Down
2 changes: 2 additions & 0 deletions projectq/setups/decompositions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
entangle,
globalphase,
ph2r,
qubitop2onequbit,
qft2crandhadamard,
r2rzandph,
rx2rz,
Expand All @@ -43,6 +44,7 @@
entangle,
globalphase,
ph2r,
qubitop2onequbit,
qft2crandhadamard,
r2rzandph,
rx2rz,
Expand Down

0 comments on commit 371ead6

Please sign in to comment.