diff --git a/cirq-google/cirq_google/__init__.py b/cirq-google/cirq_google/__init__.py index a31371ec79c..7ecf824a81e 100644 --- a/cirq-google/cirq_google/__init__.py +++ b/cirq-google/cirq_google/__init__.py @@ -106,6 +106,10 @@ optimized_for_xmon, ) +from cirq_google.transformers import ( + known_2q_op_to_sycamore_operations, + two_qubit_matrix_to_sycamore_operations, +) from cirq_google.serialization import ( arg_from_proto, diff --git a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py index 7f3b013c8c8..07ccb156b01 100644 --- a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py +++ b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py @@ -12,23 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. import math -from typing import Iterator, List, Optional +from typing import List, Optional import numpy as np -import scipy.linalg import cirq -import cirq_google as google from cirq_google.ops import SycamoreGate - - -UNITARY_ZZ = np.kron(cirq.unitary(cirq.Z), cirq.unitary(cirq.Z)) -PAULI_OPS = [ - np.eye(2), - cirq.unitary(cirq.X), - cirq.unitary(cirq.Y), - cirq.unitary(cirq.Z), -] +from cirq_google.transformers.analytical_decompositions import two_qubit_to_sycamore class ConvertToSycamoreGates(cirq.PointOptimizer): @@ -108,13 +98,22 @@ def _is_native_sycamore_op(self, op: cirq.Operation) -> bool: def _convert_one(self, op: cirq.Operation) -> cirq.OP_TREE: """The main conversion step for the PointOptimizer.""" - if len(op.qubits) == 1: - return _phased_x_z_ops(cirq.unitary(op, None), op.qubits[0]) - elif len(op.qubits) == 2: - return known_two_q_operations_to_sycamore_operations( - op.qubits[0], op.qubits[1], op, self.tabulation + if not (cirq.has_unitary(op) and 1 <= cirq.num_qubits(op) <= 2): + return NotImplemented + + if cirq.num_qubits(op) == 1: + return [*cirq.merge_single_qubit_gates_to_phxz(cirq.Circuit(op)).all_operations()] + + known_decomp = two_qubit_to_sycamore.known_2q_op_to_sycamore_operations(op) + if known_decomp is not None: + return known_decomp + if self.tabulation is not None: + return two_qubit_to_sycamore._decompose_arbitrary_into_syc_tabulation( + op, self.tabulation ) - return NotImplemented + return two_qubit_to_sycamore.two_qubit_matrix_to_sycamore_operations( + op.qubits[0], op.qubits[1], cirq.unitary(op) + ) def convert(self, op: cirq.Operation) -> List[cirq.Operation]: def on_stuck_raise(bad): @@ -139,29 +138,28 @@ def optimization_at( if op.gate is None and not isinstance(op.untagged, cirq.CircuitOperation): return None - gate = op.gate - # Check for a SWAP and ZZPowGate together - if isinstance(gate, cirq.ZZPowGate) or gate == cirq.SWAP: - gate2 = None - rads = None + if isinstance(op.gate, cirq.ZZPowGate) or op.gate == cirq.SWAP: + op2 = 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 ops_in_front[0] is not None: - gate2 = ops_in_front[0].gate + op2 = ops_in_front[0] else: next_index = 0 - - if isinstance(gate, cirq.SwapPowGate) and isinstance(gate2, cirq.ZZPowGate): - rads = gate2.exponent * np.pi / 2 - if isinstance(gate, cirq.ZZPowGate) and gate2 == cirq.SWAP: - rads = gate.exponent * np.pi / 2 - if rads is not None: + if op2 is not None and ( + (op.gate == cirq.SWAP and isinstance(op2.gate, cirq.ZZPowGate)) + or (isinstance(op.gate, cirq.ZZPowGate) and op2.gate == cirq.SWAP) + ): + swap_rzz_decomposed = two_qubit_to_sycamore.known_2q_op_to_sycamore_operations( + cirq.CircuitOperation(cirq.FrozenCircuit(op, op2)) + ) + assert swap_rzz_decomposed is not None return cirq.PointOptimizationSummary( clear_span=next_index - index + 1, clear_qubits=op.qubits, - new_operations=swap_rzz(rads, op.qubits[0], op.qubits[1]), + new_operations=swap_rzz_decomposed, ) converted = self.convert(op) @@ -171,371 +169,3 @@ def optimization_at( return cirq.PointOptimizationSummary( clear_span=1, new_operations=converted, clear_qubits=op.qubits ) - - -def known_two_q_operations_to_sycamore_operations( - qubit_a: cirq.Qid, - qubit_b: cirq.Qid, - op: cirq.Operation, - tabulation: Optional[cirq.TwoQubitGateTabulation] = None, -) -> cirq.OP_TREE: - """Synthesizes 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. - tabulation: A tabulation for the Sycamore gate to use for decomposing gates. - Returns: - The operations that yield the gate using Sycamores. - - Raises: - ValueError: If the gate is a `PhasedISwapPowGate` with an exponent of 0.25 or 1.0, - or the gate is not recognized. - """ - gate = op.gate - if isinstance(gate, cirq.PhasedISwapPowGate): - if math.isclose(gate.exponent, 1): - return decompose_phased_iswap_into_syc(gate.phase_exponent, qubit_a, qubit_b) - elif math.isclose(gate.phase_exponent, 0.25): - return decompose_phased_iswap_into_syc_precomputed( - gate.exponent * np.pi / 2, qubit_a, qubit_b - ) - else: - raise ValueError( - "To decompose PhasedISwapPowGate, it must have a phase_exponent" - " of .25 OR an exponent of 1.0, but got: {!r}".format(op) - ) - if isinstance(gate, cirq.CNotPowGate): - return [ - cirq.Y(qubit_b) ** -0.5, - cphase(gate.exponent * np.pi, qubit_a, qubit_b), - cirq.Y(qubit_b) ** 0.5, - ] - elif isinstance(gate, cirq.CZPowGate): - if math.isclose(gate.exponent, 1): # 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, cirq.SwapPowGate) and math.isclose(gate.exponent, 1): - return decompose_swap_into_syc(qubit_a, qubit_b) - elif isinstance(gate, cirq.ISwapPowGate) and math.isclose(gate.exponent, 1): - return decompose_iswap_into_syc(qubit_a, qubit_b) - elif isinstance(gate, cirq.ZZPowGate): - return rzz(gate.exponent * np.pi / 2, *op.qubits) - elif cirq.unitary(gate, None) is not None: - if tabulation: - return decompose_arbitrary_into_syc_tabulation(qubit_a, qubit_b, op, tabulation) - else: - return decompose_arbitrary_into_syc_analytic(qubit_a, qubit_b, op) - else: - raise ValueError(f"Unrecognized gate: {op!r}") - - -def decompose_phased_iswap_into_syc( - phase_exponent: float, a: cirq.Qid, b: cirq.Qid -) -> cirq.OP_TREE: - """Decompose PhasedISwap with an exponent of 1. - - This should only be called if the Gate has an exponent of 1 - otherwise, - `decompose_phased_iswap_into_syc_precomputed` should be used instead. The advantage of using - this function is that the resulting circuit will be smaller. - - Args: - phase_exponent: The exponent on the Z gates. - a: First qubit id to operate on. - b: Second qubit id to operate on. - Yields: - A `cirq.OP_TREE` implementing the Phased ISWAP gate. - """ - - yield cirq.Z(a) ** phase_exponent, - yield cirq.Z(b) ** -phase_exponent, - yield decompose_iswap_into_syc(a, b), - yield cirq.Z(a) ** -phase_exponent, - yield cirq.Z(b) ** phase_exponent, - - -def decompose_phased_iswap_into_syc_precomputed( - theta: float, a: cirq.Qid, b: cirq.Qid -) -> cirq.OP_TREE: - """Decompose PhasedISwap into sycamore gates using precomputed coefficients. - - This should only be called if the Gate has a phase_exponent of .25. If the gate has an - exponent of 1, decompose_phased_iswap_into_syc should be used instead. Converting PhasedISwap - gates to Sycamore is not supported if neither of these constraints are satisfied. - - This synthesize a PhasedISwap in terms of four sycamore gates. This compilation converts the - gate into a circuit involving two CZ gates, which themselves are each represented as two - Sycamore gates and single-qubit rotations - - Args: - theta: Rotation parameter for the phased ISWAP. - a: First qubit id to operate on. - b: Second qubit id to operate on. - Yields: - A `cirq.OP_TREE` that implements Phased ISWAP gate - """ - - yield cirq.PhasedXPowGate(phase_exponent=0.41175161497166024, exponent=0.5653807577895922).on(a) - yield cirq.PhasedXPowGate(phase_exponent=1.0, exponent=0.5).on(b), - yield (cirq.Z ** 0.7099892314883478).on(b), - yield (cirq.Z ** 0.6746023442550453).on(a), - yield SycamoreGate().on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=-0.5154334589432878, exponent=0.5228733015013345).on(b) - yield cirq.PhasedXPowGate(phase_exponent=0.06774925307475355).on(a) - yield SycamoreGate().on(a, b), - yield cirq.PhasedXPowGate(phase_exponent=-0.5987667922766213, exponent=0.4136540654256824).on(a) - yield (cirq.Z ** -0.9255092746611595).on(b) - yield (cirq.Z ** -1.333333333333333).on(a) - yield cirq.rx(-theta).on(a) - yield cirq.rx(-theta).on(b) - - yield cirq.PhasedXPowGate(phase_exponent=0.5678998743900456, exponent=0.5863459345743176).on(a) - yield cirq.PhasedXPowGate(phase_exponent=0.3549946157441739).on(b) - yield SycamoreGate().on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=-0.5154334589432878, exponent=0.5228733015013345).on(b) - yield cirq.PhasedXPowGate(phase_exponent=0.06774925307475355).on(a) - yield SycamoreGate().on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=-0.8151665352515929, exponent=0.8906746535691492).on(a) - yield cirq.PhasedXPowGate(phase_exponent=-0.07449072533884049, exponent=0.5).on(b) - yield (cirq.Z ** -0.9255092746611595).on(b) - yield (cirq.Z ** -0.9777346353961884).on(a) - - -def decompose_arbitrary_into_syc_tabulation( - qubit_a: cirq.Qid, - qubit_b: cirq.Qid, - op: cirq.Operation, - tabulation: cirq.TwoQubitGateTabulation, -) -> cirq.OP_TREE: - """Synthesize an arbitrary 2 qubit operation to a Sycamore operation using the given Tabulation. - - Args: - qubit_a: First qubit of the operation. - qubit_b: second qubit of the operation. - op: Operation to decompose. - tabulation: A tabulation for the Sycamore gate. - Yields: - A `cirq.OP_TREE` that perform the given operation using Sycamore operations. - """ - result = tabulation.compile_two_qubit_gate(cirq.unitary(op)) - local_gates = result.local_unitaries - for i, (gate_a, gate_b) in enumerate(local_gates): - yield from _phased_x_z_ops(gate_a, qubit_a) - yield from _phased_x_z_ops(gate_b, qubit_b) - if i != len(local_gates) - 1: - yield google.SYC.on(qubit_a, qubit_b) - - -def decompose_arbitrary_into_syc_analytic( - qubit_a: cirq.Qid, qubit_b: cirq.Qid, op: cirq.Operation -) -> cirq.OP_TREE: - """Synthesize an arbitrary 2 qubit operation to a Sycamore operation using the given Tabulation. - - Args: - qubit_a: First qubit of the operation. - qubit_b: Second qubit of the operation. - op: Operation to decompose. - tabulation: A tabulation for the Sycamore gate. - Yields: - A `cirq.OP_TREE` which produces the given operation using Sycamores. - """ - new_ops = cirq.two_qubit_matrix_to_operations(qubit_a, qubit_b, op, allow_partial_czs=True) - for new_op in new_ops: - num_qubits = len(new_op.qubits) - if num_qubits == 1: - (a,) = new_op.qubits - yield from _phased_x_z_ops(cirq.unitary(new_op), a) - elif num_qubits == 2: - a, b = op.qubits - yield from cirq.flatten_to_ops( - known_two_q_operations_to_sycamore_operations(a, b, new_op) - ) - - -def decompose_cz_into_syc(a: cirq.Qid, b: cirq.Qid): - """Decompose CZ into sycamore gates using precomputed coefficients.""" - yield cirq.PhasedXPowGate(phase_exponent=0.5678998743900456, exponent=0.5863459345743176).on(a) - yield cirq.PhasedXPowGate(phase_exponent=0.3549946157441739).on(b) - yield google.SYC.on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=-0.5154334589432878, exponent=0.5228733015013345).on(b) - yield cirq.PhasedXPowGate(phase_exponent=0.06774925307475355).on(a) - yield google.SYC.on(a, b), - yield cirq.PhasedXPowGate(phase_exponent=-0.5987667922766213, exponent=0.4136540654256824).on( - a - ), - yield (cirq.Z ** -0.9255092746611595).on(b), - yield (cirq.Z ** -1.333333333333333).on(a), - - -def decompose_iswap_into_syc(a: cirq.Qid, b: cirq.Qid): - """Decompose ISWAP into sycamore gates using precomputed coefficients.""" - yield cirq.PhasedXPowGate(phase_exponent=-0.27250925776964596, exponent=0.2893438375555899).on( - a - ) - yield google.SYC.on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=0.8487591858680898, exponent=0.9749387200813147).on(b), - yield cirq.PhasedXPowGate(phase_exponent=-0.3582574564210601).on(a), - yield google.SYC.on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=0.9675022326694225, exponent=0.6908986856555526).on(a), - yield google.SYC.on(a, b), - yield cirq.PhasedXPowGate(phase_exponent=0.9161706861686068, exponent=0.14818318325264102).on( - b - ), - yield cirq.PhasedXPowGate(phase_exponent=0.9408341606787907).on(a), - yield (cirq.Z ** -1.1551880579397293).on(b), - yield (cirq.Z ** 0.31848343246696365).on(a), - - -def decompose_swap_into_syc(a: cirq.Qid, b: cirq.Qid): - """Decompose SWAP into sycamore gates using precomputed coefficients""" - yield cirq.PhasedXPowGate(phase_exponent=0.44650378384076217, exponent=0.8817921214052824).on(a) - yield cirq.PhasedXPowGate(phase_exponent=-0.7656774060816165, exponent=0.6628666504604785).on(b) - yield google.SYC.on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=-0.6277589946716742, exponent=0.5659160932099687).on(a) - yield google.SYC.on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=0.28890767199499257, exponent=0.4340839067900317).on(b) - yield cirq.PhasedXPowGate(phase_exponent=-0.22592784059288928).on(a) - yield google.SYC.on(a, b) - yield cirq.PhasedXPowGate(phase_exponent=-0.4691261557936808, exponent=0.7728525693920243).on(a) - yield cirq.PhasedXPowGate(phase_exponent=-0.8150261316932077, exponent=0.11820787859471782).on( - b - ) - yield (cirq.Z ** -0.7384700844660306).on(b) - yield (cirq.Z ** -0.7034535141382525).on(a) - - -def find_local_equivalents(unitary1: np.ndarray, unitary2: np.ndarray): - """Determine the local unitaries that turn one unitary into the other. - - 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: The unitary that we target. - unitary2: The unitary that we transform the local gates to the target. - - Returns: - Four 2x2 unitaries. The first two are pre-rotations last two are post rotations. - """ - kak_u1 = cirq.kak_decomposition(unitary1) - kak_u2 = cirq.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: cirq.Circuit, q0: cirq.Qid, q1: cirq.Qid -) -> cirq.OP_TREE: - # Get the local equivalents - b_0, b_1, a_0, a_1 = find_local_equivalents( - target_unitary, program.unitary(qubit_order=cirq.QubitOrder.explicit([q0, q1])) - ) - - # Apply initial corrections - yield from _phased_x_z_ops(b_0, q0) - yield from _phased_x_z_ops(b_1, q1) - - # Apply interaction part - yield program - - # Apply final corrections - yield from _phased_x_z_ops(a_0, q0) - yield from _phased_x_z_ops(a_1, q1) - - -def _phased_x_z_ops(mat: np.ndarray, q: cirq.Qid) -> Iterator[cirq.Operation]: - gate = cirq.single_qubit_matrix_to_phxz(mat) - if gate: - yield gate(q) - - -def rzz(theta: float, q0: cirq.Qid, q1: cirq.Qid) -> cirq.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 - Yields: - The `cirq.OP_TREE` that produce the Ising gate with Sycamore gates. - """ - 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 = cirq.Circuit( - google.SYC.on(q0, q1), cirq.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: cirq.Qid, q1: cirq.Qid) -> cirq.OP_TREE: - """Implements 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: The phase to apply, exp(1j * theta). - q0: First qubit id to operate on. - q1: Second qubit id to operate on. - Yields: - The gates the perform the cphase using Sycamore gates. - """ - yield rzz(-theta / 4, q0, q1) - yield cirq.rz(theta / 2).on(q0) - yield cirq.rz(theta / 2).on(q1) - - -def swap_rzz(theta: float, q0: cirq.Qid, q1: cirq.Qid) -> cirq.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: The phase in the cphase gate, exp(1j * theta ) - q0: First qubit id to operate on. - q1: Second qubit id to operate on. - Yields: - The `cirq.OP_TREE`` that implements ZZ followed by a swap. - """ - - # Set interaction part. - circuit = cirq.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 = cirq.Circuit( - cirq.SWAP(q0, q1), cirq.ZZPowGate(exponent=2 * theta / np.pi, global_shift=-0.5).on(q0, q1) - ) - - yield create_corrected_circuit(intended_circuit, circuit, q0, q1) diff --git a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py index c193b506007..af9ed9518f1 100644 --- a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py +++ b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py @@ -2,8 +2,6 @@ import pytest import numpy as np -import scipy.linalg - import cirq import cirq_google import cirq_google.optimizers.convert_to_sycamore_gates as cgoc @@ -112,7 +110,7 @@ class UnknownGate(cirq.testing.TwoQubitGate): q0, q1 = cirq.LineQubit.range(2) circuit = cirq.Circuit(UnknownGate()(q0, q1)) - with pytest.raises(ValueError, match='Unrecognized gate: '): + with pytest.raises(TypeError, match='gate with a known unitary'): cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) @@ -124,7 +122,7 @@ class UnknownGate(cirq.testing.TwoQubitGate): q1 = cirq.LineQubit(1) subcircuit = cirq.FrozenCircuit(UnknownGate()(q0, q1)) circuit = cirq.Circuit(cirq.CircuitOperation(subcircuit)) - with pytest.raises(ValueError, match='Unrecognized gate: '): + with pytest.raises(TypeError, match='gate with a known unitary'): cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) @@ -134,8 +132,11 @@ def test_unsupported_phased_iswap(): q0 = cirq.LineQubit(0) q1 = cirq.LineQubit(1) circuit = cirq.Circuit(cirq.PhasedISwapPowGate(exponent=0.5, phase_exponent=0.33)(q0, q1)) - with pytest.raises(ValueError, match='phase_exponent of .25 OR an exponent of 1'): - cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) + converted_circuit = circuit.copy() + cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, converted_circuit, atol=1e-8 + ) def test_non_gate_operation(): @@ -186,28 +187,6 @@ def test_unitary_decomp(): assert np.isclose(abs(np.trace(cirq.unitary(circuit).conj().T @ random_unitary)), 2.0) -def test_zztheta(): - zz = np.kron(cirq.unitary(cirq.Z), cirq.unitary(cirq.Z)) - qubits = cirq.LineQubit.range(2) - for theta in np.linspace(0, 2 * np.pi, 10): - expected_unitary = scipy.linalg.expm(-1j * theta * zz) - circuit = cirq.Circuit(cgoc.rzz(theta, qubits[0], qubits[1])) - actual_unitary = cirq.unitary(circuit) - cirq.testing.assert_allclose_up_to_global_phase(actual_unitary, expected_unitary, atol=1e-7) - - -def test_zztheta_zzpow(): - qubits = cirq.LineQubit.range(2) - for theta in np.linspace(0, 2 * np.pi, 10): - syc_circuit = cirq.Circuit(cgoc.rzz(theta, qubits[0], qubits[1])) - cirq_circuit = cirq.Circuit( - [cirq.ZZPowGate(exponent=2 * theta / np.pi, global_shift=-0.5).on(*qubits)] - ) - cirq.testing.assert_allclose_up_to_global_phase( - cirq.unitary(cirq_circuit), cirq.unitary(syc_circuit), atol=1e-7 - ) - - def test_zztheta_qaoa_like(): qubits = cirq.LineQubit.range(4) for exponent in np.linspace(-1, 1, 10): diff --git a/cirq-google/cirq_google/transformers/__init__.py b/cirq-google/cirq_google/transformers/__init__.py new file mode 100644 index 00000000000..8561e45f8ed --- /dev/null +++ b/cirq-google/cirq_google/transformers/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2022 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 cirq_google.transformers.analytical_decompositions import ( + known_2q_op_to_sycamore_operations, + two_qubit_matrix_to_sycamore_operations, +) diff --git a/cirq-google/cirq_google/transformers/analytical_decompositions/__init__.py b/cirq-google/cirq_google/transformers/analytical_decompositions/__init__.py new file mode 100644 index 00000000000..2ce8047eeeb --- /dev/null +++ b/cirq-google/cirq_google/transformers/analytical_decompositions/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2022 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 cirq_google.transformers.analytical_decompositions.two_qubit_to_sycamore import ( + known_2q_op_to_sycamore_operations, + two_qubit_matrix_to_sycamore_operations, +) diff --git a/cirq-google/cirq_google/transformers/analytical_decompositions/two_qubit_to_sycamore.py b/cirq-google/cirq_google/transformers/analytical_decompositions/two_qubit_to_sycamore.py new file mode 100644 index 00000000000..c8837d3c3ea --- /dev/null +++ b/cirq-google/cirq_google/transformers/analytical_decompositions/two_qubit_to_sycamore.py @@ -0,0 +1,455 @@ +# Copyright 2022 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. + +"""Utility methods for decomposing two-qubit unitaries into Sycamore gates.""" + +from typing import Iterator, List, Optional + +import itertools +import math +import numpy as np + +import cirq +from cirq_google import ops + + +def _decompose_arbitrary_into_syc_tabulation( + op: cirq.Operation, tabulation: cirq.TwoQubitGateTabulation +) -> cirq.OP_TREE: + """Synthesize an arbitrary 2 qubit operation to a Sycamore operation using the given Tabulation. + + Args: + op: Operation to decompose. + tabulation: A `cirq.TwoQubitGateTabulation` for the Sycamore gate. + + Yields: + A `cirq.OP_TREE` that performs the given operation using Sycamore operations. + """ + qubit_a, qubit_b = op.qubits + result = tabulation.compile_two_qubit_gate(cirq.unitary(op)) + local_gates = result.local_unitaries + for i, (gate_a, gate_b) in enumerate(local_gates): + yield from _phased_x_z_ops(gate_a, qubit_a) + yield from _phased_x_z_ops(gate_b, qubit_b) + if i != len(local_gates) - 1: + yield ops.SYC.on(qubit_a, qubit_b) + + +def two_qubit_matrix_to_sycamore_operations( + q0: cirq.Qid, + q1: cirq.Qid, + mat: np.ndarray, + *, + atol: float = 1e-8, + clean_operations: bool = True, +) -> cirq.OP_TREE: + """Decomposes a two-qubit unitary matrix into `cirq_google.SYC` + single qubit rotations. + + The analytical decomposition first Synthesizes the given operation using `cirq.CZPowGate` + + single qubit rotations and then decomposes each `cirq.CZPowGate` into `cirq_google.SYC` + + single qubit rotations using `cirq_google.known_2q_op_to_sycamore_operations`. + + Note that the resulting decomposition may not be optimal, and users should first try to + decompose a given operation using `cirq_google.known_2q_op_to_sycamore_operations`. + + Args: + q0: The first qubit being operated on. + q1: The other qubit being operated on. + mat: Defines the operation to apply to the pair of qubits. + atol: A limit on the amount of absolute error introduced by the + construction. + clean_operations: Merges runs of single qubit gates to a single `cirq.PhasedXZGate` in + the resulting operations list. + + Returns: + A `cirq.OP_TREE` that implements the given unitary operation using only `cirq_google.SYC` + + single qubit rotations. + """ + decomposed_ops: List[cirq.OP_TREE] = [] + for op in cirq.two_qubit_matrix_to_operations( + q0, q1, mat, allow_partial_czs=True, atol=atol, clean_operations=clean_operations + ): + if cirq.num_qubits(op) == 2: + decomposed_cphase = known_2q_op_to_sycamore_operations(op) + assert decomposed_cphase is not None + decomposed_ops.append(decomposed_cphase) + else: + decomposed_ops.append(op) + return ( + [*cirq.merge_single_qubit_gates_to_phxz(cirq.Circuit(decomposed_ops)).all_operations()] + if clean_operations + else decomposed_ops + ) + + +def known_2q_op_to_sycamore_operations(op: cirq.Operation) -> Optional[cirq.OP_TREE]: + """Synthesizes a known two-qubit operation using `cirq_google.SYC` + single qubit rotations. + + This function dispatches to various known gate decompositions based on gate type. Currently, + the following gates are known: + 1. Adjacent `cirq.SWAP` and `cirq.ZPowGate` wrapped in a circuit operation of length 2. + 2. `cirq.PhasedISwapPowGate` with exponent = 1 or phase_exponent = 0.25. + 3. `cirq.SWAP`, `cirq.ISWAP`. + 4. `cirq.CNotPowGate`, `cirq.CZPowGate`, `cirq.ZZPowGate`. + + Args: + op: Operation to decompose. + + Returns: + - A `cirq.OP_TREE` that implements the given known operation using only `cirq_google.SYC` + + single qubit rotations OR + - None if `op` is not a known operation. + """ + if not (cirq.has_unitary(op) and cirq.num_qubits(op) == 2): + return None + + q0, q1 = op.qubits + + if isinstance(op.untagged, cirq.CircuitOperation): + flattened_gates = [o.gate for o in cirq.decompose_once(op.untagged)] + if len(flattened_gates) != 2: + return None + for g1, g2 in itertools.permutations(flattened_gates): + if g1 == cirq.SWAP and isinstance(g2, cirq.ZZPowGate): + return _swap_rzz(g2.exponent * np.pi / 2, q0, q1) + + gate = op.gate + if isinstance(gate, cirq.PhasedISwapPowGate): + if math.isclose(gate.exponent, 1): + return _decompose_phased_iswap_into_syc(gate.phase_exponent, q0, q1) + if math.isclose(gate.phase_exponent, 0.25): + return _decompose_phased_iswap_into_syc_precomputed(gate.exponent * np.pi / 2, q0, q1) + return None + if isinstance(gate, cirq.CNotPowGate): + return [ + cirq.Y(q1) ** -0.5, + _decompose_cphase_into_syc(gate.exponent * np.pi, q0, q1), + cirq.Y(q1) ** 0.5, + ] + if isinstance(gate, cirq.CZPowGate): + return ( + _decompose_cz_into_syc(q0, q1) + if math.isclose(gate.exponent, 1) + else _decompose_cphase_into_syc(gate.exponent * np.pi, q0, q1) + ) + if isinstance(gate, cirq.SwapPowGate) and math.isclose(gate.exponent, 1): + return _decompose_swap_into_syc(q0, q1) + if isinstance(gate, cirq.ISwapPowGate) and math.isclose(gate.exponent, 1): + return _decompose_iswap_into_syc(q0, q1) + if isinstance(gate, cirq.ZZPowGate): + return _rzz(gate.exponent * np.pi / 2, q0, q1) + + return None + + +def _decompose_phased_iswap_into_syc( + phase_exponent: float, a: cirq.Qid, b: cirq.Qid +) -> cirq.OP_TREE: + """Decomposes `cirq.PhasedISwapPowGate` with an exponent of 1 into Sycamore gates. + + This should only be called if the gate has an exponent of 1. Otherwise, + `_decompose_phased_iswap_into_syc_precomputed` should be used instead. The advantage of using + this function is that the resulting circuit will be smaller. + + Args: + phase_exponent: The exponent on the Z gates. + a: First qubit to operate on. + b: Second qubit to operate on. + + Yields: + A `cirq.OP_TREE` implementing the `cirq.PhasedISwapPowGate` gate using Sycamore gates. + """ + + yield cirq.Z(a) ** phase_exponent, + yield cirq.Z(b) ** -phase_exponent, + yield _decompose_iswap_into_syc(a, b), + yield cirq.Z(a) ** -phase_exponent, + yield cirq.Z(b) ** phase_exponent, + + +def _decompose_phased_iswap_into_syc_precomputed( + theta: float, a: cirq.Qid, b: cirq.Qid +) -> cirq.OP_TREE: + """Decomposes `cirq.PhasedISwapPowGate` into Sycamore gates using precomputed coefficients. + + This should only be called if the Gate has a phase_exponent of .25. If the gate has an + exponent of 1, _decompose_phased_iswap_into_syc should be used instead. Converting PhasedISwap + gates to Sycamore is not supported if neither of these constraints are satisfied. + + This synthesize a PhasedISwap in terms of four sycamore gates. This compilation converts the + gate into a circuit involving two CZ gates, which themselves are each represented as two + Sycamore gates and single-qubit rotations + + Args: + theta: Rotation parameter for the phased ISWAP. + a: First qubit to operate on. + b: Second qubit to operate on. + + Yields: + A `cirq.OP_TREE` implementing the `cirq.PhasedISwapPowGate` gate using Sycamore gates. + """ + + yield cirq.PhasedXPowGate(phase_exponent=0.41175161497166024, exponent=0.5653807577895922).on(a) + yield cirq.PhasedXPowGate(phase_exponent=1.0, exponent=0.5).on(b), + yield (cirq.Z ** 0.7099892314883478).on(b), + yield (cirq.Z ** 0.6746023442550453).on(a), + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=-0.5154334589432878, exponent=0.5228733015013345).on(b) + yield cirq.PhasedXPowGate(phase_exponent=0.06774925307475355).on(a) + yield ops.SYC(a, b), + yield cirq.PhasedXPowGate(phase_exponent=-0.5987667922766213, exponent=0.4136540654256824).on(a) + yield (cirq.Z ** -0.9255092746611595).on(b) + yield (cirq.Z ** -1.333333333333333).on(a) + yield cirq.rx(-theta).on(a) + yield cirq.rx(-theta).on(b) + + yield cirq.PhasedXPowGate(phase_exponent=0.5678998743900456, exponent=0.5863459345743176).on(a) + yield cirq.PhasedXPowGate(phase_exponent=0.3549946157441739).on(b) + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=-0.5154334589432878, exponent=0.5228733015013345).on(b) + yield cirq.PhasedXPowGate(phase_exponent=0.06774925307475355).on(a) + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=-0.8151665352515929, exponent=0.8906746535691492).on(a) + yield cirq.PhasedXPowGate(phase_exponent=-0.07449072533884049, exponent=0.5).on(b) + yield (cirq.Z ** -0.9255092746611595).on(b) + yield (cirq.Z ** -0.9777346353961884).on(a) + + +def _decompose_cz_into_syc(a: cirq.Qid, b: cirq.Qid): + """Decomposes `cirq.CZ` into sycamore gates using precomputed coefficients. + + This should only be called when exponent of `cirq.CZPowGate` is 1. Otherwise, + `_decompose_cphase_into_syc` should be called. + + Args: + a: First qubit to operate on. + b: Second qubit to operate on. + + Yields: + A `cirq.OP_TREE` implementing the `cirq.CZ` gate using Sycamore gates. + """ + yield cirq.PhasedXPowGate(phase_exponent=0.5678998743900456, exponent=0.5863459345743176).on(a) + yield cirq.PhasedXPowGate(phase_exponent=0.3549946157441739).on(b) + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=-0.5154334589432878, exponent=0.5228733015013345).on(b) + yield cirq.PhasedXPowGate(phase_exponent=0.06774925307475355).on(a) + yield ops.SYC(a, b), + yield cirq.PhasedXPowGate(phase_exponent=-0.5987667922766213, exponent=0.4136540654256824).on( + a + ), + yield (cirq.Z ** -0.9255092746611595).on(b), + yield (cirq.Z ** -1.333333333333333).on(a), + + +def _decompose_cphase_into_syc(theta: float, q0: cirq.Qid, q1: cirq.Qid) -> cirq.OP_TREE: + """Implements 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 Rzz + Ising gate + single qubit Z rotations. We drop the global phase shift of theta / 4. + + Args: + theta: The phase to apply, exp(1j * theta). + q0: First qubit to operate on. + q1: Second qubit to operate on. + + Yields: + A `cirq.OP_TREE` implementing the cphase gate using Sycamore gates. + """ + yield _rzz(-theta / 4, q0, q1) + yield cirq.rz(theta / 2).on(q0) + yield cirq.rz(theta / 2).on(q1) + + +def _decompose_iswap_into_syc(a: cirq.Qid, b: cirq.Qid): + """Decomposes `cirq.ISWAP` into sycamore gates using precomputed coefficients. + + This should only be called when exponent of `cirq.ISwapPowGate` is 1. Other cases are currently + not supported. + + Args: + a: First qubit to operate on. + b: Second qubit to operate on. + + Yields: + A `cirq.OP_TREE` implementing the `cirq.ISWAP` gate using Sycamore gates. + """ + yield cirq.PhasedXPowGate(phase_exponent=-0.27250925776964596, exponent=0.2893438375555899).on( + a + ) + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=0.8487591858680898, exponent=0.9749387200813147).on(b), + yield cirq.PhasedXPowGate(phase_exponent=-0.3582574564210601).on(a), + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=0.9675022326694225, exponent=0.6908986856555526).on(a), + yield ops.SYC(a, b), + yield cirq.PhasedXPowGate(phase_exponent=0.9161706861686068, exponent=0.14818318325264102).on( + b + ), + yield cirq.PhasedXPowGate(phase_exponent=0.9408341606787907).on(a), + yield (cirq.Z ** -1.1551880579397293).on(b), + yield (cirq.Z ** 0.31848343246696365).on(a), + + +def _decompose_swap_into_syc(a: cirq.Qid, b: cirq.Qid): + """Decomposes `cirq.SWAP` into sycamore gates using precomputed coefficients. + + This should only be called when exponent of `cirq.SwapPowGate` is 1. Other cases are currently + not supported. + + Args: + a: First qubit to operate on. + b: Second qubit to operate on. + + Yields: + A `cirq.OP_TREE` implementing the `cirq.SWAP` gate using Sycamore gates. + """ + yield cirq.PhasedXPowGate(phase_exponent=0.44650378384076217, exponent=0.8817921214052824).on(a) + yield cirq.PhasedXPowGate(phase_exponent=-0.7656774060816165, exponent=0.6628666504604785).on(b) + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=-0.6277589946716742, exponent=0.5659160932099687).on(a) + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=0.28890767199499257, exponent=0.4340839067900317).on(b) + yield cirq.PhasedXPowGate(phase_exponent=-0.22592784059288928).on(a) + yield ops.SYC(a, b) + yield cirq.PhasedXPowGate(phase_exponent=-0.4691261557936808, exponent=0.7728525693920243).on(a) + yield cirq.PhasedXPowGate(phase_exponent=-0.8150261316932077, exponent=0.11820787859471782).on( + b + ) + yield (cirq.Z ** -0.7384700844660306).on(b) + yield (cirq.Z ** -0.7034535141382525).on(a) + + +def _find_local_equivalents(target_unitary: np.ndarray, source_unitary: np.ndarray): + """Determine the local 1q rotations that turn one equivalent 2q unitary into the other. + + Given two 2q 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 Decomposition on each unitary and confirm interaction terms are equivalent. + 2) Identify the elements of SU(2) to transform source_unitary into target_unitary + + Args: + target_unitary: The unitary that we need to transform `source_unitary` to. + source_unitary: The unitary that we need to transform by adding local gates, and make it + equivalent to the target_unitary. + + Returns: + Four 2x2 unitaries. The first two are pre-rotations and last two are post rotations. + """ + kak_u1 = cirq.kak_decomposition(target_unitary) + kak_u2 = cirq.kak_decomposition(source_unitary) + + 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: cirq.Circuit, q0: cirq.Qid, q1: cirq.Qid +) -> cirq.OP_TREE: + """Adds pre/post single qubit rotations to `program` to make it equivalent to `target_unitary`. + + Adds single qubit correction terms to the given circuit on 2 qubit s.t. it implements + `target_unitary`. This assumes that `program` implements a 2q unitary effect which has same + interaction coefficients as `target_unitary` in it's KAK decomposition and differs only in + local unitary rotations. + + Args: + target_unitary: The unitary that should be implemented by the transformed `program`. + program: `cirq.Circuit` to be transformed. + q0: First qubit to operate on. + q1: Second qubit to operate on. + + Yields: + Operations in `program` with pre and post rotations added s.t. the resulting `cirq.OP_TREE` + implements `target_unitary`. + """ + # Get the local equivalents + b_0, b_1, a_0, a_1 = _find_local_equivalents( + target_unitary, program.unitary(qubit_order=cirq.QubitOrder.explicit([q0, q1])) + ) + + # Apply initial corrections + yield from _phased_x_z_ops(b_0, q0) + yield from _phased_x_z_ops(b_1, q1) + + # Apply interaction part + yield program + + # Apply final corrections + yield from _phased_x_z_ops(a_0, q0) + yield from _phased_x_z_ops(a_1, q1) + + +def _phased_x_z_ops(mat: np.ndarray, q: cirq.Qid) -> Iterator[cirq.Operation]: + """Yields `cirq.PhasedXZGate` operation implementing `mat` if it is not identity.""" + gate = cirq.single_qubit_matrix_to_phxz(mat) + if gate: + yield gate(q) + + +def _rzz(theta: float, q0: cirq.Qid, q1: cirq.Qid) -> cirq.OP_TREE: + """Implements the Rzz Ising coupling gate (i.e. exp(-1j * theta * zz)) using Sycamore gates. + + Args: + theta: The rotation parameter of Rzz Ising coupling gate. + q0: First qubit to operate on + q1: Second qubit to operate on + + Yields: + The `cirq.OP_TREE` that implements the Rzz Ising coupling gate using Sycamore gates. + """ + phi = -np.pi / 24 + c_phi = np.cos(2 * phi) + target_unitary = cirq.unitary(cirq.ZZPowGate(exponent=2 * theta / np.pi, global_shift=-0.5)) + c2 = abs(np.sin(theta) if abs(np.cos(theta)) > c_phi else np.cos(theta)) / c_phi + + # Prepare program that has same Schmidt coefficients as exp(-1j theta ZZ) + program = cirq.Circuit(ops.SYC(q0, q1), cirq.rx(2 * np.arccos(c2)).on(q1), ops.SYC(q0, q1)) + + yield _create_corrected_circuit(target_unitary, program, q0, q1) + + +def _swap_rzz(theta: float, q0: cirq.Qid, q1: cirq.Qid) -> cirq.OP_TREE: + """An implementation of SWAP * exp(-1j * theta * ZZ) using three sycamore gates. + + This builds off of the _rzz method. + + Args: + theta: The rotation parameter of Rzz Ising coupling gate. + q0: First qubit to operate on. + q1: Second qubit to operate on. + + Yields: + The `cirq.OP_TREE`` that implements ZZ followed by a swap. + """ + + # Set interaction part. + angle_offset = np.pi / 24 - np.pi / 4 + circuit = cirq.Circuit(ops.SYC(q0, q1), _rzz(theta - angle_offset, q0, q1)) + + # Get the intended circuit. + intended_circuit = cirq.Circuit( + cirq.SWAP(q0, q1), cirq.ZZPowGate(exponent=2 * theta / np.pi, global_shift=-0.5).on(q0, q1) + ) + + yield _create_corrected_circuit(cirq.unitary(intended_circuit), circuit, q0, q1) diff --git a/cirq-google/cirq_google/transformers/analytical_decompositions/two_qubit_to_sycamore_test.py b/cirq-google/cirq_google/transformers/analytical_decompositions/two_qubit_to_sycamore_test.py new file mode 100644 index 00000000000..94084191d15 --- /dev/null +++ b/cirq-google/cirq_google/transformers/analytical_decompositions/two_qubit_to_sycamore_test.py @@ -0,0 +1,81 @@ +# Copyright 2022 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. + +import cirq +import pytest +import numpy as np +import cirq_google as cg +import sympy + +EXPECTED_TARGET_GATESET = cirq.Gateset(cirq.AnyUnitaryGateFamily(1), cg.SYC) + + +def assert_implements(circuit: cirq.Circuit, target_op: cirq.Operation): + assert all(op in EXPECTED_TARGET_GATESET for op in circuit.all_operations()) + assert sum(1 for _ in circuit.findall_operations(lambda e: len(e.qubits) > 2)) <= 6 + circuit.append(cirq.I.on_each(*target_op.qubits)) + cirq.testing.assert_allclose_up_to_global_phase( + cirq.unitary(circuit), cirq.unitary(target_op), atol=1e-7 + ) + + +theta = sympy.Symbol('theta') +all_exps = np.linspace(0, 1, 10) +q = cirq.LineQubit.range(2) + + +@pytest.mark.parametrize( + 'op, theta_range', + [ + (cirq.CircuitOperation(cirq.FrozenCircuit(cirq.SWAP(*q), cirq.ZZ(*q) ** theta)), all_exps), + (cirq.CircuitOperation(cirq.FrozenCircuit(cirq.ZZ(*q) ** theta, cirq.SWAP(*q))), all_exps), + (cirq.PhasedISwapPowGate(exponent=1, phase_exponent=theta).on(*q), all_exps), + (cirq.PhasedISwapPowGate(exponent=theta, phase_exponent=0.25).on(*q), all_exps), + (cirq.CNOT(*q) ** theta, all_exps), + (cirq.CZ(*q) ** theta, all_exps), + (cirq.ZZ(*q) ** theta, all_exps), + (cirq.SWAP(*q) ** theta, [1]), + (cirq.ISWAP(*q) ** theta, [1]), + ], +) +def test_known_two_qubit_op_decomposition(op, theta_range): + for theta_val in theta_range: + op_resolved = cirq.resolve_parameters(op, {'theta': theta_val}, recursive=False) + known_2q_circuit = cirq.Circuit(cg.known_2q_op_to_sycamore_operations(op_resolved)) + matrix_2q_circuit = cirq.Circuit( + cg.two_qubit_matrix_to_sycamore_operations(q[0], q[1], cirq.unitary(op_resolved)) + ) + assert_implements(known_2q_circuit, op_resolved) + assert_implements(matrix_2q_circuit, op_resolved) + + +@pytest.mark.parametrize( + 'op', + [ + cirq.CircuitOperation(cirq.FrozenCircuit(cirq.SWAP(*q), cirq.ZZ(*q), cirq.SWAP(*q))), + cirq.X(q[0]), + cirq.XX(*q) ** theta, + cirq.FSimGate(0.25, 0.85).on(*q), + cirq.XX(*q), + cirq.YY(*q), + *[cirq.testing.random_unitary(4, random_state=1234) for _ in range(10)], + ], +) +def test_unknown_two_qubit_op_decomposition(op): + assert cg.known_2q_op_to_sycamore_operations(op) is None + if cirq.has_unitary(op) and cirq.num_qubits(op) == 2: + matrix_2q_circuit = cirq.Circuit( + cg.two_qubit_matrix_to_sycamore_operations(q[0], q[1], cirq.unitary(op)) + ) + assert_implements(matrix_2q_circuit, op)