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 typing import Callable, Dict, Iterable, TypeVar
16from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram
17from braket.circuits.instruction import Instruction
18from braket.circuits.moments import Moments
19from braket.circuits.qubit import QubitInput
20from braket.circuits.qubit_set import QubitSet, QubitSetInput
21from braket.ir.jaqcd import Program
23SubroutineReturn = TypeVar("SubroutineReturn", Iterable[Instruction], Instruction)
24SubroutineCallable = TypeVar("SubroutineCallable", bound=Callable[..., SubroutineReturn])
25AddableTypes = TypeVar("AddableTypes", SubroutineReturn, SubroutineCallable)
27# TODO: Add parameterization
30class Circuit:
31 """
32 A representation of a quantum circuit that contains the instructions to be performed on a
33 quantum device. See :mod:`braket.circuits.gates` module for all of the supported instructions.
34 """
36 @classmethod
37 def register_subroutine(cls, func: SubroutineCallable) -> None:
38 """
39 Register the subroutine `func` as an attribute of the `Circuit` class. The attribute name
40 is the name of `func`.
42 Args:
43 func (Callable[..., Union[Instruction, Iterable[Instruction]]]): The function of the
44 subroutine to add to the class.
46 Examples:
47 >>> def h_on_all(target):
48 ... circ = Circuit()
49 ... for qubit in target:
50 ... circ += Instruction(Gate.H(), qubit)
51 ... return circ
52 ...
53 >>> Circuit.register_subroutine(h_on_all)
54 >>> circ = Circuit().h_on_all(range(2))
55 >>> for instr in circ.instructions:
56 ... print(instr)
57 ...
58 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
59 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
60 """
62 def method_from_subroutine(self, *args, **kwargs) -> SubroutineReturn:
63 return self.add(func, *args, **kwargs)
65 function_name = func.__name__
66 setattr(cls, function_name, method_from_subroutine)
68 function_attr = getattr(cls, function_name)
69 setattr(function_attr, "__doc__", func.__doc__)
71 def __init__(self, addable: AddableTypes = None, *args, **kwargs):
72 """
73 Args:
74 addable (Instruction, iterable of `Instruction`, or `SubroutineCallable`, optional): The
75 instruction-like item(s) to add to self. Default = None.
76 *args: Variable length argument list. Supports any arguments that `add()` offers.
77 **kwargs: Arbitrary keyword arguments. Supports any keyword arguments that `add()`
78 offers.
80 Raises:
81 TypeError: If `addable` is an unsupported type.
83 Examples:
84 >>> circ = Circuit([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
85 >>> circ = Circuit().h(0).cnot(0, 1)
87 >>> @circuit.subroutine(register=True)
88 >>> def bell_pair(target):
89 ... return Circ().h(target[0]).cnot(target[0:2])
90 ...
91 >>> circ = Circuit(bell_pair, [4,5])
92 >>> circ = Circuit().bell_pair([4,5])
93 """
94 self._moments: Moments = Moments()
96 if addable is not None:
97 self.add(addable, *args, **kwargs)
99 @property
100 def depth(self) -> int:
101 """int: Get the circuit depth."""
102 return self._moments.depth
104 @property
105 def instructions(self) -> Iterable[Instruction]:
106 """Iterable[Instruction]: Get an `iterable` of instructions in the circuit."""
107 return self._moments.values()
109 @property
110 def moments(self) -> Moments:
111 """Moments: Get the `moments` for this circuit."""
112 return self._moments
114 @property
115 def qubit_count(self) -> int:
116 """Get the qubit count for this circuit."""
117 return self._moments.qubit_count
119 @property
120 def qubits(self) -> QubitSet:
121 """QubitSet: Get a copy of the qubits for this circuit."""
122 return QubitSet(self._moments.qubits)
124 def add_instruction(
125 self,
126 instruction: Instruction,
127 target: QubitSetInput = None,
128 target_mapping: Dict[QubitInput, QubitInput] = {},
129 ) -> "Circuit":
130 """
131 Add an instruction to `self`, returns `self` for chaining ability.
133 Args:
134 instruction (Instruction): `Instruction` to add into `self`.
135 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
136 `instruction`. If a single qubit gate, an instruction is created for every index
137 in `target`.
138 Default = None.
139 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
140 qubit mappings to apply to the `instruction.target`. Key is the qubit in
141 `instruction.target` and the value is what the key will be changed to. Default = {}.
143 Returns:
144 Circuit: self
146 Raises:
147 TypeError: If both `target_mapping` and `target` are supplied.
149 Examples:
150 >>> instr = Instruction(Gate.CNot(), [0, 1])
151 >>> circ = Circuit().add_instruction(instr)
152 >>> print(circ.instructions[0])
153 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
155 >>> instr = Instruction(Gate.CNot(), [0, 1])
156 >>> circ = Circuit().add_instruction(instr, target_mapping={0: 10, 1: 11})
157 >>> print(circ.instructions[0])
158 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
160 >>> instr = Instruction(Gate.CNot(), [0, 1])
161 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
162 >>> print(circ.instructions[0])
163 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
165 >>> instr = Instruction(Gate.H(), 0)
166 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
167 >>> print(circ.instructions[0])
168 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
169 >>> print(circ.instructions[1])
170 Instruction('operator': 'H', 'target': QubitSet(Qubit(11),))
171 """
172 if target_mapping and target is not None:
173 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
175 if not target_mapping and not target:
176 # Nothing has been supplied, add instruction
177 instructions_to_add = [instruction]
178 elif target_mapping:
179 # Target mapping has been supplied, copy instruction
180 instructions_to_add = [instruction.copy(target_mapping=target_mapping)]
181 elif hasattr(instruction.operator, "qubit_count") and instruction.operator.qubit_count == 1:
182 # single qubit operator with target, add an instruction for each target
183 instructions_to_add = [instruction.copy(target=qubit) for qubit in target]
184 else:
185 # non single qubit operator with target, add instruction with target
186 instructions_to_add = [instruction.copy(target=target)]
188 self._moments.add(instructions_to_add)
190 return self
192 def add_circuit(
193 self,
194 circuit: "Circuit",
195 target: QubitSetInput = None,
196 target_mapping: Dict[QubitInput, QubitInput] = {},
197 ) -> "Circuit":
198 """
199 Add a `circuit` to self, returns self for chaining ability. This is a composite form of
200 `add_instruction()` since it adds all of the instructions of `circuit` to this circuit.
202 Args:
203 circuit (Circuit): Circuit to add into self.
204 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
205 supplied circuit. This is a macro over `target_mapping`; `target` is converted to
206 a `target_mapping` by zipping together a sorted `circuit.qubits` and `target`.
207 Default = None.
208 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
209 qubit mappings to apply to the qubits of `circuit.instructions`. Key is the qubit
210 to map, and the Value is what to change it to. Default = {}.
212 Returns:
213 Circuit: self
215 Raises:
216 TypeError: If both `target_mapping` and `target` are supplied.
218 Note:
219 Supplying `target` sorts `circuit.qubits` to have deterministic behavior since
220 `circuit.qubits` ordering is based on how instructions are inserted.
221 Use caution when using this with circuits that with a lot of qubits, as the sort
222 can be resource-intensive. Use `target_mapping` to use a linear runtime to remap
223 the qubits.
225 Examples:
226 >>> widget = Circuit().h(0).cnot([0, 1])
227 >>> circ = Circuit().add_circuit(widget)
228 >>> print(circ.instructions[0])
229 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
230 >>> print(circ.instructions[1])
231 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
233 >>> widget = Circuit().h(0).cnot([0, 1])
234 >>> circ = Circuit().add_circuit(widget, target_mapping={0: 10, 1: 11})
235 >>> print(circ.instructions[0])
236 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
237 >>> print(circ.instructions[1])
238 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
240 >>> widget = Circuit().h(0).cnot([0, 1])
241 >>> circ = Circuit().add_circuit(widget, target=[10, 11])
242 >>> print(circ.instructions[0])
243 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
244 >>> print(circ.instructions[1])
245 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
246 """
247 if target_mapping and target is not None:
248 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
249 elif target is not None:
250 keys = sorted(circuit.qubits)
251 values = target
252 target_mapping = dict(zip(keys, values))
254 for instruction in circuit.instructions:
255 self.add_instruction(instruction, target_mapping=target_mapping)
257 return self
259 def add(self, addable: AddableTypes, *args, **kwargs) -> "Circuit":
260 """
261 Generic add method for adding instruction-like item(s) to self. Any arguments that
262 `add_circuit()` and / or `add_instruction()` supports are supported by this method.
263 If adding a subroutine, check with that subroutines documentation to determine what input it
264 allows.
266 Args:
267 addable (Instruction, iterable of Instruction, or SubroutineCallable, optional): The
268 instruction-like item(s) to add to self. Default = None.
269 *args: Variable length argument list.
270 **kwargs: Arbitrary keyword arguments.
272 Returns:
273 Circuit: self
275 Raises:
276 TypeError: If `addable` is an unsupported type
278 See Also:
279 `add_circuit()`
281 `add_instruction()`
283 Examples:
284 >>> circ = Circuit().add([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
286 >>> circ = Circuit().h(4).cnot([4, 5])
288 >>> @circuit.subroutine()
289 >>> def bell_pair(target):
290 ... return Circuit().h(target[0]).cnot(target[0: 2])
291 ...
292 >>> circ = Circuit().add(bell_pair, [4,5])
293 """
295 def _flatten(addable):
296 if isinstance(addable, Iterable):
297 for item in addable:
298 yield from _flatten(item)
299 else:
300 yield addable
302 for item in _flatten(addable):
303 if isinstance(item, Instruction):
304 self.add_instruction(item, *args, **kwargs)
305 elif isinstance(item, Circuit):
306 self.add_circuit(item, *args, **kwargs)
307 elif callable(item):
308 self.add(item(*args, **kwargs))
309 else:
310 raise TypeError(f"Cannot add a '{type(item)}' to a Circuit")
312 return self
314 def diagram(self, circuit_diagram_class=AsciiCircuitDiagram) -> str:
315 """
316 Get a diagram for the current circuit.
318 Args:
319 circuit_diagram_class (Class, optional): A `CircuitDiagram` class that builds the
320 diagram for this circuit. Default = AsciiCircuitDiagram.
322 Returns:
323 str: An ASCII string circuit diagram.
324 """
325 return circuit_diagram_class.build_diagram(self)
327 def to_ir(self) -> Program:
328 """
329 Converts the circuit into the canonical intermediate representation.
330 If the circuit is sent over the wire, this method is called before it is sent.
332 Returns:
333 (Program): An AWS quantum circuit description program in JSON format.
334 """
335 ir_instructions = [instr.to_ir() for instr in self.instructions]
336 return Program(instructions=ir_instructions)
338 def _copy(self) -> "Circuit":
339 """
340 Return a shallow copy of the circuit.
342 Returns:
343 Circuit: A shallow copy of the circuit.
344 """
345 return Circuit().add(self.instructions)
347 def __iadd__(self, addable: AddableTypes) -> "Circuit":
348 return self.add(addable)
350 def __add__(self, addable: AddableTypes) -> "Circuit":
351 new = self._copy()
352 new.add(addable)
353 return new
355 def __repr__(self):
356 return f"Circuit('instructions': {list(self.instructions)})"
358 def __str__(self):
359 return self.diagram(AsciiCircuitDiagram)
361 def __eq__(self, other):
362 if isinstance(other, Circuit):
363 return list(self.instructions) == list(other.instructions)
364 return NotImplemented
367def subroutine(register=False):
368 """
369 Subroutine is a function that returns instructions or circuits.
371 Args:
372 register (bool, optional): If `True`, adds this subroutine into the `Circuit` class.
373 Default = False.
375 Examples:
376 >>> @circuit.subroutine(register=True)
377 >>> def bell_circuit():
378 ... return Circuit().h(0).cnot(0, 1)
379 ...
380 >>> circ = Circuit().bell_circuit()
381 >>> for instr in circ.instructions:
382 ... print(instr)
383 ...
384 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
385 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
386 """
388 def subroutine_function_wrapper(func: Callable[..., SubroutineReturn]) -> SubroutineReturn:
389 if register:
390 Circuit.register_subroutine(func)
391 return func
393 return subroutine_function_wrapper