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 cirq.optimize_for_target_gateset transformer and cirq.CompilationTargetGateset interface #5005

Merged
merged 7 commits into from
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,14 @@
from cirq.transformers import (
align_left,
align_right,
CompilationTargetGateset,
compute_cphase_exponents_for_fsim_decomposition,
convert_to_target_gateset,
decompose_clifford_tableau_to_operations,
decompose_cphase_into_two_fsim,
decompose_multi_controlled_x,
decompose_multi_controlled_rotation,
decompose_operations_to_target_gateset,
decompose_two_qubit_interaction_into_four_fsim_gates,
defer_measurements,
dephase_measurements,
Expand Down
6 changes: 5 additions & 1 deletion cirq-core/cirq/protocols/decompose_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,11 @@ def decompose(
that doesn't satisfy the given `keep` predicate.
"""

if on_stuck_raise is not _value_error_describing_bad_operation and keep is None:
if (
on_stuck_raise is not _value_error_describing_bad_operation
and on_stuck_raise is not None
and keep is None
):
raise ValueError(
"Must specify 'keep' if specifying 'on_stuck_raise', because it's "
"not possible to get stuck if you don't have a criteria on what's "
Expand Down
3 changes: 1 addition & 2 deletions cirq-core/cirq/protocols/decompose_protocol_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ def test_decompose_on_stuck_raise():
_ = cirq.decompose(NoMethod(), keep=lambda _: False)
# Unless there's no operations to be unhappy about.
assert cirq.decompose([], keep=lambda _: False) == []
assert cirq.decompose([], on_stuck_raise=None) == []
# Or you say you're fine.
assert cirq.decompose(no_method, keep=lambda _: False, on_stuck_raise=None) == [no_method]
assert cirq.decompose(no_method, keep=lambda _: False, on_stuck_raise=lambda _: None) == [
Expand All @@ -198,8 +199,6 @@ def test_decompose_on_stuck_raise():
)

# There's a nice warning if you specify `on_stuck_raise` but not `keep`.
with pytest.raises(ValueError, match='on_stuck_raise'):
assert cirq.decompose([], on_stuck_raise=None)
with pytest.raises(ValueError, match='on_stuck_raise'):
assert cirq.decompose([], on_stuck_raise=TypeError('x'))

Expand Down
2 changes: 2 additions & 0 deletions cirq-core/cirq/protocols/json_test_data/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
'ApplyMixtureArgs',
'ApplyUnitaryArgs',
'OperationTarget',
# Abstract base class for creating compilation targets.
'CompilationTargetGateset',
# Circuit optimizers are function-like. Only attributes
# are ignore_failures, tolerance, and other feature flags
'AlignLeft',
Expand Down
9 changes: 9 additions & 0 deletions cirq-core/cirq/transformers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
two_qubit_gate_product_tabulation,
)

from cirq.transformers.target_gatesets import (
CompilationTargetGateset,
)

from cirq.transformers.align import align_left, align_right

from cirq.transformers.stratify import stratified_circuit
Expand All @@ -49,6 +53,11 @@

from cirq.transformers.eject_phased_paulis import eject_phased_paulis

from cirq.transformers.convert_to_target_gateset import (
convert_to_target_gateset,
decompose_operations_to_target_gateset,
)

from cirq.transformers.drop_empty_moments import drop_empty_moments

from cirq.transformers.drop_negligible_operations import drop_negligible_operations
Expand Down
129 changes: 129 additions & 0 deletions cirq-core/cirq/transformers/convert_to_target_gateset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 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.

tanujkhattar marked this conversation as resolved.
Show resolved Hide resolved
from typing import Optional, Callable, TYPE_CHECKING

from cirq import protocols
from cirq.transformers import transformer_api, transformer_primitives
from cirq.protocols.decompose_protocol import DecomposeResult
tanujkhattar marked this conversation as resolved.
Show resolved Hide resolved

if TYPE_CHECKING:
import cirq


def _create_on_stuck_raise_error(gateset: 'cirq.Gateset'):
def _value_error_describing_bad_operation(op: 'cirq.Operation') -> ValueError:
return ValueError(f"Unable to convert {op} to target gateset {gateset!r}")

return _value_error_describing_bad_operation


@transformer_api.transformer
def decompose_operations_to_target_gateset(
circuit: 'cirq.AbstractCircuit',
*,
context: Optional['cirq.TransformerContext'] = None,
gateset: Optional['cirq.Gateset'] = None,
decomposer: Callable[['cirq.Operation', int], DecomposeResult] = lambda *_: NotImplemented,
ignore_failures=True,
tanujkhattar marked this conversation as resolved.
Show resolved Hide resolved
) -> 'cirq.Circuit':
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need this transformer ? It seems like a user should be able to fairly easily accomplish this same thing by just calling into the regular decompose. Having this as a thin wrapper over decompose seems like it might be complicating things for the users.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Replied in the main comment. Tl;Dr is that having benefits of making it a transformer, like automated logging + no-compile tags, make it worth it. Plus we have other similar wrappers like expand_composite.

"""Decomposes every operation to `gateset` using `cirq.decompose` and `decomposer`.

This transformer attempts to decompose every operation `op` in the given circuit to `gateset`
using `cirq.decompose` protocol with `decomposer` used as an intercepting decomposer. This
ensures that `op` is recursively decomposed using implicitly defined known decompositions
(eg: in `_decompose_` magic method on the gaet class) till either `decomposer` knows how to
decompose the given operation or the given operation belongs to `gateset`.

Args:
circuit: Input circuit to transform. It will not be modified.
context: `cirq.TransformerContext` storing common configurable options for transformers.
gateset: Target gateset, which the decomposed operations should belong to.
decomposer: A callable type which accepts an (operation, moment_index) and returns
- An equivalent `cirq.OP_TREE` implementing `op` using gates from `gateset`.
- `None` or `NotImplemented` if does not know how to decompose a given `op`.
ignore_failures: If set, operations that fail to convert are left unchanged. If not set,
conversion failures raise a TypeError.

Returns:
An equivalent circuit containing gates accepted by `gateset`.

Raises:
TypeError: If any input operation fails to convert and `ignore_failures` is False.
"""

def map_func(op: 'cirq.Operation', moment_index: int):
return protocols.decompose(
op,
intercepting_decomposer=lambda o: decomposer(o, moment_index),
keep=gateset.validate if gateset else None,
on_stuck_raise=(
None
if ignore_failures or gateset is None
else _create_on_stuck_raise_error(gateset)
),
)

return transformer_primitives.map_operations_and_unroll(
circuit, map_func, tags_to_ignore=context.tags_to_ignore if context else ()
).unfreeze(copy=False)


@transformer_api.transformer
def convert_to_target_gateset(
circuit: 'cirq.AbstractCircuit',
*,
context: Optional['cirq.TransformerContext'] = None,
gateset: Optional['cirq.CompilationTargetGateset'] = None,
ignore_failures: bool = True,
) -> 'cirq.Circuit':
"""Transforms the given circuit into an equivalent circuit using gates accepted by `gateset`.

1. Run all `gateset.preprocess_transformers`
2. Convert operations using built-in cirq decompose + `gateset.decompose_to_target_gateset`.
3. Run all `gateset.postprocess_transformers`

Args:
circuit: Input circuit to transform. It will not be modified.
context: `cirq.TransformerContext` storing common configurable options for transformers.
gateset: Target gateset, which should be an instance of `cirq.CompilationTargetGateset`.
ignore_failures: If set, operations that fail to convert are left unchanged. If not set,
conversion failures raise a TypeError.

Returns:
An equivalent circuit containing gates accepted by `gateset`.

Raises:
TypeError: If any input operation fails to convert and `ignore_failures` is False.
"""
if gateset is None:
return decompose_operations_to_target_gateset(
circuit, context=context, ignore_failures=ignore_failures
)

for transformer in gateset.preprocess_transformers:
circuit = transformer(circuit, context=context)

circuit = decompose_operations_to_target_gateset(
circuit,
context=context,
gateset=gateset,
decomposer=gateset.decompose_to_target_gateset,
ignore_failures=ignore_failures,
)

for transformer in gateset.postprocess_transformers:
circuit = transformer(circuit, context=context)

return circuit.unfreeze(copy=False)
Loading