Skip to content

Commit

Permalink
Add ParallelGate and deprecate ParallelGateOperation (#4398)
Browse files Browse the repository at this point in the history
This PR deprecates `ParallelGateOperation` in favor of a new `ParallelGate` which should be instead. See #4358 for more details. 

This PR also adds support for `ParallleGate` in neutral atoms and pasqal devices, where currently `ParallelGateOperation` is used. 

The uses of `ParallelGateOperation` will be removed later during deprecation.
  • Loading branch information
tanujkhattar committed Aug 20, 2021
1 parent a99fef9 commit 11f751f
Show file tree
Hide file tree
Showing 16 changed files with 588 additions and 162 deletions.
2 changes: 2 additions & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@
NamedQid,
OP_TREE,
Operation,
ParallelGate,
parallel_gate_op,
ParallelGateOperation,
Pauli,
PAULI_GATE_LIKE,
Expand Down
1 change: 1 addition & 0 deletions cirq-core/cirq/json_resolver_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def two_qubit_matrix_gate(matrix):
'_PauliZ': cirq.ops.pauli_gates._PauliZ,
'ParamResolver': cirq.ParamResolver,
'ParallelGateOperation': cirq.ParallelGateOperation,
'ParallelGate': cirq.ParallelGate,
'PauliString': cirq.PauliString,
'PhaseDampingChannel': cirq.PhaseDampingChannel,
'PhaseFlipChannel': cirq.PhaseFlipChannel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ def optimization_at(

def is_native_neutral_atom_op(operation: ops.Operation) -> bool:
if isinstance(operation, (ops.GateOperation, ops.ParallelGateOperation)):
return is_native_neutral_atom_gate(operation.gate)
gate = operation.gate
if isinstance(gate, ops.ParallelGate):
gate = gate.sub_gate
return is_native_neutral_atom_gate(gate)
return False


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ def with_qubits(self, *new_qubits):
return FakeOperation(self._gate, new_qubits)

op = FakeOperation(g, q).with_qubits(*q)
c = cirq.Circuit(cirq.X.on(q[0]))
circuit_ops = [cirq.Y(q[0]), cirq.ParallelGate(cirq.X, 3).on(*q)]
c = cirq.Circuit(circuit_ops)
cirq.neutral_atoms.ConvertToNeutralAtomGates().optimize_circuit(c)
assert c == cirq.Circuit(cirq.X.on(q[0]))
assert c == cirq.Circuit(circuit_ops)
assert cirq.neutral_atoms.ConvertToNeutralAtomGates().convert(cirq.X.on(q[0])) == [
cirq.X.on(q[0])
]
Expand Down
50 changes: 31 additions & 19 deletions cirq-core/cirq/neutral_atoms/neutral_atom_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
import cirq


def _subgate_if_parallel_gate(gate: 'cirq.Gate') -> 'cirq.Gate':
"""Returns gate.sub_gate if gate is a ParallelGate, else returns gate"""
return gate.sub_gate if isinstance(gate, ops.ParallelGate) else gate


@value.value_equality
class NeutralAtomDevice(devices.Device):
"""A device with qubits placed on a grid."""
Expand Down Expand Up @@ -147,41 +152,47 @@ def validate_operation(self, operation: ops.Operation):
if not isinstance(operation, (ops.GateOperation, ops.ParallelGateOperation)):
raise ValueError(f'Unsupported operation: {operation!r}')

gate = operation.gate
if isinstance(gate, ops.ParallelGate):
if isinstance(gate.sub_gate, ops.MeasurementGate):
raise ValueError(
f'ParallelGate over MeasurementGate is not supported: {operation!r}'
)
gate = gate.sub_gate

# The gate must be valid
self.validate_gate(operation.gate)
self.validate_gate(gate)

# All qubits the operation acts on must be on the device
for q in operation.qubits:
if q not in self.qubits:
raise ValueError(f'Qubit not on device: {q!r}')

if isinstance(operation.gate, (ops.MeasurementGate, ops.IdentityGate)):
return

# Verify that a controlled gate operation is valid
if isinstance(operation, ops.GateOperation):
if len(operation.qubits) > self._max_parallel_c:
raise ValueError(
"Too many qubits acted on in parallel by a controlled gate operation"
)
if len(operation.qubits) > 1:
for p in operation.qubits:
for q in operation.qubits:
if self.distance(p, q) > self._control_radius:
raise ValueError(f"Qubits {p!r}, {q!r} are too far away")
if isinstance(gate, (ops.MeasurementGate, ops.IdentityGate)):
return

# Verify that a valid number of Z gates are applied in parallel
if isinstance(operation.gate, ops.ZPowGate):
if isinstance(gate, ops.ZPowGate):
if len(operation.qubits) > self._max_parallel_z:
raise ValueError("Too many Z gates in parallel")
return

# Verify that a valid number of XY gates are applied in parallel
if isinstance(operation.gate, (ops.XPowGate, ops.YPowGate, ops.PhasedXPowGate)):
if isinstance(gate, (ops.XPowGate, ops.YPowGate, ops.PhasedXPowGate)):
if len(operation.qubits) > self._max_parallel_xy and len(operation.qubits) != len(
self.qubits
):
raise ValueError("Bad number of XY gates in parallel")
return

# Verify that a controlled gate operation is valid
if len(operation.qubits) > self._max_parallel_c:
raise ValueError("Too many qubits acted on in parallel by a controlled gate operation")
if len(operation.qubits) > 1:
for p in operation.qubits:
for q in operation.qubits:
if self.distance(p, q) > self._control_radius:
raise ValueError(f"Qubits {p!r}, {q!r} are too far away")

def validate_moment(self, moment: ops.Moment):
"""Raises an error if the given moment is invalid on this device.
Expand Down Expand Up @@ -215,11 +226,12 @@ def validate_moment(self, moment: ops.Moment):
assert isinstance(op, (ops.GateOperation, ops.ParallelGateOperation))
for k, v in CATEGORIES.items():
assert isinstance(v, tuple)
if isinstance(op.gate, v):
gate = _subgate_if_parallel_gate(op.gate)
if isinstance(gate, v):
categorized_ops[k].append(op)

for k in ['Z', 'XY', 'controlled']:
if len(set(op.gate for op in categorized_ops[k])) > 1:
if len(set(_subgate_if_parallel_gate(op.gate) for op in categorized_ops[k])) > 1:
raise ValueError(f"Non-identical simultaneous {k} gates")

num_parallel_xy = sum([len(op.qubits) for op in categorized_ops['XY']])
Expand Down
16 changes: 10 additions & 6 deletions cirq-core/cirq/neutral_atoms/neutral_atom_devices_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ def with_qubits(self, new_qubits):

with pytest.raises(ValueError, match="Unsupported operation"):
d.validate_operation(bad_op())
not_on_device_op = cirq.ParallelGateOperation(
cirq.X, [cirq.GridQubit(row, col) for col in range(4) for row in range(4)]
not_on_device_op = cirq.parallel_gate_op(
cirq.X, *[cirq.GridQubit(row, col) for col in range(4) for row in range(4)]
)
with pytest.raises(ValueError, match="Qubit not on device"):
d.validate_operation(not_on_device_op)
Expand All @@ -137,9 +137,13 @@ def with_qubits(self, new_qubits):
with pytest.raises(ValueError, match="are too far away"):
d.validate_operation(cirq.CZ.on(cirq.GridQubit(0, 0), cirq.GridQubit(2, 2)))
with pytest.raises(ValueError, match="Too many Z gates in parallel"):
d.validate_operation(cirq.ParallelGateOperation(cirq.Z, d.qubits))
d.validate_operation(cirq.parallel_gate_op(cirq.Z, *d.qubits))
with pytest.raises(ValueError, match="Bad number of XY gates in parallel"):
d.validate_operation(cirq.ParallelGateOperation(cirq.X, d.qubit_list()[1:]))
d.validate_operation(cirq.parallel_gate_op(cirq.X, *d.qubit_list()[1:]))
with pytest.raises(ValueError, match="ParallelGate over MeasurementGate is not supported"):
d.validate_operation(
cirq.ParallelGate(cirq.MeasurementGate(1, key='a'), 4)(*d.qubit_list()[:4])
)


def test_validate_moment_errors():
Expand Down Expand Up @@ -233,9 +237,9 @@ def test_validate_circuit_errors():
q10 = cirq.GridQubit(1, 0)
q11 = cirq.GridQubit(1, 1)
c = cirq.Circuit()
c.append(cirq.ParallelGateOperation(cirq.X, d.qubits))
c.append(cirq.parallel_gate_op(cirq.X, *d.qubits))
c.append(cirq.CCZ.on(q00, q01, q10))
c.append(cirq.ParallelGateOperation(cirq.Z, [q00, q01, q10]))
c.append(cirq.parallel_gate_op(cirq.Z, q00, q01, q10))
m = cirq.Moment(cirq.X.on_each(q00, q01) + cirq.Z.on_each(q10, q11))
c.append(m)
c.append(cirq.measure_each(*d.qubits))
Expand Down
2 changes: 2 additions & 0 deletions cirq-core/cirq/ops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@
PauliSumExponential,
)

from cirq.ops.parallel_gate import ParallelGate, parallel_gate_op

from cirq.ops.parallel_gate_operation import (
ParallelGateOperation,
)
Expand Down
4 changes: 2 additions & 2 deletions cirq-core/cirq/ops/linear_combinations_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ def test_in_place_manipulations_of_linear_combination_of_gates(gates):
cirq.CZ(q0, q1),
cirq.FREDKIN(q0, q1, q2),
cirq.ControlledOperation((q0, q1), cirq.H(q2)),
cirq.ParallelGateOperation(cirq.X, (q0, q1, q2)),
cirq.ParallelGate(cirq.X, 3).on(q0, q1, q2),
cirq.PauliString({q0: cirq.X, q1: cirq.Y, q2: cirq.Z}),
),
)
Expand Down Expand Up @@ -920,7 +920,7 @@ def test_parameterized_linear_combination_of_ops(
(
(
cirq.LinearCombinationOfOperations({cirq.XX(q0, q1): 2}),
cirq.LinearCombinationOfOperations({cirq.ParallelGateOperation(cirq.X, (q0, q1)): 2}),
cirq.LinearCombinationOfOperations({cirq.ParallelGate(cirq.X, 2).on(q0, q1): 2}),
),
(
cirq.LinearCombinationOfOperations({cirq.CNOT(q0, q1): 2}),
Expand Down
173 changes: 173 additions & 0 deletions cirq-core/cirq/ops/parallel_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# 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.


from typing import AbstractSet, Union, Any, Optional, Tuple, TYPE_CHECKING, Dict

import numpy as np

from cirq import protocols, value
from cirq.ops import raw_types
from cirq.type_workarounds import NotImplementedType

if TYPE_CHECKING:
import cirq
from cirq.protocols.decompose_protocol import DecomposeResult


@value.value_equality
class ParallelGate(raw_types.Gate):
"""Augments existing gates to be applied on one or more groups of qubits."""

def __init__(self, sub_gate: 'cirq.Gate', num_copies: int) -> None:
"""Inits ParallelGate.
Args:
gate: The gate to apply.
num_copies: Number of copies of the gate to apply in parallel.
Raises:
ValueError: If gate is not a single qubit gate or num_copies <= 0.
"""
if sub_gate.num_qubits() != 1:
# TODO: If needed, support for multi-qubit sub_gates can be
# added by updating the circuit diagram plotting logic.
raise ValueError("gate must be a single qubit gate")
if not num_copies > 0:
raise ValueError("gate must be applied at least once.")
self._sub_gate = sub_gate
self._num_copies = num_copies

def num_qubits(self) -> int:
return self.sub_gate.num_qubits() * self._num_copies

@property
def sub_gate(self) -> 'cirq.Gate':
return self._sub_gate

@property
def num_copies(self) -> int:
return self._num_copies

def _decompose_(self, qubits: Tuple['cirq.Qid', ...]) -> 'DecomposeResult':
if len(qubits) != self.num_qubits():
raise ValueError(f"len(qubits)={len(qubits)} should be {self.num_qubits()}")
step = self.sub_gate.num_qubits()
return [self.sub_gate(*qubits[i : i + step]) for i in range(0, len(qubits), step)]

def with_gate(self, sub_gate: 'cirq.Gate') -> 'ParallelGate':
"""ParallelGate with same number of copies but a new gate"""
return ParallelGate(sub_gate, self._num_copies)

def with_num_copies(self, num_copies: int) -> 'ParallelGate':
"""ParallelGate with same sub_gate but different num_copies"""
return ParallelGate(self.sub_gate, num_copies)

def __repr__(self) -> str:
return f'cirq.ParallelGate(sub_gate={self.sub_gate!r}, num_copies={self._num_copies})'

def __str__(self) -> str:
return f'{self.sub_gate} x {self._num_copies}'

def _value_equality_values_(self) -> Any:
return self.sub_gate, self._num_copies

def _has_unitary_(self) -> bool:
return protocols.has_unitary(self.sub_gate)

def _is_parameterized_(self) -> bool:
return protocols.is_parameterized(self.sub_gate)

def _parameter_names_(self) -> AbstractSet[str]:
return protocols.parameter_names(self.sub_gate)

def _resolve_parameters_(
self, resolver: 'cirq.ParamResolver', recursive: bool
) -> 'ParallelGate':
return self.with_gate(
sub_gate=protocols.resolve_parameters(self.sub_gate, resolver, recursive)
)

def _unitary_(self) -> Union[np.ndarray, NotImplementedType]:
# Obtain the unitary for the single qubit gate
single_unitary = protocols.unitary(self.sub_gate, NotImplemented)

# Make sure we actually have a matrix
if single_unitary is NotImplemented:
return single_unitary

# Create a unitary which corresponds to applying the gate
# unitary _num_copies times. This will blow up memory fast.
unitary = single_unitary
for _ in range(self._num_copies - 1):
unitary = np.kron(unitary, single_unitary)

return unitary

def _trace_distance_bound_(self) -> Optional[float]:
if protocols.is_parameterized(self.sub_gate):
return None
angle = self._num_copies * np.arcsin(protocols.trace_distance_bound(self.sub_gate))
if angle >= np.pi * 0.5:
return 1.0
return np.sin(angle)

def _circuit_diagram_info_(
self, args: 'cirq.CircuitDiagramInfoArgs'
) -> 'cirq.CircuitDiagramInfo':
diagram_info = protocols.circuit_diagram_info(self.sub_gate, args, NotImplemented)
if diagram_info == NotImplemented:
return diagram_info

# Include symbols for every qubit instead of just one.
wire_symbols = tuple(diagram_info.wire_symbols) * self._num_copies

return protocols.CircuitDiagramInfo(
wire_symbols=wire_symbols, exponent=diagram_info.exponent, connected=False
)

def __pow__(self, exponent: Any) -> 'ParallelGate':
"""Raises underlying gate to a power, applying same number of copies.
For extrapolatable gate G this means the following two are equivalent:
(G ** 1.5) x k or (G x k) ** 1.5
Args:
exponent: The amount to scale the gate's effect by.
Returns:
ParallelGate with same num_copies with the scaled underlying gate.
"""
new_gate = protocols.pow(self.sub_gate, exponent, NotImplemented)
if new_gate is NotImplemented:
return NotImplemented
return self.with_gate(new_gate)

def _json_dict_(self) -> Dict[str, Any]:
return protocols.obj_to_dict_helper(self, attribute_names=["sub_gate", "num_copies"])


def parallel_gate_op(gate: 'cirq.Gate', *targets: 'cirq.Qid') -> 'cirq.Operation':
"""Constructs a ParallelGate using gate and applies to all given qubits
Args:
gate: The gate to apply
*targets: The qubits on which the ParallelGate should be applied.
Returns:
ParallelGate(gate, len(targets)).on(*targets)
"""
return ParallelGate(gate, len(targets)).on(*targets)
5 changes: 5 additions & 0 deletions cirq-core/cirq/ops/parallel_gate_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@
from cirq import protocols, value
from cirq.ops import raw_types
from cirq.type_workarounds import NotImplementedType
from cirq._compat import deprecated_class

if TYPE_CHECKING:
import cirq


@deprecated_class(
deadline='v0.14',
fix='Use cirq.ParallelGate(gate, num_copies).on(qubits) instead',
)
@value.value_equality
class ParallelGateOperation(raw_types.Operation):
"""An application of several copies of a gate to a group of qubits."""
Expand Down

0 comments on commit 11f751f

Please sign in to comment.