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
« 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
10from qml_essentials.ansaetze import Gates, Ansaetze, Circuit
11from qml_essentials.utils import PauliCircuit
14import logging
16log = logging.getLogger(__name__)
19class Model:
20 """
21 A quantum circuit model.
22 """
24 lightning_threshold = 12
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.
47 The model is initialized with the following parameters as defaults:
48 - noise_params: None
49 - execution_type: "expval"
50 - shots: None
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.
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
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
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
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()
111 # Initialize rng in Gates
112 Gates.init_rng(random_seed)
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
126 if len(self._enc) == 1:
127 self._enc = self._enc[0]
128 else:
129 # default to callable
130 self._enc = encoding
132 log.info(f"Using {circuit_type} circuit.")
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
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
152 # ..here! where we only require a rng
153 self.initialize_params(np.random.default_rng(random_seed))
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
160 self.circuit_mixed: qml.QNode = qml.QNode(
161 self._circuit,
162 qml.device("default.mixed", shots=self.shots, wires=self.n_qubits),
163 )
165 @property
166 def as_pauli_circuit(self) -> bool:
167 return self._as_pauli_circuit
169 @as_pauli_circuit.setter
170 def as_pauli_circuit(self, value: bool) -> None:
171 self._as_pauli_circuit = value
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 )
188 if value:
189 pauli_circuit_transform = qml.transform(
190 PauliCircuit.from_parameterised_circuit
191 )
192 self.circuit = pauli_circuit_transform(self.circuit)
194 @property
195 def noise_params(self) -> Optional[Dict[str, Union[float, Dict[str, float]]]]:
196 """
197 Gets the noise parameters of the model.
199 Returns:
200 Optional[Dict[str, float]]: A dictionary of
201 noise parameters or None if not set.
202 """
203 return self._noise_params
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.
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 },
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.
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
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)
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 )
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
289 self._noise_params = kvs
291 @property
292 def execution_type(self) -> str:
293 """
294 Gets the execution type of the model.
296 Returns:
297 str: The execution type, one of 'density', 'expval', or 'probs'.
298 """
299 return self._execution_type
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}.")
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 )
313 if value == "probs" and self.shots is None:
314 warnings.warn(
315 "Setting execution_type to probs without specifying shots.",
316 UserWarning,
317 )
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 )
325 self._execution_type = value
327 @property
328 def shots(self) -> Optional[int]:
329 """
330 Gets the number of shots to use for the quantum device.
332 Returns:
333 Optional[int]: The number of shots.
334 """
335 return self._shots
337 @shots.setter
338 def shots(self, value: Optional[int]) -> None:
339 """
340 Sets the number of shots to use for the quantum device.
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.
346 Returns:
347 None
348 """
349 if type(value) is int and value <= 0:
350 value = None
351 self._shots = value
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.
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.
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
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
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")
419 log.info(
420 f"Initialized parameters with shape {self.params.shape}\
421 using strategy {initialization}."
422 )
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
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.
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
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)
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.
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()
481 def _variational(self, params, inputs):
482 if self.noise_params is not None:
483 self._apply_state_prep_noise()
485 for layer in range(0, self.n_layers):
486 self.pqc(params[layer], self.n_qubits, noise_params=self.noise_params)
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 )
496 qml.Barrier(wires=list(range(self.n_qubits)), only_visual=True)
498 if self.data_reupload:
499 self.pqc(params[-1], self.n_qubits, noise_params=self.noise_params)
501 if self.noise_params is not None:
502 self._apply_general_noise()
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}.")
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)
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).
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)
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 ""
584 inputs = self._inputs_validation(inputs)
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
592 def draw(self, inputs=None, figure=False) -> None:
594 return self._draw(inputs, figure)
596 def __repr__(self) -> str:
597 return self._draw(figure=False)
599 def __str__(self) -> str:
600 return self._draw(figure=False)
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.
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.
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 )
654 def _params_validation(self, params) -> np.ndarray:
655 """
656 Sets the parameters when calling the quantum circuit
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
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).
676 Args:
677 inputs (Union[None, List, float, int, np.ndarray]): The input to validate.
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])
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)
697 return inputs
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.
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.
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),).
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
751 params = self._params_validation(params)
752 inputs = self._inputs_validation(inputs)
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()
772 result: Optional[np.ndarray] = None
773 if cache:
774 name: str = f"pqc_{hs}.npy"
776 cache_folder: str = ".cache"
777 if not os.path.exists(cache_folder):
778 os.mkdir(cache_folder)
780 file_path: str = os.path.join(cache_folder, name)
782 if os.path.isfile(file_path):
783 result = np.load(file_path)
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 )
803 if isinstance(result, list):
804 result = np.stack(result)
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)
819 if len(result.shape) == 3 and result.shape[0] == 1:
820 result = result[0]
822 if cache:
823 np.save(file_path, result)
825 return result
827 def get_specs(self, inputs: Optional[np.ndarray] = None) -> dict:
828 """
829 Get pennylane specs for the model.
831 Args:
832 inputs (Optional[np.ndarray]): The inputs, with which to call the
833 circuit. Defaults to None.
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)
844 def get_circuit_depth(self, inputs: Optional[np.ndarray] = None) -> int:
845 """
846 Obtain circuit depth for the model
848 Args:
849 inputs (Optional[np.ndarray]): The inputs, with which to call the
850 circuit. Defaults to None.
852 Returns:
853 int: Circuit depth (longest path of gates in circuit.)
854 """
855 return self.get_specs(inputs)["resources"].depth