Suggestion: support __pow__ for rotation gates.
Because pi is transcendental, involving it in the definition of gates forces floating point error even when performing rational fractions of a whole turn.
So, instead of a transcendental angle, I suggest we use a fractional exponent.
Something like this:
class ZPow(BasicRotationGate):
def __init__(self, exponent):
BasicGate.__init__(self)
self.exponent = (exponent + 1) % 2 - 1
def __str__(self):
return "Z**(" + str(self.exponent) + ")"
def tex_str(self):
return "Z^{" + str(self.exponent) + "}"
def get_inverse(self):
return ZPow(-self.exponent)
def get_merged(self, other):
if isinstance(other, ZPow):
return ZPow(self.exponent + other.exponent)
raise NotMergeable("Can't merge different types of rotation gates.")
def __eq__(self, other):
return isinstance(other, ZPow) and other.exponent == self.exponent
def __ne__(self, other):
return not self.__eq__(other)
@property
def matrix(self):
# Note: expiTau should ensure quarter turns are exact
return np.matrix([[1, 0], [0, expiTau(self.exponent)]])
Note that passing in a Fraction instead of a float will work fine.
A secondary benefit of this change is that the names of gates become easier to read. I tweaked the Shor example to use ZPow(Fraction(1, 1 << (k - i))) instead of R(-math.pi/(1 << (k - i))), and this is the resource count:
CMultiplyByConstantModN(1, 35) : 11
CMultiplyByConstantModN(6, 35) : 1
Deallocate : 7
H : 24
Measure : 13
X : 11
Z**(1/2) : 1
Z**(1015/1024) : 1
Z**(119/128) : 1
Z**(2039/2048) : 1
Z**(23/32) : 1
Z**(247/256) : 1
Z**(3/4) : 1
Z**(503/512) : 1
Z**(55/64) : 1
Z**(7/16) : 1
Z**(7/8) : 1
Max. width (number of qubits) : 7.
7 * 5 = 35
Which is a lot clearer than this:
...
R(10.9955742876) : 1
R(11.0569335191) : 1
R(11.3882733693) : 1
R(11.8116520667) : 1
R(12.1890113405) : 1
R(9.5474964238) : 1
R(9.67021488683) : 1
R(9.91565181289) : 1
...
(It also makes much nicer latex circuit output.)
I agree that there are benefits, but there is no pi in the definition of Rz (I'd expect its argument to be theta in exp(+/- i*theta/2)).
Not providing Rz at all (and going with ZPow instead) is not a good solution either, since Rx,y,z can be found in most textbooks.
As for the resource count / circuit drawing: We could also just run a continued fraction expansion on both the angle itself and angle/pi (if the first cfe didn't converge) to have a nicer output.
You definitely wouldn't want to remove Rz (since people will indeed want to use it). But the internal constructions can prefer ZPow over Rz and thereby gain free precision.
There'd be other things to do, like ensure they interop when merging etc.
Having rotation gates support __pow__, with the gate as either the base or the exponent, would subsume this idea.
Some followup on what this would look like:
def _exp_i_pi(exponent):
exponent %= 2
exponent = float(exponent)
# At half-steps, give results without floating point error.
if exponent % 0.5 == 0:
return 1j**int(exponent * 2)
return cmath.exp(np.pi * 1j * exponent)
class VectorPhaseGate(BasicGate):
def __init__(self, vector_name, vector, phase_exponent=1):
"""
:param vector_name: The root name used when describing the gate.
:param vector: The eigenvector to phase. All vectors perpendicular
to this one are not affected by the operation.
Doesn't need to be normalized.
:param phase_exponent: The eigenvector is phased by -1 raised to this
power, defined as (-1)^x = exp(i π x).
"""
super(VectorPhaseGate, self).__init__()
self.vector = vector
self.vector_name = vector_name
# Wrap into (-1, +1].
self.phase_exponent = -((1-phase_exponent) % 2 - 1)
def __str__(self):
g = self.vector_name
e = self.phase_exponent
if e == 1:
return g
return g + '^' + str(e)
def tex_str(self):
g = self.vector_name
e = self.phase_exponent
if e == 1:
return g
return g + '^{' + str(e) + '}'
@property
def matrix(self):
d = len(self.vector)
m = np.dot(self.vector, self.vector.T)
p = _exp_i_pi(self.phase_exponent)
return np.identity(d) + (p - 1) * m / m.trace()
def __and__(self, other):
return OperationWithControl(self, other)
def __eq__(self, other):
return (isinstance(other, VectorPhaseGate) and
np.array_equal(self.vector, other.vector) and
self.vector_name == other.vector_name and
self.phase_exponent % 2 == other.phase_exponent % 2)
def same_axis_as(self, other):
return (isinstance(other, VectorPhaseGate) and
np.array_equal(self.vector, other.vector) and
self.vector_name == other.vector_name)
def __pow__(self, power):
return VectorPhaseGate(self.vector_name,
self.vector,
self.phase_exponent * power)
def get_inverse(self):
return VectorPhaseGate(self.vector_name,
self.vector,
-self.phase_exponent)
def get_merged(self, other):
if not self.same_axis_as(other):
raise NotMergeable('Different axis.')
return VectorPhaseGate(self.vector_name,
self.vector,
self.phase_exponent + other.phase_exponent)
X = VectorPhaseGate('X', np.mat([[1], [-1]]))
Y = VectorPhaseGate('Y', np.mat([[1], [1j]]))
Z = VectorPhaseGate('Z', np.mat([[0], [1]]))
H = VectorPhaseGate('H', np.mat([[1 - np.sqrt(2)], [1]]))
S = Z**0.5
Sdag = Z**-0.5
T = Z**0.25
Tdag = Z**-0.25
I think adding __pow__ to BasicRotationGate would be nice. I would not do the above for X, Y, Z, H, ..., as it makes the compilation harder (one always has to run rotation synthesis although it was just, e.g., H which got merged with S).