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 map_operations and map_moments transformer primitives #4692

Merged
merged 7 commits into from
Nov 23, 2021
6 changes: 6 additions & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@
EjectZ,
ExpandComposite,
is_negligible_turn,
map_moments,
map_operations,
map_operations_and_unroll,
merge_single_qubit_gates_into_phased_x_z,
merge_single_qubit_gates_into_phxz,
MergeInteractions,
Expand All @@ -352,6 +355,9 @@
two_qubit_matrix_to_diagonal_and_operations,
two_qubit_matrix_to_sqrt_iswap_operations,
three_qubit_matrix_to_operations,
unroll_circuit_op,
unroll_circuit_op_greedy_earliest,
unroll_circuit_op_greedy_frontier,
)

from cirq.qis import (
Expand Down
10 changes: 10 additions & 0 deletions cirq-core/cirq/optimizers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@
three_qubit_matrix_to_operations,
)

from cirq.optimizers.transformer_primitives import (
map_moments,
map_operations,
map_operations_and_unroll,
unroll_circuit_op,
unroll_circuit_op_greedy_earliest,
unroll_circuit_op_greedy_frontier,
)


from cirq.optimizers.two_qubit_decompositions import (
two_qubit_matrix_to_operations,
two_qubit_matrix_to_diagonal_and_operations,
Expand Down
197 changes: 197 additions & 0 deletions cirq-core/cirq/optimizers/transformer_primitives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# 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.

"""Defines primitives for common transformer patterns."""

from collections import defaultdict
from typing import (
cast,
Callable,
Dict,
Hashable,
Optional,
Sequence,
TYPE_CHECKING,
)

from cirq import circuits, ops, protocols
from cirq.circuits.circuit import CIRCUIT_TYPE

if TYPE_CHECKING:
import cirq

MAPPED_CIRCUIT_OP_TAG = '<mapped_circuit_op>'


def _to_target_circuit_type(
circuit: circuits.AbstractCircuit, target_circuit: CIRCUIT_TYPE
) -> CIRCUIT_TYPE:
return cast(
CIRCUIT_TYPE,
circuit.unfreeze(copy=False)
if isinstance(target_circuit, circuits.Circuit)
else circuit.freeze(),
)


def _create_target_circuit_type(ops: ops.OP_TREE, target_circuit: CIRCUIT_TYPE) -> CIRCUIT_TYPE:
return cast(
CIRCUIT_TYPE,
circuits.Circuit(ops)
if isinstance(target_circuit, circuits.Circuit)
else circuits.FrozenCircuit(ops),
)


def map_moments(
circuit: CIRCUIT_TYPE,
map_func: Callable[[ops.Moment, int], Sequence[ops.Moment]],
) -> CIRCUIT_TYPE:
"""Applies local transformation on moments, by calling `map_func(moment)` for each moment.

Args:
circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
map_func: Mapping function from (cirq.Moment, moment_index) to a sequence of moments.
"""
return _create_target_circuit_type(
(map_func(circuit[i], i) for i in range(len(circuit))), circuit
)


def map_operations(
circuit: CIRCUIT_TYPE,
map_func: Callable[[ops.Operation, int], ops.OP_TREE],
) -> CIRCUIT_TYPE:
"""Applies local transformations on operations, by calling `map_func(op)` for each op.

Note that the function assumes `issubset(qubit_set(map_func(op)), op.qubits)` is True.

Args:
circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
map_func: Mapping function from (cirq.Operation, moment_index) to a cirq.OP_TREE. If the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the index i needed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index i is needed for situations when the mapping to be applied on the operation also depends upon which moment the operation is currently present in. For example, when characterizing and compensating for fsim gate angles, the moment index can be useful to determine what compensation to apply.

resulting optree spans more than 1 moment, it's inserted in-place in the same moment as
`cirq.CircuitOperation(cirq.FrozenCircuit(op_tree)).with_tags(MAPPED_CIRCUIT_OP_TAG)`
to preserve moment structure. Utility methods like `cirq.unroll_circuit_op` can
subsequently be used to unroll the mapped circuit operation.

Raises:
ValueError if `issubset(qubit_set(map_func(op)), op.qubits) is False`.
"""

def apply_map(op: ops.Operation, idx: int) -> ops.OP_TREE:
c = circuits.FrozenCircuit(map_func(op, idx))
if not c.all_qubits().issubset(op.qubits):
raise ValueError(
f"Mapped operations {c.all_operations()} should act on a subset "
f"of qubits of the original operation {op}"
)
if len(c) == 1:
# All operations act in the same moment; so we don't need to wrap them in a circuit_op.
return c[0].operations
circuit_op = circuits.CircuitOperation(c).with_tags(MAPPED_CIRCUIT_OP_TAG)
return circuit_op

return map_moments(circuit, lambda m, i: [ops.Moment(apply_map(op, i) for op in m.operations)])


def map_operations_and_unroll(
circuit: CIRCUIT_TYPE,
map_func: Callable[[ops.Operation, int], ops.OP_TREE],
) -> CIRCUIT_TYPE:
"""Applies local transformations via `cirq.map_operations` & unrolls intermediate circuit ops.

See `cirq.map_operations` and `cirq.unroll_circuit_op` for more details.

Args:
circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
map_func: Mapping function from (cirq.Operation, moment_index) to a cirq.OP_TREE.
"""
return unroll_circuit_op(map_operations(circuit, map_func))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map_operations_and_unroll method? It's expected to be the most common use case of these two primitives and hence the convenience method will reduce 2 function calls to 1 function call at all call sites.



def _check_circuit_op(op, tags_to_check: Optional[Sequence[Hashable]]):
return isinstance(op.untagged, circuits.CircuitOperation) and (
tags_to_check is None or any(tag in op.tags for tag in tags_to_check)
)


def unroll_circuit_op(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we merge these three unroll_circuit_op_*** into one ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a clean way to merge the unrol_circuit_op** into a single method because 2/3 implementations do not correspond to a standard insert strategy and hence the insert strategy parameter would be misleading at best. We can come up with another enum and pass it as a parameter but I don't see the advantage, especially because the implementations of the 3 methods are also very different.

circuit: CIRCUIT_TYPE, *, tags_to_check: Optional[Sequence[Hashable]] = (MAPPED_CIRCUIT_OP_TAG,)
) -> CIRCUIT_TYPE:
"""Unrolls (tagged) `cirq.CircuitOperation`s while preserving the moment structure.

Each moment containing a matching circuit operation is expanded into a list of moments with the
unrolled operations, hence preserving the original moment structure.

Args:
circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
tags_to_check: If specified, only circuit operations tagged with one of the `tags_to_check`
are unrolled.
"""

def map_func(m: ops.Moment, _: int):
to_zip = [
cast(circuits.CircuitOperation, op.untagged).mapped_circuit()
if _check_circuit_op(op, tags_to_check)
else circuits.Circuit(op)
for op in m
]
return circuits.Circuit.zip(*to_zip).moments

return map_moments(circuit, map_func)


def unroll_circuit_op_greedy_earliest(
circuit: CIRCUIT_TYPE, *, tags_to_check=(MAPPED_CIRCUIT_OP_TAG,)
) -> CIRCUIT_TYPE:
"""Unrolls (tagged) `cirq.CircuitOperation`s by inserting operations using EARLIEST strategy.

Each matching `cirq.CircuitOperation` is replaced by inserting underlying operations using the
`cirq.InsertStrategy.EARLIEST` strategy. The greedy approach attempts to minimize circuit depth
of the resulting circuit.

Args:
circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
tags_to_check: If specified, only circuit operations tagged with one of the `tags_to_check`
are unrolled.
"""
batch_removals = [*circuit.findall_operations(lambda op: _check_circuit_op(op, tags_to_check))]
batch_inserts = [(i, protocols.decompose_once(op)) for i, op in batch_removals]
unrolled_circuit = circuit.unfreeze(copy=True)
unrolled_circuit.batch_remove(batch_removals)
unrolled_circuit.batch_insert(batch_inserts)
return _to_target_circuit_type(unrolled_circuit, circuit)


def unroll_circuit_op_greedy_frontier(
circuit: CIRCUIT_TYPE, *, tags_to_check=(MAPPED_CIRCUIT_OP_TAG,)
) -> CIRCUIT_TYPE:
"""Unrolls (tagged) `cirq.CircuitOperation`s by inserting operations inline at qubit frontier.

Each matching `cirq.CircuitOperation` is replaced by inserting underlying operations using the
`circuit.insert_at_frontier` method. The greedy approach attempts to reuse any available space
in existing moments on the right of circuit_op before inserting new moments.

Args:
circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
tags_to_check: If specified, only circuit operations tagged with one of the `tags_to_check`
are unrolled.
"""
unrolled_circuit = circuit.unfreeze(copy=True)
frontier: Dict['cirq.Qid', int] = defaultdict(lambda: 0)
for idx, op in circuit.findall_operations(lambda op: _check_circuit_op(op, tags_to_check)):
idx = max(idx, max(frontier[q] for q in op.qubits))
unrolled_circuit.clear_operations_touching(op.qubits, [idx])
frontier = unrolled_circuit.insert_at_frontier(protocols.decompose_once(op), idx, frontier)
return _to_target_circuit_type(unrolled_circuit, circuit)
Loading