qml-essentials 0.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,485 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Optional
3
+ import pennylane.numpy as np
4
+ import pennylane as qml
5
+
6
+ from typing import List
7
+
8
+ import logging
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class Circuit(ABC):
14
+ def __init__(self):
15
+ pass
16
+
17
+ @abstractmethod
18
+ def n_params_per_layer(n_qubits: int) -> int:
19
+ return
20
+
21
+ @abstractmethod
22
+ def get_control_indices(self, n_qubits: int) -> List[int]:
23
+ """
24
+ Returns the indices for the controlled rotation gates for one layer.
25
+ Indices should slice the list of all parameters for one layer as follows:
26
+ [indices[0]:indices[1]:indices[2]]
27
+
28
+ Parameters
29
+ ----------
30
+ n_qubits : int
31
+ Number of qubits in the circuit
32
+
33
+ Returns
34
+ -------
35
+ Optional[np.ndarray]
36
+ List of all controlled indices, or None if the circuit does not
37
+ contain controlled rotation gates.
38
+ """
39
+ return
40
+
41
+ def get_control_angles(self, w: np.ndarray, n_qubits: int) -> Optional[np.ndarray]:
42
+ """
43
+ Returns the angles for the controlled rotation gates from the list of
44
+ all parameters for one layer.
45
+
46
+ Parameters
47
+ ----------
48
+ w : np.ndarray
49
+ List of parameters for one layer
50
+ n_qubits : int
51
+ Number of qubits in the circuit
52
+
53
+ Returns
54
+ -------
55
+ Optional[np.ndarray]
56
+ List of all controlled parameters, or None if the circuit does not
57
+ contain controlled rotation gates.
58
+ """
59
+ indices = self.get_control_indices(n_qubits)
60
+ if indices is None:
61
+ return None
62
+
63
+ return w[indices[0] : indices[1] : indices[2]]
64
+
65
+ @abstractmethod
66
+ def build(self, n_qubits: int, n_layers: int):
67
+ return
68
+
69
+ def __call__(self, *args: Any, **kwds: Any) -> Any:
70
+ self.build(*args, **kwds)
71
+
72
+
73
+ class Ansaetze:
74
+ def get_available():
75
+ return [
76
+ Ansaetze.No_Ansatz,
77
+ Ansaetze.Circuit_1,
78
+ Ansaetze.Circuit_6,
79
+ Ansaetze.Circuit_9,
80
+ Ansaetze.Circuit_15,
81
+ Ansaetze.Circuit_18,
82
+ Ansaetze.Circuit_19,
83
+ Ansaetze.No_Entangling,
84
+ Ansaetze.Strongly_Entangling,
85
+ Ansaetze.Hardware_Efficient,
86
+ Ansaetze.Bansatz,
87
+ ]
88
+
89
+ class No_Ansatz(Circuit):
90
+ @staticmethod
91
+ def n_params_per_layer(n_qubits: int) -> int:
92
+ return 0
93
+
94
+ @staticmethod
95
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
96
+ return None
97
+
98
+ @staticmethod
99
+ def build(w: np.ndarray, n_qubits: int):
100
+ pass
101
+
102
+ class Hardware_Efficient(Circuit):
103
+ @staticmethod
104
+ def n_params_per_layer(n_qubits: int) -> int:
105
+ if n_qubits > 1:
106
+ return n_qubits * 3
107
+ else:
108
+ log.warning("Number of Qubits < 2, no entanglement available")
109
+ return 3
110
+
111
+ @staticmethod
112
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
113
+ return None
114
+
115
+ @staticmethod
116
+ def build(w: np.ndarray, n_qubits: int):
117
+ """
118
+ Creates a Circuit19 ansatz.
119
+
120
+ Length of flattened vector must be n_qubits*3-1
121
+ because for >1 qubits there are three gates
122
+
123
+ Args:
124
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
125
+ n_qubits (int): number of qubits
126
+ """
127
+ w_idx = 0
128
+ for q in range(n_qubits):
129
+ qml.RY(w[w_idx], wires=q)
130
+ w_idx += 1
131
+ qml.RZ(w[w_idx], wires=q)
132
+ w_idx += 1
133
+ qml.RY(w[w_idx], wires=q)
134
+ w_idx += 1
135
+
136
+ if n_qubits > 1:
137
+ for q in range(n_qubits // 2):
138
+ qml.CZ(wires=[(2 * q), (2 * q + 1)])
139
+ for q in range((n_qubits - 1) // 2):
140
+ qml.CZ(wires=[(2 * q + 1), (2 * q + 2)])
141
+
142
+ class Bansatz(Circuit):
143
+ @staticmethod
144
+ def n_params_per_layer(n_qubits: int) -> int:
145
+ if n_qubits > 1:
146
+ return n_qubits * 3
147
+ else:
148
+ log.warning("Number of Qubits < 2, no entanglement available")
149
+ return 3
150
+
151
+ @staticmethod
152
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
153
+ if n_qubits > 1:
154
+ return [-n_qubits, None, None]
155
+ else:
156
+ return None
157
+
158
+ @staticmethod
159
+ def build(w: np.ndarray, n_qubits: int):
160
+ """
161
+ Creates a Circuit19 ansatz.
162
+
163
+ Length of flattened vector must be n_qubits*3-1
164
+ because for >1 qubits there are three gates
165
+
166
+ Args:
167
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
168
+ n_qubits (int): number of qubits
169
+ """
170
+ w_idx = 0
171
+ for q in range(n_qubits):
172
+ qml.RY(w[w_idx], wires=q)
173
+ w_idx += 1
174
+ qml.RZ(w[w_idx], wires=q)
175
+ w_idx += 1
176
+
177
+ if n_qubits > 1:
178
+ for q in range(n_qubits // 2):
179
+ qml.CRX(w[w_idx], wires=[(2 * q), (2 * q + 1)])
180
+ w_idx += 1
181
+
182
+ for q in range((n_qubits - 1) // 2):
183
+ qml.CRX(w[w_idx], wires=[(2 * q + 1), (2 * q + 2)])
184
+ w_idx += 1
185
+
186
+ class Circuit_19(Circuit):
187
+ @staticmethod
188
+ def n_params_per_layer(n_qubits: int) -> int:
189
+ if n_qubits > 1:
190
+ return n_qubits * 3
191
+ else:
192
+ log.warning("Number of Qubits < 2, no entanglement available")
193
+ return 2
194
+
195
+ @staticmethod
196
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
197
+ if n_qubits > 1:
198
+ return [-n_qubits, None, None]
199
+ else:
200
+ return None
201
+
202
+ @staticmethod
203
+ def build(w: np.ndarray, n_qubits: int):
204
+ """
205
+ Creates a Circuit19 ansatz.
206
+
207
+ Length of flattened vector must be n_qubits*3-1
208
+ because for >1 qubits there are three gates
209
+
210
+ Args:
211
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
212
+ n_qubits (int): number of qubits
213
+ """
214
+ w_idx = 0
215
+ for q in range(n_qubits):
216
+ qml.RX(w[w_idx], wires=q)
217
+ w_idx += 1
218
+ qml.RZ(w[w_idx], wires=q)
219
+ w_idx += 1
220
+
221
+ if n_qubits > 1:
222
+ for q in range(n_qubits):
223
+ qml.CRX(
224
+ w[w_idx],
225
+ wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
226
+ )
227
+ w_idx += 1
228
+
229
+ class Circuit_18(Circuit):
230
+ @staticmethod
231
+ def n_params_per_layer(n_qubits: int) -> int:
232
+ if n_qubits > 1:
233
+ return n_qubits * 3
234
+ else:
235
+ log.warning("Number of Qubits < 2, no entanglement available")
236
+ return 2
237
+
238
+ @staticmethod
239
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
240
+ if n_qubits > 1:
241
+ return [-n_qubits, None, None]
242
+ else:
243
+ return None
244
+
245
+ @staticmethod
246
+ def build(w: np.ndarray, n_qubits: int):
247
+ """
248
+ Creates a Circuit18 ansatz.
249
+
250
+ Length of flattened vector must be n_qubits*3-1
251
+ because for >1 qubits there are three gates
252
+
253
+ Args:
254
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
255
+ n_qubits (int): number of qubits
256
+ """
257
+ w_idx = 0
258
+ for q in range(n_qubits):
259
+ qml.RX(w[w_idx], wires=q)
260
+ w_idx += 1
261
+ qml.RZ(w[w_idx], wires=q)
262
+ w_idx += 1
263
+
264
+ if n_qubits > 1:
265
+ for q in range(n_qubits):
266
+ qml.CRZ(
267
+ w[w_idx],
268
+ wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
269
+ )
270
+ w_idx += 1
271
+
272
+ class Circuit_15(Circuit):
273
+ @staticmethod
274
+ def n_params_per_layer(n_qubits: int) -> int:
275
+ if n_qubits > 1:
276
+ return n_qubits * 3
277
+ else:
278
+ log.warning("Number of Qubits < 2, no entanglement available")
279
+ return 2
280
+
281
+ @staticmethod
282
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
283
+ return None
284
+
285
+ @staticmethod
286
+ def build(w: np.ndarray, n_qubits: int):
287
+ """
288
+ Creates a Circuit15 ansatz.
289
+
290
+ Length of flattened vector must be n_qubits*3-1
291
+ because for >1 qubits there are three gates
292
+
293
+ Args:
294
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
295
+ n_qubits (int): number of qubits
296
+ """
297
+ raise NotImplementedError # Did not figured out the entangling sequence yet
298
+
299
+ w_idx = 0
300
+ for q in range(n_qubits):
301
+ qml.RX(w[w_idx], wires=q)
302
+ w_idx += 1
303
+
304
+ if n_qubits > 1:
305
+ for q in range(n_qubits):
306
+ qml.CNOT(wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits])
307
+
308
+ for q in range(n_qubits):
309
+ qml.RZ(w[w_idx], wires=q)
310
+ w_idx += 1
311
+
312
+ class Circuit_9(Circuit):
313
+ @staticmethod
314
+ def n_params_per_layer(n_qubits: int) -> int:
315
+ return n_qubits
316
+
317
+ @staticmethod
318
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
319
+ return None
320
+
321
+ @staticmethod
322
+ def build(w: np.ndarray, n_qubits: int):
323
+ """
324
+ Creates a Circuit19 ansatz.
325
+
326
+ Length of flattened vector must be n_qubits*3-1
327
+ because for >1 qubits there are three gates
328
+
329
+ Args:
330
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
331
+ n_qubits (int): number of qubits
332
+ """
333
+ w_idx = 0
334
+ for q in range(n_qubits):
335
+ qml.Hadamard(wires=q)
336
+
337
+ if n_qubits > 1:
338
+ for q in range(n_qubits - 1):
339
+ qml.CZ(wires=[n_qubits - q - 2, n_qubits - q - 1])
340
+
341
+ for q in range(n_qubits):
342
+ qml.RX(w[w_idx], wires=q)
343
+ w_idx += 1
344
+
345
+ class Circuit_6(Circuit):
346
+ @staticmethod
347
+ def n_params_per_layer(n_qubits: int) -> int:
348
+ if n_qubits > 1:
349
+ return n_qubits * 3 + n_qubits**2
350
+ else:
351
+ log.warning("Number of Qubits < 2, no entanglement available")
352
+ return 4
353
+
354
+ @staticmethod
355
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
356
+ if n_qubits > 1:
357
+ return [-n_qubits, None, None]
358
+ else:
359
+ return None
360
+
361
+ @staticmethod
362
+ def build(w: np.ndarray, n_qubits: int):
363
+ """
364
+ Creates a Circuit1 ansatz.
365
+
366
+ Length of flattened vector must be n_qubits*2
367
+
368
+ Args:
369
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
370
+ n_qubits (int): number of qubits
371
+ """
372
+ w_idx = 0
373
+ for q in range(n_qubits):
374
+ qml.RX(w[w_idx], wires=q)
375
+ w_idx += 1
376
+ qml.RZ(w[w_idx], wires=q)
377
+ w_idx += 1
378
+
379
+ if n_qubits > 1:
380
+ for ql in range(n_qubits):
381
+ for q in range(n_qubits):
382
+ if q == ql:
383
+ continue
384
+ qml.CRX(
385
+ w[w_idx],
386
+ wires=[n_qubits - ql - 1, (n_qubits - q - 1) % n_qubits],
387
+ )
388
+ w_idx += 1
389
+
390
+ for q in range(n_qubits):
391
+ qml.RX(w[w_idx], wires=q)
392
+ w_idx += 1
393
+ qml.RZ(w[w_idx], wires=q)
394
+ w_idx += 1
395
+
396
+ class Circuit_1(Circuit):
397
+ @staticmethod
398
+ def n_params_per_layer(n_qubits: int) -> int:
399
+ return n_qubits * 2
400
+
401
+ @staticmethod
402
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
403
+ return None
404
+
405
+ @staticmethod
406
+ def build(w: np.ndarray, n_qubits: int):
407
+ """
408
+ Creates a Circuit1 ansatz.
409
+
410
+ Length of flattened vector must be n_qubits*2
411
+
412
+ Args:
413
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
414
+ n_qubits (int): number of qubits
415
+ """
416
+ w_idx = 0
417
+ for q in range(n_qubits):
418
+ qml.RX(w[w_idx], wires=q)
419
+ w_idx += 1
420
+ qml.RZ(w[w_idx], wires=q)
421
+ w_idx += 1
422
+
423
+ class Strongly_Entangling(Circuit):
424
+ @staticmethod
425
+ def n_params_per_layer(n_qubits: int) -> int:
426
+ if n_qubits > 1:
427
+ return n_qubits * 6
428
+ else:
429
+ log.warning("Number of Qubits < 2, no entanglement available")
430
+ return 2
431
+
432
+ @staticmethod
433
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
434
+ return None
435
+
436
+ @staticmethod
437
+ def build(w: np.ndarray, n_qubits: int) -> None:
438
+ """
439
+ Creates a StronglyEntanglingLayers ansatz.
440
+
441
+ Args:
442
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
443
+ n_qubits (int): number of qubits
444
+ """
445
+ w_idx = 0
446
+ for q in range(n_qubits):
447
+ qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
448
+ w_idx += 3
449
+
450
+ if n_qubits > 1:
451
+ for q in range(n_qubits):
452
+ qml.CNOT(wires=[q, (q + 1) % n_qubits])
453
+
454
+ for q in range(n_qubits):
455
+ qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
456
+ w_idx += 3
457
+
458
+ if n_qubits > 1:
459
+ for q in range(n_qubits):
460
+ qml.CNOT(wires=[q, (q + n_qubits // 2) % n_qubits])
461
+
462
+ class No_Entangling(Circuit):
463
+ @staticmethod
464
+ def n_params_per_layer(n_qubits: int) -> int:
465
+ return n_qubits * 3
466
+
467
+ @staticmethod
468
+ def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
469
+ return None
470
+
471
+ @staticmethod
472
+ def build(w: np.ndarray, n_qubits: int):
473
+ """
474
+ Creates a circuit without entangling, but with U3 gates on all qubits
475
+
476
+ Length of flattened vector must be n_qubits*3
477
+
478
+ Args:
479
+ w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
480
+ n_qubits (int): number of qubits
481
+ """
482
+ w_idx = 0
483
+ for q in range(n_qubits):
484
+ qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
485
+ w_idx += 3
@@ -0,0 +1,37 @@
1
+ from qml_essentials.model import Model
2
+ from functools import partial
3
+ from pennylane.fourier import coefficients
4
+ import numpy as np
5
+
6
+
7
+ class Coefficients:
8
+
9
+ @staticmethod
10
+ def sample_coefficients(model: Model, **kwargs) -> np.ndarray:
11
+ """
12
+ Sample the Fourier coefficients of a given model
13
+ using Pennylane fourier.coefficients function.
14
+
15
+ Note that the coefficients are complex numbers, but the imaginary part
16
+ of the coefficients should be very close to zero, since the expectation
17
+ values of the Pauli operators are real numbers.
18
+
19
+ Args:
20
+ model (Model): The model to sample.
21
+
22
+ Returns:
23
+ np.ndarray: The sampled Fourier coefficients.
24
+ """
25
+ kwargs.setdefault("force_mean", True)
26
+ kwargs.setdefault("execution_type", "expval")
27
+
28
+ partial_circuit = partial(model, model.params, **kwargs)
29
+ coeffs = coefficients(partial_circuit, 1, model.degree)
30
+
31
+ if not np.isclose(np.sum(coeffs).imag, 0.0, rtol=1.0e-5):
32
+ raise ValueError(
33
+ f"Spectrum is not real. Imaginary part of coefficients is:\
34
+ {np.sum(coeffs).imag}"
35
+ )
36
+
37
+ return coeffs
@@ -0,0 +1,105 @@
1
+ from typing import Callable, Optional, List, Any
2
+ import pennylane as qml
3
+ import pennylane.numpy as np
4
+
5
+ import logging
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+
10
+ class Entanglement:
11
+
12
+ @staticmethod
13
+ def meyer_wallach(
14
+ model: Callable, # type: ignore
15
+ n_samples: int,
16
+ seed: Optional[int],
17
+ **kwargs: Any
18
+ ) -> float:
19
+ """
20
+ Calculates the entangling capacity of a given quantum circuit
21
+ using Meyer-Wallach measure.
22
+
23
+ Args:
24
+ model (Callable): Function that models the quantum circuit. It must
25
+ have a `n_qubits` attribute representing the number of qubits.
26
+ It must accept a `params` argument representing the parameters
27
+ of the circuit.
28
+ n_samples (int): Number of samples per qubit.
29
+ seed (Optional[int]): Seed for the random number generator.
30
+ kwargs (Any): Additional keyword arguments for the model function.
31
+
32
+ Returns:
33
+ float: Entangling capacity of the given circuit. It is guaranteed
34
+ to be between 0.0 and 1.0.
35
+ """
36
+
37
+ def _meyer_wallach(
38
+ evaluate: Callable[[np.ndarray], np.ndarray],
39
+ n_qubits: int,
40
+ samples: int,
41
+ params: np.ndarray,
42
+ ) -> float:
43
+ """
44
+ Calculates the Meyer-Wallach sampling of the entangling capacity
45
+ of a quantum circuit.
46
+
47
+ Args:
48
+ evaluate (Callable[[np.ndarray], np.ndarray]): Callable that
49
+ evaluates the quantum circuit It must accept a `params`
50
+ argument representing the parameters of the circuit and may
51
+ accept additional keyword arguments.
52
+ n_qubits (int): Number of qubits in the circuit
53
+ samples (int): Number of samples to be taken
54
+ params (np.ndarray): Parameters of the instructor. Shape:
55
+ (samples, *model.params.shape)
56
+
57
+ Returns:
58
+ float: Entangling capacity of the given circuit. It is
59
+ guaranteed to be between 0.0 and 1.0
60
+ """
61
+ assert (
62
+ params.shape[0] == samples
63
+ ), "Number of samples does not match number of parameters"
64
+
65
+ mw_measure = np.zeros(samples, dtype=complex)
66
+ qb = list(range(n_qubits))
67
+
68
+ for i in range(samples):
69
+ # implicitly set input to none in case it's not needed
70
+ kwargs.setdefault("inputs", None)
71
+ # explicitly set execution type because everything else won't work
72
+ U = evaluate(params=params[i], execution_type="density", **kwargs)
73
+
74
+ entropy = 0
75
+
76
+ for j in range(n_qubits):
77
+ density = qml.math.partial_trace(U, qb[:j] + qb[j + 1 :])
78
+ entropy += np.trace((density @ density).real)
79
+
80
+ mw_measure[i] = 1 - entropy / n_qubits
81
+
82
+ mw = 2 * np.sum(mw_measure.real) / samples
83
+
84
+ # catch floating point errors
85
+ return min(max(mw, 0.0), 1.0)
86
+
87
+ if n_samples > 0:
88
+ assert seed is not None, "Seed must be provided when samples > 0"
89
+ # TODO: maybe switch to JAX rng
90
+ rng = np.random.default_rng(seed)
91
+ params = rng.uniform(0, 2 * np.pi, size=(n_samples, *model.params.shape))
92
+ else:
93
+ if seed is not None:
94
+ log.warning("Seed is ignored when samples is 0")
95
+ n_samples = 1
96
+ params = model.params.reshape(1, *model.params.shape)
97
+
98
+ entangling_capability = _meyer_wallach(
99
+ evaluate=model,
100
+ n_qubits=model.n_qubits,
101
+ samples=n_samples,
102
+ params=params,
103
+ )
104
+
105
+ return float(entangling_capability)