Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to apply unitary qubit operators to qubits #263

Merged
merged 10 commits into from
Aug 26, 2018
Merged
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
165 changes: 164 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,141 @@ 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is missing indentation I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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("Only unitary QubitOperators can be applied to "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only QubitOperators consisting of a single Pauli string with a coefficient of unit length can be applied.
Otherwise, people might get confused if their operator actually is unitary but consists of multiple terms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be unitary with multiple terms?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1/sqrt(2) * X + 1/sqrt(2) * Z = H

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for the example

"qubits.")
(term, coefficient), = self.terms.items()
phase = cmath.phase(coefficient)
if (abs(coefficient) < 1 - EQ_TOLERANCE or
abs(coefficient) > 1 + EQ_TOLERANCE):
raise TypeError("Only unitary QubitOperators can be applied to "
"qubits.")
# 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 if QubitOperator is unitary.

Raises:
NotInvertible: inverse is not implemented if not unitary
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there are multiple terms or the coefficient is not 1 in absolute value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"""
if len(self.terms) == 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and abs(coefficient)...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't raise an exception now, though...

(term, coefficient), = self.terms.items()
return QubitOperator(term, coefficient**(-1))
else:
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 implemented
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs are a bit strange ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, changed.

"""
if (isinstance(other, self.__class__) and
len(other.terms) == 1 and
len(self.terms) == 1):
return self * other

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the comments below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm nope

# (term, coefficient), = self.terms.items()
# (other_term, other_coefficient), = other.terms.items()
# if term == other_term:
# return self.__class__(term, coefficient * other_coefficient)
# else:
# raise NotMergeable()
else:
raise NotMergeable()

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

def __repr__(self):
return str(self)

def __hash__(self):
return hash(str(self))
103 changes: 103 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,97 @@ 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", 2)
qo1 = qo.QubitOperator("", 2j)
assert qo0.get_inverse().isclose(qo.QubitOperator("X1 Z2", 0.5))
assert qo1.get_inverse().isclose(qo.QubitOperator("", -0.5j))
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 +542,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
Loading