Skip to content

Commit

Permalink
Initial draft of ConvertToSycamore (#2516)
Browse files Browse the repository at this point in the history
* Initial draft of ConvertToSycamore

- Port of ConvertToSycamore from cirq_internal
- Cleaned up format, mypy, deprecated commands
- Refactored various functions to try to clean them up.
- Got rid of half-pi only rotations
- Combines textbook_gates into convert_to_sycamore
  • Loading branch information
dstrain115 committed Nov 22, 2019
1 parent 1c5084a commit 2963285
Show file tree
Hide file tree
Showing 2 changed files with 611 additions and 0 deletions.
386 changes: 386 additions & 0 deletions cirq/google/optimizers/convert_to_sycamore_gates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
# Copyright 2019 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import List, cast

import math
import numpy as np
import scipy.linalg
from cirq import circuits, google, linalg, ops, optimizers, protocols
from cirq.google import SycamoreGate

UNITARY_ZZ = np.kron(protocols.unitary(ops.Z), protocols.unitary(ops.Z))
PAULI_OPS = [
np.eye(2),
protocols.unitary(ops.X),
protocols.unitary(ops.Y),
protocols.unitary(ops.Z)
]


class ConvertToSycamoreGates(circuits.PointOptimizer):
"""Attempts to convert non-native gates into SycamoreGates.
First, checks if the given operation is already a native sycamore operation.
Second, checks if the operation has a known unitary. If so, and the gate
is a 1-qubit or 2-qubit gate, then performs circuit synthesis of the
operation.
Third, attempts to `cirq.decompose` to the operation.
Fourth, if ignore_failures is set, gives up and returns the gate unchanged.
Otherwise raises a TypeError.
"""

def __init__(self, ignore_failures=False) -> None:
"""
Args:
ignore_failures: If set, gates that fail to convert are forwarded
unchanged. If not set, conversion failures raise a TypeError.
"""
super().__init__()
self.ignore_failures = ignore_failures

def _is_native_sycamore_op(self, op: ops.Operation) -> bool:
"""Check if the given operation is native to a Sycamore device.
Args:
op: Input operation.
Returns:
True if the operation is native to the gmon, false otherwise.
"""
return (isinstance(op, ops.GateOperation) and isinstance(
cast(ops.GateOperation, op).gate,
(SycamoreGate, ops.MeasurementGate, ops.PhasedXPowGate,
ops.XPowGate, ops.YPowGate, ops.ZPowGate)))

def _convert_one(self, op: ops.Operation) -> ops.OP_TREE:
"""
Decomposer intercept: Upon cirq.protocols.decompose catch and
return new OP_Tree
This should decompose based on number of qubits.
"""
if len(op.qubits) == 1:
mat = protocols.unitary(op, None)
gates = optimizers.single_qubit_matrix_to_phased_x_z(mat)
return [g.on(op.qubits[0]) for g in gates]
elif len(op.qubits) == 2 and isinstance(op, ops.GateOperation):
return self.known_two_q_operations_to_sycamore_operations(
op.qubits[0], op.qubits[1], op)
return NotImplemented

def convert(self, op: ops.Operation) -> List[ops.Operation]:

def on_stuck_raise(bad):
return TypeError("Don't know how to work with {!r}. "
"It isn't a native xmon operation, "
"a 1 or 2 qubit gate with a known unitary, "
"or composite.".format(bad))

return protocols.decompose(
op,
keep=self._is_native_sycamore_op,
intercepting_decomposer=self._convert_one,
on_stuck_raise=None if self.ignore_failures else on_stuck_raise)

def optimization_at(self, circuit, index, op):

if not isinstance(op, ops.GateOperation):
return None

gate = op.gate

# Check for a SWAP and ZZPowGate together
if isinstance(gate, ops.ZZPowGate) or gate == ops.SWAP:
gate2 = None
rads = None
next_index = circuit.next_moment_operating_on(op.qubits, index + 1)
if next_index is not None:
ops_in_front = list(
{circuit.operation_at(q, next_index) for q in op.qubits})
if len(ops_in_front) == 1 and isinstance(
ops_in_front[0], ops.GateOperation):
gate2 = ops_in_front[0].gate

if (isinstance(gate, ops.SwapPowGate) and
isinstance(gate2, ops.ZZPowGate)):
rads = gate2.exponent * np.pi / 2
if (isinstance(gate, ops.ZZPowGate) and gate2 == ops.SWAP):
rads = gate.exponent * np.pi / 2
if rads is not None:
return circuits.PointOptimizationSummary(
clear_span=next_index - index + 1,
clear_qubits=op.qubits,
new_operations=swap_rzz(rads, op.qubits[0], op.qubits[1]))

converted = self.convert(op)
if len(converted) == 1 and converted[0] is op:
return None

return circuits.PointOptimizationSummary(clear_span=1,
new_operations=converted,
clear_qubits=op.qubits)

def known_two_q_operations_to_sycamore_operations(self, qubit_a: ops.Qid,
qubit_b: ops.Qid,
op: ops.GateOperation
) -> ops.OP_TREE:
"""
Synthesize a known gate operation to a sycamore operation
This function dispatches based on gate type
Args:
qubit_a: first qubit of GateOperation
qubit_b: second qubit of GateOperation
op: operation to decompose
Returns:
New operations iterable object
"""
gate = op.gate
if isinstance(gate, ops.CNotPowGate):
return [
ops.Y(qubit_b)**-0.5,
cphase(
cast(ops.CNotPowGate, gate).exponent * np.pi, qubit_a,
qubit_b),
ops.Y(qubit_b)**0.5,
]
elif isinstance(gate, ops.CZPowGate):
gate = cast(ops.CZPowGate, gate)
if math.isclose(gate.exponent, 1.0): # check if CZ or CPHASE
return decompose_cz_into_syc(qubit_a, qubit_b)
else:
# because CZPowGate == diag([1, 1, 1, e^{i pi phi}])
return cphase(gate.exponent * np.pi, qubit_a, qubit_b)
elif isinstance(gate, ops.SwapPowGate) and math.isclose(
cast(ops.SwapPowGate, gate).exponent, 1.0):
return decompose_swap_into_syc(qubit_a, qubit_b)
elif isinstance(gate, ops.ISwapPowGate) and math.isclose(
cast(ops.ISwapPowGate, gate).exponent, 1.0):
return decompose_iswap_into_syc(qubit_a, qubit_b)
elif isinstance(gate, ops.ZZPowGate):
return rzz(
cast(ops.ZZPowGate, gate).exponent * np.pi / 2, *op.qubits)
elif isinstance(gate, ops.MatrixGate) and len(op.qubits) == 2:
new_ops = optimizers.two_qubit_matrix_to_operations(
op.qubits[0], op.qubits[1], op, allow_partial_czs=True)
gate_ops = []
for new_op in new_ops:
num_qubits = len(new_op.qubits)
if num_qubits == 1:
gate_ops.extend([
term.on(new_op.qubits[0])
for term in optimizers.single_qubit_matrix_to_gates(
protocols.unitary(new_op))
])
elif num_qubits == 2:
gate_ops.extend(
ops.flatten_to_ops(
self.known_two_q_operations_to_sycamore_operations(
new_op.qubits[0], new_op.qubits[1],
cast(ops.GateOperation, new_op))))
return gate_ops
else:
raise ValueError("Unrecognized gate: {!r}".format(op))


def decompose_cz_into_syc(a: ops.Qid, b: ops.Qid):
"""Decompose CZ into sycamore gates using precomputed coefficients"""
yield ops.PhasedXPowGate(phase_exponent=0.5678998743900456,
exponent=0.5863459345743176).on(a)
yield ops.PhasedXPowGate(phase_exponent=0.3549946157441739).on(b)
yield google.SYC.on(a, b)
yield ops.PhasedXPowGate(phase_exponent=-0.5154334589432878,
exponent=0.5228733015013345).on(b)
yield ops.PhasedXPowGate(phase_exponent=0.06774925307475355).on(a)
yield google.SYC.on(a, b),
yield ops.PhasedXPowGate(phase_exponent=-0.5987667922766213,
exponent=0.4136540654256824).on(a),
yield (ops.Z**-0.9255092746611595).on(b),
yield (ops.Z**-1.333333333333333).on(a),


def decompose_iswap_into_syc(a: ops.Qid, b: ops.Qid):
"""Decompose ISWAP into sycamore gates using precomputed coefficients"""
yield ops.PhasedXPowGate(phase_exponent=-0.27250925776964596,
exponent=0.2893438375555899).on(a)
yield google.SYC.on(a, b)
yield ops.PhasedXPowGate(phase_exponent=0.8487591858680898,
exponent=0.9749387200813147).on(b),
yield ops.PhasedXPowGate(phase_exponent=-0.3582574564210601).on(a),
yield google.SYC.on(a, b)
yield ops.PhasedXPowGate(phase_exponent=0.9675022326694225,
exponent=0.6908986856555526).on(a),
yield google.SYC.on(a, b),
yield ops.PhasedXPowGate(phase_exponent=0.9161706861686068,
exponent=0.14818318325264102).on(b),
yield ops.PhasedXPowGate(phase_exponent=0.9408341606787907).on(a),
yield (ops.Z**-1.1551880579397293).on(b),
yield (ops.Z**0.31848343246696365).on(a),


def decompose_swap_into_syc(a: ops.Qid, b: ops.Qid):
"""Decompose SWAP into sycamore gates using precomputed coefficients"""
yield ops.PhasedXPowGate(phase_exponent=0.44650378384076217,
exponent=0.8817921214052824).on(a)
yield ops.PhasedXPowGate(phase_exponent=-0.7656774060816165,
exponent=0.6628666504604785).on(b)
yield google.SYC.on(a, b)
yield ops.PhasedXPowGate(phase_exponent=-0.6277589946716742,
exponent=0.5659160932099687).on(a)
yield google.SYC.on(a, b)
yield ops.PhasedXPowGate(phase_exponent=0.28890767199499257,
exponent=0.4340839067900317).on(b)
yield ops.PhasedXPowGate(phase_exponent=-0.22592784059288928).on(a)
yield google.SYC.on(a, b)
yield ops.PhasedXPowGate(phase_exponent=-0.4691261557936808,
exponent=0.7728525693920243).on(a)
yield ops.PhasedXPowGate(phase_exponent=-0.8150261316932077,
exponent=0.11820787859471782).on(b)
yield (ops.Z**-0.7384700844660306).on(b)
yield (ops.Z**-0.7034535141382525).on(a)


def find_local_equivalents(unitary1: np.ndarray, unitary2: np.ndarray):
"""
Given two unitaries with the same interaction coefficients but different
local unitary rotations determine the local unitaries that turns
one type of gate into another.
1) perform the kak decomp on each unitary and confirm interaction terms
are equivalent
2) identify the elements of SU(2) to transform unitary2 into unitary1
Args:
unitary1: unitary that we target
unitary2: unitary that we transform the local gates to the target
Returns:
four 2x2 unitaries. first two are pre-rotations last two are post
rotations.
"""
kak_u1 = linalg.kak_decomposition(unitary1)
kak_u2 = linalg.kak_decomposition(unitary2)

u_0 = (kak_u1.single_qubit_operations_after[0]
@ kak_u2.single_qubit_operations_after[0].conj().T)
u_1 = (kak_u1.single_qubit_operations_after[1]
@ kak_u2.single_qubit_operations_after[1].conj().T)

v_0 = (kak_u2.single_qubit_operations_before[0].conj().T
@ kak_u1.single_qubit_operations_before[0])
v_1 = (kak_u2.single_qubit_operations_before[1].conj().T
@ kak_u1.single_qubit_operations_before[1])

return v_0, v_1, u_0, u_1


def create_corrected_circuit(target_unitary: np.ndarray,
program: circuits.Circuit, q0: ops.Qid,
q1: ops.Qid):
# Get the local equivalents
b_0, b_1, a_0, a_1 = find_local_equivalents(
target_unitary,
program.unitary(qubit_order=ops.QubitOrder.explicit([q0, q1])))

# Apply initial corrections
yield (gate(q0) for gate in optimizers.single_qubit_matrix_to_gates(b_0))
yield (gate(q1) for gate in optimizers.single_qubit_matrix_to_gates(b_1))

# Apply interaction part
yield program

# Apply final corrections
yield (gate(q0) for gate in optimizers.single_qubit_matrix_to_gates(a_0))
yield (gate(q1) for gate in optimizers.single_qubit_matrix_to_gates(a_1))


def rzz(theta: float, q0: ops.Qid, q1: ops.Qid) -> ops.OP_TREE:
"""Generate exp(-1j * theta * zz) from Sycamore gates.
Args:
theta: rotation parameter
q0: First qubit id to operate on
q1: Second qubit id to operate on
Returns:
a Cirq program implementing the Ising gate
rtype:
cirq.OP_Tree
"""
phi = -np.pi / 24
c_phi = np.cos(2 * phi)
target_unitary = scipy.linalg.expm(-1j * theta * UNITARY_ZZ)

if abs(np.cos(theta)) > c_phi:
c2 = abs(np.sin(theta)) / c_phi
else:
c2 = abs(np.cos(theta)) / c_phi

# Prepare program that has same Schmidt coeffs as exp(1j theta ZZ)
program = circuits.Circuit(google.SYC.on(q0, q1),
ops.rx(2 * np.arccos(c2)).on(q1),
google.SYC.on(q0, q1))

yield create_corrected_circuit(target_unitary, program, q0, q1)


def cphase(theta: float, q0: ops.Qid, q1: ops.Qid) -> ops.OP_TREE:
"""
Implement a cphase using the Ising gate generated from 2 Sycamore gates
A CPHASE gate has the matrix diag([1, 1, 1, exp(1j * theta)]) and can
be mapped to the Ising gate by prep and post rotations of Z-pi/4.
We drop the global phase shift of theta/4.
Args:
theta: exp(1j * theta )
q0: First qubit id to operate on
q1: Second qubit id to operate on
returns:
a cirq program implementating cphase
"""
yield rzz(-theta / 4, q0, q1)
yield ops.rz(theta / 2).on(q0)
yield ops.rz(theta / 2).on(q1)


def swap_rzz(theta: float, q0: ops.Qid, q1: ops.Qid) -> ops.OP_TREE:
"""
An implementation of SWAP * EXP(1j theta ZZ) using three sycamore gates.
This builds off of the zztheta method. A sycamore gate following the
zz-gate is a SWAP EXP(1j (THETA - pi/24) ZZ).
Args:
theta: exp(1j * theta )
q0: First qubit id to operate on
q1: Second qubit id to operate on
Returns:
The circuit that implements ZZ followed by a swap
"""

# Set interaction part.
circuit = circuits.Circuit()
angle_offset = np.pi / 24 - np.pi / 4
circuit.append(google.SYC(q0, q1))
circuit.append(rzz(theta - angle_offset, q0, q1))

# Get the intended circuit.
intended_circuit = circuits.Circuit(
ops.SWAP(q0, q1),
ops.ZZPowGate(exponent=2 * theta / np.pi, global_shift=-0.5).on(q0, q1))

yield create_corrected_circuit(intended_circuit, circuit, q0, q1)

0 comments on commit 2963285

Please sign in to comment.