Skip to content

Commit 371ead6

Browse files
Allow to apply unitary qubit operators to qubits (#263)
1 parent 3eaab56 commit 371ead6

File tree

8 files changed

+424
-4
lines changed

8 files changed

+424
-4
lines changed

projectq/backends/_sim/_simulator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,8 @@ def collapse_wavefunction(self, qureg, values):
303303
304304
Args:
305305
qureg (Qureg|list[Qubit]): Qubits to collapse.
306-
values (list[bool|int]|string[0|1]): Measurement outcome for each of the qubits
307-
in `qureg`.
306+
values (list[bool|int]|string[0|1]): Measurement outcome for each
307+
of the qubits in `qureg`.
308308
309309
Raises:
310310
RuntimeError: If an outcome has probability (approximately) 0 or

projectq/libs/revkit/_control_function_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def test_control_function_majority():
4141

4242
assert len(saving_backend.received_commands) == 7
4343

44+
4445
def test_control_function_majority_from_python():
4546
dormouse = pytest.importorskip('dormouse')
4647

projectq/ops/_qubit_operator.py

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
# limitations under the License.
1414

1515
"""QubitOperator stores a sum of Pauli operators acting on qubits."""
16+
import cmath
1617
import copy
1718
import itertools
1819

1920
import numpy
2021

22+
from ._basics import BasicGate, NotInvertible, NotMergeable
23+
from ._command import apply_command
24+
from ._gates import Ph, X, Y, Z
25+
2126

2227
EQ_TOLERANCE = 1e-12
2328

@@ -45,7 +50,7 @@ class QubitOperatorError(Exception):
4550
pass
4651

4752

48-
class QubitOperator(object):
53+
class QubitOperator(BasicGate):
4954
"""
5055
A sum of terms acting on qubits, e.g., 0.5 * 'X0 X5' + 0.3 * 'Z1 Z2'.
5156
@@ -68,6 +73,25 @@ class QubitOperator(object):
6873
6974
hamiltonian = 0.5 * QubitOperator('X0 X5') + 0.3 * QubitOperator('Z0')
7075
76+
Our Simulator takes a hermitian QubitOperator to directly calculate the
77+
expectation value (see Simulator.get_expectation_value) of this observable.
78+
79+
A hermitian QubitOperator can also be used as input for the
80+
TimeEvolution gate.
81+
82+
If the QubitOperator is unitary, i.e., it contains only one term with a
83+
coefficient, whose absolute value is 1, then one can apply it directly to
84+
qubits:
85+
86+
.. code-block:: python
87+
88+
eng = projectq.MainEngine()
89+
qureg = eng.allocate_qureg(6)
90+
QubitOperator('X0 X5', 1.j) | qureg # Applies X to qubit 0 and 5
91+
# with an additional global phase
92+
# of 1.j
93+
94+
7195
Attributes:
7296
terms (dict): **key**: A term represented by a tuple containing all
7397
non-trivial local Pauli operators ('X', 'Y', or 'Z').
@@ -128,6 +152,7 @@ def __init__(self, term=None, coefficient=1.):
128152
Raises:
129153
QubitOperatorError: Invalid operators provided to QubitOperator.
130154
"""
155+
BasicGate.__init__(self)
131156
if not isinstance(coefficient, (int, float, complex)):
132157
raise ValueError('Coefficient must be a numeric type.')
133158
self.terms = {}
@@ -226,6 +251,143 @@ def isclose(self, other, rel_tol=1e-12, abs_tol=1e-12):
226251
return False
227252
return True
228253

254+
def __or__(self, qubits):
255+
"""
256+
Operator| overload which enables the following syntax:
257+
258+
.. code-block:: python
259+
260+
QubitOperator(...) | qureg
261+
QubitOperator(...) | (qureg,)
262+
QubitOperator(...) | qubit
263+
QubitOperator(...) | (qubit,)
264+
265+
Unlike other gates, this gate is only allowed to be applied to one
266+
quantum register or one qubit and only if the QubitOperator is
267+
unitary, i.e., consists of one term with a coefficient whose absolute
268+
values is 1.
269+
270+
Example:
271+
272+
.. code-block:: python
273+
274+
eng = projectq.MainEngine()
275+
qureg = eng.allocate_qureg(6)
276+
QubitOperator('X0 X5', 1.j) | qureg # Applies X to qubit 0 and 5
277+
# with an additional global
278+
# phase of 1.j
279+
280+
While in the above example the QubitOperator gate is applied to 6
281+
qubits, it only acts non-trivially on the two qubits qureg[0] and
282+
qureg[5]. Therefore, the operator| will create a new rescaled
283+
QubitOperator, i.e, it sends the equivalent of the following new gate
284+
to the MainEngine:
285+
286+
.. code-block:: python
287+
288+
QubitOperator('X0 X1', 1.j) | [qureg[0], qureg[5]]
289+
290+
which is only a two qubit gate.
291+
292+
Args:
293+
qubits: one Qubit object, one list of Qubit objects, one Qureg
294+
object, or a tuple of the former three cases.
295+
296+
Raises:
297+
TypeError: If QubitOperator is not unitary or applied to more than
298+
one quantum register.
299+
ValueError: If quantum register does not have enough qubits
300+
"""
301+
# Check that input is only one qureg or one qubit
302+
qubits = self.make_tuple_of_qureg(qubits)
303+
if len(qubits) != 1:
304+
raise TypeError("Only one qubit or qureg allowed.")
305+
# Check that operator is unitary
306+
if not len(self.terms) == 1:
307+
raise TypeError("Too many terms. Only QubitOperators consisting "
308+
"of a single term (single n-qubit Pauli operator) "
309+
"with a coefficient of unit length can be applied "
310+
"to qubits with this function.")
311+
(term, coefficient), = self.terms.items()
312+
phase = cmath.phase(coefficient)
313+
if (abs(coefficient) < 1 - EQ_TOLERANCE or
314+
abs(coefficient) > 1 + EQ_TOLERANCE):
315+
raise TypeError("abs(coefficient) != 1. Only QubitOperators "
316+
"consisting of a single term (single n-qubit "
317+
"Pauli operator) with a coefficient of unit "
318+
"length can be applied to qubits with this "
319+
"function.")
320+
# Test if we need to apply only Ph
321+
if term == ():
322+
Ph(phase) | qubits[0][0]
323+
return
324+
# Check that Qureg has enough qubits:
325+
num_qubits = len(qubits[0])
326+
non_trivial_qubits = set()
327+
for index, action in term:
328+
non_trivial_qubits.add(index)
329+
if max(non_trivial_qubits) >= num_qubits:
330+
raise ValueError("QubitOperator acts on more qubits than the gate "
331+
"is applied to.")
332+
# Apply X, Y, Z, if QubitOperator acts only on one qubit
333+
if len(term) == 1:
334+
if term[0][1] == "X":
335+
X | qubits[0][term[0][0]]
336+
elif term[0][1] == "Y":
337+
Y | qubits[0][term[0][0]]
338+
elif term[0][1] == "Z":
339+
Z | qubits[0][term[0][0]]
340+
Ph(phase) | qubits[0][term[0][0]]
341+
return
342+
# Create new QubitOperator gate with rescaled qubit indices in
343+
# 0,..., len(non_trivial_qubits) - 1
344+
new_index = dict()
345+
non_trivial_qubits = sorted(list(non_trivial_qubits))
346+
for i in range(len(non_trivial_qubits)):
347+
new_index[non_trivial_qubits[i]] = i
348+
new_qubitoperator = QubitOperator()
349+
assert len(new_qubitoperator.terms) == 0
350+
new_term = tuple([(new_index[index], action)
351+
for index, action in term])
352+
new_qubitoperator.terms[new_term] = coefficient
353+
new_qubits = [qubits[0][i] for i in non_trivial_qubits]
354+
# Apply new gate
355+
cmd = new_qubitoperator.generate_command(new_qubits)
356+
apply_command(cmd)
357+
358+
def get_inverse(self):
359+
"""
360+
Return the inverse gate of a QubitOperator if applied as a gate.
361+
362+
Raises:
363+
NotInvertible: Not implemented for QubitOperators which have
364+
multiple terms or a coefficient with absolute value
365+
not equal to 1.
366+
"""
367+
368+
if len(self.terms) == 1:
369+
(term, coefficient), = self.terms.items()
370+
if (not abs(coefficient) < 1 - EQ_TOLERANCE and not
371+
abs(coefficient) > 1 + EQ_TOLERANCE):
372+
return QubitOperator(term, coefficient**(-1))
373+
raise NotInvertible("BasicGate: No get_inverse() implemented.")
374+
375+
def get_merged(self, other):
376+
"""
377+
Return this gate merged with another gate.
378+
379+
Standard implementation of get_merged:
380+
381+
Raises:
382+
NotMergeable: merging is not possible
383+
"""
384+
if (isinstance(other, self.__class__) and
385+
len(other.terms) == 1 and
386+
len(self.terms) == 1):
387+
return self * other
388+
else:
389+
raise NotMergeable()
390+
229391
def __imul__(self, multiplier):
230392
"""
231393
In-place multiply (*=) terms with scalar or QubitOperator.
@@ -463,3 +625,6 @@ def __str__(self):
463625

464626
def __repr__(self):
465627
return str(self)
628+
629+
def __hash__(self):
630+
return hash(str(self))

projectq/ops/_qubit_operator_test.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@
1313
# limitations under the License.
1414

1515
"""Tests for _qubit_operator.py."""
16+
import cmath
1617
import copy
18+
import math
1719

1820
import numpy
1921
import pytest
2022

23+
from projectq import MainEngine
24+
from projectq.cengines import DummyEngine
25+
from ._basics import NotInvertible, NotMergeable
26+
from ._gates import Ph, T, X, Y, Z
27+
2128
from projectq.ops import _qubit_operator as qo
2229

2330

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

186193

194+
def test_get_inverse():
195+
qo0 = qo.QubitOperator("X1 Z2", cmath.exp(0.6j))
196+
qo1 = qo.QubitOperator("", 1j)
197+
assert qo0.get_inverse().isclose(
198+
qo.QubitOperator("X1 Z2", cmath.exp(-0.6j)))
199+
assert qo1.get_inverse().isclose(qo.QubitOperator("", -1j))
200+
qo0 += qo1
201+
with pytest.raises(NotInvertible):
202+
qo0.get_inverse()
203+
204+
205+
def test_get_merged():
206+
qo0 = qo.QubitOperator("X1 Z2", 1j)
207+
qo1 = qo.QubitOperator("Y3", 1j)
208+
merged = qo0.get_merged(qo1)
209+
assert qo0.isclose(qo.QubitOperator("X1 Z2", 1j))
210+
assert qo1.isclose(qo.QubitOperator("Y3", 1j))
211+
assert qo0.get_merged(qo1).isclose(qo.QubitOperator("X1 Z2 Y3", -1))
212+
with pytest.raises(NotMergeable):
213+
qo1.get_merged(T)
214+
qo2 = qo0 + qo1
215+
with pytest.raises(NotMergeable):
216+
qo2.get_merged(qo0)
217+
with pytest.raises(NotMergeable):
218+
qo0.get_merged(qo2)
219+
220+
221+
def test_or_one_qubit():
222+
saving_backend = DummyEngine(save_commands=True)
223+
eng = MainEngine(backend=saving_backend, engine_list=[])
224+
qureg = eng.allocate_qureg(3)
225+
eng.flush()
226+
identity = qo.QubitOperator("", 1j)
227+
x = qo.QubitOperator("X1", cmath.exp(0.5j))
228+
y = qo.QubitOperator("Y2", cmath.exp(0.6j))
229+
z = qo.QubitOperator("Z0", cmath.exp(4.5j))
230+
identity | qureg
231+
eng.flush()
232+
x | qureg
233+
eng.flush()
234+
y | qureg
235+
eng.flush()
236+
z | qureg
237+
eng.flush()
238+
assert saving_backend.received_commands[4].gate == Ph(math.pi/2.)
239+
240+
assert saving_backend.received_commands[6].gate == X
241+
assert saving_backend.received_commands[6].qubits == ([qureg[1]],)
242+
assert saving_backend.received_commands[7].gate == Ph(0.5)
243+
assert saving_backend.received_commands[7].qubits == ([qureg[1]],)
244+
245+
assert saving_backend.received_commands[9].gate == Y
246+
assert saving_backend.received_commands[9].qubits == ([qureg[2]],)
247+
assert saving_backend.received_commands[10].gate == Ph(0.6)
248+
assert saving_backend.received_commands[10].qubits == ([qureg[2]],)
249+
250+
assert saving_backend.received_commands[12].gate == Z
251+
assert saving_backend.received_commands[12].qubits == ([qureg[0]],)
252+
assert saving_backend.received_commands[13].gate == Ph(4.5)
253+
assert saving_backend.received_commands[13].qubits == ([qureg[0]],)
254+
255+
256+
def test_wrong_input():
257+
eng = MainEngine()
258+
qureg = eng.allocate_qureg(3)
259+
op0 = qo.QubitOperator("X1", 0.99)
260+
with pytest.raises(TypeError):
261+
op0 | qureg
262+
op1 = qo.QubitOperator("X2", 1)
263+
with pytest.raises(ValueError):
264+
op1 | qureg[1]
265+
with pytest.raises(TypeError):
266+
op0 | (qureg[1], qureg[2])
267+
op2 = op0 + op1
268+
with pytest.raises(TypeError):
269+
op2 | qureg
270+
271+
272+
def test_rescaling_of_indices():
273+
saving_backend = DummyEngine(save_commands=True)
274+
eng = MainEngine(backend=saving_backend, engine_list=[])
275+
qureg = eng.allocate_qureg(4)
276+
eng.flush()
277+
op = qo.QubitOperator("X0 Y1 Z3", 1j)
278+
op | qureg
279+
eng.flush()
280+
assert saving_backend.received_commands[5].gate.isclose(
281+
qo.QubitOperator("X0 Y1 Z2", 1j))
282+
# test that gate creates a new QubitOperator
283+
assert op.isclose(qo.QubitOperator("X0 Y1 Z3", 1j))
284+
285+
187286
def test_imul_inplace():
188287
qubit_op = qo.QubitOperator("X1")
189288
prev_id = id(qubit_op)
@@ -444,6 +543,11 @@ def test_str():
444543
assert str(op2) == "2 I"
445544

446545

546+
def test_hash():
547+
op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5)
548+
assert hash(op) == hash("0.5 X1 Y3 Z8")
549+
550+
447551
def test_str_empty():
448552
op = qo.QubitOperator()
449553
assert str(op) == '0'

projectq/setups/decompositions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
entangle,
2222
globalphase,
2323
ph2r,
24+
qubitop2onequbit,
2425
qft2crandhadamard,
2526
r2rzandph,
2627
rx2rz,
@@ -43,6 +44,7 @@
4344
entangle,
4445
globalphase,
4546
ph2r,
47+
qubitop2onequbit,
4648
qft2crandhadamard,
4749
r2rzandph,
4850
rx2rz,

0 commit comments

Comments
 (0)