Coverage for qml_essentials/model.py: 91%
208 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-23 11:23 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-23 11:23 +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
9from qml_essentials.ansaetze import Gates, Ansaetze, Circuit
11import logging
13log = logging.getLogger(__name__)
16class Model:
17 """
18 A quantum circuit model.
19 """
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 encoding: Union[str, Callable, List[str], List[Callable]] = Gates.RX,
28 initialization: str = "random",
29 initialization_domain: List[float] = [0, 2 * np.pi],
30 output_qubit: Union[List[int], int] = -1,
31 shots: Optional[int] = None,
32 random_seed: int = 1000,
33 ) -> None:
34 """
35 Initialize the quantum circuit model.
36 Parameters will have the shape [impl_n_layers, parameters_per_layer]
37 where impl_n_layers is the number of layers provided and added by one
38 depending if data_reupload is True and parameters_per_layer is given by
39 the chosen ansatz.
41 The model is initialized with the following parameters as defaults:
42 - noise_params: None
43 - execution_type: "expval"
44 - shots: None
46 Args:
47 n_qubits (int): The number of qubits in the circuit.
48 n_layers (int): The number of layers in the circuit.
49 circuit_type (str, Circuit): The type of quantum circuit to use.
50 If None, defaults to "no_ansatz".
51 data_reupload (bool, optional): Whether to reupload data to the
52 quantum device on each measurement. Defaults to True.
53 encoding (Union[str, Callable, List[str], List[Callable]], optional):
54 The unitary to use for encoding the input data. Can be a string
55 (e.g. "RX") or a callable (e.g. qml.RX). Defaults to qml.RX.
56 If input is multidimensional it is assumed to be a list of
57 unitaries or a list of strings.
58 initialization (str, optional): The strategy to initialize the parameters.
59 Can be "random", "zeros", "zero-controlled", "pi", or "pi-controlled".
60 Defaults to "random".
61 output_qubit (List[int], int, optional): The index of the output
62 qubit (or qubits). When set to -1 all qubits are measured, or a
63 global measurement is conducted, depending on the execution
64 type.
65 shots (Optional[int], optional): The number of shots to use for
66 the quantum device. Defaults to None.
67 random_seed (int, optional): seed for the random number generator
68 in initialization is "random", Defaults to 1000.
70 Returns:
71 None
72 """
73 # Initialize default parameters needed for circuit evaluation
74 self.noise_params: Optional[Dict[str, float]] = None
75 self.execution_type: Optional[str] = "expval"
76 self.shots = shots
77 self.output_qubit: Union[List[int], int] = output_qubit
79 # Copy the parameters
80 self.n_qubits: int = n_qubits
81 self.n_layers: int = n_layers
82 self.data_reupload: bool = data_reupload
84 lightning_threshold = 12
86 # Initialize ansatz
87 # only weak check for str. We trust the user to provide sth useful
88 if isinstance(circuit_type, str):
89 self.pqc: Callable[[Optional[np.ndarray], int], int] = getattr(
90 Ansaetze, circuit_type or "No_Ansatz"
91 )()
92 else:
93 self.pqc = circuit_type()
95 # Initialize encoding
96 # first check if we have a str, list or callable
97 if isinstance(encoding, str):
98 # if str, use the pennylane fct
99 self._enc = getattr(Gates, f"{encoding}")
100 elif isinstance(encoding, list):
101 # if list, check if str or callable
102 if isinstance(encoding[0], str):
103 self._enc = [getattr(Gates, f"{enc}") for enc in encoding]
104 else:
105 self._enc = encoding
107 if len(self._enc) == 1:
108 self._enc = self._enc[0]
109 else:
110 # default to callable
111 self._enc = encoding
113 log.info(f"Using {circuit_type} circuit.")
115 if data_reupload:
116 impl_n_layers: int = n_layers + 1 # we need L+1 according to Schuld et al.
117 self.degree = n_layers * n_qubits
118 else:
119 impl_n_layers: int = n_layers
120 self.degree = 1
122 log.info(f"Number of implicit layers set to {impl_n_layers}.")
123 # calculate the shape of the parameter vector here, we will re-use this in init.
124 self._params_shape: Tuple[int, int] = (
125 impl_n_layers,
126 self.pqc.n_params_per_layer(self.n_qubits),
127 )
128 # this will also be re-used in the init method,
129 # however, only if nothing is provided
130 self._inialization_strategy = initialization
131 self._initialization_domain = initialization_domain
133 # ..here! where we only require a rng
134 self.initialize_params(np.random.default_rng(random_seed))
136 # Initialize two circuits, one with the default device and
137 # one with the mixed device
138 # which allows us to later route depending on the state_vector flag
139 self.circuit: qml.QNode = qml.QNode(
140 self._circuit,
141 qml.device(
142 (
143 "default.qubit"
144 if self.n_qubits < lightning_threshold
145 else "lightning.qubit"
146 ),
147 shots=self.shots,
148 wires=self.n_qubits,
149 ),
150 interface="autograd" if self.shots is not None else "auto",
151 diff_method="parameter-shift" if self.shots is not None else "best",
152 )
153 self.circuit_mixed: qml.QNode = qml.QNode(
154 self._circuit,
155 qml.device("default.mixed", shots=self.shots, wires=self.n_qubits),
156 )
158 @property
159 def noise_params(self) -> Optional[Dict[str, float]]:
160 """
161 Gets the noise parameters of the model.
163 Returns:
164 Optional[Dict[str, float]]: A dictionary of
165 noise parameters or None if not set.
166 """
167 return self._noise_params
169 @noise_params.setter
170 def noise_params(self, value: Optional[Dict[str, float]]) -> None:
171 """
172 Sets the noise parameters of the model.
174 Args:
175 value (Optional[Dict[str, float]]): A dictionary of noise parameters.
176 If all values are 0.0, the noise parameters are set to None.
178 Returns:
179 None
180 """
181 if value is not None and all(np == 0.0 for np in value.values()):
182 value = None
183 self._noise_params = value
185 @property
186 def execution_type(self) -> str:
187 """
188 Gets the execution type of the model.
190 Returns:
191 str: The execution type, one of 'density', 'expval', or 'probs'.
192 """
193 return self._execution_type
195 @execution_type.setter
196 def execution_type(self, value: str) -> None:
197 if value not in ["density", "expval", "probs"]:
198 raise ValueError(f"Invalid execution type: {value}.")
200 if value == "density" and self.output_qubit != -1:
201 warnings.warn(
202 f"{value} measurement does ignore output_qubit, which is "
203 f"{self.output_qubit}.",
204 UserWarning,
205 )
207 if value == "probs" and self.shots is None:
208 warnings.warn(
209 "Setting execution_type to probs without specifying shots.", UserWarning
210 )
212 if value == "density" and self.shots is not None:
213 warnings.warn(
214 "Setting execution_type to density with specified shots.", UserWarning
215 )
217 self._execution_type = value
219 @property
220 def shots(self) -> Optional[int]:
221 """
222 Gets the number of shots to use for the quantum device.
224 Returns:
225 Optional[int]: The number of shots.
226 """
227 return self._shots
229 @shots.setter
230 def shots(self, value: Optional[int]) -> None:
231 """
232 Sets the number of shots to use for the quantum device.
234 Args:
235 value (Optional[int]): The number of shots.
236 If an integer less than or equal to 0 is provided, it is set to None.
238 Returns:
239 None
240 """
241 if type(value) is int and value <= 0:
242 value = None
243 self._shots = value
245 def initialize_params(
246 self,
247 rng,
248 repeat: int = None,
249 initialization: str = None,
250 initialization_domain: List[float] = None,
251 ) -> None:
252 """
253 Initializes the parameters of the model.
255 Args:
256 rng: A random number generator to use for initialization.
257 repeat: The number of times to repeat the parameters.
258 If None, the number of layers is used.
259 initialization: The strategy to use for parameter initialization.
260 If None, the strategy specified in the constructor is used.
261 initialization_domain: The domain to use for parameter initialization.
262 If None, the domain specified in the constructor is used.
264 Returns:
265 None
266 """
267 params_shape = (
268 self._params_shape if repeat is None else [*self._params_shape, repeat]
269 )
270 # use existing strategy if not specified
271 initialization = initialization or self._inialization_strategy
272 initialization_domain = initialization_domain or self._initialization_domain
274 def set_control_params(params: np.ndarray, value: float) -> np.ndarray:
275 indices = self.pqc.get_control_indices(self.n_qubits)
276 if indices is None:
277 warnings.warn(
278 f"Specified {initialization} but circuit\
279 does not contain controlled rotation gates.\
280 Parameters are intialized randomly.",
281 UserWarning,
282 )
283 else:
284 params[:, indices[0] : indices[1] : indices[2]] = (
285 np.ones_like(params[:, indices[0] : indices[1] : indices[2]])
286 * value
287 )
288 return params
290 if initialization == "random":
291 self.params: np.ndarray = rng.uniform(
292 *initialization_domain, params_shape, requires_grad=True
293 )
294 elif initialization == "zeros":
295 self.params: np.ndarray = np.zeros(params_shape, requires_grad=True)
296 elif initialization == "pi":
297 self.params: np.ndarray = np.ones(params_shape, requires_grad=True) * np.pi
298 elif initialization == "zero-controlled":
299 self.params: np.ndarray = rng.uniform(
300 *initialization_domain, params_shape, requires_grad=True
301 )
302 self.params = set_control_params(self.params, 0)
303 elif initialization == "pi-controlled":
304 self.params: np.ndarray = rng.uniform(
305 *initialization_domain, params_shape, requires_grad=True
306 )
307 self.params = set_control_params(self.params, np.pi)
308 else:
309 raise Exception("Invalid initialization method")
311 log.info(
312 f"Initialized parameters with shape {self.params.shape}\
313 using strategy {initialization}."
314 )
316 def _iec(
317 self,
318 inputs: np.ndarray,
319 data_reupload: bool,
320 enc: Union[Callable, List[Callable]],
321 noise_params: Optional[np.ndarray] = None,
322 ) -> None:
323 """
324 Creates an AngleEncoding using RX gates
326 Args:
327 inputs (np.ndarray): length of vector must be 1, shape (1,)
328 data_reupload (bool, optional): Whether to reupload the data
329 for the IEC or not, default is True.
331 Returns:
332 None
333 """
334 # check for zero, because due to input validation, input cannot be none
335 if not inputs.any():
336 return
338 if data_reupload:
339 if inputs.shape[1] == 1:
340 for q in range(self.n_qubits):
341 enc(inputs[:, 0], wires=q, noise_params=noise_params)
342 else:
343 for q in range(self.n_qubits):
344 for idx in range(inputs.shape[1]):
345 enc[idx](inputs[:, idx], wires=q, noise_params=noise_params)
346 else:
347 if inputs.shape[1] == 1:
348 enc(inputs[:, 0], wires=0, noise_params=noise_params)
349 else:
350 for idx in range(inputs.shape[1]):
351 enc[idx](inputs[:, idx], wires=0, noise_params=noise_params)
353 def _circuit(
354 self,
355 params: np.ndarray,
356 inputs: np.ndarray,
357 ) -> Union[float, np.ndarray]:
358 """
359 Creates a circuit with noise.
361 Args:
362 params (np.ndarray): weight vector of shape
363 [n_layers, n_qubits*n_params_per_layer]
364 inputs (np.ndarray): input vector of size 1
365 Returns:
366 Union[float, np.ndarray]: Expectation value of PauliZ(0)
367 of the circuit if state_vector is False and exp_val is True,
368 otherwise the density matrix of all qubits.
369 """
371 for layer in range(0, self.n_layers):
372 self.pqc(params[layer], self.n_qubits, noise_params=self.noise_params)
374 if self.data_reupload or layer == 0:
375 self._iec(
376 inputs,
377 data_reupload=self.data_reupload,
378 enc=self._enc,
379 noise_params=self.noise_params,
380 )
382 qml.Barrier(wires=list(range(self.n_qubits)), only_visual=True)
384 if self.data_reupload:
385 self.pqc(params[-1], self.n_qubits, noise_params=self.noise_params)
387 if self.noise_params is not None:
388 for q in range(self.n_qubits):
389 qml.AmplitudeDamping(
390 self.noise_params.get("AmplitudeDamping", 0.0), wires=q
391 )
392 qml.PhaseDamping(self.noise_params.get("PhaseDamping", 0.0), wires=q)
394 # run mixed simualtion and get density matrix
395 if self.execution_type == "density":
396 return qml.density_matrix(wires=list(range(self.n_qubits)))
397 # run default simulation and get expectation value
398 elif self.execution_type == "expval":
399 # global measurement (tensored Pauli Z, i.e. parity)
400 if self.output_qubit == -1:
401 return [qml.expval(qml.PauliZ(q)) for q in range(self.n_qubits)]
402 # local measurement(s)
403 elif isinstance(self.output_qubit, int):
404 return qml.expval(qml.PauliZ(self.output_qubit))
405 # n-local measurenment
406 elif isinstance(self.output_qubit, list):
407 obs = qml.simplify(
408 qml.Hamiltonian(
409 [1.0] * self.n_qubits,
410 [qml.PauliZ(q) for q in self.output_qubit],
411 )
412 )
413 return qml.expval(obs)
414 else:
415 raise ValueError(
416 f"Invalid parameter 'output_qubit': {self.output_qubit}.\
417 Must be int, list or -1."
418 )
419 # run default simulation and get probs
420 elif self.execution_type == "probs":
421 if self.output_qubit == -1:
422 return qml.probs(wires=list(range(self.n_qubits)))
423 else:
424 return qml.probs(wires=self.output_qubit)
425 else:
426 raise ValueError(f"Invalid execution_type: {self.execution_type}.")
428 def _draw(self, inputs=None, figure=False) -> None:
429 if isinstance(self.circuit, qml.qnn.torch.TorchLayer):
430 # TODO: throws strange argument error if not catched
431 return ""
433 inputs = self._inputs_validation(inputs)
435 if figure:
436 result = qml.draw_mpl(self.circuit)(params=self.params, inputs=inputs)
437 else:
438 result = qml.draw(self.circuit)(params=self.params, inputs=inputs)
439 return result
441 def draw(self, inputs=None, figure=False) -> None:
443 return self._draw(inputs, figure)
445 def __repr__(self) -> str:
446 return self._draw(figure=False)
448 def __str__(self) -> str:
449 return self._draw(figure=False)
451 def __call__(
452 self,
453 params: Optional[np.ndarray] = None,
454 inputs: Optional[np.ndarray] = None,
455 noise_params: Optional[Dict[str, float]] = None,
456 cache: Optional[bool] = False,
457 execution_type: Optional[str] = None,
458 force_mean: Optional[bool] = False,
459 ) -> np.ndarray:
460 """
461 Perform a forward pass of the quantum circuit.
463 Args:
464 params (Optional[np.ndarray]): Weight vector of shape
465 [n_layers, n_qubits*n_params_per_layer].
466 If None, model internal parameters are used.
467 inputs (Optional[np.ndarray]): Input vector of shape [1].
468 If None, zeros are used.
469 noise_params (Optional[Dict[str, float]], optional): The noise parameters.
470 Defaults to None which results in the last
471 set noise parameters being used.
472 cache (Optional[bool], optional): Whether to cache the results.
473 Defaults to False.
474 execution_type (str, optional): The type of execution.
475 Must be one of 'expval', 'density', or 'probs'.
476 Defaults to None which results in the last set execution type
477 being used.
479 Returns:
480 np.ndarray: The output of the quantum circuit.
481 The shape depends on the execution_type.
482 - If execution_type is 'expval', returns an ndarray of shape
483 (1,) if output_qubit is -1, else (len(output_qubit),).
484 - If execution_type is 'density', returns an ndarray
485 of shape (2**n_qubits, 2**n_qubits).
486 - If execution_type is 'probs', returns an ndarray
487 of shape (2**n_qubits,) if output_qubit is -1, else
488 (2**len(output_qubit),).
489 """
490 # Call forward method which handles the actual caching etc.
491 return self._forward(
492 params=params,
493 inputs=inputs,
494 noise_params=noise_params,
495 cache=cache,
496 execution_type=execution_type,
497 force_mean=force_mean,
498 )
500 def _inputs_validation(
501 self, inputs: Union[None, List, float, int, np.ndarray]
502 ) -> np.ndarray:
503 """
504 Validate the inputs to be a 2D numpy array of shape (batch_size, n_inputs).
506 Args:
507 inputs (Union[None, List, float, int, np.ndarray]): The input to validate.
509 Returns:
510 np.ndarray: The validated input.
511 """
512 if inputs is None:
513 # initialize to zero
514 inputs = np.array([[0]])
515 elif isinstance(inputs, List):
516 inputs = np.stack(inputs)
517 elif isinstance(inputs, float) or isinstance(inputs, int):
518 inputs = np.array([inputs])
520 if len(inputs.shape) == 1:
521 if isinstance(self._enc, List):
522 inputs = inputs.reshape(-1, 1)
523 else:
524 # add a batch dimension
525 inputs = inputs.reshape(inputs.shape[0], 1)
527 return inputs
529 def _forward(
530 self,
531 params: Optional[np.ndarray] = None,
532 inputs: Optional[np.ndarray] = None,
533 noise_params: Optional[Dict[str, float]] = None,
534 cache: Optional[bool] = False,
535 execution_type: Optional[str] = None,
536 force_mean: Optional[bool] = False,
537 ) -> np.ndarray:
538 """
539 Perform a forward pass of the quantum circuit.
541 Args:
542 params (Optional[np.ndarray]): Weight vector of shape
543 [n_layers, n_qubits*n_params_per_layer].
544 If None, model internal parameters are used.
545 inputs (Optional[np.ndarray]): Input vector of shape [1].
546 If None, zeros are used.
547 noise_params (Optional[Dict[str, float]], optional): The noise parameters.
548 Defaults to None which results in the last
549 set noise parameters being used.
550 cache (Optional[bool], optional): Whether to cache the results.
551 Defaults to False.
552 execution_type (str, optional): The type of execution.
553 Must be one of 'expval', 'density', or 'probs'.
554 Defaults to None which results in the last set execution type
555 being used.
557 Returns:
558 np.ndarray: The output of the quantum circuit.
559 The shape depends on the execution_type.
560 - If execution_type is 'expval', returns an ndarray of shape
561 (1,) if output_qubit is -1, else (len(output_qubit),).
562 - If execution_type is 'density', returns an ndarray
563 of shape (2**n_qubits, 2**n_qubits).
564 - If execution_type is 'probs', returns an ndarray
565 of shape (2**n_qubits,) if output_qubit is -1, else
566 (2**len(output_qubit),).
568 Raises:
569 NotImplementedError: If the number of shots is not None or if the
570 expectation value is True.
571 """
572 # set the parameters as object attributes
573 if noise_params is not None:
574 self.noise_params = noise_params
575 if execution_type is not None:
576 self.execution_type = execution_type
578 if params is None:
579 params = self.params
580 else:
581 if numpy_boxes.ArrayBox == type(params):
582 self.params = params._value
583 else:
584 self.params = params
586 inputs = self._inputs_validation(inputs)
588 # the qasm representation contains the bound parameters,
589 # thus it is ok to hash that
590 hs = hashlib.md5(
591 repr(
592 {
593 "n_qubits": self.n_qubits,
594 "n_layers": self.n_layers,
595 "pqc": self.pqc.__class__.__name__,
596 "dru": self.data_reupload,
597 "params": self.params, # use safe-params
598 "noise_params": self.noise_params,
599 "execution_type": self.execution_type,
600 "inputs": inputs,
601 "output_qubit": self.output_qubit,
602 }
603 ).encode("utf-8")
604 ).hexdigest()
606 result: Optional[np.ndarray] = None
607 if cache:
608 name: str = f"pqc_{hs}.npy"
610 cache_folder: str = ".cache"
611 if not os.path.exists(cache_folder):
612 os.mkdir(cache_folder)
614 file_path: str = os.path.join(cache_folder, name)
616 if os.path.isfile(file_path):
617 result = np.load(file_path)
619 if result is None:
620 # if density matrix requested or noise params used
621 if self.execution_type == "density" or self.noise_params is not None:
622 result = self.circuit_mixed(
623 params=params, # use arraybox params
624 inputs=inputs,
625 )
626 else:
627 if isinstance(self.circuit, qml.qnn.torch.TorchLayer):
628 result = self.circuit(
629 inputs=inputs,
630 )
631 else:
632 result = self.circuit(
633 params=params, # use arraybox params
634 inputs=inputs,
635 )
637 if isinstance(result, list):
638 result = np.stack(result)
640 if self.execution_type == "expval" and self.output_qubit == -1:
642 # Calculating mean value after stacking, to not
643 # discard gradient information
644 if force_mean:
645 # exception for torch layer because it swaps batch and output dimension
646 if isinstance(self.circuit, qml.qnn.torch.TorchLayer):
647 result = result.mean(axis=-1)
648 else:
649 result = result.mean(axis=0)
651 if len(result.shape) == 3 and result.shape[0] == 1:
652 result = result[0]
654 if cache:
655 np.save(file_path, result)
657 return result