Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: support __pow__ for rotation gates. #32

Open
Strilanc opened this issue Mar 20, 2017 · 5 comments
Open

Suggestion: support __pow__ for rotation gates. #32

Strilanc opened this issue Mar 20, 2017 · 5 comments

Comments

@Strilanc
Copy link
Contributor

Strilanc commented Mar 20, 2017

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 Strilanc changed the title Suggestion: avoid float error by using Z^{exponent} instead of Rz(angle). Suggestion: avoid printing float error by using Z^{exponent} instead of Rz(angle). Mar 20, 2017
@thomashaener
Copy link
Contributor

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.

@Strilanc
Copy link
Contributor Author

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
Copy link
Contributor Author

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

@Strilanc
Copy link
Contributor Author

Strilanc commented Mar 23, 2017

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 Strilanc changed the title Suggestion: avoid printing float error by using Z^{exponent} instead of Rz(angle). Suggestion: support __pow__ for single-qubit gates. Z**0.5 instead of Rz(pi/2). Apr 6, 2017
@thomashaener
Copy link
Contributor

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 thomashaener reopened this Apr 22, 2017
@thomashaener thomashaener changed the title Suggestion: support __pow__ for single-qubit gates. Z**0.5 instead of Rz(pi/2). Suggestion: support __pow__ for rotation gates. Apr 22, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants