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, List, TypeVar, Union
18from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram
19from braket.circuits.instruction import Instruction
20from braket.circuits.moments import Moments
21from braket.circuits.observable import Observable
22from braket.circuits.qubit import QubitInput
23from braket.circuits.qubit_set import QubitSet, QubitSetInput
24from braket.circuits.result_type import ObservableResultType, ResultType
25from braket.ir.jaqcd import Program
27SubroutineReturn = TypeVar(
28 "SubroutineReturn", Iterable[Instruction], Instruction, ResultType, Iterable[ResultType]
29)
30SubroutineCallable = TypeVar("SubroutineCallable", bound=Callable[..., SubroutineReturn])
31AddableTypes = TypeVar("AddableTypes", SubroutineReturn, SubroutineCallable)
33# TODO: Add parameterization
36class Circuit:
37 """
38 A representation of a quantum circuit that contains the instructions to be performed on a
39 quantum device and the requested result types.
41 See :mod:`braket.circuits.gates` module for all of the supported instructions.
43 See :mod:`braket.circuits.result_types` module for all of the supported result types.
45 `AddableTypes` are `Instruction`, iterable of `Instruction`, `ResultType`,
46 iterable of `ResultType`, or `SubroutineCallable`
47 """
49 _ALL_QUBITS = "ALL" # Flag to indicate all qubits in _qubit_observable_mapping
51 @classmethod
52 def register_subroutine(cls, func: SubroutineCallable) -> None:
53 """
54 Register the subroutine `func` as an attribute of the `Circuit` class. The attribute name
55 is the name of `func`.
57 Args:
58 func (Callable[..., Union[Instruction, Iterable[Instruction], ResultType,
59 Iterable[ResultType]]): The function of the subroutine to add to the class.
61 Examples:
62 >>> def h_on_all(target):
63 ... circ = Circuit()
64 ... for qubit in target:
65 ... circ += Instruction(Gate.H(), qubit)
66 ... return circ
67 ...
68 >>> Circuit.register_subroutine(h_on_all)
69 >>> circ = Circuit().h_on_all(range(2))
70 >>> for instr in circ.instructions:
71 ... print(instr)
72 ...
73 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
74 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
75 """
77 def method_from_subroutine(self, *args, **kwargs) -> SubroutineReturn:
78 return self.add(func, *args, **kwargs)
80 function_name = func.__name__
81 setattr(cls, function_name, method_from_subroutine)
83 function_attr = getattr(cls, function_name)
84 setattr(function_attr, "__doc__", func.__doc__)
86 def __init__(self, addable: AddableTypes = None, *args, **kwargs):
87 """
88 Args:
89 addable (AddableTypes): The item(s) to add to self.
90 Default = None.
91 *args: Variable length argument list. Supports any arguments that `add()` offers.
92 **kwargs: Arbitrary keyword arguments. Supports any keyword arguments that `add()`
93 offers.
95 Raises:
96 TypeError: If `addable` is an unsupported type.
98 Examples:
99 >>> circ = Circuit([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
100 >>> circ = Circuit().h(0).cnot(0, 1)
101 >>> circ = Circuit().h(0).cnot(0, 1).probability([0, 1])
103 >>> @circuit.subroutine(register=True)
104 >>> def bell_pair(target):
105 ... return Circ().h(target[0]).cnot(target[0:2])
106 ...
107 >>> circ = Circuit(bell_pair, [4,5])
108 >>> circ = Circuit().bell_pair([4,5])
110 """
111 self._moments: Moments = Moments()
112 self._result_types: List[ResultType] = []
113 self._qubit_observable_mapping: Dict[Union[int, Circuit._ALL_QUBITS], Observable] = {}
115 if addable is not None:
116 self.add(addable, *args, **kwargs)
118 @property
119 def depth(self) -> int:
120 """int: Get the circuit depth."""
121 return self._moments.depth
123 @property
124 def instructions(self) -> Iterable[Instruction]:
125 """Iterable[Instruction]: Get an `iterable` of instructions in the circuit."""
126 return self._moments.values()
128 @property
129 def result_types(self) -> List[ResultType]:
130 """List[ResultType]: Get a list of requested result types in the circuit."""
131 return self._result_types
133 @property
134 def basis_rotation_instructions(self) -> List[Instruction]:
135 """List[Instruction]: Get a list of basis rotation instructions in the circuit.
136 These basis rotation instructions are added if result types are requested for
137 an observable other than Pauli-Z.
138 """
139 # Note that basis_rotation_instructions can change each time a new instruction
140 # is added to the circuit because `self._moments.qubits` would change
141 basis_rotation_instructions = []
142 observable_return_types = filter(
143 lambda x: isinstance(x, ObservableResultType), self._result_types
144 )
145 for target, observable in [(obs.target, obs.observable) for obs in observable_return_types]:
146 for gate in observable.basis_rotation_gates:
147 if not target:
148 basis_rotation_instructions.extend(
149 [Instruction(gate, target) for target in self._moments.qubits]
150 )
151 else:
152 basis_rotation_instructions.append(Instruction(gate, target))
153 return basis_rotation_instructions
155 @property
156 def moments(self) -> Moments:
157 """Moments: Get the `moments` for this circuit."""
158 return self._moments
160 @property
161 def qubit_count(self) -> int:
162 """Get the qubit count for this circuit."""
163 return self._moments.qubit_count
165 @property
166 def qubits(self) -> QubitSet:
167 """QubitSet: Get a copy of the qubits for this circuit."""
168 return QubitSet(self._moments.qubits)
170 def add_result_type(
171 self,
172 result_type: ResultType,
173 target: QubitSetInput = None,
174 target_mapping: Dict[QubitInput, QubitInput] = {},
175 ) -> Circuit:
176 """
177 Add a requested result type to `self`, returns `self` for chaining ability.
179 Args:
180 result_type (ResultType): `ResultType` to add into `self`.
181 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
182 `result_type`.
183 Default = None.
184 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
185 qubit mappings to apply to the `result_type.target`. Key is the qubit in
186 `result_type.target` and the value is what the key will be changed to. Default = {}.
189 Note: target and target_mapping will only be applied to those requested result types with
190 the attribute `target`. The result_type will be appended to the end of the list of
191 `circuit.result_types` only if it does not already exist in `circuit.result_types`
193 Returns:
194 Circuit: self
196 Raises:
197 TypeError: If both `target_mapping` and `target` are supplied.
198 ValueError: If the observable specified for a qubit is different from what is
199 specified by the result types already added to the circuit. Only one observable
200 is allowed for a qubit.
202 Examples:
203 >>> result_type = ResultType.Probability(target=[0, 1])
204 >>> circ = Circuit().add_result_type(result_type)
205 >>> print(circ.result_types[0])
206 Probability(target=QubitSet([Qubit(0), Qubit(1)]))
208 >>> result_type = ResultType.Probability(target=[0, 1])
209 >>> circ = Circuit().add_result_type(result_type, target_mapping={0: 10, 1: 11})
210 >>> print(circ.result_types[0])
211 Probability(target=QubitSet([Qubit(10), Qubit(11)]))
213 >>> result_type = ResultType.Probability(target=[0, 1])
214 >>> circ = Circuit().add_result_type(result_type, target=[10, 11])
215 >>> print(circ.result_types[0])
216 Probability(target=QubitSet([Qubit(10), Qubit(11)]))
218 >>> result_type = ResultType.StateVector()
219 >>> circ = Circuit().add_result_type(result_type)
220 >>> print(circ.result_types[0])
221 StateVector()
222 """
223 if target_mapping and target is not None:
224 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
226 if not target_mapping and not target:
227 # Nothing has been supplied, add result_type
228 result_type_to_add = result_type
229 elif target_mapping:
230 # Target mapping has been supplied, copy result_type
231 result_type_to_add = result_type.copy(target_mapping=target_mapping)
232 else:
233 # ResultType with target
234 result_type_to_add = result_type.copy(target=target)
236 if result_type_to_add not in self._result_types:
237 self._add_to_qubit_observable_mapping(result_type)
238 self._result_types.append(result_type_to_add)
239 return self
241 def _add_to_qubit_observable_mapping(self, result_type: ResultType) -> None:
242 if isinstance(result_type, ResultType.Probability):
243 observable = Observable.Z() # computational basis
244 elif isinstance(result_type, ObservableResultType):
245 observable = result_type.observable
246 else:
247 return
248 targets = result_type.target if result_type.target else [Circuit._ALL_QUBITS]
249 all_qubits_observable = self._qubit_observable_mapping.get(Circuit._ALL_QUBITS)
250 for target in targets:
251 current_observable = all_qubits_observable or self._qubit_observable_mapping.get(target)
252 if current_observable and current_observable != observable:
253 raise ValueError(
254 f"Existing result type for observable {current_observable} for target {target}"
255 + f"conflicts with observable {observable} for new result type"
256 )
257 self._qubit_observable_mapping[target] = observable
259 def add_instruction(
260 self,
261 instruction: Instruction,
262 target: QubitSetInput = None,
263 target_mapping: Dict[QubitInput, QubitInput] = {},
264 ) -> Circuit:
265 """
266 Add an instruction to `self`, returns `self` for chaining ability.
268 Args:
269 instruction (Instruction): `Instruction` to add into `self`.
270 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
271 `instruction`. If a single qubit gate, an instruction is created for every index
272 in `target`.
273 Default = None.
274 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
275 qubit mappings to apply to the `instruction.target`. Key is the qubit in
276 `instruction.target` and the value is what the key will be changed to. Default = {}.
278 Returns:
279 Circuit: self
281 Raises:
282 TypeError: If both `target_mapping` and `target` are supplied.
284 Examples:
285 >>> instr = Instruction(Gate.CNot(), [0, 1])
286 >>> circ = Circuit().add_instruction(instr)
287 >>> print(circ.instructions[0])
288 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
290 >>> instr = Instruction(Gate.CNot(), [0, 1])
291 >>> circ = Circuit().add_instruction(instr, target_mapping={0: 10, 1: 11})
292 >>> print(circ.instructions[0])
293 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
295 >>> instr = Instruction(Gate.CNot(), [0, 1])
296 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
297 >>> print(circ.instructions[0])
298 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
300 >>> instr = Instruction(Gate.H(), 0)
301 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
302 >>> print(circ.instructions[0])
303 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
304 >>> print(circ.instructions[1])
305 Instruction('operator': 'H', 'target': QubitSet(Qubit(11),))
306 """
307 if target_mapping and target is not None:
308 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
310 if not target_mapping and not target:
311 # Nothing has been supplied, add instruction
312 instructions_to_add = [instruction]
313 elif target_mapping:
314 # Target mapping has been supplied, copy instruction
315 instructions_to_add = [instruction.copy(target_mapping=target_mapping)]
316 elif hasattr(instruction.operator, "qubit_count") and instruction.operator.qubit_count == 1:
317 # single qubit operator with target, add an instruction for each target
318 instructions_to_add = [instruction.copy(target=qubit) for qubit in target]
319 else:
320 # non single qubit operator with target, add instruction with target
321 instructions_to_add = [instruction.copy(target=target)]
323 self._moments.add(instructions_to_add)
325 return self
327 def add_circuit(
328 self,
329 circuit: Circuit,
330 target: QubitSetInput = None,
331 target_mapping: Dict[QubitInput, QubitInput] = {},
332 ) -> Circuit:
333 """
334 Add a `circuit` to self, returns self for chaining ability.
336 Args:
337 circuit (Circuit): Circuit to add into self.
338 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
339 supplied circuit. This is a macro over `target_mapping`; `target` is converted to
340 a `target_mapping` by zipping together a sorted `circuit.qubits` and `target`.
341 Default = None.
342 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
343 qubit mappings to apply to the qubits of `circuit.instructions`. Key is the qubit
344 to map, and the Value is what to change it to. Default = {}.
346 Returns:
347 Circuit: self
349 Raises:
350 TypeError: If both `target_mapping` and `target` are supplied.
352 Note:
353 Supplying `target` sorts `circuit.qubits` to have deterministic behavior since
354 `circuit.qubits` ordering is based on how instructions are inserted.
355 Use caution when using this with circuits that with a lot of qubits, as the sort
356 can be resource-intensive. Use `target_mapping` to use a linear runtime to remap
357 the qubits.
359 Requested result types of the circuit that will be added will be appended to the end
360 of the list for the existing requested result types. A result type to be added that is
361 equivalent to an existing requested result type will not be added.
363 Examples:
364 >>> widget = Circuit().h(0).cnot([0, 1])
365 >>> circ = Circuit().add_circuit(widget)
366 >>> print(circ.instructions[0])
367 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
368 >>> print(circ.instructions[1])
369 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
371 >>> widget = Circuit().h(0).cnot([0, 1])
372 >>> circ = Circuit().add_circuit(widget, target_mapping={0: 10, 1: 11})
373 >>> print(circ.instructions[0])
374 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
375 >>> print(circ.instructions[1])
376 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
378 >>> widget = Circuit().h(0).cnot([0, 1])
379 >>> circ = Circuit().add_circuit(widget, target=[10, 11])
380 >>> print(circ.instructions[0])
381 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
382 >>> print(circ.instructions[1])
383 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
384 """
385 if target_mapping and target is not None:
386 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
387 elif target is not None:
388 keys = sorted(circuit.qubits)
389 values = target
390 target_mapping = dict(zip(keys, values))
392 for instruction in circuit.instructions:
393 self.add_instruction(instruction, target_mapping=target_mapping)
395 for result_type in circuit.result_types:
396 self.add_result_type(result_type, target_mapping=target_mapping)
398 return self
400 def add(self, addable: AddableTypes, *args, **kwargs) -> Circuit:
401 """
402 Generic add method for adding item(s) to self. Any arguments that
403 `add_circuit()` and / or `add_instruction()` and / or `add_result_type`
404 supports are supported by this method. If adding a subroutine,
405 check with that subroutines documentation to determine what input it
406 allows.
408 Args:
409 addable (AddableTypes): The item(s) to add to self. Default = None.
410 *args: Variable length argument list.
411 **kwargs: Arbitrary keyword arguments.
413 Returns:
414 Circuit: self
416 Raises:
417 TypeError: If `addable` is an unsupported type
419 See Also:
420 `add_circuit()`
422 `add_instruction()`
424 `add_result_type()`
426 Examples:
427 >>> circ = Circuit().add([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
428 >>> circ = Circuit().add([ResultType.StateVector()])
430 >>> circ = Circuit().h(4).cnot([4, 5])
432 >>> @circuit.subroutine()
433 >>> def bell_pair(target):
434 ... return Circuit().h(target[0]).cnot(target[0: 2])
435 ...
436 >>> circ = Circuit().add(bell_pair, [4,5])
437 """
439 def _flatten(addable):
440 if isinstance(addable, Iterable):
441 for item in addable:
442 yield from _flatten(item)
443 else:
444 yield addable
446 for item in _flatten(addable):
447 if isinstance(item, Instruction):
448 self.add_instruction(item, *args, **kwargs)
449 elif isinstance(item, ResultType):
450 self.add_result_type(item, *args, **kwargs)
451 elif isinstance(item, Circuit):
452 self.add_circuit(item, *args, **kwargs)
453 elif callable(item):
454 self.add(item(*args, **kwargs))
455 else:
456 raise TypeError(f"Cannot add a '{type(item)}' to a Circuit")
458 return self
460 def diagram(self, circuit_diagram_class=AsciiCircuitDiagram) -> str:
461 """
462 Get a diagram for the current circuit.
464 Args:
465 circuit_diagram_class (Class, optional): A `CircuitDiagram` class that builds the
466 diagram for this circuit. Default = AsciiCircuitDiagram.
468 Returns:
469 str: An ASCII string circuit diagram.
470 """
471 return circuit_diagram_class.build_diagram(self)
473 def to_ir(self) -> Program:
474 """
475 Converts the circuit into the canonical intermediate representation.
476 If the circuit is sent over the wire, this method is called before it is sent.
478 Returns:
479 (Program): An AWS quantum circuit description program in JSON format.
480 """
481 ir_instructions = [instr.to_ir() for instr in self.instructions]
482 ir_results = [result_type.to_ir() for result_type in self.result_types]
483 ir_basis_rotation_instructions = [
484 instr.to_ir() for instr in self.basis_rotation_instructions
485 ]
486 return Program.construct(
487 instructions=ir_instructions,
488 results=ir_results,
489 basis_rotation_instructions=ir_basis_rotation_instructions,
490 )
492 def _copy(self) -> Circuit:
493 """
494 Return a shallow copy of the circuit.
496 Returns:
497 Circuit: A shallow copy of the circuit.
498 """
499 copy = Circuit().add(self.instructions)
500 copy.add(self.result_types)
501 return copy
503 def __iadd__(self, addable: AddableTypes) -> Circuit:
504 return self.add(addable)
506 def __add__(self, addable: AddableTypes) -> Circuit:
507 new = self._copy()
508 new.add(addable)
509 return new
511 def __repr__(self) -> str:
512 if not self.result_types:
513 return f"Circuit('instructions': {list(self.instructions)})"
514 else:
515 return (
516 f"Circuit('instructions': {list(self.instructions)}"
517 + f"result_types': {self.result_types})"
518 )
520 def __str__(self):
521 return self.diagram(AsciiCircuitDiagram)
523 def __eq__(self, other):
524 if isinstance(other, Circuit):
525 return (
526 list(self.instructions) == list(other.instructions)
527 and self.result_types == self.result_types
528 )
529 return NotImplemented
532def subroutine(register=False):
533 """
534 Subroutine is a function that returns instructions, result types, or circuits.
536 Args:
537 register (bool, optional): If `True`, adds this subroutine into the `Circuit` class.
538 Default = False.
540 Examples:
541 >>> @circuit.subroutine(register=True)
542 >>> def bell_circuit():
543 ... return Circuit().h(0).cnot(0, 1)
544 ...
545 >>> circ = Circuit().bell_circuit()
546 >>> for instr in circ.instructions:
547 ... print(instr)
548 ...
549 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
550 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
551 """
553 def subroutine_function_wrapper(func: Callable[..., SubroutineReturn]) -> SubroutineReturn:
554 if register:
555 Circuit.register_subroutine(func)
556 return func
558 return subroutine_function_wrapper