Hide keyboard shortcuts

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. 

13 

14from __future__ import annotations 

15 

16from typing import Callable, Dict, Iterable, List, TypeVar, Union 

17 

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 

27 

28SubroutineReturn = TypeVar( 

29 "SubroutineReturn", Iterable[Instruction], Instruction, ResultType, Iterable[ResultType] 

30) 

31SubroutineCallable = TypeVar("SubroutineCallable", bound=Callable[..., SubroutineReturn]) 

32AddableTypes = TypeVar("AddableTypes", SubroutineReturn, SubroutineCallable) 

33 

34# TODO: Add parameterization 

35 

36 

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. 

41 

42 See :mod:`braket.circuits.gates` module for all of the supported instructions. 

43 

44 See :mod:`braket.circuits.result_types` module for all of the supported result types. 

45 

46 `AddableTypes` are `Instruction`, iterable of `Instruction`, `ResultType`, 

47 iterable of `ResultType`, or `SubroutineCallable` 

48 """ 

49 

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 

52 

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`. 

58 

59 Args: 

60 func (Callable[..., Union[Instruction, Iterable[Instruction], ResultType, 

61 Iterable[ResultType]]): The function of the subroutine to add to the class. 

62 

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 """ 

78 

79 def method_from_subroutine(self, *args, **kwargs) -> SubroutineReturn: 

80 return self.add(func, *args, **kwargs) 

81 

82 function_name = func.__name__ 

83 setattr(cls, function_name, method_from_subroutine) 

84 

85 function_attr = getattr(cls, function_name) 

86 setattr(function_attr, "__doc__", func.__doc__) 

87 

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. 

96 

97 Raises: 

98 TypeError: If `addable` is an unsupported type. 

99 

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]) 

104 

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]) 

111 

112 """ 

113 self._moments: Moments = Moments() 

114 self._result_types: List[ResultType] = [] 

115 self._qubit_observable_mapping: Dict[Union[int, Circuit._ALL_QUBITS], Observable] = {} 

116 

117 if addable is not None: 

118 self.add(addable, *args, **kwargs) 

119 

120 @property 

121 def depth(self) -> int: 

122 """int: Get the circuit depth.""" 

123 return self._moments.depth 

124 

125 @property 

126 def instructions(self) -> Iterable[Instruction]: 

127 """Iterable[Instruction]: Get an `iterable` of instructions in the circuit.""" 

128 return self._moments.values() 

129 

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 

134 

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 ) 

149 

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 

172 

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] 

183 

184 @property 

185 def moments(self) -> Moments: 

186 """Moments: Get the `moments` for this circuit.""" 

187 return self._moments 

188 

189 @property 

190 def qubit_count(self) -> int: 

191 """Get the qubit count for this circuit.""" 

192 return self._moments.qubit_count 

193 

194 @property 

195 def qubits(self) -> QubitSet: 

196 """QubitSet: Get a copy of the qubits for this circuit.""" 

197 return QubitSet(self._moments.qubits) 

198 

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. 

207 

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 = {}. 

216 

217 

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` 

221 

222 Returns: 

223 Circuit: self 

224 

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. 

230 

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)])) 

236 

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)])) 

241 

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)])) 

246 

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.") 

254 

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) 

264 

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 

269 

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 

277 

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) 

280 

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 

292 

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. 

301 

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 = {}. 

311 

312 Returns: 

313 Circuit: self 

314 

315 Raises: 

316 TypeError: If both `target_mapping` and `target` are supplied. 

317 

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))) 

323 

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))) 

328 

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))) 

333 

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.") 

343 

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)] 

356 

357 self._moments.add(instructions_to_add) 

358 

359 return self 

360 

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. 

369 

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 = {}. 

379 

380 Returns: 

381 Circuit: self 

382 

383 Raises: 

384 TypeError: If both `target_mapping` and `target` are supplied. 

385 

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. 

392 

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. 

396 

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))) 

404 

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))) 

411 

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)) 

425 

426 for instruction in circuit.instructions: 

427 self.add_instruction(instruction, target_mapping=target_mapping) 

428 

429 for result_type in circuit.result_types: 

430 self.add_result_type(result_type, target_mapping=target_mapping) 

431 

432 return self 

433 

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. 

441 

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. 

446 

447 Returns: 

448 Circuit: self 

449 

450 Raises: 

451 TypeError: If `addable` is an unsupported type 

452 

453 See Also: 

454 `add_circuit()` 

455 

456 `add_instruction()` 

457 

458 `add_result_type()` 

459 

460 Examples: 

461 >>> circ = Circuit().add([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])]) 

462 >>> circ = Circuit().add([ResultType.StateVector()]) 

463 

464 >>> circ = Circuit().h(4).cnot([4, 5]) 

465 

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 """ 

472 

473 def _flatten(addable): 

474 if isinstance(addable, Iterable): 

475 for item in addable: 

476 yield from _flatten(item) 

477 else: 

478 yield addable 

479 

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") 

491 

492 return self 

493 

494 def diagram(self, circuit_diagram_class=AsciiCircuitDiagram) -> str: 

495 """ 

496 Get a diagram for the current circuit. 

497 

498 Args: 

499 circuit_diagram_class (Class, optional): A `CircuitDiagram` class that builds the 

500 diagram for this circuit. Default = AsciiCircuitDiagram. 

501 

502 Returns: 

503 str: An ASCII string circuit diagram. 

504 """ 

505 return circuit_diagram_class.build_diagram(self) 

506 

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. 

511 

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 ) 

525 

526 def _copy(self) -> Circuit: 

527 """ 

528 Return a shallow copy of the circuit. 

529 

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 

536 

537 def __iadd__(self, addable: AddableTypes) -> Circuit: 

538 return self.add(addable) 

539 

540 def __add__(self, addable: AddableTypes) -> Circuit: 

541 new = self._copy() 

542 new.add(addable) 

543 return new 

544 

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 ) 

553 

554 def __str__(self): 

555 return self.diagram(AsciiCircuitDiagram) 

556 

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 

564 

565 

566def subroutine(register=False): 

567 """ 

568 Subroutine is a function that returns instructions, result types, or circuits. 

569 

570 Args: 

571 register (bool, optional): If `True`, adds this subroutine into the `Circuit` class. 

572 Default = False. 

573 

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 """ 

586 

587 def subroutine_function_wrapper(func: Callable[..., SubroutineReturn]) -> SubroutineReturn: 

588 if register: 

589 Circuit.register_subroutine(func) 

590 return func 

591 

592 return subroutine_function_wrapper