qml-essentials 0.1.13__tar.gz

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.
@@ -0,0 +1,7 @@
1
+ Copyright 2024, Copyright Owner: Ostbayerische Technische Hochschule Regensburg (OTH Regensburg), Karlsruhe Institute of Technology (KIT). Author: Maja Franz (OTH), Melvin Strobl (KIT). Contact: melvin.strobl@kit.edu, Scientific Computing Center (SCC)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.1
2
+ Name: qml-essentials
3
+ Version: 0.1.13
4
+ Summary:
5
+ Author: Melvin Strobl
6
+ Author-email: lc3267@kit.edu
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: pennylane (>=0.37.0,<0.38.0)
12
+ Description-Content-Type: text/markdown
13
+
14
+ # QML Essentials
15
+
16
+ [![version](https://img.shields.io/badge/version-0.1.12-green.svg)](https://ea3a0fbb-599f-4d83-86f1-0e71abe27513.ka.bw-cloud-instance.org/lc3267/quantum/) [![Pipx Status](https://servers.stroblme.de/api/badge/3/uptime/72?color=%2331c754&labelColor=%233f4850)](https://servers.stroblme.de/status/open) [![Lint and Pytest](https://github.com/cirKITers/qml-essentials/actions/workflows/python-app.yml/badge.svg)](https://github.com/cirKITers/qml-essentials/actions/workflows/python-app.yml) [![Page Build](https://github.com/cirKITers/qml-essentials/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/cirKITers/qml-essentials/actions/workflows/pages/pages-build-deployment)
17
+
18
+ ## :scroll: About
19
+
20
+ This repo contains some of the commonly used Ansaetze and coding stuff required for working with QML and Data-Reuploading models.
21
+ There are also dedicated classes to calculate entanglement and expressiblity of a provided model as well as its Fourier coefficients.
22
+
23
+ ## :rocket: Getting Started
24
+
25
+ You can find installation instructions and documentation on the corresponding [Github Page](https://cirkiters.github.io/qml-essentials/).
26
+
27
+ ## :construction: Contributing
28
+
29
+ Contributions are very welcome! See [Contribution Guidelines](https://github.com/cirKITers/qml-essentials/blob/main/CONTRIBUTING.md).
@@ -0,0 +1,16 @@
1
+ # QML Essentials
2
+
3
+ [![version](https://img.shields.io/badge/version-0.1.12-green.svg)](https://ea3a0fbb-599f-4d83-86f1-0e71abe27513.ka.bw-cloud-instance.org/lc3267/quantum/) [![Pipx Status](https://servers.stroblme.de/api/badge/3/uptime/72?color=%2331c754&labelColor=%233f4850)](https://servers.stroblme.de/status/open) [![Lint and Pytest](https://github.com/cirKITers/qml-essentials/actions/workflows/python-app.yml/badge.svg)](https://github.com/cirKITers/qml-essentials/actions/workflows/python-app.yml) [![Page Build](https://github.com/cirKITers/qml-essentials/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/cirKITers/qml-essentials/actions/workflows/pages/pages-build-deployment)
4
+
5
+ ## :scroll: About
6
+
7
+ This repo contains some of the commonly used Ansaetze and coding stuff required for working with QML and Data-Reuploading models.
8
+ There are also dedicated classes to calculate entanglement and expressiblity of a provided model as well as its Fourier coefficients.
9
+
10
+ ## :rocket: Getting Started
11
+
12
+ You can find installation instructions and documentation on the corresponding [Github Page](https://cirkiters.github.io/qml-essentials/).
13
+
14
+ ## :construction: Contributing
15
+
16
+ Contributions are very welcome! See [Contribution Guidelines](https://github.com/cirKITers/qml-essentials/blob/main/CONTRIBUTING.md).
@@ -0,0 +1,25 @@
1
+ [tool.poetry]
2
+ name = "qml-essentials"
3
+ version = "0.1.13"
4
+ description = ""
5
+ authors = ["Melvin Strobl <lc3267@kit.edu>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.11"
10
+ pennylane = "^0.37.0"
11
+
12
+
13
+ [tool.poetry.group.dev.dependencies]
14
+ devpi-client = "^7.0.3"
15
+ black = "^24.4.2"
16
+ pytest = "^8.2.2"
17
+ flake8 = "^7.1.1"
18
+ mkdocs = "^1.6.0"
19
+ mkdocs-material = "^9.5.31"
20
+ mkdocstrings-python = "^1.10.7"
21
+ markdown-include = "^0.8.1"
22
+
23
+ [build-system]
24
+ requires = ["poetry-core"]
25
+ build-backend = "poetry.core.masonry.api"
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