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
« 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
9from qml_essentials.ansaetze import 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 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.
40 The model is initialized with the following parameters as defaults:
41 - noise_params: None
42 - execution_type: "expval"
43 - shots: None
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.
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
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
78 lightning_threshold = 12
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()
89 log.info(f"Using {circuit_type} circuit.")
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
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
109 # ..here! where we only require a rng
110 self.initialize_params(np.random.default_rng(random_seed))
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 )
134 log.debug(self._draw())
136 @property
137 def noise_params(self) -> Optional[Dict[str, float]]:
138 """
139 Gets the noise parameters of the model.
141 Returns:
142 Optional[Dict[str, float]]: A dictionary of
143 noise parameters or None if not set.
144 """
145 return self._noise_params
147 @noise_params.setter
148 def noise_params(self, value: Optional[Dict[str, float]]) -> None:
149 """
150 Sets the noise parameters of the model.
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.
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
163 @property
164 def execution_type(self) -> str:
165 """
166 Gets the execution type of the model.
168 Returns:
169 str: The execution type, one of 'density', 'expval', or 'probs'.
170 """
171 return self._execution_type
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}.")
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 )
185 if value == "probs" and self.shots is None:
186 warnings.warn(
187 "Setting execution_type to probs without specifying shots.", UserWarning
188 )
190 if value == "density" and self.shots is not None:
191 warnings.warn(
192 "Setting execution_type to density with specified shots.", UserWarning
193 )
195 self._execution_type = value
197 @property
198 def shots(self) -> Optional[int]:
199 """
200 Gets the number of shots to use for the quantum device.
202 Returns:
203 Optional[int]: The number of shots.
204 """
205 return self._shots
207 @shots.setter
208 def shots(self, value: Optional[int]) -> None:
209 """
210 Sets the number of shots to use for the quantum device.
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.
216 Returns:
217 None
218 """
219 if type(value) is int and value <= 0:
220 value = None
221 self._shots = value
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.
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.
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
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
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")
289 log.info(
290 f"Initialized parameters with shape {self.params.shape}\
291 using strategy {initialization}."
292 )
294 def _iec(
295 self,
296 inputs: np.ndarray,
297 data_reupload: bool = True,
298 ) -> None:
299 """
300 Creates an AngleEncoding using RX gates
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.
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)
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 )
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.
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 """
363 for layer in range(0, self.n_layers):
364 self.pqc(params[layer], self.n_qubits)
366 if self.data_reupload or layer == 0:
367 self._iec(inputs, data_reupload=self.data_reupload)
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 )
384 qml.Barrier(wires=list(range(self.n_qubits)), only_visual=True)
386 if self.data_reupload:
387 self.pqc(params[-1], self.n_qubits)
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}.")
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 ""
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
434 def draw(self, inputs=None, figure=False) -> None:
435 return self._draw(inputs, figure)
437 def __repr__(self) -> str:
438 return self._draw(figure=False)
440 def __str__(self) -> str:
441 return self._draw(figure=False)
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.
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.
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 )
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.
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.
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),).
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
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
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()
567 result: Optional[np.ndarray] = None
568 if cache:
569 name: str = f"pqc_{hs}.npy"
571 cache_folder: str = ".cache"
572 if not os.path.exists(cache_folder):
573 os.mkdir(cache_folder)
575 file_path: str = os.path.join(cache_folder, name)
577 if os.path.isfile(file_path):
578 result = np.load(file_path)
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 )
598 if isinstance(result, list):
599 result = np.stack(result)
601 if self.execution_type == "expval" and self.output_qubit == -1:
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)
612 if len(result.shape) == 3 and result.shape[0] == 1:
613 result = result[0]
615 if cache:
616 np.save(file_path, result)
618 return result