Coverage for qml_essentials/model.py: 90%

200 statements  

« prev     ^ index     » next       coverage.py v7.6.5, created at 2024-11-15 11:13 +0000

1from typing import Dict, Optional, Tuple, Callable, Union, List 

2import pennylane as qml 

3import pennylane.numpy as np 

4import hashlib 

5import os 

6import warnings 

7from autograd.numpy import numpy_boxes 

8 

9from qml_essentials.ansaetze import Ansaetze, Circuit 

10 

11import logging 

12 

13log = logging.getLogger(__name__) 

14 

15 

16class Model: 

17 """ 

18 A quantum circuit model. 

19 """ 

20 

21 def __init__( 

22 self, 

23 n_qubits: int, 

24 n_layers: int, 

25 circuit_type: Union[str, Circuit], 

26 data_reupload: bool = True, 

27 initialization: str = "random", 

28 initialization_domain: List[float] = [0, 2 * np.pi], 

29 output_qubit: Union[List[int], int] = -1, 

30 shots: Optional[int] = None, 

31 random_seed: int = 1000, 

32 ) -> None: 

33 """ 

34 Initialize the quantum circuit model. 

35 Parameters will have the shape [impl_n_layers, parameters_per_layer] 

36 where impl_n_layers is the number of layers provided and added by one 

37 depending if data_reupload is True and parameters_per_layer is given by 

38 the chosen ansatz. 

39 

40 The model is initialized with the following parameters as defaults: 

41 - noise_params: None 

42 - execution_type: "expval" 

43 - shots: None 

44 

45 Args: 

46 n_qubits (int): The number of qubits in the circuit. 

47 n_layers (int): The number of layers in the circuit. 

48 circuit_type (str, Circuit): The type of quantum circuit to use. 

49 If None, defaults to "no_ansatz". 

50 data_reupload (bool, optional): Whether to reupload data to the 

51 quantum device on each measurement. Defaults to True. 

52 initialization (str, optional): The strategy to initialize the parameters. 

53 Can be "random", "zeros", "zero-controlled", "pi", or "pi-controlled". 

54 Defaults to "random". 

55 output_qubit (List[int], int, optional): The index of the output 

56 qubit (or qubits). When set to -1 all qubits are measured, or a 

57 global measurement is conducted, depending on the execution 

58 type. 

59 shots (Optional[int], optional): The number of shots to use for 

60 the quantum device. Defaults to None. 

61 random_seed (int, optional): seed for the random number generator 

62 in initialization is "random", Defaults to 1000. 

63 

64 Returns: 

65 None 

66 """ 

67 # Initialize default parameters needed for circuit evaluation 

68 self.noise_params: Optional[Dict[str, float]] = None 

69 self.execution_type: Optional[str] = "expval" 

70 self.shots = shots 

71 self.output_qubit: Union[List[int], int] = output_qubit 

72 

73 # Copy the parameters 

74 self.n_qubits: int = n_qubits 

75 self.n_layers: int = n_layers 

76 self.data_reupload: bool = data_reupload 

77 

78 lightning_threshold = 12 

79 

80 # Initialize ansatz 

81 # only weak check for str. We trust the user to provide sth useful 

82 if isinstance(circuit_type, str): 

83 self.pqc: Callable[[Optional[np.ndarray], int], int] = getattr( 

84 Ansaetze, circuit_type or "No_Ansatz" 

85 )() 

86 else: 

87 self.pqc = circuit_type() 

88 

89 log.info(f"Using {circuit_type} circuit.") 

90 

91 if data_reupload: 

92 impl_n_layers: int = n_layers + 1 # we need L+1 according to Schuld et al. 

93 self.degree = n_layers * n_qubits 

94 else: 

95 impl_n_layers: int = n_layers 

96 self.degree = 1 

97 

98 log.info(f"Number of implicit layers set to {impl_n_layers}.") 

99 # calculate the shape of the parameter vector here, we will re-use this in init. 

100 self._params_shape: Tuple[int, int] = ( 

101 impl_n_layers, 

102 self.pqc.n_params_per_layer(self.n_qubits), 

103 ) 

104 # this will also be re-used in the init method, 

105 # however, only if nothing is provided 

106 self._inialization_strategy = initialization 

107 self._initialization_domain = initialization_domain 

108 

109 # ..here! where we only require a rng 

110 self.initialize_params(np.random.default_rng(random_seed)) 

111 

112 # Initialize two circuits, one with the default device and 

113 # one with the mixed device 

114 # which allows us to later route depending on the state_vector flag 

115 self.circuit: qml.QNode = qml.QNode( 

116 self._circuit, 

117 qml.device( 

118 ( 

119 "default.qubit" 

120 if self.n_qubits < lightning_threshold 

121 else "lightning.qubit" 

122 ), 

123 shots=self.shots, 

124 wires=self.n_qubits, 

125 ), 

126 interface="autograd" if self.shots is not None else "auto", 

127 diff_method="parameter-shift" if self.shots is not None else "best", 

128 ) 

129 self.circuit_mixed: qml.QNode = qml.QNode( 

130 self._circuit, 

131 qml.device("default.mixed", shots=self.shots, wires=self.n_qubits), 

132 ) 

133 

134 log.debug(self._draw()) 

135 

136 @property 

137 def noise_params(self) -> Optional[Dict[str, float]]: 

138 """ 

139 Gets the noise parameters of the model. 

140 

141 Returns: 

142 Optional[Dict[str, float]]: A dictionary of 

143 noise parameters or None if not set. 

144 """ 

145 return self._noise_params 

146 

147 @noise_params.setter 

148 def noise_params(self, value: Optional[Dict[str, float]]) -> None: 

149 """ 

150 Sets the noise parameters of the model. 

151 

152 Args: 

153 value (Optional[Dict[str, float]]): A dictionary of noise parameters. 

154 If all values are 0.0, the noise parameters are set to None. 

155 

156 Returns: 

157 None 

158 """ 

159 if value is not None and all(np == 0.0 for np in value.values()): 

160 value = None 

161 self._noise_params = value 

162 

163 @property 

164 def execution_type(self) -> str: 

165 """ 

166 Gets the execution type of the model. 

167 

168 Returns: 

169 str: The execution type, one of 'density', 'expval', or 'probs'. 

170 """ 

171 return self._execution_type 

172 

173 @execution_type.setter 

174 def execution_type(self, value: str) -> None: 

175 if value not in ["density", "expval", "probs"]: 

176 raise ValueError(f"Invalid execution type: {value}.") 

177 

178 if value == "density" and self.output_qubit != -1: 

179 warnings.warn( 

180 f"{value} measurement does ignore output_qubit, which is " 

181 f"{self.output_qubit}.", 

182 UserWarning, 

183 ) 

184 

185 if value == "probs" and self.shots is None: 

186 warnings.warn( 

187 "Setting execution_type to probs without specifying shots.", UserWarning 

188 ) 

189 

190 if value == "density" and self.shots is not None: 

191 warnings.warn( 

192 "Setting execution_type to density with specified shots.", UserWarning 

193 ) 

194 

195 self._execution_type = value 

196 

197 @property 

198 def shots(self) -> Optional[int]: 

199 """ 

200 Gets the number of shots to use for the quantum device. 

201 

202 Returns: 

203 Optional[int]: The number of shots. 

204 """ 

205 return self._shots 

206 

207 @shots.setter 

208 def shots(self, value: Optional[int]) -> None: 

209 """ 

210 Sets the number of shots to use for the quantum device. 

211 

212 Args: 

213 value (Optional[int]): The number of shots. 

214 If an integer less than or equal to 0 is provided, it is set to None. 

215 

216 Returns: 

217 None 

218 """ 

219 if type(value) is int and value <= 0: 

220 value = None 

221 self._shots = value 

222 

223 def initialize_params( 

224 self, 

225 rng, 

226 repeat: int = None, 

227 initialization: str = None, 

228 initialization_domain: List[float] = None, 

229 ) -> None: 

230 """ 

231 Initializes the parameters of the model. 

232 

233 Args: 

234 rng: A random number generator to use for initialization. 

235 repeat: The number of times to repeat the parameters. 

236 If None, the number of layers is used. 

237 initialization: The strategy to use for parameter initialization. 

238 If None, the strategy specified in the constructor is used. 

239 initialization_domain: The domain to use for parameter initialization. 

240 If None, the domain specified in the constructor is used. 

241 

242 Returns: 

243 None 

244 """ 

245 params_shape = ( 

246 self._params_shape if repeat is None else [*self._params_shape, repeat] 

247 ) 

248 # use existing strategy if not specified 

249 initialization = initialization or self._inialization_strategy 

250 initialization_domain = initialization_domain or self._initialization_domain 

251 

252 def set_control_params(params: np.ndarray, value: float) -> np.ndarray: 

253 indices = self.pqc.get_control_indices(self.n_qubits) 

254 if indices is None: 

255 warnings.warn( 

256 f"Specified {initialization} but circuit\ 

257 does not contain controlled rotation gates.\ 

258 Parameters are intialized randomly.", 

259 UserWarning, 

260 ) 

261 else: 

262 params[:, indices[0] : indices[1] : indices[2]] = ( 

263 np.ones_like(params[:, indices[0] : indices[1] : indices[2]]) 

264 * value 

265 ) 

266 return params 

267 

268 if initialization == "random": 

269 self.params: np.ndarray = rng.uniform( 

270 *initialization_domain, params_shape, requires_grad=True 

271 ) 

272 elif initialization == "zeros": 

273 self.params: np.ndarray = np.zeros(params_shape, requires_grad=True) 

274 elif initialization == "pi": 

275 self.params: np.ndarray = np.ones(params_shape, requires_grad=True) * np.pi 

276 elif initialization == "zero-controlled": 

277 self.params: np.ndarray = rng.uniform( 

278 *initialization_domain, params_shape, requires_grad=True 

279 ) 

280 self.params = set_control_params(self.params, 0) 

281 elif initialization == "pi-controlled": 

282 self.params: np.ndarray = rng.uniform( 

283 *initialization_domain, params_shape, requires_grad=True 

284 ) 

285 self.params = set_control_params(self.params, np.pi) 

286 else: 

287 raise Exception("Invalid initialization method") 

288 

289 log.info( 

290 f"Initialized parameters with shape {self.params.shape}\ 

291 using strategy {initialization}." 

292 ) 

293 

294 def _iec( 

295 self, 

296 inputs: np.ndarray, 

297 data_reupload: bool = True, 

298 ) -> None: 

299 """ 

300 Creates an AngleEncoding using RX gates 

301 

302 Args: 

303 inputs (np.ndarray): length of vector must be 1, shape (1,) 

304 data_reupload (bool, optional): Whether to reupload the data 

305 for the IEC or not, default is True. 

306 

307 Returns: 

308 None 

309 """ 

310 if inputs is None: 

311 # initialize to zero 

312 inputs = np.array([[0]]) 

313 elif len(inputs.shape) == 1: 

314 # add a batch dimension 

315 inputs = inputs.reshape(-1, 1) 

316 

317 if data_reupload: 

318 if inputs.shape[1] == 1: 

319 for q in range(self.n_qubits): 

320 qml.RX(inputs[:, 0], wires=q) 

321 elif inputs.shape[1] == 2: 

322 for q in range(self.n_qubits): 

323 qml.RX(inputs[:, 0], wires=q) 

324 qml.RY(inputs[:, 1], wires=q) 

325 elif inputs.shape[1] == 3: 

326 for q in range(self.n_qubits): 

327 qml.Rot(inputs[:, 0], inputs[:, 1], inputs[:, 2], wires=q) 

328 else: 

329 raise ValueError( 

330 "The number of parameters for this IEC cannot be greater than 3" 

331 ) 

332 else: 

333 if inputs.shape[1] == 1: 

334 qml.RX(inputs[:, 0], wires=0) 

335 elif inputs.shape[1] == 2: 

336 qml.RX(inputs[:, 0], wires=0) 

337 qml.RY(inputs[:, 1], wires=0) 

338 elif inputs.shape[1] == 3: 

339 qml.Rot(inputs[:, 0], inputs[:, 1], inputs[:, 2], wires=0) 

340 else: 

341 raise ValueError( 

342 "The number of parameters for this IEC cannot be greater than 3" 

343 ) 

344 

345 def _circuit( 

346 self, 

347 params: np.ndarray, 

348 inputs: np.ndarray, 

349 ) -> Union[float, np.ndarray]: 

350 """ 

351 Creates a circuit with noise. 

352 

353 Args: 

354 params (np.ndarray): weight vector of shape 

355 [n_layers, n_qubits*n_params_per_layer] 

356 inputs (np.ndarray): input vector of size 1 

357 Returns: 

358 Union[float, np.ndarray]: Expectation value of PauliZ(0) 

359 of the circuit if state_vector is False and exp_val is True, 

360 otherwise the density matrix of all qubits. 

361 """ 

362 

363 for layer in range(0, self.n_layers): 

364 self.pqc(params[layer], self.n_qubits) 

365 

366 if self.data_reupload or layer == 0: 

367 self._iec(inputs, data_reupload=self.data_reupload) 

368 

369 if self.noise_params is not None: 

370 for q in range(self.n_qubits): 

371 qml.BitFlip(self.noise_params.get("BitFlip", 0.0), wires=q) 

372 qml.PhaseFlip(self.noise_params.get("PhaseFlip", 0.0), wires=q) 

373 qml.AmplitudeDamping( 

374 self.noise_params.get("AmplitudeDamping", 0.0), wires=q 

375 ) 

376 qml.PhaseDamping( 

377 self.noise_params.get("PhaseDamping", 0.0), wires=q 

378 ) 

379 qml.DepolarizingChannel( 

380 self.noise_params.get("DepolarizingChannel", 0.0), 

381 wires=q, 

382 ) 

383 

384 qml.Barrier(wires=list(range(self.n_qubits)), only_visual=True) 

385 

386 if self.data_reupload: 

387 self.pqc(params[-1], self.n_qubits) 

388 

389 # run mixed simualtion and get density matrix 

390 if self.execution_type == "density": 

391 return qml.density_matrix(wires=list(range(self.n_qubits))) 

392 # run default simulation and get expectation value 

393 elif self.execution_type == "expval": 

394 # global measurement (tensored Pauli Z, i.e. parity) 

395 if self.output_qubit == -1: 

396 return [qml.expval(qml.PauliZ(q)) for q in range(self.n_qubits)] 

397 # local measurement(s) 

398 elif isinstance(self.output_qubit, int): 

399 return qml.expval(qml.PauliZ(self.output_qubit)) 

400 # n-local measurenment 

401 elif isinstance(self.output_qubit, list): 

402 obs = qml.simplify( 

403 qml.Hamiltonian( 

404 [1.0] * self.n_qubits, 

405 [qml.PauliZ(q) for q in self.output_qubit], 

406 ) 

407 ) 

408 return qml.expval(obs) 

409 else: 

410 raise ValueError( 

411 f"Invalid parameter 'output_qubit': {self.output_qubit}.\ 

412 Must be int, list or -1." 

413 ) 

414 # run default simulation and get probs 

415 elif self.execution_type == "probs": 

416 if self.output_qubit == -1: 

417 return qml.probs(wires=list(range(self.n_qubits))) 

418 else: 

419 return qml.probs(wires=self.output_qubit) 

420 else: 

421 raise ValueError(f"Invalid execution_type: {self.execution_type}.") 

422 

423 def _draw(self, inputs=None, figure=False) -> None: 

424 if isinstance(self.circuit, qml.qnn.torch.TorchLayer): 

425 # TODO: throws strange argument error if not catched 

426 return "" 

427 

428 if figure: 

429 result = qml.draw_mpl(self.circuit)(params=self.params, inputs=inputs) 

430 else: 

431 result = qml.draw(self.circuit)(params=self.params, inputs=inputs) 

432 return result 

433 

434 def draw(self, inputs=None, figure=False) -> None: 

435 return self._draw(inputs, figure) 

436 

437 def __repr__(self) -> str: 

438 return self._draw(figure=False) 

439 

440 def __str__(self) -> str: 

441 return self._draw(figure=False) 

442 

443 def __call__( 

444 self, 

445 params: Optional[np.ndarray] = None, 

446 inputs: Optional[np.ndarray] = None, 

447 noise_params: Optional[Dict[str, float]] = None, 

448 cache: Optional[bool] = False, 

449 execution_type: Optional[str] = None, 

450 force_mean: Optional[bool] = False, 

451 ) -> np.ndarray: 

452 """ 

453 Perform a forward pass of the quantum circuit. 

454 

455 Args: 

456 params (Optional[np.ndarray]): Weight vector of shape 

457 [n_layers, n_qubits*n_params_per_layer]. 

458 If None, model internal parameters are used. 

459 inputs (Optional[np.ndarray]): Input vector of shape [1]. 

460 If None, zeros are used. 

461 noise_params (Optional[Dict[str, float]], optional): The noise parameters. 

462 Defaults to None which results in the last 

463 set noise parameters being used. 

464 cache (Optional[bool], optional): Whether to cache the results. 

465 Defaults to False. 

466 execution_type (str, optional): The type of execution. 

467 Must be one of 'expval', 'density', or 'probs'. 

468 Defaults to None which results in the last set execution type 

469 being used. 

470 

471 Returns: 

472 np.ndarray: The output of the quantum circuit. 

473 The shape depends on the execution_type. 

474 - If execution_type is 'expval', returns an ndarray of shape 

475 (1,) if output_qubit is -1, else (len(output_qubit),). 

476 - If execution_type is 'density', returns an ndarray 

477 of shape (2**n_qubits, 2**n_qubits). 

478 - If execution_type is 'probs', returns an ndarray 

479 of shape (2**n_qubits,) if output_qubit is -1, else 

480 (2**len(output_qubit),). 

481 """ 

482 # Call forward method which handles the actual caching etc. 

483 return self._forward( 

484 params=params, 

485 inputs=inputs, 

486 noise_params=noise_params, 

487 cache=cache, 

488 execution_type=execution_type, 

489 force_mean=force_mean, 

490 ) 

491 

492 def _forward( 

493 self, 

494 params: Optional[np.ndarray] = None, 

495 inputs: Optional[np.ndarray] = None, 

496 noise_params: Optional[Dict[str, float]] = None, 

497 cache: Optional[bool] = False, 

498 execution_type: Optional[str] = None, 

499 force_mean: Optional[bool] = False, 

500 ) -> np.ndarray: 

501 """ 

502 Perform a forward pass of the quantum circuit. 

503 

504 Args: 

505 params (Optional[np.ndarray]): Weight vector of shape 

506 [n_layers, n_qubits*n_params_per_layer]. 

507 If None, model internal parameters are used. 

508 inputs (Optional[np.ndarray]): Input vector of shape [1]. 

509 If None, zeros are used. 

510 noise_params (Optional[Dict[str, float]], optional): The noise parameters. 

511 Defaults to None which results in the last 

512 set noise parameters being used. 

513 cache (Optional[bool], optional): Whether to cache the results. 

514 Defaults to False. 

515 execution_type (str, optional): The type of execution. 

516 Must be one of 'expval', 'density', or 'probs'. 

517 Defaults to None which results in the last set execution type 

518 being used. 

519 

520 Returns: 

521 np.ndarray: The output of the quantum circuit. 

522 The shape depends on the execution_type. 

523 - If execution_type is 'expval', returns an ndarray of shape 

524 (1,) if output_qubit is -1, else (len(output_qubit),). 

525 - If execution_type is 'density', returns an ndarray 

526 of shape (2**n_qubits, 2**n_qubits). 

527 - If execution_type is 'probs', returns an ndarray 

528 of shape (2**n_qubits,) if output_qubit is -1, else 

529 (2**len(output_qubit),). 

530 

531 Raises: 

532 NotImplementedError: If the number of shots is not None or if the 

533 expectation value is True. 

534 """ 

535 # set the parameters as object attributes 

536 if noise_params is not None: 

537 self.noise_params = noise_params 

538 if execution_type is not None: 

539 self.execution_type = execution_type 

540 

541 if params is None: 

542 params = self.params 

543 else: 

544 if numpy_boxes.ArrayBox == type(params): 

545 self.params = params._value 

546 else: 

547 self.params = params 

548 

549 # the qasm representation contains the bound parameters, 

550 # thus it is ok to hash that 

551 hs = hashlib.md5( 

552 repr( 

553 { 

554 "n_qubits": self.n_qubits, 

555 "n_layers": self.n_layers, 

556 "pqc": self.pqc.__class__.__name__, 

557 "dru": self.data_reupload, 

558 "params": self.params, # use safe-params 

559 "noise_params": self.noise_params, 

560 "execution_type": self.execution_type, 

561 "inputs": inputs, 

562 "output_qubit": self.output_qubit, 

563 } 

564 ).encode("utf-8") 

565 ).hexdigest() 

566 

567 result: Optional[np.ndarray] = None 

568 if cache: 

569 name: str = f"pqc_{hs}.npy" 

570 

571 cache_folder: str = ".cache" 

572 if not os.path.exists(cache_folder): 

573 os.mkdir(cache_folder) 

574 

575 file_path: str = os.path.join(cache_folder, name) 

576 

577 if os.path.isfile(file_path): 

578 result = np.load(file_path) 

579 

580 if result is None: 

581 # if density matrix requested or noise params used 

582 if self.execution_type == "density" or self.noise_params is not None: 

583 result = self.circuit_mixed( 

584 params=params, # use arraybox params 

585 inputs=inputs, 

586 ) 

587 else: 

588 if isinstance(self.circuit, qml.qnn.torch.TorchLayer): 

589 result = self.circuit( 

590 inputs=inputs, 

591 ) 

592 else: 

593 result = self.circuit( 

594 params=params, # use arraybox params 

595 inputs=inputs, 

596 ) 

597 

598 if isinstance(result, list): 

599 result = np.stack(result) 

600 

601 if self.execution_type == "expval" and self.output_qubit == -1: 

602 

603 # Calculating mean value after stacking, to not 

604 # discard gradient information 

605 if force_mean: 

606 # exception for torch layer because it swaps batch and output dimension 

607 if isinstance(self.circuit, qml.qnn.torch.TorchLayer): 

608 result = result.mean(axis=-1) 

609 else: 

610 result = result.mean(axis=0) 

611 

612 if len(result.shape) == 3 and result.shape[0] == 1: 

613 result = result[0] 

614 

615 if cache: 

616 np.save(file_path, result) 

617 

618 return result