1# Copyright 2021 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
15"""Data structures for programs executable on a quantum runtime."""
16
17import abc
18import dataclasses
19from dataclasses import dataclass
20from typing import Union, Tuple, Optional, Sequence, cast, Iterable, Dict, Any, List
21
22from cirq import _compat, study
23import cirq
24
25
26class ExecutableSpec(metaclass=abc.ABCMeta):
27    """Specification metadata about an executable.
28
29    Subclasses should add problem-specific fields.
30    """
31
32    executable_family: str = NotImplemented
33    """A unique name to group executables."""
34
35
36@dataclass(frozen=True)
37class KeyValueExecutableSpec(ExecutableSpec):
38    """A generic executable spec whose metadata is a list of key-value pairs.
39
40    The key-value pairs define an implicit data schema. Consider defining a problem-specific
41    subclass of `ExecutableSpec` instead of using this class to realize the benefits of having
42    an explicit schema.
43
44    See Also:
45        `KeyValueExecutableSpec.from_dict` will use a dictionary to populate `key_value_pairs`.
46
47    Args:
48        executable_family: A unique name to group executables.
49        key_value_pairs: A tuple of key-value pairs. The keys should be strings but the values
50            can be any immutable object.
51    """
52
53    executable_family: str
54    key_value_pairs: Tuple[Tuple[str, Any], ...] = ()
55
56    def _json_dict_(self) -> Dict[str, Any]:
57        return cirq.dataclass_json_dict(self, namespace='cirq.google')
58
59    @classmethod
60    def from_dict(cls, d: Dict[str, Any], *, executable_family: str) -> 'KeyValueExecutableSpec':
61        return cls(
62            executable_family=executable_family,
63            key_value_pairs=tuple((k, v) for k, v in d.items()),
64        )
65
66    @classmethod
67    def _from_json_dict_(
68        cls, executable_family: str, key_value_pairs: List[List[Union[str, Any]]], **kwargs
69    ) -> 'KeyValueExecutableSpec':
70        return cls(
71            executable_family=executable_family,
72            key_value_pairs=tuple((k, v) for k, v in key_value_pairs),
73        )
74
75    def __repr__(self) -> str:
76        return cirq._compat.dataclass_repr(self, namespace='cirq_google')
77
78
79@dataclass(frozen=True)
80class BitstringsMeasurement:
81    """Use in-circuit MeasurementGate to collect many repetitions of strings of bits.
82
83    This is the lowest-level measurement type allowed in `QuantumExecutable` and behaves
84    identically to the `cirq.Sampler.run` function. The executable's circuit must contain
85    explicit measurement gates.
86
87    Args:
88        n_repeitions: The number of repetitions to execute the circuit.
89    """
90
91    n_repetitions: int
92
93    def _json_dict_(self):
94        return cirq.dataclass_json_dict(self, namespace='cirq.google')
95
96    def __repr__(self):
97        return cirq._compat.dataclass_repr(self, namespace='cirq_google')
98
99
100TParamPair = Tuple[cirq.TParamKey, cirq.TParamVal]
101
102
103@dataclass(frozen=True)
104class QuantumExecutable:
105    """An executable quantum program.
106
107    This serves a similar purpose to `cirq.Circuit` with some key differences. First, a quantum
108    executable contains all the relevant context for execution including parameters as well as
109    the desired number of repetitions. Second, this object is immutable. Finally, there are
110    optional fields enabling a higher level of abstraction for certain aspects of the executable.
111
112    Attributes:
113        circuit: A `cirq.Circuit` describing the quantum operations to execute.
114        measurement: A description of the measurement properties or process.
115        params: An immutable `cirq.ParamResolver` (or similar type). It's representation is
116            normalized to a tuple of key value pairs.
117        spec: Optional `cg.ExecutableSpec` containing metadata about this executable that is not
118            used by the quantum runtime, but will be forwarded to all downstream result objects.
119        problem_topology: Optional `cirq.NamedTopology` instance specifying the topology of the
120            circuit. This is useful when optimizing on-device layout. If none is provided we
121            assume `circuit` already has a valid on-device layout.
122        initial_state: A `cirq.ProductState` specifying the desired initial state before executing
123            `circuit`. If not specified, default to the all-zeros state.
124    """
125
126    circuit: cirq.FrozenCircuit
127    measurement: BitstringsMeasurement
128    params: Optional[Tuple[TParamPair, ...]] = None
129    spec: Optional[ExecutableSpec] = None
130    problem_topology: Optional[cirq.NamedTopology] = None
131    initial_state: Optional[cirq.ProductState] = None
132
133    # pylint: disable=missing-raises-doc
134    def __init__(
135        self,
136        circuit: cirq.AbstractCircuit,
137        measurement: BitstringsMeasurement,
138        params: Union[Sequence[TParamPair], cirq.ParamResolverOrSimilarType] = None,
139        spec: Optional[ExecutableSpec] = None,
140        problem_topology: Optional[cirq.NamedTopology] = None,
141        initial_state: Optional[cirq.ProductState] = None,
142    ):
143        """Initialize the quantum executable.
144
145        The actual fields in this class are immutable, but we allow more liberal input types
146        which will be frozen in this __init__ method.
147
148        Args:
149            circuit: The circuit. This will be frozen before being set as an attribute.
150            measurement: A description of the measurement properties or process.
151            params: A cirq.ParamResolverOrSimilarType which will be frozen into a tuple of
152                key value pairs.
153            spec: Specification metadata about this executable that is not used by the quantum
154                runtime, but is persisted in result objects to associate executables with results.
155            problem_topology: Description of the multiqubit gate topology present in the circuit.
156                If not specified, the circuit must be compatible with the device topology.
157            initial_state: How to initialize the quantum system before running `circuit`. If not
158                specified, the device will be initialized into the all-zeros state.
159        """
160
161        # We care a lot about mutability in this class. No object is truly immutable in Python,
162        # but we can get pretty close by following the example of dataclass(frozen=True), which
163        # deletes this class's __setattr__ magic method. To set values ever, we use
164        # object.__setattr__ in this __init__ function.
165        #
166        # We write our own __init__ function to be able to accept a wider range of input formats
167        # that can be easily converted to our native, immutable format.
168        object.__setattr__(self, 'circuit', circuit.freeze())
169        object.__setattr__(self, 'measurement', measurement)
170
171        if isinstance(params, tuple) and all(
172            isinstance(param_kv, tuple) and len(param_kv) == 2 for param_kv in params
173        ):
174            frozen_params = params
175        elif isinstance(params, Sequence) and all(
176            isinstance(param_kv, Sequence) and len(param_kv) == 2 for param_kv in params
177        ):
178            frozen_params = tuple((k, v) for k, v in params)
179        elif study.resolver._is_param_resolver_or_similar_type(params):
180            param_resolver = cirq.ParamResolver(cast(cirq.ParamResolverOrSimilarType, params))
181            frozen_params = tuple(param_resolver.param_dict.items())
182        else:
183            raise ValueError(f"`params` should be a ParamResolverOrSimilarType, not {params}.")
184        object.__setattr__(self, 'params', frozen_params)
185
186        object.__setattr__(self, 'spec', spec)
187        object.__setattr__(self, 'problem_topology', problem_topology)
188        object.__setattr__(self, 'initial_state', initial_state)
189
190        # Hash may be expensive to compute, especially for large circuits.
191        # This should be safe since this class should be immutable. This line will
192        # also check for hashibility of members at construction time.
193        object.__setattr__(self, '_hash', hash(dataclasses.astuple(self)))
194
195    def __str__(self):
196        return f'QuantumExecutable(spec={self.spec})'
197
198    def __repr__(self):
199        return _compat.dataclass_repr(self, namespace='cirq_google')
200
201    def _json_dict_(self):
202        return cirq.dataclass_json_dict(self, namespace='cirq.google')
203
204
205@dataclass(frozen=True)
206class QuantumExecutableGroup:
207    """A collection of `QuantumExecutable`s.
208
209    Attributes:
210        executables: A tuple of `cg.QuantumExecutable`.
211    """
212
213    executables: Tuple[QuantumExecutable, ...]
214
215    def __init__(
216        self,
217        executables: Sequence[QuantumExecutable],
218    ):
219        """Initialize and normalize the quantum executable group.
220
221        Args:
222             executables: A sequence of `cg.QuantumExecutable` which will be frozen into a
223                tuple.
224        """
225
226        if not isinstance(executables, tuple):
227            executables = tuple(executables)
228        object.__setattr__(self, 'executables', executables)
229
230        object.__setattr__(self, '_hash', hash(dataclasses.astuple(self)))
231
232    def __len__(self) -> int:
233        return len(self.executables)
234
235    def __iter__(self) -> Iterable[QuantumExecutable]:
236        yield from self.executables
237
238    def __str__(self) -> str:
239        exe_str = ', '.join(str(exe) for exe in self.executables[:2])
240        if len(self.executables) > 2:
241            exe_str += ', ...'
242
243        return f'QuantumExecutableGroup(executables=[{exe_str}])'
244
245    def __repr__(self) -> str:
246        return _compat.dataclass_repr(self, namespace='cirq_google')
247
248    def __hash__(self) -> int:
249        return self._hash  # type: ignore
250
251    def _json_dict_(self) -> Dict[str, Any]:
252        return cirq.dataclass_json_dict(self, namespace='cirq.google')
253