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

26 

27SubroutineReturn = TypeVar( 

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

29) 

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

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

32 

33# TODO: Add parameterization 

34 

35 

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. 

40 

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

42 

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

44 

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

46 iterable of `ResultType`, or `SubroutineCallable` 

47 """ 

48 

49 _ALL_QUBITS = "ALL" # Flag to indicate all qubits in _qubit_observable_mapping 

50 

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

56 

57 Args: 

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

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

60 

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

76 

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

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

79 

80 function_name = func.__name__ 

81 setattr(cls, function_name, method_from_subroutine) 

82 

83 function_attr = getattr(cls, function_name) 

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

85 

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. 

94 

95 Raises: 

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

97 

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

102 

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

109 

110 """ 

111 self._moments: Moments = Moments() 

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

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

114 

115 if addable is not None: 

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

117 

118 @property 

119 def depth(self) -> int: 

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

121 return self._moments.depth 

122 

123 @property 

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

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

126 return self._moments.values() 

127 

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 

132 

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 

154 

155 @property 

156 def moments(self) -> Moments: 

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

158 return self._moments 

159 

160 @property 

161 def qubit_count(self) -> int: 

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

163 return self._moments.qubit_count 

164 

165 @property 

166 def qubits(self) -> QubitSet: 

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

168 return QubitSet(self._moments.qubits) 

169 

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. 

178 

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

187 

188 

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` 

192 

193 Returns: 

194 Circuit: self 

195 

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. 

201 

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

207 

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

212 

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

217 

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

225 

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) 

235 

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 

240 

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 

258 

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. 

267 

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

277 

278 Returns: 

279 Circuit: self 

280 

281 Raises: 

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

283 

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

289 

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

294 

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

299 

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

309 

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

322 

323 self._moments.add(instructions_to_add) 

324 

325 return self 

326 

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. 

335 

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

345 

346 Returns: 

347 Circuit: self 

348 

349 Raises: 

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

351 

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. 

358 

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. 

362 

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

370 

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

377 

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

391 

392 for instruction in circuit.instructions: 

393 self.add_instruction(instruction, target_mapping=target_mapping) 

394 

395 for result_type in circuit.result_types: 

396 self.add_result_type(result_type, target_mapping=target_mapping) 

397 

398 return self 

399 

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. 

407 

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. 

412 

413 Returns: 

414 Circuit: self 

415 

416 Raises: 

417 TypeError: If `addable` is an unsupported type 

418 

419 See Also: 

420 `add_circuit()` 

421 

422 `add_instruction()` 

423 

424 `add_result_type()` 

425 

426 Examples: 

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

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

429 

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

431 

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

438 

439 def _flatten(addable): 

440 if isinstance(addable, Iterable): 

441 for item in addable: 

442 yield from _flatten(item) 

443 else: 

444 yield addable 

445 

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

457 

458 return self 

459 

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

461 """ 

462 Get a diagram for the current circuit. 

463 

464 Args: 

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

466 diagram for this circuit. Default = AsciiCircuitDiagram. 

467 

468 Returns: 

469 str: An ASCII string circuit diagram. 

470 """ 

471 return circuit_diagram_class.build_diagram(self) 

472 

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. 

477 

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 ) 

491 

492 def _copy(self) -> Circuit: 

493 """ 

494 Return a shallow copy of the circuit. 

495 

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 

502 

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

504 return self.add(addable) 

505 

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

507 new = self._copy() 

508 new.add(addable) 

509 return new 

510 

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 ) 

519 

520 def __str__(self): 

521 return self.diagram(AsciiCircuitDiagram) 

522 

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 

530 

531 

532def subroutine(register=False): 

533 """ 

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

535 

536 Args: 

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

538 Default = False. 

539 

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

552 

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

554 if register: 

555 Circuit.register_subroutine(func) 

556 return func 

557 

558 return subroutine_function_wrapper