ProjectQ icon indicating copy to clipboard operation
ProjectQ copied to clipboard

Suggestion: support __pow__ for rotation gates.

Open Strilanc opened this issue 8 years ago • 5 comments

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.)

Strilanc avatar Mar 20 '17 19:03 Strilanc

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.

thomashaener avatar Mar 20 '17 19:03 thomashaener

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.

Strilanc avatar Mar 20 '17 19:03 Strilanc

Having rotation gates support __pow__, with the gate as either the base or the exponent, would subsume this idea.

Strilanc avatar Mar 22 '17 23:03 Strilanc

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

Strilanc avatar Mar 23 '17 19:03 Strilanc

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).

thomashaener avatar Apr 22 '17 09:04 thomashaener