Coverage for qml_essentials/model.py: 89%

292 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-07 14:54 +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 

8from copy import deepcopy 

9 

10from qml_essentials.ansaetze import Gates, Ansaetze, Circuit 

11from qml_essentials.utils import PauliCircuit 

12 

13 

14import logging 

15 

16log = logging.getLogger(__name__) 

17 

18 

19class Model: 

20 """ 

21 A quantum circuit model. 

22 """ 

23 

24 lightning_threshold = 12 

25 

26 def __init__( 

27 self, 

28 n_qubits: int, 

29 n_layers: int, 

30 circuit_type: Union[str, Circuit], 

31 data_reupload: bool = True, 

32 encoding: Union[str, Callable, List[str], List[Callable]] = Gates.RX, 

33 initialization: str = "random", 

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

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

36 shots: Optional[int] = None, 

37 random_seed: int = 1000, 

38 as_pauli_circuit: bool = False, 

39 ) -> None: 

40 """ 

41 Initialize the quantum circuit model. 

42 Parameters will have the shape [impl_n_layers, parameters_per_layer] 

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

44 depending if data_reupload is True and parameters_per_layer is given by 

45 the chosen ansatz. 

46 

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

48 - noise_params: None 

49 - execution_type: "expval" 

50 - shots: None 

51 

52 Args: 

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

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

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

56 If None, defaults to "no_ansatz". 

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

58 quantum device on each measurement. Defaults to True. 

59 encoding (Union[str, Callable, List[str], List[Callable]], optional): 

60 The unitary to use for encoding the input data. Can be a string 

61 (e.g. "RX") or a callable (e.g. qml.RX). Defaults to qml.RX. 

62 If input is multidimensional it is assumed to be a list of 

63 unitaries or a list of strings. 

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

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

66 Defaults to "random". 

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

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

69 global measurement is conducted, depending on the execution 

70 type. 

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

72 the quantum device. Defaults to None. 

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

74 in initialization is "random" and for random noise parameters. 

75 Defaults to 1000. 

76 as_pauli_circuit (bool, optional): whether the circuit is 

77 transformed to a Pauli-Clifford circuit as described by Nemkov 

78 et al. (https://doi.org/10.1103/PhysRevA.108.032406), which is 

79 required for analytical Fourier coefficient computation. 

80 Defaults to False. 

81 

82 Returns: 

83 None 

84 """ 

85 # Initialize default parameters needed for circuit evaluation 

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

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

88 self.shots = shots 

89 

90 if isinstance(output_qubit, list): 

91 assert ( 

92 len(output_qubit) <= n_qubits 

93 ), f"Size of output_qubit {len(output_qubit)} cannot be\ 

94 larger than number of qubits {n_qubits}." 

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

96 

97 # Copy the parameters 

98 self.n_qubits: int = n_qubits 

99 self.n_layers: int = n_layers 

100 self.data_reupload: bool = data_reupload 

101 

102 # Initialize ansatz 

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

104 if isinstance(circuit_type, str): 

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

106 Ansaetze, circuit_type or "No_Ansatz" 

107 )() 

108 else: 

109 self.pqc = circuit_type() 

110 

111 # Initialize rng in Gates 

112 Gates.init_rng(random_seed) 

113 

114 # Initialize encoding 

115 # first check if we have a str, list or callable 

116 if isinstance(encoding, str): 

117 # if str, use the pennylane fct 

118 self._enc = getattr(Gates, f"{encoding}") 

119 elif isinstance(encoding, list): 

120 # if list, check if str or callable 

121 if isinstance(encoding[0], str): 

122 self._enc = [getattr(Gates, f"{enc}") for enc in encoding] 

123 else: 

124 self._enc = encoding 

125 

126 if len(self._enc) == 1: 

127 self._enc = self._enc[0] 

128 else: 

129 # default to callable 

130 self._enc = encoding 

131 

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

133 

134 if data_reupload: 

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

136 self.degree = n_layers * n_qubits 

137 else: 

138 impl_n_layers: int = n_layers 

139 self.degree = 1 

140 

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

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

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

144 impl_n_layers, 

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

146 ) 

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

148 # however, only if nothing is provided 

149 self._inialization_strategy = initialization 

150 self._initialization_domain = initialization_domain 

151 

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

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

154 

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

156 # one with the mixed device 

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

158 self.as_pauli_circuit = as_pauli_circuit 

159 

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

161 self._circuit, 

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

163 ) 

164 

165 @property 

166 def as_pauli_circuit(self) -> bool: 

167 return self._as_pauli_circuit 

168 

169 @as_pauli_circuit.setter 

170 def as_pauli_circuit(self, value: bool) -> None: 

171 self._as_pauli_circuit = value 

172 

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

174 self._circuit, 

175 qml.device( 

176 ( 

177 "default.qubit" 

178 if self.n_qubits < self.lightning_threshold 

179 else "lightning.qubit" 

180 ), 

181 shots=self.shots, 

182 wires=self.n_qubits, 

183 ), 

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

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

186 ) 

187 

188 if value: 

189 pauli_circuit_transform = qml.transform( 

190 PauliCircuit.from_parameterised_circuit 

191 ) 

192 self.circuit = pauli_circuit_transform(self.circuit) 

193 

194 @property 

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

196 """ 

197 Gets the noise parameters of the model. 

198 

199 Returns: 

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

201 noise parameters or None if not set. 

202 """ 

203 return self._noise_params 

204 

205 @noise_params.setter 

206 def noise_params( 

207 self, kvs: Optional[Dict[str, Union[float, Dict[str, float]]]] 

208 ) -> None: 

209 """ 

210 Sets the noise parameters of the model. 

211 

212 Typically a "noise parameter" refers to the error probability. 

213 ThermalRelaxation is a special case, and supports a dict as value with 

214 structure: 

215 "ThermalRelaxation": 

216 { 

217 "t1": 2000, # relative t1 time. 

218 "t2": 1000, # relative t2 time 

219 "t_factor" 1: # relative gate time factor 

220 }, 

221 

222 Args: 

223 value (Optional[Dict[str, Union[float, Dict[str, float]]]]): A 

224 dictionary of noise parameters. If all values are 0.0, the noise 

225 parameters are set to None. 

226 

227 Returns: 

228 None 

229 """ 

230 # set to None if only zero values provided 

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

232 kvs = None 

233 

234 # set default values 

235 if kvs is not None: 

236 kvs.setdefault("BitFlip", 0.0) 

237 kvs.setdefault("PhaseFlip", 0.0) 

238 kvs.setdefault("Depolarizing", 0.0) 

239 kvs.setdefault("AmplitudeDamping", 0.0) 

240 kvs.setdefault("PhaseDamping", 0.0) 

241 kvs.setdefault("GateError", 0.0) 

242 kvs.setdefault("ThermalRelaxation", None) 

243 kvs.setdefault("StatePreparation", 0.0) 

244 kvs.setdefault("Measurement", 0.0) 

245 

246 # check if there are any keys not supported 

247 for key in kvs.keys(): 

248 if key not in [ 

249 "BitFlip", 

250 "PhaseFlip", 

251 "Depolarizing", 

252 "AmplitudeDamping", 

253 "PhaseDamping", 

254 "GateError", 

255 "ThermalRelaxation", 

256 "StatePreparation", 

257 "Measurement", 

258 ]: 

259 warnings.warn( 

260 f"Noise type {key} is not supported by this package", 

261 UserWarning, 

262 ) 

263 

264 # check valid params for thermal relaxation noise channel 

265 tr_params = kvs["ThermalRelaxation"] 

266 if isinstance(tr_params, dict): 

267 tr_params.setdefault("t1", 0.0) 

268 tr_params.setdefault("t2", 0.0) 

269 tr_params.setdefault("t_factor", 0.0) 

270 for k in tr_params.keys(): 

271 if k not in [ 

272 "t1", 

273 "t2", 

274 "t_factor", 

275 ]: 

276 warnings.warn( 

277 f"Thermal Relaxation parameter {k} is not supported " 

278 f"by this package", 

279 UserWarning, 

280 ) 

281 if not all(tr_params.values()) or tr_params["t2"] > 2 * tr_params["t1"]: 

282 warnings.warn( 

283 "Received invalid values for Thermal Relaxation noise " 

284 "parameter. Thermal relaxation is not applied!", 

285 UserWarning, 

286 ) 

287 kvs["ThermalRelaxation"] = 0.0 

288 

289 self._noise_params = kvs 

290 

291 @property 

292 def execution_type(self) -> str: 

293 """ 

294 Gets the execution type of the model. 

295 

296 Returns: 

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

298 """ 

299 return self._execution_type 

300 

301 @execution_type.setter 

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

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

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

305 

306 if (value == "density" or value == "state") and self.output_qubit != -1: 

307 warnings.warn( 

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

309 f"{self.output_qubit}.", 

310 UserWarning, 

311 ) 

312 

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

314 warnings.warn( 

315 "Setting execution_type to probs without specifying shots.", 

316 UserWarning, 

317 ) 

318 

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

320 warnings.warn( 

321 "Setting execution_type to density with specified shots.", 

322 UserWarning, 

323 ) 

324 

325 self._execution_type = value 

326 

327 @property 

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

329 """ 

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

331 

332 Returns: 

333 Optional[int]: The number of shots. 

334 """ 

335 return self._shots 

336 

337 @shots.setter 

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

339 """ 

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

341 

342 Args: 

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

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

345 

346 Returns: 

347 None 

348 """ 

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

350 value = None 

351 self._shots = value 

352 

353 def initialize_params( 

354 self, 

355 rng, 

356 repeat: int = None, 

357 initialization: str = None, 

358 initialization_domain: List[float] = None, 

359 ) -> None: 

360 """ 

361 Initializes the parameters of the model. 

362 

363 Args: 

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

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

366 If None, the number of layers is used. 

367 initialization: The strategy to use for parameter initialization. 

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

369 initialization_domain: The domain to use for parameter initialization. 

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

371 

372 Returns: 

373 None 

374 """ 

375 params_shape = ( 

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

377 ) 

378 # use existing strategy if not specified 

379 initialization = initialization or self._inialization_strategy 

380 initialization_domain = initialization_domain or self._initialization_domain 

381 

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

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

384 if indices is None: 

385 warnings.warn( 

386 f"Specified {initialization} but circuit\ 

387 does not contain controlled rotation gates.\ 

388 Parameters are intialized randomly.", 

389 UserWarning, 

390 ) 

391 else: 

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

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

394 * value 

395 ) 

396 return params 

397 

398 if initialization == "random": 

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

400 *initialization_domain, params_shape, requires_grad=True 

401 ) 

402 elif initialization == "zeros": 

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

404 elif initialization == "pi": 

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

406 elif initialization == "zero-controlled": 

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

408 *initialization_domain, params_shape, requires_grad=True 

409 ) 

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

411 elif initialization == "pi-controlled": 

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

413 *initialization_domain, params_shape, requires_grad=True 

414 ) 

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

416 else: 

417 raise Exception("Invalid initialization method") 

418 

419 log.info( 

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

421 using strategy {initialization}." 

422 ) 

423 

424 def _iec( 

425 self, 

426 inputs: np.ndarray, 

427 data_reupload: bool, 

428 enc: Union[Callable, List[Callable]], 

429 noise_params: Optional[Dict[str, Union[float, Dict[str, float]]]] = None, 

430 ) -> None: 

431 """ 

432 Creates an AngleEncoding using RX gates 

433 

434 Args: 

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

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

437 for the IEC or not, default is True. 

438 

439 Returns: 

440 None 

441 """ 

442 # check for zero, because due to input validation, input cannot be none 

443 if not inputs.any(): 

444 return 

445 

446 if data_reupload: 

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

448 for q in range(self.n_qubits): 

449 enc(inputs[:, 0], wires=q, noise_params=noise_params) 

450 else: 

451 for q in range(self.n_qubits): 

452 for idx in range(inputs.shape[1]): 

453 enc[idx](inputs[:, idx], wires=q, noise_params=noise_params) 

454 else: 

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

456 enc(inputs[:, 0], wires=0, noise_params=noise_params) 

457 else: 

458 for idx in range(inputs.shape[1]): 

459 enc[idx](inputs[:, idx], wires=0, noise_params=noise_params) 

460 

461 def _circuit( 

462 self, 

463 params: np.ndarray, 

464 inputs: np.ndarray, 

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

466 """ 

467 Creates a circuit with noise. 

468 

469 Args: 

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

471 [n_layers, n_qubits*n_params_per_layer] 

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

473 Returns: 

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

475 of the circuit if state_vector is False and expval is True, 

476 otherwise the density matrix of all qubits. 

477 """ 

478 self._variational(params=params, inputs=inputs) 

479 return self._observable() 

480 

481 def _variational(self, params, inputs): 

482 if self.noise_params is not None: 

483 self._apply_state_prep_noise() 

484 

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

486 self.pqc(params[layer], self.n_qubits, noise_params=self.noise_params) 

487 

488 if self.data_reupload or layer == 0: 

489 self._iec( 

490 inputs, 

491 data_reupload=self.data_reupload, 

492 enc=self._enc, 

493 noise_params=self.noise_params, 

494 ) 

495 

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

497 

498 if self.data_reupload: 

499 self.pqc(params[-1], self.n_qubits, noise_params=self.noise_params) 

500 

501 if self.noise_params is not None: 

502 self._apply_general_noise() 

503 

504 def _observable(self): 

505 # run mixed simualtion and get density matrix 

506 if self.execution_type == "density": 

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

508 elif self.execution_type == "state": 

509 return qml.state() 

510 # run default simulation and get expectation value 

511 elif self.execution_type == "expval": 

512 # n-local measurement 

513 if self.output_qubit == -1: 

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

515 # local measurement(s) 

516 elif isinstance(self.output_qubit, int): 

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

518 # parity measurenment 

519 elif isinstance(self.output_qubit, list): 

520 obs = qml.PauliZ(self.output_qubit[0]) 

521 for out_qubit in self.output_qubit[1:]: 

522 obs = obs @ qml.PauliZ(out_qubit) 

523 return qml.expval(obs) 

524 else: 

525 raise ValueError( 

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

527 Must be int, list or -1." 

528 ) 

529 # run default simulation and get probs 

530 elif self.execution_type == "probs": 

531 if self.output_qubit == -1: 

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

533 else: 

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

535 else: 

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

537 

538 def _apply_state_prep_noise(self) -> None: 

539 """ 

540 Applies a state preparation error on each qubit according to the 

541 probability for StatePreparation provided in the noise_params. 

542 """ 

543 sp = self.noise_params.get("StatePreparation", 0.0) 

544 for q in range(self.n_qubits): 

545 if sp > 0: 

546 qml.BitFlip(sp, wires=q) 

547 

548 def _apply_general_noise(self) -> None: 

549 """ 

550 Applies general types of noise the full circuit (in contrast to gate 

551 errors, applied directly at gate level, see Gates.Noise). 

552 

553 Possible types of noise are: 

554 - AmplitudeDamping (specified through probability) 

555 - PhaseDamping (specified through probability) 

556 - ThermalRelaxation (specified through a dict, containing keys 

557 "t1", "t2", "t_factor") 

558 - Measurement (specified through probability) 

559 """ 

560 amp_damp = self.noise_params.get("AmplitudeDamping", 0.0) 

561 phase_damp = self.noise_params.get("PhaseDamping", 0.0) 

562 thermal_relax = self.noise_params.get("ThermalRelaxation", 0.0) 

563 meas = self.noise_params.get("Measurement", 0.0) 

564 for q in range(self.n_qubits): 

565 if amp_damp > 0: 

566 qml.AmplitudeDamping(amp_damp, wires=q) 

567 if phase_damp > 0: 

568 qml.PhaseDamping(phase_damp, wires=q) 

569 if meas > 0: 

570 qml.BitFlip(meas, wires=q) 

571 if isinstance(thermal_relax, dict): 

572 t1 = thermal_relax["t1"] 

573 t2 = thermal_relax["t2"] 

574 t_factor = thermal_relax["t_factor"] 

575 circuit_depth = self.get_circuit_depth() 

576 tg = circuit_depth * t_factor 

577 qml.ThermalRelaxationError(1.0, t1, t2, tg, q) 

578 

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

580 if not isinstance(self.circuit, qml.QNode): 

581 # TODO: throws strange argument error if not catched 

582 return "" 

583 

584 inputs = self._inputs_validation(inputs) 

585 

586 if figure: 

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

588 else: 

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

590 return result 

591 

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

593 

594 return self._draw(inputs, figure) 

595 

596 def __repr__(self) -> str: 

597 return self._draw(figure=False) 

598 

599 def __str__(self) -> str: 

600 return self._draw(figure=False) 

601 

602 def __call__( 

603 self, 

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

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

606 noise_params: Optional[Dict[str, Union[float, Dict[str, float]]]] = None, 

607 cache: Optional[bool] = False, 

608 execution_type: Optional[str] = None, 

609 force_mean: bool = False, 

610 ) -> np.ndarray: 

611 """ 

612 Perform a forward pass of the quantum circuit. 

613 

614 Args: 

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

616 [n_layers, n_qubits*n_params_per_layer]. 

617 If None, model internal parameters are used. 

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

619 If None, zeros are used. 

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

621 Defaults to None which results in the last 

622 set noise parameters being used. 

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

624 Defaults to False. 

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

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

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

628 being used. 

629 force_mean (bool, optional): Whether to average 

630 when performing n-local measurements. 

631 Defaults to False. 

632 

633 Returns: 

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

635 The shape depends on the execution_type. 

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

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

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

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

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

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

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

643 """ 

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

645 return self._forward( 

646 params=params, 

647 inputs=inputs, 

648 noise_params=noise_params, 

649 cache=cache, 

650 execution_type=execution_type, 

651 force_mean=force_mean, 

652 ) 

653 

654 def _params_validation(self, params) -> np.ndarray: 

655 """ 

656 Sets the parameters when calling the quantum circuit 

657 

658 Args: 

659 params (np.ndarray): The parameters used for the call 

660 """ 

661 if params is None: 

662 params = self.params 

663 else: 

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

665 self.params = params._value 

666 else: 

667 self.params = params 

668 return params 

669 

670 def _inputs_validation( 

671 self, inputs: Union[None, List, float, int, np.ndarray] 

672 ) -> np.ndarray: 

673 """ 

674 Validate the inputs to be a 2D numpy array of shape (batch_size, n_inputs). 

675 

676 Args: 

677 inputs (Union[None, List, float, int, np.ndarray]): The input to validate. 

678 

679 Returns: 

680 np.ndarray: The validated input. 

681 """ 

682 if inputs is None: 

683 # initialize to zero 

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

685 elif isinstance(inputs, List): 

686 inputs = np.stack(inputs) 

687 elif isinstance(inputs, float) or isinstance(inputs, int): 

688 inputs = np.array([inputs]) 

689 

690 if len(inputs.shape) == 1: 

691 if isinstance(self._enc, List): 

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

693 else: 

694 # add a batch dimension 

695 inputs = inputs.reshape(inputs.shape[0], 1) 

696 

697 return inputs 

698 

699 def _forward( 

700 self, 

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

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

703 noise_params: Optional[Dict[str, Union[float, Dict[str, float]]]] = None, 

704 cache: Optional[bool] = False, 

705 execution_type: Optional[str] = None, 

706 force_mean: bool = False, 

707 ) -> np.ndarray: 

708 """ 

709 Perform a forward pass of the quantum circuit. 

710 

711 Args: 

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

713 [n_layers, n_qubits*n_params_per_layer]. 

714 If None, model internal parameters are used. 

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

716 If None, zeros are used. 

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

718 Defaults to None which results in the last 

719 set noise parameters being used. 

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

721 Defaults to False. 

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

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

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

725 being used. 

726 force_mean (bool, optional): Whether to average 

727 when performing n-local measurements. 

728 Defaults to False. 

729 

730 Returns: 

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

732 The shape depends on the execution_type. 

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

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

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

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

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

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

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

740 

741 Raises: 

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

743 expectation value is True. 

744 """ 

745 # set the parameters as object attributes 

746 if noise_params is not None: 

747 self.noise_params = noise_params 

748 if execution_type is not None: 

749 self.execution_type = execution_type 

750 

751 params = self._params_validation(params) 

752 inputs = self._inputs_validation(inputs) 

753 

754 # the qasm representation contains the bound parameters, 

755 # thus it is ok to hash that 

756 hs = hashlib.md5( 

757 repr( 

758 { 

759 "n_qubits": self.n_qubits, 

760 "n_layers": self.n_layers, 

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

762 "dru": self.data_reupload, 

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

764 "noise_params": self.noise_params, 

765 "execution_type": self.execution_type, 

766 "inputs": inputs, 

767 "output_qubit": self.output_qubit, 

768 } 

769 ).encode("utf-8") 

770 ).hexdigest() 

771 

772 result: Optional[np.ndarray] = None 

773 if cache: 

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

775 

776 cache_folder: str = ".cache" 

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

778 os.mkdir(cache_folder) 

779 

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

781 

782 if os.path.isfile(file_path): 

783 result = np.load(file_path) 

784 

785 if result is None: 

786 # if density matrix requested or noise params used 

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

788 result = self.circuit_mixed( 

789 params=params, # use arraybox params 

790 inputs=inputs, 

791 ) 

792 else: 

793 if not isinstance(self.circuit, qml.QNode): 

794 result = self.circuit( 

795 inputs=inputs, 

796 ) 

797 else: 

798 result = self.circuit( 

799 params=params, # use arraybox params 

800 inputs=inputs, 

801 ) 

802 

803 if isinstance(result, list): 

804 result = np.stack(result) 

805 

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

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

808 if not isinstance(self.circuit, qml.QNode): 

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

810 else: 

811 result = result.mean(axis=0) 

812 elif self.execution_type == "probs" and force_mean and self.output_qubit == -1: 

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

814 if not isinstance(self.circuit, qml.QNode): 

815 result = result[..., -1].sum(axis=-1) 

816 else: 

817 result = result[1:, ...].sum(axis=0) 

818 

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

820 result = result[0] 

821 

822 if cache: 

823 np.save(file_path, result) 

824 

825 return result 

826 

827 def get_specs(self, inputs: Optional[np.ndarray] = None) -> dict: 

828 """ 

829 Get pennylane specs for the model. 

830 

831 Args: 

832 inputs (Optional[np.ndarray]): The inputs, with which to call the 

833 circuit. Defaults to None. 

834 

835 Returns: 

836 dict: Dictionary of specs. The key "resources" contains information 

837 about the circuit size and gate statistics. 

838 """ 

839 inputs = self._inputs_validation(inputs) 

840 spec_model = deepcopy(self) 

841 spec_model.noise_params = None # remove noise 

842 return qml.specs(spec_model.circuit)(self.params, inputs) 

843 

844 def get_circuit_depth(self, inputs: Optional[np.ndarray] = None) -> int: 

845 """ 

846 Obtain circuit depth for the model 

847 

848 Args: 

849 inputs (Optional[np.ndarray]): The inputs, with which to call the 

850 circuit. Defaults to None. 

851 

852 Returns: 

853 int: Circuit depth (longest path of gates in circuit.) 

854 """ 

855 return self.get_specs(inputs)["resources"].depth