Coverage for src/braket/circuits/circuit.py : 100%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
14from __future__ import annotations
16from typing import Callable, Dict, Iterable, TypeVar
18from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram
19from braket.circuits.instruction import Instruction
20from braket.circuits.moments import Moments
21from braket.circuits.qubit import QubitInput
22from braket.circuits.qubit_set import QubitSet, QubitSetInput
23from braket.ir.jaqcd import Program
25SubroutineReturn = TypeVar("SubroutineReturn", Iterable[Instruction], Instruction)
26SubroutineCallable = TypeVar("SubroutineCallable", bound=Callable[..., SubroutineReturn])
27AddableTypes = TypeVar("AddableTypes", SubroutineReturn, SubroutineCallable)
29# TODO: Add parameterization
32class Circuit:
33 """
34 A representation of a quantum circuit that contains the instructions to be performed on a
35 quantum device. See :mod:`braket.circuits.gates` module for all of the supported instructions.
36 """
38 @classmethod
39 def register_subroutine(cls, func: SubroutineCallable) -> None:
40 """
41 Register the subroutine `func` as an attribute of the `Circuit` class. The attribute name
42 is the name of `func`.
44 Args:
45 func (Callable[..., Union[Instruction, Iterable[Instruction]]]): The function of the
46 subroutine to add to the class.
48 Examples:
49 >>> def h_on_all(target):
50 ... circ = Circuit()
51 ... for qubit in target:
52 ... circ += Instruction(Gate.H(), qubit)
53 ... return circ
54 ...
55 >>> Circuit.register_subroutine(h_on_all)
56 >>> circ = Circuit().h_on_all(range(2))
57 >>> for instr in circ.instructions:
58 ... print(instr)
59 ...
60 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
61 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
62 """
64 def method_from_subroutine(self, *args, **kwargs) -> SubroutineReturn:
65 return self.add(func, *args, **kwargs)
67 function_name = func.__name__
68 setattr(cls, function_name, method_from_subroutine)
70 function_attr = getattr(cls, function_name)
71 setattr(function_attr, "__doc__", func.__doc__)
73 def __init__(self, addable: AddableTypes = None, *args, **kwargs):
74 """
75 Args:
76 addable (Instruction, iterable of `Instruction`, or `SubroutineCallable`, optional): The
77 instruction-like item(s) to add to self. Default = None.
78 *args: Variable length argument list. Supports any arguments that `add()` offers.
79 **kwargs: Arbitrary keyword arguments. Supports any keyword arguments that `add()`
80 offers.
82 Raises:
83 TypeError: If `addable` is an unsupported type.
85 Examples:
86 >>> circ = Circuit([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
87 >>> circ = Circuit().h(0).cnot(0, 1)
89 >>> @circuit.subroutine(register=True)
90 >>> def bell_pair(target):
91 ... return Circ().h(target[0]).cnot(target[0:2])
92 ...
93 >>> circ = Circuit(bell_pair, [4,5])
94 >>> circ = Circuit().bell_pair([4,5])
95 """
96 self._moments: Moments = Moments()
98 if addable is not None:
99 self.add(addable, *args, **kwargs)
101 @property
102 def depth(self) -> int:
103 """int: Get the circuit depth."""
104 return self._moments.depth
106 @property
107 def instructions(self) -> Iterable[Instruction]:
108 """Iterable[Instruction]: Get an `iterable` of instructions in the circuit."""
109 return self._moments.values()
111 @property
112 def moments(self) -> Moments:
113 """Moments: Get the `moments` for this circuit."""
114 return self._moments
116 @property
117 def qubit_count(self) -> int:
118 """Get the qubit count for this circuit."""
119 return self._moments.qubit_count
121 @property
122 def qubits(self) -> QubitSet:
123 """QubitSet: Get a copy of the qubits for this circuit."""
124 return QubitSet(self._moments.qubits)
126 def add_instruction(
127 self,
128 instruction: Instruction,
129 target: QubitSetInput = None,
130 target_mapping: Dict[QubitInput, QubitInput] = {},
131 ) -> Circuit:
132 """
133 Add an instruction to `self`, returns `self` for chaining ability.
135 Args:
136 instruction (Instruction): `Instruction` to add into `self`.
137 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
138 `instruction`. If a single qubit gate, an instruction is created for every index
139 in `target`.
140 Default = None.
141 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
142 qubit mappings to apply to the `instruction.target`. Key is the qubit in
143 `instruction.target` and the value is what the key will be changed to. Default = {}.
145 Returns:
146 Circuit: self
148 Raises:
149 TypeError: If both `target_mapping` and `target` are supplied.
151 Examples:
152 >>> instr = Instruction(Gate.CNot(), [0, 1])
153 >>> circ = Circuit().add_instruction(instr)
154 >>> print(circ.instructions[0])
155 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
157 >>> instr = Instruction(Gate.CNot(), [0, 1])
158 >>> circ = Circuit().add_instruction(instr, target_mapping={0: 10, 1: 11})
159 >>> print(circ.instructions[0])
160 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
162 >>> instr = Instruction(Gate.CNot(), [0, 1])
163 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
164 >>> print(circ.instructions[0])
165 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
167 >>> instr = Instruction(Gate.H(), 0)
168 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
169 >>> print(circ.instructions[0])
170 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
171 >>> print(circ.instructions[1])
172 Instruction('operator': 'H', 'target': QubitSet(Qubit(11),))
173 """
174 if target_mapping and target is not None:
175 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
177 if not target_mapping and not target:
178 # Nothing has been supplied, add instruction
179 instructions_to_add = [instruction]
180 elif target_mapping:
181 # Target mapping has been supplied, copy instruction
182 instructions_to_add = [instruction.copy(target_mapping=target_mapping)]
183 elif hasattr(instruction.operator, "qubit_count") and instruction.operator.qubit_count == 1:
184 # single qubit operator with target, add an instruction for each target
185 instructions_to_add = [instruction.copy(target=qubit) for qubit in target]
186 else:
187 # non single qubit operator with target, add instruction with target
188 instructions_to_add = [instruction.copy(target=target)]
190 self._moments.add(instructions_to_add)
192 return self
194 def add_circuit(
195 self,
196 circuit: Circuit,
197 target: QubitSetInput = None,
198 target_mapping: Dict[QubitInput, QubitInput] = {},
199 ) -> Circuit:
200 """
201 Add a `circuit` to self, returns self for chaining ability. This is a composite form of
202 `add_instruction()` since it adds all of the instructions of `circuit` to this circuit.
204 Args:
205 circuit (Circuit): Circuit to add into self.
206 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
207 supplied circuit. This is a macro over `target_mapping`; `target` is converted to
208 a `target_mapping` by zipping together a sorted `circuit.qubits` and `target`.
209 Default = None.
210 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
211 qubit mappings to apply to the qubits of `circuit.instructions`. Key is the qubit
212 to map, and the Value is what to change it to. Default = {}.
214 Returns:
215 Circuit: self
217 Raises:
218 TypeError: If both `target_mapping` and `target` are supplied.
220 Note:
221 Supplying `target` sorts `circuit.qubits` to have deterministic behavior since
222 `circuit.qubits` ordering is based on how instructions are inserted.
223 Use caution when using this with circuits that with a lot of qubits, as the sort
224 can be resource-intensive. Use `target_mapping` to use a linear runtime to remap
225 the qubits.
227 Examples:
228 >>> widget = Circuit().h(0).cnot([0, 1])
229 >>> circ = Circuit().add_circuit(widget)
230 >>> print(circ.instructions[0])
231 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
232 >>> print(circ.instructions[1])
233 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
235 >>> widget = Circuit().h(0).cnot([0, 1])
236 >>> circ = Circuit().add_circuit(widget, target_mapping={0: 10, 1: 11})
237 >>> print(circ.instructions[0])
238 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
239 >>> print(circ.instructions[1])
240 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
242 >>> widget = Circuit().h(0).cnot([0, 1])
243 >>> circ = Circuit().add_circuit(widget, target=[10, 11])
244 >>> print(circ.instructions[0])
245 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
246 >>> print(circ.instructions[1])
247 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
248 """
249 if target_mapping and target is not None:
250 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
251 elif target is not None:
252 keys = sorted(circuit.qubits)
253 values = target
254 target_mapping = dict(zip(keys, values))
256 for instruction in circuit.instructions:
257 self.add_instruction(instruction, target_mapping=target_mapping)
259 return self
261 def add(self, addable: AddableTypes, *args, **kwargs) -> Circuit:
262 """
263 Generic add method for adding instruction-like item(s) to self. Any arguments that
264 `add_circuit()` and / or `add_instruction()` supports are supported by this method.
265 If adding a subroutine, check with that subroutines documentation to determine what input it
266 allows.
268 Args:
269 addable (Instruction, iterable of Instruction, or SubroutineCallable, optional): The
270 instruction-like item(s) to add to self. Default = None.
271 *args: Variable length argument list.
272 **kwargs: Arbitrary keyword arguments.
274 Returns:
275 Circuit: self
277 Raises:
278 TypeError: If `addable` is an unsupported type
280 See Also:
281 `add_circuit()`
283 `add_instruction()`
285 Examples:
286 >>> circ = Circuit().add([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
288 >>> circ = Circuit().h(4).cnot([4, 5])
290 >>> @circuit.subroutine()
291 >>> def bell_pair(target):
292 ... return Circuit().h(target[0]).cnot(target[0: 2])
293 ...
294 >>> circ = Circuit().add(bell_pair, [4,5])
295 """
297 def _flatten(addable):
298 if isinstance(addable, Iterable):
299 for item in addable:
300 yield from _flatten(item)
301 else:
302 yield addable
304 for item in _flatten(addable):
305 if isinstance(item, Instruction):
306 self.add_instruction(item, *args, **kwargs)
307 elif isinstance(item, Circuit):
308 self.add_circuit(item, *args, **kwargs)
309 elif callable(item):
310 self.add(item(*args, **kwargs))
311 else:
312 raise TypeError(f"Cannot add a '{type(item)}' to a Circuit")
314 return self
316 def diagram(self, circuit_diagram_class=AsciiCircuitDiagram) -> str:
317 """
318 Get a diagram for the current circuit.
320 Args:
321 circuit_diagram_class (Class, optional): A `CircuitDiagram` class that builds the
322 diagram for this circuit. Default = AsciiCircuitDiagram.
324 Returns:
325 str: An ASCII string circuit diagram.
326 """
327 return circuit_diagram_class.build_diagram(self)
329 def to_ir(self) -> Program:
330 """
331 Converts the circuit into the canonical intermediate representation.
332 If the circuit is sent over the wire, this method is called before it is sent.
334 Returns:
335 (Program): An AWS quantum circuit description program in JSON format.
336 """
337 ir_instructions = [instr.to_ir() for instr in self.instructions]
338 return Program(instructions=ir_instructions)
340 def _copy(self) -> Circuit:
341 """
342 Return a shallow copy of the circuit.
344 Returns:
345 Circuit: A shallow copy of the circuit.
346 """
347 return Circuit().add(self.instructions)
349 def __iadd__(self, addable: AddableTypes) -> Circuit:
350 return self.add(addable)
352 def __add__(self, addable: AddableTypes) -> Circuit:
353 new = self._copy()
354 new.add(addable)
355 return new
357 def __repr__(self):
358 return f"Circuit('instructions': {list(self.instructions)})"
360 def __str__(self):
361 return self.diagram(AsciiCircuitDiagram)
363 def __eq__(self, other):
364 if isinstance(other, Circuit):
365 return list(self.instructions) == list(other.instructions)
366 return NotImplemented
369def subroutine(register=False):
370 """
371 Subroutine is a function that returns instructions or circuits.
373 Args:
374 register (bool, optional): If `True`, adds this subroutine into the `Circuit` class.
375 Default = False.
377 Examples:
378 >>> @circuit.subroutine(register=True)
379 >>> def bell_circuit():
380 ... return Circuit().h(0).cnot(0, 1)
381 ...
382 >>> circ = Circuit().bell_circuit()
383 >>> for instr in circ.instructions:
384 ... print(instr)
385 ...
386 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
387 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
388 """
390 def subroutine_function_wrapper(func: Callable[..., SubroutineReturn]) -> SubroutineReturn:
391 if register:
392 Circuit.register_subroutine(func)
393 return func
395 return subroutine_function_wrapper