1# Copyright 2018 The Cirq Developers
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from typing import List, Optional, cast, TYPE_CHECKING
16
17import numpy as np
18
19from cirq import ops, optimizers, protocols, linalg
20from cirq.circuits.circuit import Circuit
21from cirq.circuits.optimization_pass import (
22    PointOptimizationSummary,
23    PointOptimizer,
24)
25
26if TYPE_CHECKING:
27    import cirq
28
29
30class ConvertToPauliStringPhasors(PointOptimizer):
31    """Attempts to convert single-qubit gates into single-qubit
32    PauliStringPhasor operations.
33
34    Checks if the operation has a known unitary effect. If so, and the gate is a
35        1-qubit gate, then decomposes it into x, y, or z rotations and creates a
36        PauliStringPhasor for each.
37    """
38
39    def __init__(
40        self, ignore_failures: bool = False, keep_clifford: bool = False, atol: float = 1e-14
41    ) -> None:
42        """Inits ConvertToPauliStringPhasors.
43
44        Args:
45            ignore_failures: If set, gates that fail to convert are forwarded
46                unchanged. If not set, conversion failures raise a TypeError.
47            keep_clifford: If set, single qubit rotations in the Clifford group
48                are converted to SingleQubitCliffordGates.
49            atol: Maximum absolute error tolerance. The optimization is
50                permitted to round angles with a threshold determined by this
51                tolerance.
52        """
53        super().__init__()
54        self.ignore_failures = ignore_failures
55        self.keep_clifford = keep_clifford
56        self.atol = atol
57
58    def _matrix_to_pauli_string_phasors(self, mat: np.ndarray, qubit: 'cirq.Qid') -> ops.OP_TREE:
59        rotations = optimizers.single_qubit_matrix_to_pauli_rotations(mat, self.atol)
60        out_ops: List[ops.Operation] = []
61        for pauli, half_turns in rotations:
62            if self.keep_clifford and linalg.all_near_zero_mod(half_turns, 0.5):
63                cliff_gate = ops.SingleQubitCliffordGate.from_quarter_turns(
64                    pauli, round(half_turns * 2)
65                )
66                if out_ops and not isinstance(out_ops[-1], ops.PauliStringPhasor):
67                    op = cast(ops.GateOperation, out_ops[-1])
68                    gate = cast(ops.SingleQubitCliffordGate, op.gate)
69                    out_ops[-1] = gate.merged_with(cliff_gate)(qubit)
70                else:
71                    out_ops.append(cliff_gate(qubit))
72            else:
73                out_ops.append(
74                    ops.PauliStringPhasor(
75                        ops.PauliString(pauli.on(qubit)), exponent_neg=round(half_turns, 10)
76                    )
77                )
78        return out_ops
79
80    def _convert_one(self, op: ops.Operation) -> ops.OP_TREE:
81        # Don't change if it's already a ops.PauliStringPhasor
82        if isinstance(op, ops.PauliStringPhasor):
83            return op
84
85        if (
86            self.keep_clifford
87            and isinstance(op, ops.GateOperation)
88            and isinstance(op.gate, ops.SingleQubitCliffordGate)
89        ):
90            return op
91
92        # Single qubit gate with known matrix?
93        if len(op.qubits) == 1:
94            mat = protocols.unitary(op, None)
95            if mat is not None:
96                return self._matrix_to_pauli_string_phasors(mat, op.qubits[0])
97
98        # Just let it be?
99        if self.ignore_failures:
100            return op
101
102        raise TypeError(
103            "Don't know how to work with {!r}. "
104            "It isn't a 1-qubit operation with a known unitary "
105            "effect.".format(op)
106        )
107
108    def convert(self, op: ops.Operation) -> ops.OP_TREE:
109        converted = self._convert_one(op)
110        if converted is op:
111            return converted
112        return [self.convert(cast(ops.Operation, e)) for e in ops.flatten_op_tree(converted)]
113
114    def optimization_at(
115        self, circuit: Circuit, index: int, op: ops.Operation
116    ) -> Optional[PointOptimizationSummary]:
117        converted = self.convert(op)
118        if converted is op:
119            return None
120
121        return PointOptimizationSummary(
122            clear_span=1, new_operations=converted, clear_qubits=op.qubits
123        )
124