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

Add Circuit Optimizer for sqrt-iSWAP #4224

Merged
merged 11 commits into from
Jul 7, 2021
1 change: 1 addition & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@
merge_single_qubit_gates_into_phased_x_z,
merge_single_qubit_gates_into_phxz,
MergeInteractions,
MergeInteractionsToSqrtIswap,
MergeSingleQubitGates,
single_qubit_matrix_to_gates,
single_qubit_matrix_to_pauli_rotations,
Expand Down
2 changes: 1 addition & 1 deletion cirq-core/cirq/circuits/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2312,7 +2312,7 @@ def _pick_inserted_ops_moment_indices(
frontier = defaultdict(lambda: 0)
moment_indices = []
for op in operations:
op_start = max(start, max(frontier[q] for q in op.qubits))
op_start = max(start, max((frontier[q] for q in op.qubits), default=0))
moment_indices.append(op_start)
for q in op.qubits:
frontier[q] = max(frontier[q], op_start + 1)
Expand Down
4 changes: 4 additions & 0 deletions cirq-core/cirq/optimizers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
MergeInteractions,
)

from cirq.optimizers.merge_interactions_to_sqrt_iswap import (
MergeInteractionsToSqrtIswap,
)

from cirq.optimizers.merge_single_qubit_gates import (
merge_single_qubit_gates_into_phased_x_z,
merge_single_qubit_gates_into_phxz,
Expand Down
97 changes: 85 additions & 12 deletions cirq-core/cirq/optimizers/merge_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""An optimization pass that combines adjacent single-qubit rotations."""
"""An optimization pass that combines adjacent series of gates on two qubits."""

from typing import Callable, List, Optional, Sequence, Tuple, cast, TYPE_CHECKING

import abc
import numpy as np

from cirq import circuits, ops, protocols
Expand All @@ -25,19 +26,25 @@
import cirq


class MergeInteractions(circuits.PointOptimizer):
class MergeInteractionsAbc(circuits.PointOptimizer, metaclass=abc.ABCMeta):
"""Combines series of adjacent one and two-qubit gates operating on a pair
of qubits."""

def __init__(
self,
tolerance: float = 1e-8,
allow_partial_czs: bool = True,
post_clean_up: Callable[[Sequence[ops.Operation]], ops.OP_TREE] = lambda op_list: op_list,
) -> None:
"""
Args:
tolerance: A limit on the amount of absolute error introduced by the
construction.
post_clean_up: This function is called on each set of optimized
operations before they are put into the circuit to replace the
old operations.
"""
super().__init__(post_clean_up=post_clean_up)
self.tolerance = tolerance
self.allow_partial_czs = allow_partial_czs

def optimization_at(
self, circuit: circuits.Circuit, index: int, op: ops.Operation
Expand All @@ -63,10 +70,8 @@ def optimization_at(
if not switch_to_new and old_interaction_count <= 1:
return None

# Find a max-3-cz construction.
new_operations = two_qubit_decompositions.two_qubit_matrix_to_operations(
op.qubits[0], op.qubits[1], matrix, self.allow_partial_czs, self.tolerance, False
)
# Find a (possibly ideal) decomposition of the merged operations.
new_operations = self._two_qubit_matrix_to_operations(op.qubits[0], op.qubits[1], matrix)
new_interaction_count = len(
[new_op for new_op in new_operations if len(new_op.qubits) == 2]
)
Expand All @@ -82,12 +87,29 @@ def optimization_at(
new_operations=new_operations,
)

@abc.abstractmethod
def _may_keep_old_op(self, old_op: 'cirq.Operation') -> bool:
"""Returns True if the old two-qubit operation may be left unchanged
without decomposition."""
if self.allow_partial_czs:
return isinstance(old_op.gate, ops.CZPowGate)
return isinstance(old_op.gate, ops.CZPowGate) and old_op.gate.exponent == 1

@abc.abstractmethod
def _two_qubit_matrix_to_operations(
self,
q0: 'cirq.Qid',
q1: 'cirq.Qid',
mat: np.ndarray,
) -> Sequence['cirq.Operation']:
"""Decomposes the merged two-qubit gate unitary into the minimum number
of two-qubit gates.

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.

Returns:
A list of operations implementing the matrix.
"""

def _op_to_matrix(
self, op: ops.Operation, qubits: Tuple['cirq.Qid', ...]
Expand Down Expand Up @@ -130,7 +152,7 @@ def _op_to_matrix(

def _scan_two_qubit_ops_into_matrix(
self, circuit: circuits.Circuit, index: Optional[int], qubits: Tuple['cirq.Qid', ...]
) -> Tuple[List[ops.Operation], List[int], np.ndarray]:
) -> Tuple[Sequence[ops.Operation], List[int], np.ndarray]:
"""Accumulates operations affecting the given pair of qubits.

The scan terminates when it hits the end of the circuit, finds an
Expand Down Expand Up @@ -181,3 +203,54 @@ def _flip_kron_order(mat4x4: np.ndarray) -> np.ndarray:
for j in range(4):
result[order[i], order[j]] = mat4x4[i, j]
return result


class MergeInteractions(MergeInteractionsAbc):
"""Combines series of adjacent one and two-qubit gates operating on a pair
cduck marked this conversation as resolved.
Show resolved Hide resolved
of qubits and replaces each series with the minimum number of CZ gates."""

def __init__(
self,
tolerance: float = 1e-8,
allow_partial_czs: bool = True,
post_clean_up: Callable[[Sequence[ops.Operation]], ops.OP_TREE] = lambda op_list: op_list,
) -> None:
"""
Args:
tolerance: A limit on the amount of absolute error introduced by the
construction.
allow_partial_czs: Enables the use of Partial-CZ gates.
post_clean_up: This function is called on each set of optimized
operations before they are put into the circuit to replace the
old operations.
"""
super().__init__(tolerance=tolerance, post_clean_up=post_clean_up)
self.allow_partial_czs = allow_partial_czs

def _may_keep_old_op(self, old_op: 'cirq.Operation') -> bool:
"""Returns True if the old two-qubit operation may be left unchanged
without decomposition."""
if self.allow_partial_czs:
return isinstance(old_op.gate, ops.CZPowGate)
return isinstance(old_op.gate, ops.CZPowGate) and old_op.gate.exponent == 1
cduck marked this conversation as resolved.
Show resolved Hide resolved

def _two_qubit_matrix_to_operations(
self,
q0: 'cirq.Qid',
q1: 'cirq.Qid',
mat: np.ndarray,
) -> Sequence['cirq.Operation']:
"""Decomposes the merged two-qubit gate unitary into the minimum number
of CZ gates.

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.

Returns:
A list of operations implementing the matrix.
"""
return two_qubit_decompositions.two_qubit_matrix_to_operations(
q0, q1, mat, self.allow_partial_czs, self.tolerance, False
)
104 changes: 104 additions & 0 deletions cirq-core/cirq/optimizers/merge_interactions_to_sqrt_iswap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright 2021 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.

"""An optimization pass that combines adjacent series of gates on two qubits and
outputs a circuit with SQRT_ISWAP or SQRT_ISWAP_INV gates."""

from typing import Callable, Optional, Sequence, TYPE_CHECKING

import numpy as np

from cirq import ops
from cirq.optimizers import two_qubit_to_sqrt_iswap, merge_interactions

if TYPE_CHECKING:
import cirq


class MergeInteractionsToSqrtIswap(merge_interactions.MergeInteractionsAbc):
"""Combines series of adjacent one and two-qubit gates operating on a pair
of qubits and replaces each series with the minimum number of SQRT_ISWAP
gates.

See also: ``two_qubit_matrix_to_sqrt_iswap_operations``
"""

def __init__(
self,
tolerance: float = 1e-8,
*,
required_sqrt_iswap_count: Optional[int] = None,
use_sqrt_iswap_inv: bool = False,
post_clean_up: Callable[[Sequence[ops.Operation]], ops.OP_TREE] = lambda op_list: op_list,
) -> None:
"""
Args:
tolerance: A limit on the amount of absolute error introduced by the
construction.
required_sqrt_iswap_count: When specified, each merged group of
two-qubit gates will be decomposed into exactly this many
sqrt-iSWAP gates even if fewer is possible (maximum 3). Circuit
optimization will raise a ``ValueError`` if this number is 2 or
lower and synthesis of any set of merged interactions requires
more.
use_sqrt_iswap_inv: If True, optimizes circuits using
``SQRT_ISWAP_INV`` gates instead of ``SQRT_ISWAP``.
post_clean_up: This function is called on each set of optimized
operations before they are put into the circuit to replace the
old operations.

Raises:
ValueError:
If ``required_sqrt_iswap_count`` is not one of the supported
values 0, 1, 2, or 3.
"""
if required_sqrt_iswap_count is not None and not 0 <= required_sqrt_iswap_count <= 3:
raise ValueError('the argument `required_sqrt_iswap_count` must be 0, 1, 2, or 3.')
super().__init__(tolerance=tolerance, post_clean_up=post_clean_up)
self.required_sqrt_iswap_count = required_sqrt_iswap_count
self.use_sqrt_iswap_inv = use_sqrt_iswap_inv

def _may_keep_old_op(self, old_op: 'cirq.Operation') -> bool:
"""Returns True if the old two-qubit operation may be left unchanged
without decomposition."""
if self.use_sqrt_iswap_inv:
return isinstance(old_op.gate, ops.ISwapPowGate) and old_op.gate.exponent == -0.5
return isinstance(old_op.gate, ops.ISwapPowGate) and old_op.gate.exponent == 0.5
cduck marked this conversation as resolved.
Show resolved Hide resolved

def _two_qubit_matrix_to_operations(
self,
q0: 'cirq.Qid',
q1: 'cirq.Qid',
mat: np.ndarray,
) -> Sequence['cirq.Operation']:
"""Decomposes the merged two-qubit gate unitary into the minimum number
of SQRT_ISWAP gates.

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.

Returns:
A list of operations implementing the matrix.
"""
return two_qubit_to_sqrt_iswap.two_qubit_matrix_to_sqrt_iswap_operations(
q0,
q1,
mat,
required_sqrt_iswap_count=self.required_sqrt_iswap_count,
use_sqrt_iswap_inv=self.use_sqrt_iswap_inv,
atol=self.tolerance,
check_preconditions=False,
)