Skip to content

Commit a317ce4

Browse files
committed
Add U2 and U3 gates with definitions identical to IBM QisKit
1 parent dff6adc commit a317ce4

File tree

4 files changed

+277
-19
lines changed

4 files changed

+277
-19
lines changed

projectq/ops/_basics.py

Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2017 ProjectQ-Framework (www.projectq.ch)
1+
# Copyright 2020 ProjectQ-Framework (www.projectq.ch)
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -62,6 +62,22 @@ class NotInvertible(Exception):
6262
pass
6363

6464

65+
def _round_angle(angle, mod_pi):
66+
rounded_angle = round(float(angle) % (mod_pi * math.pi), ANGLE_PRECISION)
67+
if rounded_angle > mod_pi * math.pi - ANGLE_TOLERANCE:
68+
rounded_angle = 0.
69+
return rounded_angle
70+
71+
72+
def _angle_to_str(angle, symbols):
73+
if symbols:
74+
return ('{}{}'.format(round(angle / math.pi, 3),
75+
unicodedata.lookup("GREEK SMALL LETTER PI")))
76+
else:
77+
return "{}".format(angle)
78+
79+
80+
6581
class BasicGate(object):
6682
"""
6783
Base class of all gates. (Don't use it directly but derive from it)
@@ -340,10 +356,7 @@ def __init__(self, angle):
340356
angle (float): Angle of rotation (saved modulo 4 * pi)
341357
"""
342358
BasicGate.__init__(self)
343-
rounded_angle = round(float(angle) % (4. * math.pi), ANGLE_PRECISION)
344-
if rounded_angle > 4 * math.pi - ANGLE_TOLERANCE:
345-
rounded_angle = 0.
346-
self.angle = rounded_angle
359+
self.angle = _round_angle(angle, 4)
347360

348361
def __str__(self):
349362
"""
@@ -366,12 +379,8 @@ def to_string(self, symbols=False):
366379
more user friendly display if True, full angle
367380
written in radian otherwise.
368381
"""
369-
if symbols:
370-
angle = ("(" + str(round(self.angle / math.pi, 3))
371-
+ unicodedata.lookup("GREEK SMALL LETTER PI") + ")")
372-
else:
373-
angle = "(" + str(self.angle) + ")"
374-
return str(self.__class__.__name__) + angle
382+
return (str(self.__class__.__name__) + '(' + _angle_to_str(
383+
self.angle, symbols) + ')')
375384

376385
def tex_str(self):
377386
"""
@@ -437,6 +446,127 @@ def is_identity(self):
437446
return self.angle == 0. or self.angle == 4 * math.pi
438447

439448

449+
class U3Gate(BasicGate):
450+
"""
451+
Defines a base class for a general unitary single-qubit gate.
452+
453+
All three angles are continuous parameters. The inverse is the same gate
454+
with the negated argument. Rotation gates of the same class can be merged
455+
by adding the angles. The continuous parameter are modulo 4 * pi.
456+
"""
457+
def __init__(self, theta, phi, lamda):
458+
"""
459+
Initialize a general unitary single-qubit gate.
460+
461+
Args:
462+
theta (float): Angle of rotation (saved modulo 4 * pi)
463+
phi (float): Angle of rotation (saved modulo 4 * pi)
464+
lamda (float): Angle of rotation (saved modulo 4 * pi)
465+
"""
466+
BasicGate.__init__(self)
467+
self.theta = _round_angle(theta, 4)
468+
self.phi = _round_angle(phi, 4)
469+
self.lamda = _round_angle(lamda, 4)
470+
471+
def __str__(self):
472+
"""
473+
Return the string representation of a U3Gate.
474+
475+
Returns the class name and the angle as
476+
477+
.. code-block:: python
478+
479+
[CLASSNAME]([ANGLE])
480+
"""
481+
return self.to_string()
482+
483+
def to_string(self, symbols=False):
484+
"""
485+
Return the string representation of a U3Gate.
486+
487+
Args:
488+
symbols (bool): uses the pi character and round the angle for a
489+
more user friendly display if True, full angle
490+
written in radian otherwise.
491+
"""
492+
return (str(self.__class__.__name__) + '({},{},{})'.format(
493+
_angle_to_str(self.theta, symbols),
494+
_angle_to_str(self.phi, symbols),
495+
_angle_to_str(self.lamda, symbols)))
496+
497+
def tex_str(self):
498+
"""
499+
Return the Latex string representation of a BasicRotationGate.
500+
501+
Returns the class name and the angle as a subscript, i.e.
502+
503+
.. code-block:: latex
504+
505+
[CLASSNAME]$_[ANGLE]$
506+
"""
507+
return str(self.__class__.__name__) + "$({}\\pi,{}\\pi,{}\\pi)$".format(
508+
round(self.theta / math.pi, 3),
509+
round(self.phi / math.pi, 3),
510+
round(self.lamda / math.pi, 3))
511+
512+
def get_inverse(self):
513+
"""
514+
Return the inverse of this rotation gate (negate the angle, return new
515+
object).
516+
"""
517+
if (self.theta, self.phi, self.lamda) == (0, 0, 0):
518+
return self.__class__(0, 0, 0)
519+
else:
520+
return self.__class__(-self.theta + 4 * math.pi,
521+
-self.phi + 4 * math.pi,
522+
-self.lamda + 4 * math.pi)
523+
524+
def get_merged(self, other):
525+
"""
526+
Return self merged with another gate.
527+
528+
Default implementation handles rotation gate of the same type, where
529+
angles are simply added.
530+
531+
Args:
532+
other: Rotation gate of same type.
533+
534+
Raises:
535+
NotMergeable: For non-rotation gates or rotation gates of
536+
different type.
537+
538+
Returns:
539+
New object representing the merged gates.
540+
"""
541+
if isinstance(other, self.__class__):
542+
return self.__class__(self.theta + other.theta,
543+
self.phi + other.phi,
544+
self.lamda + other.lamda)
545+
raise NotMergeable("Can't merge different types of rotation gates.")
546+
547+
def __eq__(self, other):
548+
""" Return True if same class and same rotation angle. """
549+
if isinstance(other, self.__class__):
550+
return ((self.theta, self.phi, self.lamda)
551+
== (other.theta, other.phi, other.lamda))
552+
else:
553+
return False
554+
555+
def __ne__(self, other):
556+
return not self.__eq__(other)
557+
558+
def __hash__(self):
559+
return hash(str(self))
560+
561+
def is_identity(self):
562+
"""
563+
Return True if the gate is equivalent to an Identity gate
564+
"""
565+
return ((self.theta == 0. or self.theta == 4 * math.pi)
566+
and (self.phi == 0. or self.phi == 4 * math.pi)
567+
and (self.lamda == 0. or self.lamda == 4 * math.pi))
568+
569+
440570
class BasicPhaseGate(BasicGate):
441571
"""
442572
Defines a base class of a phase gate.
@@ -455,10 +585,7 @@ def __init__(self, angle):
455585
angle (float): Angle of rotation (saved modulo 2 * pi)
456586
"""
457587
BasicGate.__init__(self)
458-
rounded_angle = round(float(angle) % (2. * math.pi), ANGLE_PRECISION)
459-
if rounded_angle > 2 * math.pi - ANGLE_TOLERANCE:
460-
rounded_angle = 0.
461-
self.angle = rounded_angle
588+
self.angle = _round_angle(angle, 2)
462589

463590
def __str__(self):
464591
"""

projectq/ops/_basics_test.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright 2017 ProjectQ-Framework (www.projectq.ch)
2+
# Copyright 2020 ProjectQ-Framework (www.projectq.ch)
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -204,7 +204,7 @@ def test_basic_rotation_gate_is_identity():
204204
assert basic_rotation_gate5.is_identity()
205205

206206

207-
def test_basic_rotation_gate_comparison_and_hash():
207+
def test_u3_gate_comparison_and_hash():
208208
basic_rotation_gate1 = _basics.BasicRotationGate(0.5)
209209
basic_rotation_gate2 = _basics.BasicRotationGate(0.5)
210210
basic_rotation_gate3 = _basics.BasicRotationGate(0.5 + 4 * math.pi)
@@ -226,6 +226,100 @@ def test_basic_rotation_gate_comparison_and_hash():
226226
assert not basic_gate == basic_rotation_gate6
227227
assert basic_rotation_gate2 != _basics.BasicRotationGate(0.5 + 2 * math.pi)
228228

229+
@pytest.mark.parametrize("input_angle, modulo_angle",
230+
[(2.0, 2.0), (17., 4.4336293856408275),
231+
(-0.5 * math.pi, 3.5 * math.pi), (4 * math.pi, 0)])
232+
def test_u3_gate_init(input_angle, modulo_angle):
233+
# Test internal representation
234+
gate = _basics.U3Gate(input_angle, input_angle, input_angle)
235+
assert gate.theta == pytest.approx(modulo_angle)
236+
assert gate.phi == pytest.approx(modulo_angle)
237+
assert gate.lamda == pytest.approx(modulo_angle)
238+
239+
240+
def test_u3_gate_str():
241+
gate = _basics.U3Gate(math.pi, math.pi, math.pi)
242+
assert (str(gate) == "U3Gate(3.14159265359,3.14159265359,3.14159265359)")
243+
assert gate.to_string(symbols=True) == u"U3Gate(1.0π,1.0π,1.0π)"
244+
assert (gate.to_string(symbols=False) ==
245+
"U3Gate(3.14159265359,3.14159265359,3.14159265359)")
246+
247+
248+
def test_u3_tex_str():
249+
gate = _basics.U3Gate(0.5 * math.pi, 0.5 * math.pi, 0.5 * math.pi)
250+
assert gate.tex_str() == "U3Gate$(0.5\\pi,0.5\\pi,0.5\\pi)$"
251+
gate = _basics.U3Gate(4 * math.pi - 1e-13,
252+
4 * math.pi - 1e-13,
253+
4 * math.pi - 1e-13)
254+
assert gate.tex_str() == "U3Gate$(0.0\\pi,0.0\\pi,0.0\\pi)$"
255+
256+
257+
@pytest.mark.parametrize("input_angle, inverse_angle",
258+
[(2.0, -2.0 + 4 * math.pi), (-0.5, 0.5), (0.0, 0)])
259+
def test_u3_gate_get_inverse(input_angle, inverse_angle):
260+
u3_gate = _basics.U3Gate(input_angle, input_angle, input_angle)
261+
inverse = u3_gate.get_inverse()
262+
assert isinstance(inverse, _basics.U3Gate)
263+
assert inverse.theta == pytest.approx(inverse_angle)
264+
assert inverse.phi == pytest.approx(inverse_angle)
265+
assert inverse.lamda == pytest.approx(inverse_angle)
266+
267+
268+
def test_u3_gate_get_merged():
269+
basic_gate = _basics.BasicGate()
270+
u3_gate1 = _basics.U3Gate(0.5, 0.5, 0.5)
271+
u3_gate2 = _basics.U3Gate(1.0, 1.0, 1.0)
272+
u3_gate3 = _basics.U3Gate(1.5, 1.5, 1.5)
273+
with pytest.raises(_basics.NotMergeable):
274+
u3_gate1.get_merged(basic_gate)
275+
merged_gate = u3_gate1.get_merged(u3_gate2)
276+
assert merged_gate == u3_gate3
277+
278+
279+
def test_u3_gate_is_identity():
280+
u3_gate1 = _basics.U3Gate(0., 0., 0.)
281+
u3_gate2 = _basics.U3Gate(
282+
1. * math.pi, 1. * math.pi, 1. * math.pi)
283+
u3_gate3 = _basics.U3Gate(
284+
2. * math.pi, 2. * math.pi, 2. * math.pi)
285+
u3_gate4 = _basics.U3Gate(
286+
3. * math.pi, 3. * math.pi, 3. * math.pi)
287+
u3_gate5 = _basics.U3Gate(
288+
4. * math.pi, 4. * math.pi, 4. * math.pi)
289+
assert u3_gate1.is_identity()
290+
assert not u3_gate2.is_identity()
291+
assert not u3_gate3.is_identity()
292+
assert not u3_gate4.is_identity()
293+
assert u3_gate5.is_identity()
294+
295+
296+
def test_u3_gate_comparison_and_hash():
297+
u3gate1 = _basics.U3Gate(0.5, 0.5, 0.5)
298+
u3gate2 = _basics.U3Gate(0.5, 0.5, 0.5)
299+
u3gate3 = _basics.U3Gate(
300+
0.5 + 4 * math.pi, 0.5 + 4 * math.pi, 0.5 + 4 * math.pi)
301+
assert u3gate1 == u3gate2
302+
assert hash(u3gate1) == hash(u3gate2)
303+
assert u3gate1 == u3gate3
304+
assert hash(u3gate1) == hash(u3gate3)
305+
u3gate4 = _basics.U3Gate(0.50000001, 0.50000001, 0.50000001)
306+
# Test __ne__:
307+
assert u3gate4 != u3gate1
308+
# Test one gate close to 4*pi the other one close to 0
309+
u3gate5 = _basics.U3Gate(1.e-13, 1.e-13, 1.e-13)
310+
u3gate6 = _basics.U3Gate(4 * math.pi - 1.e-13,
311+
4 * math.pi - 1.e-13,
312+
4 * math.pi - 1.e-13)
313+
assert u3gate5 == u3gate6
314+
assert u3gate6 == u3gate5
315+
assert hash(u3gate5) == hash(u3gate6)
316+
# Test different types of gates
317+
basic_gate = _basics.BasicGate()
318+
assert not basic_gate == u3gate6
319+
assert u3gate2 != _basics.U3Gate(0.5 + 2 * math.pi,
320+
0.5 + 2 * math.pi,
321+
0.5 + 2 * math.pi)
322+
229323

230324
@pytest.mark.parametrize("input_angle, modulo_angle",
231325
[(2.0, 2.0), (17., 4.4336293856408275),

projectq/ops/_gates.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2017 ProjectQ-Framework (www.projectq.ch)
1+
# Copyright 2020 ProjectQ-Framework (www.projectq.ch)
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -52,6 +52,7 @@
5252
MatrixGate,
5353
SelfInverseGate,
5454
BasicRotationGate,
55+
U3Gate,
5556
BasicPhaseGate,
5657
ClassicalInstructionGate,
5758
FastForwardingGate,
@@ -245,6 +246,18 @@ def matrix(self):
245246
[0, cmath.exp(.5 * 1j * self.angle)]])
246247

247248

249+
class U3(U3Gate):
250+
@property
251+
def matrix(self):
252+
return np.matrix([[cmath.exp(-.5j * (self.phi + self.lamda)) + math.cos(.5* self.theta),
253+
-cmath.exp(-.5j * (self.phi - self.lamda)) + math.sin(.5* self.theta)],
254+
[cmath.exp(.5j * (self.phi - self.lamda)) + math.sin(.5* self.theta),
255+
cmath.exp(.5j * (self.phi + self.lamda)) + math.cos(.5* self.theta)]])
256+
257+
class U2(U3):
258+
def __init__(self, phi, lamda):
259+
super().__init__(math.pi/2, phi, lamda)
260+
248261
class Rxx(BasicRotationGate):
249262
""" RotationXX gate class """
250263
@property

projectq/ops/_gates_test.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2017 ProjectQ-Framework (www.projectq.ch)
1+
# Copyright 2020 ProjectQ-Framework (www.projectq.ch)
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -156,6 +156,30 @@ def test_rz(angle):
156156
assert np.allclose(gate.matrix, expected_matrix)
157157

158158

159+
@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi,
160+
4 * math.pi])
161+
def test_u3(angle):
162+
gate = _gates.U3(angle, angle, angle)
163+
expected_matrix = np.matrix([[cmath.exp(-1j * angle) + math.cos(.5 * angle),
164+
-1 + math.sin(.5 * angle)],
165+
[1 + math.sin(.5 * angle),
166+
cmath.exp(1j * angle) + math.cos(.5 * angle)]])
167+
assert gate.matrix.shape == expected_matrix.shape
168+
assert np.allclose(gate.matrix, expected_matrix)
169+
170+
171+
@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi,
172+
4 * math.pi])
173+
def test_u2(angle):
174+
gate = _gates.U2(angle, angle)
175+
expected_matrix = np.matrix([[cmath.exp(-1j * angle) + math.cos(.25 * math.pi),
176+
-1 + math.sin(.25 * math.pi)],
177+
[1 + math.sin(.25 * math.pi),
178+
cmath.exp(1j * angle) + math.cos(.25 * math.pi)]])
179+
assert gate.matrix.shape == expected_matrix.shape
180+
assert np.allclose(gate.matrix, expected_matrix)
181+
182+
159183
@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi,
160184
4 * math.pi])
161185
def test_rxx(angle):

0 commit comments

Comments
 (0)