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.observables import TensorProduct
23from braket.circuits.qubit import QubitInput
24from braket.circuits.qubit_set import QubitSet, QubitSetInput
25from braket.circuits.result_type import ObservableResultType, ResultType
26from braket.ir.jaqcd import Program
28SubroutineReturn = TypeVar(
29 "SubroutineReturn", Iterable[Instruction], Instruction, ResultType, Iterable[ResultType]
30)
31SubroutineCallable = TypeVar("SubroutineCallable", bound=Callable[..., SubroutineReturn])
32AddableTypes = TypeVar("AddableTypes", SubroutineReturn, SubroutineCallable)
34# TODO: Add parameterization
37class Circuit:
38 """
39 A representation of a quantum circuit that contains the instructions to be performed on a
40 quantum device and the requested result types.
42 See :mod:`braket.circuits.gates` module for all of the supported instructions.
44 See :mod:`braket.circuits.result_types` module for all of the supported result types.
46 `AddableTypes` are `Instruction`, iterable of `Instruction`, `ResultType`,
47 iterable of `ResultType`, or `SubroutineCallable`
48 """
50 _ALL_QUBITS = "ALL" # Flag to indicate all qubits in _qubit_observable_mapping
51 _EXISTING_QUBITS = "EXISTING" # Flag to indicate existing qubits in _qubit_observable_mapping
53 @classmethod
54 def register_subroutine(cls, func: SubroutineCallable) -> None:
55 """
56 Register the subroutine `func` as an attribute of the `Circuit` class. The attribute name
57 is the name of `func`.
59 Args:
60 func (Callable[..., Union[Instruction, Iterable[Instruction], ResultType,
61 Iterable[ResultType]]): The function of the subroutine to add to the class.
63 Examples:
64 >>> def h_on_all(target):
65 ... circ = Circuit()
66 ... for qubit in target:
67 ... circ += Instruction(Gate.H(), qubit)
68 ... return circ
69 ...
70 >>> Circuit.register_subroutine(h_on_all)
71 >>> circ = Circuit().h_on_all(range(2))
72 >>> for instr in circ.instructions:
73 ... print(instr)
74 ...
75 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
76 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
77 """
79 def method_from_subroutine(self, *args, **kwargs) -> SubroutineReturn:
80 return self.add(func, *args, **kwargs)
82 function_name = func.__name__
83 setattr(cls, function_name, method_from_subroutine)
85 function_attr = getattr(cls, function_name)
86 setattr(function_attr, "__doc__", func.__doc__)
88 def __init__(self, addable: AddableTypes = None, *args, **kwargs):
89 """
90 Args:
91 addable (AddableTypes): The item(s) to add to self.
92 Default = None.
93 *args: Variable length argument list. Supports any arguments that `add()` offers.
94 **kwargs: Arbitrary keyword arguments. Supports any keyword arguments that `add()`
95 offers.
97 Raises:
98 TypeError: If `addable` is an unsupported type.
100 Examples:
101 >>> circ = Circuit([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
102 >>> circ = Circuit().h(0).cnot(0, 1)
103 >>> circ = Circuit().h(0).cnot(0, 1).probability([0, 1])
105 >>> @circuit.subroutine(register=True)
106 >>> def bell_pair(target):
107 ... return Circ().h(target[0]).cnot(target[0:2])
108 ...
109 >>> circ = Circuit(bell_pair, [4,5])
110 >>> circ = Circuit().bell_pair([4,5])
112 """
113 self._moments: Moments = Moments()
114 self._result_types: List[ResultType] = []
115 self._qubit_observable_mapping: Dict[Union[int, Circuit._ALL_QUBITS], Observable] = {}
117 if addable is not None:
118 self.add(addable, *args, **kwargs)
120 @property
121 def depth(self) -> int:
122 """int: Get the circuit depth."""
123 return self._moments.depth
125 @property
126 def instructions(self) -> Iterable[Instruction]:
127 """Iterable[Instruction]: Get an `iterable` of instructions in the circuit."""
128 return self._moments.values()
130 @property
131 def result_types(self) -> List[ResultType]:
132 """List[ResultType]: Get a list of requested result types in the circuit."""
133 return self._result_types
135 @property
136 def basis_rotation_instructions(self) -> List[Instruction]:
137 """List[Instruction]: Get a list of basis rotation instructions in the circuit.
138 These basis rotation instructions are added if result types are requested for
139 an observable other than Pauli-Z.
140 """
141 # Note that basis_rotation_instructions can change each time a new instruction
142 # is added to the circuit because `self._moments.qubits` would change
143 basis_rotation_instructions = []
144 observable_return_types = (
145 result_type
146 for result_type in self._result_types
147 if isinstance(result_type, ObservableResultType)
148 )
150 added_observables_targets = []
151 for return_type in observable_return_types:
152 target: List[int] = return_type.target
153 observable: Observable = return_type.observable
154 str_observables_targets = f"{observable}; {target}"
155 # only add gates for observables and targets that
156 # have not been processed
157 if str_observables_targets in added_observables_targets:
158 continue
159 added_observables_targets.append(str_observables_targets)
160 if not target:
161 # There will be only one result type in observable_return_types,
162 # and its observable acts on all qubits
163 for target in self._moments.qubits:
164 basis_rotation_instructions += Circuit._observable_to_instruction(
165 observable, target
166 )
167 else:
168 basis_rotation_instructions += Circuit._observable_to_instruction(
169 observable, target
170 )
171 return basis_rotation_instructions
173 @staticmethod
174 def _observable_to_instruction(observable: Observable, targets: List[int]):
175 if isinstance(observable, TensorProduct):
176 instructions = []
177 for factor in observable.factors:
178 target = [targets.pop(0) for _ in range(factor.qubit_count)]
179 instructions += Circuit._observable_to_instruction(factor, target)
180 return instructions
181 else:
182 return [Instruction(gate, targets) for gate in observable.basis_rotation_gates]
184 @property
185 def moments(self) -> Moments:
186 """Moments: Get the `moments` for this circuit."""
187 return self._moments
189 @property
190 def qubit_count(self) -> int:
191 """Get the qubit count for this circuit."""
192 return self._moments.qubit_count
194 @property
195 def qubits(self) -> QubitSet:
196 """QubitSet: Get a copy of the qubits for this circuit."""
197 return QubitSet(self._moments.qubits)
199 def add_result_type(
200 self,
201 result_type: ResultType,
202 target: QubitSetInput = None,
203 target_mapping: Dict[QubitInput, QubitInput] = {},
204 ) -> Circuit:
205 """
206 Add a requested result type to `self`, returns `self` for chaining ability.
208 Args:
209 result_type (ResultType): `ResultType` to add into `self`.
210 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
211 `result_type`.
212 Default = None.
213 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
214 qubit mappings to apply to the `result_type.target`. Key is the qubit in
215 `result_type.target` and the value is what the key will be changed to. Default = {}.
218 Note: target and target_mapping will only be applied to those requested result types with
219 the attribute `target`. The result_type will be appended to the end of the list of
220 `circuit.result_types` only if it does not already exist in `circuit.result_types`
222 Returns:
223 Circuit: self
225 Raises:
226 TypeError: If both `target_mapping` and `target` are supplied.
227 ValueError: If the observable specified for a qubit is different from what is
228 specified by the result types already added to the circuit. Only one observable
229 is allowed for a qubit.
231 Examples:
232 >>> result_type = ResultType.Probability(target=[0, 1])
233 >>> circ = Circuit().add_result_type(result_type)
234 >>> print(circ.result_types[0])
235 Probability(target=QubitSet([Qubit(0), Qubit(1)]))
237 >>> result_type = ResultType.Probability(target=[0, 1])
238 >>> circ = Circuit().add_result_type(result_type, target_mapping={0: 10, 1: 11})
239 >>> print(circ.result_types[0])
240 Probability(target=QubitSet([Qubit(10), Qubit(11)]))
242 >>> result_type = ResultType.Probability(target=[0, 1])
243 >>> circ = Circuit().add_result_type(result_type, target=[10, 11])
244 >>> print(circ.result_types[0])
245 Probability(target=QubitSet([Qubit(10), Qubit(11)]))
247 >>> result_type = ResultType.StateVector()
248 >>> circ = Circuit().add_result_type(result_type)
249 >>> print(circ.result_types[0])
250 StateVector()
251 """
252 if target_mapping and target is not None:
253 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
255 if not target_mapping and not target:
256 # Nothing has been supplied, add result_type
257 result_type_to_add = result_type
258 elif target_mapping:
259 # Target mapping has been supplied, copy result_type
260 result_type_to_add = result_type.copy(target_mapping=target_mapping)
261 else:
262 # ResultType with target
263 result_type_to_add = result_type.copy(target=target)
265 if result_type_to_add not in self._result_types:
266 self._add_to_qubit_observable_mapping(result_type)
267 self._result_types.append(result_type_to_add)
268 return self
270 def _add_to_qubit_observable_mapping(self, result_type: ResultType) -> None:
271 if isinstance(result_type, ResultType.Probability):
272 observable = Observable.Z() # computational basis
273 elif isinstance(result_type, ObservableResultType):
274 observable = result_type.observable
275 else:
276 return
278 targets = result_type.target if result_type.target else [Circuit._EXISTING_QUBITS]
279 all_qubits_observable = self._qubit_observable_mapping.get(Circuit._ALL_QUBITS)
281 for target in targets:
282 current_observable = all_qubits_observable or self._qubit_observable_mapping.get(target)
283 if current_observable and current_observable != observable:
284 raise ValueError(
285 f"Existing result type for observable {current_observable} for target {target}"
286 f" conflicts with observable {observable} for new result type"
287 )
288 if target == Circuit._EXISTING_QUBITS:
289 target = Circuit._ALL_QUBITS
290 self._qubit_observable_mapping[target] = observable
291 self._qubit_observable_mapping[Circuit._EXISTING_QUBITS] = observable
293 def add_instruction(
294 self,
295 instruction: Instruction,
296 target: QubitSetInput = None,
297 target_mapping: Dict[QubitInput, QubitInput] = {},
298 ) -> Circuit:
299 """
300 Add an instruction to `self`, returns `self` for chaining ability.
302 Args:
303 instruction (Instruction): `Instruction` to add into `self`.
304 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
305 `instruction`. If a single qubit gate, an instruction is created for every index
306 in `target`.
307 Default = None.
308 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
309 qubit mappings to apply to the `instruction.target`. Key is the qubit in
310 `instruction.target` and the value is what the key will be changed to. Default = {}.
312 Returns:
313 Circuit: self
315 Raises:
316 TypeError: If both `target_mapping` and `target` are supplied.
318 Examples:
319 >>> instr = Instruction(Gate.CNot(), [0, 1])
320 >>> circ = Circuit().add_instruction(instr)
321 >>> print(circ.instructions[0])
322 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
324 >>> instr = Instruction(Gate.CNot(), [0, 1])
325 >>> circ = Circuit().add_instruction(instr, target_mapping={0: 10, 1: 11})
326 >>> print(circ.instructions[0])
327 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
329 >>> instr = Instruction(Gate.CNot(), [0, 1])
330 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
331 >>> print(circ.instructions[0])
332 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
334 >>> instr = Instruction(Gate.H(), 0)
335 >>> circ = Circuit().add_instruction(instr, target=[10, 11])
336 >>> print(circ.instructions[0])
337 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
338 >>> print(circ.instructions[1])
339 Instruction('operator': 'H', 'target': QubitSet(Qubit(11),))
340 """
341 if target_mapping and target is not None:
342 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
344 if not target_mapping and not target:
345 # Nothing has been supplied, add instruction
346 instructions_to_add = [instruction]
347 elif target_mapping:
348 # Target mapping has been supplied, copy instruction
349 instructions_to_add = [instruction.copy(target_mapping=target_mapping)]
350 elif hasattr(instruction.operator, "qubit_count") and instruction.operator.qubit_count == 1:
351 # single qubit operator with target, add an instruction for each target
352 instructions_to_add = [instruction.copy(target=qubit) for qubit in target]
353 else:
354 # non single qubit operator with target, add instruction with target
355 instructions_to_add = [instruction.copy(target=target)]
357 self._moments.add(instructions_to_add)
359 return self
361 def add_circuit(
362 self,
363 circuit: Circuit,
364 target: QubitSetInput = None,
365 target_mapping: Dict[QubitInput, QubitInput] = {},
366 ) -> Circuit:
367 """
368 Add a `circuit` to self, returns self for chaining ability.
370 Args:
371 circuit (Circuit): Circuit to add into self.
372 target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the
373 supplied circuit. This is a macro over `target_mapping`; `target` is converted to
374 a `target_mapping` by zipping together a sorted `circuit.qubits` and `target`.
375 Default = None.
376 target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of
377 qubit mappings to apply to the qubits of `circuit.instructions`. Key is the qubit
378 to map, and the Value is what to change it to. Default = {}.
380 Returns:
381 Circuit: self
383 Raises:
384 TypeError: If both `target_mapping` and `target` are supplied.
386 Note:
387 Supplying `target` sorts `circuit.qubits` to have deterministic behavior since
388 `circuit.qubits` ordering is based on how instructions are inserted.
389 Use caution when using this with circuits that with a lot of qubits, as the sort
390 can be resource-intensive. Use `target_mapping` to use a linear runtime to remap
391 the qubits.
393 Requested result types of the circuit that will be added will be appended to the end
394 of the list for the existing requested result types. A result type to be added that is
395 equivalent to an existing requested result type will not be added.
397 Examples:
398 >>> widget = Circuit().h(0).cnot([0, 1])
399 >>> circ = Circuit().add_circuit(widget)
400 >>> print(circ.instructions[0])
401 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
402 >>> print(circ.instructions[1])
403 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(0), Qubit(1)))
405 >>> widget = Circuit().h(0).cnot([0, 1])
406 >>> circ = Circuit().add_circuit(widget, target_mapping={0: 10, 1: 11})
407 >>> print(circ.instructions[0])
408 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
409 >>> print(circ.instructions[1])
410 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
412 >>> widget = Circuit().h(0).cnot([0, 1])
413 >>> circ = Circuit().add_circuit(widget, target=[10, 11])
414 >>> print(circ.instructions[0])
415 Instruction('operator': 'H', 'target': QubitSet(Qubit(10),))
416 >>> print(circ.instructions[1])
417 Instruction('operator': 'CNOT', 'target': QubitSet(Qubit(10), Qubit(11)))
418 """
419 if target_mapping and target is not None:
420 raise TypeError("Only one of 'target_mapping' or 'target' can be supplied.")
421 elif target is not None:
422 keys = sorted(circuit.qubits)
423 values = target
424 target_mapping = dict(zip(keys, values))
426 for instruction in circuit.instructions:
427 self.add_instruction(instruction, target_mapping=target_mapping)
429 for result_type in circuit.result_types:
430 self.add_result_type(result_type, target_mapping=target_mapping)
432 return self
434 def add(self, addable: AddableTypes, *args, **kwargs) -> Circuit:
435 """
436 Generic add method for adding item(s) to self. Any arguments that
437 `add_circuit()` and / or `add_instruction()` and / or `add_result_type`
438 supports are supported by this method. If adding a subroutine,
439 check with that subroutines documentation to determine what input it
440 allows.
442 Args:
443 addable (AddableTypes): The item(s) to add to self. Default = None.
444 *args: Variable length argument list.
445 **kwargs: Arbitrary keyword arguments.
447 Returns:
448 Circuit: self
450 Raises:
451 TypeError: If `addable` is an unsupported type
453 See Also:
454 `add_circuit()`
456 `add_instruction()`
458 `add_result_type()`
460 Examples:
461 >>> circ = Circuit().add([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])])
462 >>> circ = Circuit().add([ResultType.StateVector()])
464 >>> circ = Circuit().h(4).cnot([4, 5])
466 >>> @circuit.subroutine()
467 >>> def bell_pair(target):
468 ... return Circuit().h(target[0]).cnot(target[0: 2])
469 ...
470 >>> circ = Circuit().add(bell_pair, [4,5])
471 """
473 def _flatten(addable):
474 if isinstance(addable, Iterable):
475 for item in addable:
476 yield from _flatten(item)
477 else:
478 yield addable
480 for item in _flatten(addable):
481 if isinstance(item, Instruction):
482 self.add_instruction(item, *args, **kwargs)
483 elif isinstance(item, ResultType):
484 self.add_result_type(item, *args, **kwargs)
485 elif isinstance(item, Circuit):
486 self.add_circuit(item, *args, **kwargs)
487 elif callable(item):
488 self.add(item(*args, **kwargs))
489 else:
490 raise TypeError(f"Cannot add a '{type(item)}' to a Circuit")
492 return self
494 def diagram(self, circuit_diagram_class=AsciiCircuitDiagram) -> str:
495 """
496 Get a diagram for the current circuit.
498 Args:
499 circuit_diagram_class (Class, optional): A `CircuitDiagram` class that builds the
500 diagram for this circuit. Default = AsciiCircuitDiagram.
502 Returns:
503 str: An ASCII string circuit diagram.
504 """
505 return circuit_diagram_class.build_diagram(self)
507 def to_ir(self) -> Program:
508 """
509 Converts the circuit into the canonical intermediate representation.
510 If the circuit is sent over the wire, this method is called before it is sent.
512 Returns:
513 (Program): An AWS quantum circuit description program in JSON format.
514 """
515 ir_instructions = [instr.to_ir() for instr in self.instructions]
516 ir_results = [result_type.to_ir() for result_type in self.result_types]
517 ir_basis_rotation_instructions = [
518 instr.to_ir() for instr in self.basis_rotation_instructions
519 ]
520 return Program.construct(
521 instructions=ir_instructions,
522 results=ir_results,
523 basis_rotation_instructions=ir_basis_rotation_instructions,
524 )
526 def _copy(self) -> Circuit:
527 """
528 Return a shallow copy of the circuit.
530 Returns:
531 Circuit: A shallow copy of the circuit.
532 """
533 copy = Circuit().add(self.instructions)
534 copy.add(self.result_types)
535 return copy
537 def __iadd__(self, addable: AddableTypes) -> Circuit:
538 return self.add(addable)
540 def __add__(self, addable: AddableTypes) -> Circuit:
541 new = self._copy()
542 new.add(addable)
543 return new
545 def __repr__(self) -> str:
546 if not self.result_types:
547 return f"Circuit('instructions': {list(self.instructions)})"
548 else:
549 return (
550 f"Circuit('instructions': {list(self.instructions)}"
551 + f"result_types': {self.result_types})"
552 )
554 def __str__(self):
555 return self.diagram(AsciiCircuitDiagram)
557 def __eq__(self, other):
558 if isinstance(other, Circuit):
559 return (
560 list(self.instructions) == list(other.instructions)
561 and self.result_types == self.result_types
562 )
563 return NotImplemented
566def subroutine(register=False):
567 """
568 Subroutine is a function that returns instructions, result types, or circuits.
570 Args:
571 register (bool, optional): If `True`, adds this subroutine into the `Circuit` class.
572 Default = False.
574 Examples:
575 >>> @circuit.subroutine(register=True)
576 >>> def bell_circuit():
577 ... return Circuit().h(0).cnot(0, 1)
578 ...
579 >>> circ = Circuit().bell_circuit()
580 >>> for instr in circ.instructions:
581 ... print(instr)
582 ...
583 Instruction('operator': 'H', 'target': QubitSet(Qubit(0),))
584 Instruction('operator': 'H', 'target': QubitSet(Qubit(1),))
585 """
587 def subroutine_function_wrapper(func: Callable[..., SubroutineReturn]) -> SubroutineReturn:
588 if register:
589 Circuit.register_subroutine(func)
590 return func
592 return subroutine_function_wrapper