Skip to content

Commit

Permalink
Added TimeEvolution gate
Browse files Browse the repository at this point in the history
  • Loading branch information
Damian S. Steiger committed Apr 22, 2017
1 parent 7d542ab commit 97eb8da
Show file tree
Hide file tree
Showing 6 changed files with 516 additions and 27 deletions.
1 change: 1 addition & 0 deletions projectq/ops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@
from ._qftgate import QFT
from ._qubit_operator import QubitOperator
from ._shortcuts import *
from ._time_evolution import TimeEvolution
18 changes: 11 additions & 7 deletions projectq/ops/_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
.. code-block:: python
Gate | (qreg1, qreg2, qreg2)
Gate | (qreg, qubit)
Gate | qreg
Gate | (qureg1, qureg2, qureg2)
Gate | (qureg, qubit)
Gate | qureg
Gate | qubit
Gate | (qubit,)
Expand All @@ -36,6 +36,9 @@
from ._command import Command, apply_command


EQ_TOLERANCE = 1e-12


class NotMergeable(Exception):
"""
Exception thrown when trying to merge two gates which are not mergeable (or
Expand Down Expand Up @@ -178,7 +181,8 @@ def generate_command(self, qubits):
return Command(qubits[0][0].engine, self, qubits)

def __or__(self, qubits):
""" Operator| overload which enables the syntax Gate | qubits.
"""
Operator| overload which enables the syntax Gate | qubits.
Example:
1) Gate | qubit
Expand All @@ -188,8 +192,8 @@ def __or__(self, qubits):
5) Gate | (qureg, qubit)
Args:
qubits: a Qubit object, a list of Qubit objects, a Qureg object, or
a tuple of Qubit or Qureg objects (can be mixed).
qubits: a Qubit object, a list of Qubit objects, a Qureg object,
or a tuple of Qubit or Qureg objects (can be mixed).
"""
cmd = self.generate_command(qubits)
apply_command(cmd)
Expand Down Expand Up @@ -296,7 +300,7 @@ def get_merged(self, other):

def __eq__(self, other):
""" Return True if same class and same rotation angle. """
tolerance = 1.e-12
tolerance = EQ_TOLERANCE
if isinstance(other, self.__class__):
difference = abs(self._angle - other._angle) % (4 * math.pi)
# Return True if angles are close to each other modulo 4 * pi
Expand Down
37 changes: 19 additions & 18 deletions projectq/ops/_qubit_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,18 @@ class QubitOperator(object):
hamiltonian = 0.5 * QubitOperator('X0 X5') + 0.3 * QubitOperator('Z0')
Attributes:
terms (dict): key: A term represented by a tuple of tuples. Each tuple
represents a local operator and is a Pauli operator
('I', 'X', 'Y', or 'Z') which acts on one qubit
stored as a tuple. The first element is an integer
indicating the qubit on which a non-trivial local
operator acts and the second element is a string,
either 'X', 'Y', or 'Z', indicating which non-trivial
Pauli operator acts on that qubit.
E.g. 'X1 Y5' is ((1, 'X'), (5, 'Y'))
The tuples are sorted according to the qubit number
they act on, starting from 0.
value: Coefficient of this term as a (complex) float
terms (dict): **key**: A term represented by a tuple of tuples. Each
tuple represents a local operator and is a Pauli
operator ('I', 'X', 'Y', or 'Z') which acts on one qubit
stored as a tuple. The first element is an integer
indicating the qubit on which a non-trivial local
operator acts and the second element is a string,
either 'X', 'Y', or 'Z', indicating which non-trivial
Pauli operator acts on that qubit.
E.g. 'X1 Y5' is ((1, 'X'), (5, 'Y'))
The tuples are sorted according to the qubit number
they act on, starting from 0.
**value**: Coefficient of this term as a (complex) float
"""

def __init__(self, term=(), coefficient=1.):
Expand All @@ -92,10 +92,11 @@ def __init__(self, term=(), coefficient=1.):
Example:
.. code-block:: python
ham = (QubitOperator('X0 Y3', 0.5) + 0.6 * QubitOperator('X0 Y3'))
# Equivalently
ham2 = QubitOperator('X0 Y3', 0.5)
ham2 += 0.6 * QubitOperator('X0 Y3')
ham = ((QubitOperator('X0 Y3', 0.5)
+ 0.6 * QubitOperator('X0 Y3')))
# Equivalently
ham2 = QubitOperator('X0 Y3', 0.5)
ham2 += 0.6 * QubitOperator('X0 Y3')
Note:
Adding terms to QubitOperator is faster using += (as this is done
Expand Down Expand Up @@ -416,8 +417,8 @@ def __str__(self):
else:
assert operator[1] == 'Z'
tmp_string += ' Z{}'.format(operator[0])
string_rep += '{}\n'.format(tmp_string)
return string_rep
string_rep += '{} +\n'.format(tmp_string)
return string_rep[:-3]

def __repr__(self):
return str(self)
13 changes: 11 additions & 2 deletions projectq/ops/_qubit_operator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,18 @@ def test_neg():

def test_str():
op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5)
assert str(op) == "0.5 X1 Y3 Z8\n"
assert str(op) == "0.5 X1 Y3 Z8"
op2 = qo.QubitOperator((), 2)
assert str(op2) == "2 I\n"
assert str(op2) == "2 I"


def test_str_multiple_terms():
op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5)
op += qo.QubitOperator(((1, 'Y'), (3, 'Y'), (8, 'Z')), 0.6)
assert (str(op) == "0.5 X1 Y3 Z8 +\n0.6 Y1 Y3 Z8" or
str(op) == "0.6 Y1 Y3 Z8 +\n0.5 X1 Y3 Z8")
op2 = qo.QubitOperator((), 2)
assert str(op2) == "2 I"


def test_rep():
Expand Down
216 changes: 216 additions & 0 deletions projectq/ops/_time_evolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# 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 copy

from ._basics import BasicGate, NotMergeable
from ._qubit_operator import QubitOperator
from ._command import apply_command


class NotHermitianOperatorError(Exception):
pass


class TimeEvolution(BasicGate):
"""
Gate for time evolution under a Hamiltonian (QubitOperator object).
This gate is the unitary time evolution propagator:
exp(-i * H * t),
where H is the Hamiltonian of the system and t is the time.
Example:
.. code-block:: python
wavefuction = eng.allocate_qureg(5)
hamiltonian = 0.5 * QubitOperator("X0 Z1 Y5")
# Apply exp(-i * H * t) to the wavefunction:
TimeEvolution(time=2.0, hamiltonian=hamiltonian) | wavefunction
Attributes:
time(float, int): time t
hamiltonian(QubitOperator): hamiltonaian H
"""
def __init__(self, time, hamiltonian):
"""
Initialize time evolution gate.
Note:
The hamiltonian must be hermitian and therefore only terms with
real coefficients are allowed.
Coefficients are internally converted to float.
Args:
time (float, or int): time to evolve under (can be negative).
hamiltonian (QubitOperator): hamiltonian to evolve under.
Raises:
TypeError: If time is not a numeric type and hamiltonian is not a
QubitOperator.
NotHermitianOperatorError: If the input hamiltonian is not
hermitian (only real coefficients).
"""
BasicGate.__init__(self)
if not isinstance(time, (float, int)):
raise TypeError("time needs to be a (real) numeric type.")
if not isinstance(hamiltonian, QubitOperator):
raise TypeError("hamiltonian needs to be QubitOperator object.")
self.time = time
self.hamiltonian = copy.deepcopy(hamiltonian)
for term in hamiltonian.terms:
if self.hamiltonian.terms[term].imag == 0:
self.hamiltonian.terms[term] = float(
self.hamiltonian.terms[term])
else:
raise NotHermitianOperatorError("hamiltonian must be "
"hermitian and hence only have real coefficients.")

def get_inverse(self):
"""
Return the inverse gate.
"""
return TimeEvolution(self.time * -1.0, self.hamiltonian)

def get_merged(self, other):
"""
Return self merged with another TimeEvolution gate if possible.
Two TimeEvolution gates are merged if:
1) both have the same terms
2) the proportionality factor for each of the terms
must have relative error <= 1e-9 compared to the
proportionality factors of the other terms.
Note:
While one could merge gates for which both hamiltonians commute,
we are not doing this as in general the resulting gate would have
to be decomposed again.
Note:
We are not comparing if terms are proportional to each other with
an absolute tolerance. It is up to the user to remove terms close
to zero because we cannot choose a suitable absolute error which
works for everyone. Use, e.g., a decomposition rule for that.
Args:
other: TimeEvolution gate
Raises:
NotMergeable: If the other gate is not a TimeEvolution gate or
hamiltonians are not suitable for merging.
Returns:
New TimeEvolution gate equivalent to the two merged gates.
"""
rel_tol = 1e-9
if (isinstance(other, TimeEvolution) and
set(self.hamiltonian.terms) == set(other.hamiltonian.terms)):
factor = None
for term in self.hamiltonian.terms:
if factor == None:
factor = self.hamiltonian.terms[term] / float(
other.hamiltonian.terms[term])
else:
tmp = self.hamiltonian.terms[term] / float(
other.hamiltonian.terms[term])
if not abs(factor - tmp) <= (
rel_tol * max(abs(factor), abs(tmp))):
raise NotMergeable("Cannot merge these two gates.")
# Terms are proportional to each other
new_time = self.time + other.time / factor
return TimeEvolution(time=new_time, hamiltonian=self.hamiltonian)
else:
raise NotMergeable("Cannot merge these two gates.")

def __or__(self, qubits):
"""
Operator| overload which enables the following syntax:
.. code-block:: python
TimeEvolution(...) | qureg
TimeEvolution(...) | (qureg,)
TimeEvolution(...) | qubit
TimeEvolution(...) | (qubit,)
Unlike other gates, this gate is only allowed to be applied to one
quantum register or one qubit.
Example:
.. code-block:: python
wavefunction = eng.allocate_qureg(5)
hamiltonian = QubitOperator("X1 Y3", 0.5)
TimeEvolution(time=2.0, hamiltonian=hamiltonian) | wavefunction
While in the above example the TimeEvolution gate is applied to 5
qubits, the hamiltonian of this TimeEvolution gate acts only
non-trivially on the two qubits wavefuction[1] and wavefunction[3].
Therefore, the operator| will rescale the indices in the hamiltonian
and sends the equivalent of the following new gate to the MainEngine:
.. code-block:: python
h = QubitOperator("X0 Y1", 0.5)
TimeEvolution(2.0, h) | [wavefunction[1], wavefunction[3]]
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.
"""
# 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.")
num_qubits = len(qubits[0])
non_trivial_qubits = set()
for term in self.hamiltonian.terms:
for index, action in term:
non_trivial_qubits.add(index)
if max(non_trivial_qubits) >= num_qubits:
raise ValueError("hamiltonian acts on more qubits than the gate "
"is applied to.")
# create new TimeEvolution gate with rescaled qubit indices in
# self.hamiltonian which are ordered from
# 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_hamiltonian = QubitOperator()
del new_hamiltonian.terms[()] # remove default identity
assert len(new_hamiltonian.terms) == 0
for term in self.hamiltonian.terms:
new_term = tuple([(new_index[index], action)
for index, action in term])
new_hamiltonian.terms[new_term] = self.hamiltonian.terms[term]
new_gate = TimeEvolution(time=self.time, hamiltonian=new_hamiltonian)
new_qubits = [qubits[0][i] for i in non_trivial_qubits]
# Apply new gate
cmd = new_gate.generate_command(new_qubits)
apply_command(cmd)

def __eq__(self, other):
""" Not implemented as this object is a floating point type."""
return NotImplemented

def __ne__(self, other):
""" Not implemented as this object is a floating point type."""
return NotImplemented

def __str__(self):
return "exp({0} * ({1}))".format(-1j * self.time, self.hamiltonian)

0 comments on commit 97eb8da

Please sign in to comment.